@gugananuvem/aws-local-simulator 1.0.11 → 1.0.12

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.
@@ -140,6 +140,215 @@ class CognitoSimulator {
140
140
  };
141
141
  }
142
142
 
143
+ listUsers(params = {}) {
144
+ const { UserPoolId, Filter, Limit = 60, PaginationToken } = params;
145
+ const userPool = this.userPools.get(UserPoolId);
146
+
147
+ if (!userPool) {
148
+ throw new Error(`User pool ${UserPoolId} not found`);
149
+ }
150
+
151
+ let users = Array.from(this.users.values()).filter(u => u.UserPoolId === UserPoolId);
152
+
153
+ if (PaginationToken) {
154
+ const startIndex = parseInt(PaginationToken);
155
+ users = users.slice(startIndex);
156
+ }
157
+
158
+ const results = users.slice(0, Limit);
159
+ const nextToken = results.length === Limit && users.length > Limit ? String(Limit) : null;
160
+
161
+ return {
162
+ Users: results.map(u => ({
163
+ Username: u.Username,
164
+ UserStatus: u.UserStatus,
165
+ Enabled: u.Enabled,
166
+ UserCreateDate: u.CreatedDate,
167
+ UserLastModifiedDate: u.LastModifiedDate,
168
+ Attributes: this.formatUserAttributes(u.Attributes)
169
+ })),
170
+ PaginationToken: nextToken
171
+ };
172
+ }
173
+
174
+ listUserPoolClients(params = {}) {
175
+ const { UserPoolId, MaxResults = 60, NextToken } = params;
176
+ const userPool = this.userPools.get(UserPoolId);
177
+ if (!userPool) throw new Error(`User pool ${UserPoolId} not found`);
178
+
179
+ let clients = Array.from(userPool.Clients.values());
180
+ if (NextToken) clients = clients.slice(parseInt(NextToken));
181
+ const results = clients.slice(0, MaxResults);
182
+
183
+ return {
184
+ UserPoolClients: results.map(c => ({
185
+ ClientId: c.ClientId,
186
+ ClientName: c.ClientName,
187
+ UserPoolId: c.UserPoolId
188
+ })),
189
+ NextToken: results.length === MaxResults && clients.length > MaxResults ? String(MaxResults) : null
190
+ };
191
+ }
192
+
193
+ describeUserPoolClient(params = {}) {
194
+ const { UserPoolId, ClientId } = params;
195
+ const userPool = this.userPools.get(UserPoolId);
196
+ if (!userPool) throw new Error(`User pool ${UserPoolId} not found`);
197
+ const client = userPool.Clients.get(ClientId);
198
+ if (!client) throw new Error(`Client ${ClientId} not found`);
199
+ return { UserPoolClient: client };
200
+ }
201
+
202
+ deleteUserPoolClient(params = {}) {
203
+ const { UserPoolId, ClientId } = params;
204
+ const userPool = this.userPools.get(UserPoolId);
205
+ if (!userPool) throw new Error(`User pool ${UserPoolId} not found`);
206
+ userPool.Clients.delete(ClientId);
207
+ this.persistUserPools();
208
+ return {};
209
+ }
210
+
211
+ forgotPassword(params = {}) {
212
+ const { ClientId, Username } = params;
213
+ const userPool = this.findUserPoolByClientId(ClientId);
214
+ if (!userPool) throw new Error(`Client ${ClientId} not found`);
215
+ return { CodeDeliveryDetails: { Destination: 'test@example.com', DeliveryMedium: 'EMAIL', AttributeName: 'email' } };
216
+ }
217
+
218
+ confirmForgotPassword(params = {}) {
219
+ const { ClientId, Username, ConfirmationCode, Password } = params;
220
+ const user = this.findUserByUsername(Username, ClientId);
221
+ if (!user) throw new Error(`User not found: ${Username}`);
222
+ user.Password = this.hashPassword(Password);
223
+ user.UserStatus = 'CONFIRMED';
224
+ this.persistUsers();
225
+ return {};
226
+ }
227
+
228
+ changePassword(params = {}) {
229
+ const { AccessToken, PreviousPassword, ProposedPassword } = params;
230
+ const session = this.accessTokens.get(AccessToken);
231
+ if (!session) throw new Error('Invalid access token');
232
+ const user = this.users.get(session.UserId);
233
+ if (!user) throw new Error('User not found');
234
+ if (!this.verifyPassword(PreviousPassword, user.Password)) throw new Error('Incorrect previous password');
235
+ user.Password = this.hashPassword(ProposedPassword);
236
+ this.persistUsers();
237
+ return {};
238
+ }
239
+
240
+ respondToAuthChallenge(params = {}) {
241
+ return { AuthenticationResult: null, ChallengeName: null };
242
+ }
243
+
244
+ revokeToken(params = {}) {
245
+ const { Token, ClientId } = params;
246
+ // Remove refresh token session if it exists
247
+ const session = this.refreshTokens.get(Token);
248
+ if (session) {
249
+ this.sessions.delete(session.Id);
250
+ this.accessTokens.delete(session.AccessToken);
251
+ this.refreshTokens.delete(Token);
252
+ this.persistSessions();
253
+ }
254
+ return {};
255
+ }
256
+
257
+ globalSignOut(params = {}) {
258
+ const { AccessToken } = params;
259
+ const session = this.accessTokens.get(AccessToken);
260
+ if (session) {
261
+ this.sessions.delete(session.Id);
262
+ this.accessTokens.delete(AccessToken);
263
+ this.refreshTokens.delete(session.RefreshToken);
264
+ this.persistSessions();
265
+ }
266
+ return {};
267
+ }
268
+
269
+ getUser(params = {}) {
270
+ const { AccessToken } = params;
271
+ const session = this.accessTokens.get(AccessToken);
272
+ if (!session) throw new Error('Invalid access token');
273
+ // Check session is still active
274
+ if (!this.sessions.has(session.Id)) throw new Error('Token has been revoked');
275
+ const user = this.users.get(session.UserId);
276
+ if (!user) throw new Error('User not found');
277
+ return {
278
+ Username: user.Username,
279
+ UserAttributes: this.formatUserAttributes(user.Attributes),
280
+ UserStatus: user.UserStatus
281
+ };
282
+ }
283
+
284
+ updateUserAttributes(params = {}) {
285
+ const { AccessToken, UserAttributes } = params;
286
+ const session = this.accessTokens.get(AccessToken);
287
+ if (!session) throw new Error('Invalid access token');
288
+ const user = this.users.get(session.UserId);
289
+ if (!user) throw new Error('User not found');
290
+ const updates = this.normalizeUserAttributes(UserAttributes || []);
291
+ Object.assign(user.Attributes, updates);
292
+ this.persistUsers();
293
+ return { CodeDeliveryDetailsList: [] };
294
+ }
295
+
296
+ deleteUser(params = {}) {
297
+ const { AccessToken } = params;
298
+ const session = this.accessTokens.get(AccessToken);
299
+ if (!session) throw new Error('Invalid access token');
300
+ this.users.delete(session.UserId);
301
+ this.persistUsers();
302
+ return {};
303
+ }
304
+
305
+ adminDisableUser(params = {}) {
306
+ const { UserPoolId, Username } = params;
307
+ const user = this.findUserByUsername(Username, null, UserPoolId);
308
+ if (!user) throw new Error(`User not found: ${Username}`);
309
+ user.Enabled = false;
310
+ this.persistUsers();
311
+ return {};
312
+ }
313
+
314
+ adminEnableUser(params = {}) {
315
+ const { UserPoolId, Username } = params;
316
+ const user = this.findUserByUsername(Username, null, UserPoolId);
317
+ if (!user) throw new Error(`User not found: ${Username}`);
318
+ user.Enabled = true;
319
+ this.persistUsers();
320
+ return {};
321
+ }
322
+
323
+ adminResetUserPassword(params = {}) {
324
+ const { UserPoolId, Username } = params;
325
+ const user = this.findUserByUsername(Username, null, UserPoolId);
326
+ if (!user) throw new Error(`User not found: ${Username}`);
327
+ user.UserStatus = 'RESET_REQUIRED';
328
+ this.persistUsers();
329
+ return {};
330
+ }
331
+
332
+ adminUserGlobalSignOut(params = {}) {
333
+ const { UserPoolId, Username } = params;
334
+ const user = this.findUserByUsername(Username, null, UserPoolId);
335
+ if (!user) throw new Error(`User not found: ${Username}`);
336
+ // Invalidate all sessions for this user
337
+ for (const [id, session] of this.sessions.entries()) {
338
+ if (session.UserId === user.UserId) {
339
+ this.accessTokens.delete(session.AccessToken);
340
+ this.refreshTokens.delete(session.RefreshToken);
341
+ this.sessions.delete(id);
342
+ }
343
+ }
344
+ this.persistSessions();
345
+ return {};
346
+ }
347
+
348
+ adminListGroupsForUser(params = {}) {
349
+ return { Groups: [], NextToken: null };
350
+ }
351
+
143
352
  deleteUserPool(params) {
144
353
  const { UserPoolId } = params;
145
354
 
@@ -207,7 +416,7 @@ class CognitoSimulator {
207
416
 
208
417
  // ============ User Operations ============
209
418
 
210
- signUp(params) {
419
+ signUp(params = {}) {
211
420
  const { ClientId, Username, Password, UserAttributes, ValidationData } = params;
212
421
 
213
422
  // Encontra o user pool pelo client id
@@ -726,10 +935,10 @@ class CognitoSimulator {
726
935
  // ============ Persistence ============
727
936
 
728
937
  loadUserPools() {
938
+ // Load persisted pools first
729
939
  const saved = this.store.read('__userpools__');
730
940
  if (saved) {
731
941
  for (const [id, data] of Object.entries(saved)) {
732
- // Reconstitui Maps
733
942
  data.Clients = new Map(Object.entries(data.Clients || {}));
734
943
  data.Groups = new Map(Object.entries(data.Groups || {}));
735
944
  data.IdentityProviders = new Map(Object.entries(data.IdentityProviders || {}));
@@ -737,6 +946,64 @@ class CognitoSimulator {
737
946
  this.userPools.set(id, data);
738
947
  }
739
948
  }
949
+
950
+ // Create user pools from config if not already persisted
951
+ if (this.config.cognito?.userPools) {
952
+ let configChanged = false;
953
+ const configPath = this.config._configPath;
954
+
955
+ for (let i = 0; i < this.config.cognito.userPools.length; i++) {
956
+ const poolConfig = this.config.cognito.userPools[i];
957
+ const existing = Array.from(this.userPools.values()).find(p => p.Name === poolConfig.PoolName);
958
+
959
+ if (!existing) {
960
+ const result = this.createUserPool(poolConfig);
961
+ const poolId = result.UserPool.Id;
962
+ logger.debug(`✅ User Pool criado a partir da config: ${poolConfig.PoolName} (${poolId})`);
963
+
964
+ // Auto-create a default client if not specified
965
+ if (!poolConfig.ClientId) {
966
+ const clientResult = this.createUserPoolClient({
967
+ UserPoolId: poolId,
968
+ ClientName: `${poolConfig.PoolName}-client`,
969
+ GenerateSecret: false
970
+ });
971
+ const clientId = clientResult.UserPoolClient.ClientId;
972
+ this.config.cognito.userPools[i].ClientId = clientId;
973
+ this.config.cognito.userPools[i].UserPoolId = poolId;
974
+ configChanged = true;
975
+ logger.debug(`✅ Client criado automaticamente: ${clientId}`);
976
+ }
977
+ } else if (!poolConfig.ClientId) {
978
+ // Pool exists but no clientId in config — write it back
979
+ const firstClient = existing.Clients.size > 0 ? existing.Clients.values().next().value : null;
980
+ if (firstClient) {
981
+ this.config.cognito.userPools[i].ClientId = firstClient.ClientId;
982
+ this.config.cognito.userPools[i].UserPoolId = existing.Id;
983
+ configChanged = true;
984
+ }
985
+ }
986
+ }
987
+
988
+ // Write clientId back to aws-local-simulator.json
989
+ if (configChanged && configPath) {
990
+ try {
991
+ const fs = require('fs');
992
+ const fileContent = JSON.parse(fs.readFileSync(configPath, 'utf8'));
993
+ fileContent.cognito = fileContent.cognito || {};
994
+ fileContent.cognito.userPools = this.config.cognito.userPools.map(p => ({
995
+ PoolName: p.PoolName,
996
+ AutoVerifiedAttributes: p.AutoVerifiedAttributes,
997
+ UserPoolId: p.UserPoolId,
998
+ ClientId: p.ClientId
999
+ }));
1000
+ fs.writeFileSync(configPath, JSON.stringify(fileContent, null, 2));
1001
+ logger.info(`✅ ClientId gravado em: ${configPath}`);
1002
+ } catch (err) {
1003
+ logger.warn(`⚠️ Não foi possível gravar clientId no config: ${err.message}`);
1004
+ }
1005
+ }
1006
+ }
740
1007
  }
741
1008
 
742
1009
  loadIdentityPools() {
@@ -16,8 +16,19 @@ class HandlerLoader {
16
16
  * @returns {Promise<Function>} - Função handler
17
17
  */
18
18
  static async load(handlerPath, type = 'auto') {
19
- const fullPath = path.resolve(process.cwd(), handlerPath);
20
-
19
+ // Resolve path: try cwd first, then data dir
20
+ let fullPath = path.resolve(process.cwd(), handlerPath);
21
+
22
+ if (!fs.existsSync(fullPath)) {
23
+ const dataDir = process.env.AWS_LOCAL_SIMULATOR_DATA_DIR;
24
+ if (dataDir) {
25
+ const dataPath = path.resolve(dataDir, 'lambda', handlerPath.replace(/^\.\//, ''));
26
+ if (fs.existsSync(dataPath)) {
27
+ fullPath = dataPath;
28
+ }
29
+ }
30
+ }
31
+
21
32
  if (!fs.existsSync(fullPath)) {
22
33
  throw new Error(`Handler não encontrado: ${fullPath}`);
23
34
  }
@@ -22,7 +22,8 @@ class LambdaService {
22
22
 
23
23
  // Cria o simulador
24
24
  this.simulator = new LambdaSimulator(this.config);
25
-
25
+ await this.simulator.initialize();
26
+
26
27
  // Cria o servidor HTTP
27
28
  this.server = new LambdaServer(this.port, this.config);
28
29
  this.server.simulator = this.simulator;
@@ -44,53 +44,48 @@ class LambdaServer {
44
44
  setupRoutes() {
45
45
  // Health check
46
46
  this.app.get('/health', (req, res) => {
47
- res.json({
48
- status: 'healthy',
49
- version: require('../../../package.json').version,
50
- lambdas: this.simulator.getLambdasCount(),
51
- routes: this.simulator.listRoutes()
52
- });
47
+ res.json({ status: 'healthy', lambdas: this.simulator.getLambdasCount() });
53
48
  });
54
-
55
- // Rota catch-all para todas as Lambdas
56
- this.app.all('*', async (req, res) => {
57
- const result = await this.simulator.handleRequest(req, res);
58
-
59
- if (result && result.error) {
60
- res.status(result.status).json(result.error);
49
+
50
+ // AWS Lambda Invoke API: POST /2015-03-31/functions/{functionName}/invocations
51
+ this.app.post('/2015-03-31/functions/:functionName/invocations', async (req, res) => {
52
+ const { functionName } = req.params;
53
+ const invocationType = req.headers['x-amz-invocation-type'] || 'RequestResponse';
54
+ const event = req.body || {};
55
+
56
+ logger.debug(`Lambda invoke: ${functionName} (${invocationType})`);
57
+
58
+ try {
59
+ const result = await this.simulator.invoke(functionName, event, invocationType);
60
+
61
+ if (invocationType === 'Event') {
62
+ return res.status(202).send();
63
+ }
64
+
65
+ res.status(result.StatusCode || 200).json(result.Payload);
66
+ } catch (err) {
67
+ if (err.message && err.message.includes('Function not found')) {
68
+ return res.status(404).json({ __type: 'ResourceNotFoundException', message: err.message });
69
+ }
70
+ logger.error('Lambda invoke error:', err);
71
+ res.status(500).json({ __type: 'ServiceException', message: err.message });
61
72
  }
62
73
  });
63
-
74
+
64
75
  // Admin endpoints
65
76
  this.setupAdminRoutes();
66
77
  }
67
78
 
68
79
  setupAdminRoutes() {
69
- // Listar todas as Lambdas
70
- this.app.get('/__admin/lambdas', (req, res) => {
80
+ this.app.get('/__admin/functions', (req, res) => {
71
81
  res.json(this.simulator.listLambdas());
72
82
  });
73
-
74
- // Detalhes de uma Lambda
75
- this.app.get('/__admin/lambdas/:path', (req, res) => {
76
- const lambda = this.simulator.getLambda(req.params.path);
77
- if (lambda) {
78
- res.json(lambda);
79
- } else {
80
- res.status(404).json({ error: 'Lambda not found' });
81
- }
82
- });
83
-
84
- // Recarregar Lambdas
83
+
85
84
  this.app.post('/__admin/reload', async (req, res) => {
86
85
  await this.simulator.reloadLambdas();
87
- res.json({
88
- message: 'Lambdas recarregadas',
89
- count: this.simulator.getLambdasCount()
90
- });
86
+ res.json({ message: 'Lambdas recarregadas', count: this.simulator.getLambdasCount() });
91
87
  });
92
-
93
- // Injetar variável de ambiente
88
+
94
89
  this.app.post('/__admin/env', (req, res) => {
95
90
  const { key, value } = req.body;
96
91
  if (key && value !== undefined) {
@@ -100,13 +95,11 @@ class LambdaServer {
100
95
  res.status(400).json({ error: 'Missing key or value' });
101
96
  }
102
97
  });
103
-
104
- // Listar variáveis de ambiente
98
+
105
99
  this.app.get('/__admin/env', (req, res) => {
106
100
  res.json(this.simulator.getEnvironmentVariables());
107
101
  });
108
-
109
- // Estatísticas
102
+
110
103
  this.app.get('/__admin/stats', (req, res) => {
111
104
  res.json(this.simulator.getStats());
112
105
  });
@@ -126,7 +119,7 @@ class LambdaServer {
126
119
  logger.info('\n📚 Lambdas registradas:');
127
120
  const lambdas = this.simulator.listLambdas();
128
121
  for (const lambda of lambdas) {
129
- logger.info(` ${lambda.path.padEnd(30)} -> ${lambda.handlerName || 'anonymous'}`);
122
+ logger.info(` ${lambda.name.padEnd(30)} -> ${lambda.handlerName || 'anonymous'}`);
130
123
  }
131
124
  }
132
125