@gugananuvem/aws-local-simulator 1.0.14 → 1.0.16
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 +310 -2
- package/package.json +11 -10
- package/src/server.js +2 -2
- package/src/services/apigateway/index.js +2 -0
- package/src/services/apigateway/server.js +67 -4
- package/src/services/cognito/index.js +9 -0
- package/src/services/cognito/server.js +28 -6
- package/src/services/cognito/simulator.js +768 -232
- package/src/services/dynamodb/server.js +2 -0
- package/src/services/ecs/server.js +2 -0
- package/src/services/eventbridge/server.js +2 -2
- package/src/services/lambda/simulator.js +18 -1
- package/src/services/s3/server.js +86 -2
- package/src/services/s3/simulator.js +77 -8
- package/src/services/sqs/index.js +10 -5
- package/src/services/sqs/server.js +2 -0
- package/src/services/sts/server.js +2 -0
package/README.md
CHANGED
|
@@ -81,7 +81,9 @@ npm install --save-dev aws-local-simulator
|
|
|
81
81
|
]
|
|
82
82
|
},
|
|
83
83
|
"s3": {
|
|
84
|
-
"buckets": [
|
|
84
|
+
"buckets": [
|
|
85
|
+
{ "name": "my-bucket", "region": "us-east-1" }
|
|
86
|
+
]
|
|
85
87
|
},
|
|
86
88
|
"sqs": {
|
|
87
89
|
"queues": ["my-queue", "dead-letter-queue"]
|
|
@@ -190,6 +192,7 @@ npx aws-local-simulator status
|
|
|
190
192
|
|---------|----------|-------|
|
|
191
193
|
| DynamoDB | http://localhost:8000 | http://localhost:8000/__admin/tables |
|
|
192
194
|
| S3 | http://localhost:4566 | http://localhost:4566/__admin/buckets |
|
|
195
|
+
| S3 Website | http://localhost:4566/website/{bucket}/ | — |
|
|
193
196
|
| SQS | http://localhost:9324 | http://localhost:9324/__admin/queues |
|
|
194
197
|
| Lambda | http://localhost:3001 | http://localhost:3001/__admin/functions |
|
|
195
198
|
| Cognito | http://localhost:9229 | http://localhost:9229/__admin/userpools |
|
|
@@ -230,6 +233,46 @@ aws lambda invoke \
|
|
|
230
233
|
# Cognito
|
|
231
234
|
aws cognito-idp list-user-pools --max-results 10 --endpoint-url http://localhost:9229
|
|
232
235
|
|
|
236
|
+
# Cognito cadastrar um usuario pelo adminstrador
|
|
237
|
+
aws cognito-idp admin-create-user \
|
|
238
|
+
--user-pool-id us-east-xxxx\
|
|
239
|
+
--username usuario@email.com \
|
|
240
|
+
--user-attributes \
|
|
241
|
+
Name=email,Value=usuario@email.com \
|
|
242
|
+
Name=email_verified,Value=false \
|
|
243
|
+
Name=name,Value="nome usuario" \
|
|
244
|
+
Name=custom:role,Value="user" \
|
|
245
|
+
temporary-password "Teste@123456" \
|
|
246
|
+
--message-action SUPPRESS \
|
|
247
|
+
--endpoint-url http://localhost:9229
|
|
248
|
+
|
|
249
|
+
# Cognito excluir um usuario
|
|
250
|
+
aws cognito-idp admin-delete-user \
|
|
251
|
+
--user-pool-id us-east-xxxx \
|
|
252
|
+
--username usuario@email.com
|
|
253
|
+
--endpoint-url http://localhost:9229
|
|
254
|
+
|
|
255
|
+
# Cognito Registrar usuário
|
|
256
|
+
aws cognito-idp sign-up \
|
|
257
|
+
--client-id $CLIENT_ID \
|
|
258
|
+
--username usuario@email.com \
|
|
259
|
+
--password "Teste@123456" \
|
|
260
|
+
--user-attributes Name=email,Value=usuario@email.com Name=name,Value="nome usuario"
|
|
261
|
+
--endpoint-url http://localhost:9229
|
|
262
|
+
|
|
263
|
+
# 2. Confirmar usuário administrativamente
|
|
264
|
+
aws cognito-idp admin-confirm-sign-up \
|
|
265
|
+
--user-pool-id us-east-xxxx \
|
|
266
|
+
--username usuario@email.com
|
|
267
|
+
--endpoint-url http://localhost:9229
|
|
268
|
+
|
|
269
|
+
# O código chega no e-mail do usuário. Algo como: "Your confirmation code is 123456"
|
|
270
|
+
aws cognito-idp confirm-sign-up \
|
|
271
|
+
--client-id 3n4b5urk1ft4fl3mg5e62d9ado \
|
|
272
|
+
--username usuario@email.com \
|
|
273
|
+
--confirmation-code 123456
|
|
274
|
+
--endpoint-url http://localhost:9229
|
|
275
|
+
|
|
233
276
|
# STS
|
|
234
277
|
aws sts get-caller-identity --endpoint-url http://localhost:9326
|
|
235
278
|
aws sts assume-role \
|
|
@@ -359,6 +402,76 @@ aws athena create-named-query \
|
|
|
359
402
|
aws athena list-named-queries --endpoint-url http://localhost:4599
|
|
360
403
|
```
|
|
361
404
|
|
|
405
|
+
## ⚙️ Configuração S3
|
|
406
|
+
|
|
407
|
+
### Buckets simples
|
|
408
|
+
|
|
409
|
+
```json
|
|
410
|
+
{
|
|
411
|
+
"s3": {
|
|
412
|
+
"buckets": [
|
|
413
|
+
"my-bucket"
|
|
414
|
+
]
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Buckets com região e website estático
|
|
420
|
+
|
|
421
|
+
```json
|
|
422
|
+
{
|
|
423
|
+
"s3": {
|
|
424
|
+
"buckets": [
|
|
425
|
+
{ "name": "my-bucket", "region": "us-east-1" },
|
|
426
|
+
{
|
|
427
|
+
"name": "my-site-bucket",
|
|
428
|
+
"region": "us-east-1",
|
|
429
|
+
"websiteConfiguration": {
|
|
430
|
+
"IndexDocument": { "Suffix": "index.html" },
|
|
431
|
+
"ErrorDocument": { "Key": "error.html" }
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
]
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Quando `websiteConfiguration` está presente, o bucket serve arquivos estáticos.
|
|
440
|
+
|
|
441
|
+
### URL do website estático
|
|
442
|
+
|
|
443
|
+
```
|
|
444
|
+
http://localhost:4566/website/{bucket-name}/
|
|
445
|
+
http://localhost:4566/website/{bucket-name}/caminho/pagina.html
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Exemplos:
|
|
449
|
+
```
|
|
450
|
+
http://localhost:4566/website/my-site-bucket/ → serve index.html
|
|
451
|
+
http://localhost:4566/website/my-site-bucket/about.html → serve about.html
|
|
452
|
+
http://localhost:4566/website/my-site-bucket/app/ → serve app/index.html
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Gerenciar website config via AWS CLI
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
# Habilitar website em um bucket existente
|
|
459
|
+
aws s3api put-bucket-website \
|
|
460
|
+
--bucket my-site-bucket \
|
|
461
|
+
--website-configuration '{"IndexDocument":{"Suffix":"index.html"},"ErrorDocument":{"Key":"error.html"}}' \
|
|
462
|
+
--endpoint-url http://localhost:4566
|
|
463
|
+
|
|
464
|
+
# Ver configuração de website
|
|
465
|
+
aws s3api get-bucket-website \
|
|
466
|
+
--bucket my-site-bucket \
|
|
467
|
+
--endpoint-url http://localhost:4566
|
|
468
|
+
|
|
469
|
+
# Remover website
|
|
470
|
+
aws s3api delete-bucket-website \
|
|
471
|
+
--bucket my-site-bucket \
|
|
472
|
+
--endpoint-url http://localhost:4566
|
|
473
|
+
```
|
|
474
|
+
|
|
362
475
|
## ⚙️ Configuração de Lambdas
|
|
363
476
|
|
|
364
477
|
Lambdas são registradas por **nome** e invocadas via API de invocação (igual à AWS real). O roteamento HTTP é feito pelo API Gateway.
|
|
@@ -378,6 +491,50 @@ Lambdas são registradas por **nome** e invocadas via API de invocação (igual
|
|
|
378
491
|
}
|
|
379
492
|
```
|
|
380
493
|
|
|
494
|
+
## ⚙️ Configuração SQS com Lambda Trigger
|
|
495
|
+
|
|
496
|
+
Para disparar uma Lambda automaticamente quando uma mensagem chega na fila, use o formato de objeto na lista de filas com `lambdaName`:
|
|
497
|
+
|
|
498
|
+
```json
|
|
499
|
+
{
|
|
500
|
+
"lambdas": [
|
|
501
|
+
{
|
|
502
|
+
"name": "process-orders",
|
|
503
|
+
"handler": "./src/handlers/process-orders.js"
|
|
504
|
+
}
|
|
505
|
+
],
|
|
506
|
+
"sqs": {
|
|
507
|
+
"queues": [
|
|
508
|
+
"simple-queue",
|
|
509
|
+
{
|
|
510
|
+
"name": "orders-queue",
|
|
511
|
+
"lambdaName": "process-orders",
|
|
512
|
+
"batchSize": 5
|
|
513
|
+
}
|
|
514
|
+
]
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
- Filas simples (string) são criadas sem trigger
|
|
520
|
+
- Filas com objeto aceitam `lambdaName` (nome da Lambda registrada) e `batchSize` (padrão: 10)
|
|
521
|
+
- Quando uma mensagem é enviada para `orders-queue`, a Lambda `process-orders` é invocada automaticamente com o evento no formato SQS padrão da AWS
|
|
522
|
+
|
|
523
|
+
O handler recebe o evento no formato padrão AWS SQS:
|
|
524
|
+
|
|
525
|
+
```javascript
|
|
526
|
+
exports.handler = async (event) => {
|
|
527
|
+
for (const record of event.Records) {
|
|
528
|
+
const body = JSON.parse(record.body);
|
|
529
|
+
console.log('Mensagem recebida:', body);
|
|
530
|
+
// processar...
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Retornar batchItemFailures para reprocessar mensagens específicas
|
|
534
|
+
return { batchItemFailures: [] };
|
|
535
|
+
};
|
|
536
|
+
```
|
|
537
|
+
|
|
381
538
|
## ⚙️ Configuração API GATEWAY
|
|
382
539
|
|
|
383
540
|
O valor do **lambdaName** deve igual ao nome Lambda que está registrada com o valor **name**. Ex: "my-user-function".
|
|
@@ -413,15 +570,166 @@ O valor do **lambdaName** deve igual ao nome Lambda que está registrada com o v
|
|
|
413
570
|
"method": "DELETE",
|
|
414
571
|
"lambdaName": "my-user-function",
|
|
415
572
|
"integrationType": "lambda"
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
"path": "/user/{id}",
|
|
576
|
+
"method": "ANY",
|
|
577
|
+
"lambdaName": "my-user-function",
|
|
578
|
+
"integrationType": "lambda"
|
|
416
579
|
}
|
|
417
580
|
]
|
|
418
581
|
}
|
|
419
582
|
]
|
|
420
|
-
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
```
|
|
421
586
|
|
|
587
|
+
### Método ANY
|
|
588
|
+
|
|
589
|
+
Use `"method": "ANY"` para registrar um endpoint que aceita todos os verbos HTTP (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) — equivalente ao `ANY` do AWS API Gateway real.
|
|
590
|
+
|
|
591
|
+
```json
|
|
592
|
+
{
|
|
593
|
+
"path": "/webhook",
|
|
594
|
+
"method": "ANY",
|
|
595
|
+
"lambdaName": "my-webhook-handler",
|
|
596
|
+
"integrationType": "lambda"
|
|
422
597
|
}
|
|
423
598
|
```
|
|
424
599
|
|
|
600
|
+
### API Key
|
|
601
|
+
|
|
602
|
+
O API Gateway suporta validação de API Key via header `x-api-key`. Para proteger um endpoint, declare as `apiKeys` na configuração e use `"apiKeyRequired": true` no endpoint:
|
|
603
|
+
|
|
604
|
+
```json
|
|
605
|
+
{
|
|
606
|
+
"apigateway": {
|
|
607
|
+
"apiKeys": [
|
|
608
|
+
{
|
|
609
|
+
"name": "my-app-key",
|
|
610
|
+
"value": "minha-chave-secreta-123"
|
|
611
|
+
}
|
|
612
|
+
],
|
|
613
|
+
"apis": [
|
|
614
|
+
{
|
|
615
|
+
"name": "Protected API",
|
|
616
|
+
"endpoints": [
|
|
617
|
+
{
|
|
618
|
+
"path": "/protected",
|
|
619
|
+
"method": "GET",
|
|
620
|
+
"lambdaName": "my-user-function",
|
|
621
|
+
"integrationType": "lambda",
|
|
622
|
+
"apiKeyRequired": true
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
"path": "/public",
|
|
626
|
+
"method": "GET",
|
|
627
|
+
"lambdaName": "my-user-function",
|
|
628
|
+
"integrationType": "lambda"
|
|
629
|
+
}
|
|
630
|
+
]
|
|
631
|
+
}
|
|
632
|
+
]
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
Ao chamar um endpoint protegido, envie o header:
|
|
638
|
+
|
|
639
|
+
```bash
|
|
640
|
+
curl http://localhost:4567/protected \
|
|
641
|
+
-H "x-api-key: minha-chave-secreta-123"
|
|
642
|
+
|
|
643
|
+
# Sem a key → 403 Forbidden
|
|
644
|
+
curl http://localhost:4567/protected
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
> **Nota:** A validação de API Key só está ativa no fluxo de proxy (`/:apiId/:stageName/*`). Endpoints declarados diretamente no `aws-local-simulator.json` via `setupConfigRoutes` não aplicam a validação de API Key no momento.
|
|
648
|
+
|
|
649
|
+
### Cognito Authorizer
|
|
650
|
+
|
|
651
|
+
O API Gateway suporta autenticação via Cognito User Pools. Configure um `authorizer` na API e marque os endpoints protegidos com `"authorizerRequired": true`:
|
|
652
|
+
|
|
653
|
+
```json
|
|
654
|
+
{
|
|
655
|
+
"cognito": {
|
|
656
|
+
"userPools": [
|
|
657
|
+
{
|
|
658
|
+
"PoolName": "my-user-pool",
|
|
659
|
+
"UserPoolId": "us-east-XXXXX",
|
|
660
|
+
"ClientId": "XXXXXX",
|
|
661
|
+
"AutoVerifiedAttributes": ["email"]
|
|
662
|
+
}
|
|
663
|
+
]
|
|
664
|
+
},
|
|
665
|
+
"apigateway": {
|
|
666
|
+
"apis": [
|
|
667
|
+
{
|
|
668
|
+
"name": "My API",
|
|
669
|
+
"authorizer": {
|
|
670
|
+
"type": "COGNITO_USER_POOLS",
|
|
671
|
+
"userPoolId": "us-east-XXXXX"
|
|
672
|
+
},
|
|
673
|
+
"endpoints": [
|
|
674
|
+
{
|
|
675
|
+
"path": "/profile",
|
|
676
|
+
"method": "GET",
|
|
677
|
+
"lambdaName": "my-user-function",
|
|
678
|
+
"integrationType": "lambda",
|
|
679
|
+
"authorizerRequired": true
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
"path": "/public",
|
|
683
|
+
"method": "GET",
|
|
684
|
+
"lambdaName": "my-user-function",
|
|
685
|
+
"integrationType": "lambda"
|
|
686
|
+
}
|
|
687
|
+
]
|
|
688
|
+
}
|
|
689
|
+
]
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
O simulador valida o JWT do Cognito local no header `Authorization: Bearer <token>`. Se o token for inválido ou ausente, retorna `401 Unauthorized`.
|
|
695
|
+
|
|
696
|
+
```bash
|
|
697
|
+
# 1. Autenticar e obter o token
|
|
698
|
+
TOKEN=$(curl -s -X POST http://localhost:9229/ \
|
|
699
|
+
-H "Content-Type: application/x-amz-json-1.1" \
|
|
700
|
+
-H "X-Amz-Target: AWSCognitoIdentityProviderService.InitiateAuth" \
|
|
701
|
+
-d '{
|
|
702
|
+
"AuthFlow": "USER_PASSWORD_AUTH",
|
|
703
|
+
"ClientId": "XXXXXX",
|
|
704
|
+
"AuthParameters": {
|
|
705
|
+
"USERNAME": "usuario@email.com",
|
|
706
|
+
"PASSWORD": "Senha@123"
|
|
707
|
+
}
|
|
708
|
+
}' | jq -r '.AuthenticationResult.IdToken')
|
|
709
|
+
|
|
710
|
+
# 2. Chamar endpoint protegido com o token
|
|
711
|
+
curl http://localhost:4567/profile \
|
|
712
|
+
-H "Authorization: Bearer $TOKEN"
|
|
713
|
+
|
|
714
|
+
# Sem token → 401 Unauthorized
|
|
715
|
+
curl http://localhost:4567/profile
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
O token decodificado fica disponível no Lambda em `event.requestContext.authorizer.claims`:
|
|
719
|
+
|
|
720
|
+
```javascript
|
|
721
|
+
exports.handler = async (event) => {
|
|
722
|
+
const claims = event.requestContext.authorizer?.claims;
|
|
723
|
+
const userId = claims?.sub;
|
|
724
|
+
const email = claims?.email;
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
statusCode: 200,
|
|
728
|
+
body: JSON.stringify({ userId, email })
|
|
729
|
+
};
|
|
730
|
+
};
|
|
731
|
+
```
|
|
732
|
+
|
|
425
733
|
O handler deve exportar uma função padrão:
|
|
426
734
|
|
|
427
735
|
```javascript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gugananuvem/aws-local-simulator",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "Simulador local completo para serviços AWS",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -81,22 +81,23 @@
|
|
|
81
81
|
"directory": "dist"
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
|
-
"express": "^4.18.2",
|
|
85
84
|
"cors": "^2.8.5",
|
|
86
|
-
"uuid": "^9.0.0",
|
|
87
|
-
"mkdirp": "^3.0.1",
|
|
88
|
-
"glob": "^10.3.0",
|
|
89
85
|
"dotenv": "^16.3.1",
|
|
86
|
+
"express": "^4.18.2",
|
|
87
|
+
"glob": "^10.3.0",
|
|
90
88
|
"jsonwebtoken": "^9.0.2",
|
|
91
|
-
"
|
|
89
|
+
"mkdirp": "^3.0.1",
|
|
90
|
+
"urlpattern-polyfill": "^10.0.0",
|
|
91
|
+
"uuid": "^9.0.0"
|
|
92
92
|
},
|
|
93
93
|
"peerDependencies": {
|
|
94
|
+
"@aws-sdk/client-api-gateway": "^3.0.0",
|
|
95
|
+
"@aws-sdk/client-cognito-identity-provider": "^3.0.0",
|
|
94
96
|
"@aws-sdk/client-dynamodb": "^3.0.0",
|
|
97
|
+
"@aws-sdk/client-ecs": "^3.0.0",
|
|
98
|
+
"@aws-sdk/client-lambda": "^3.1032.0",
|
|
95
99
|
"@aws-sdk/client-s3": "^3.0.0",
|
|
96
100
|
"@aws-sdk/client-sqs": "^3.0.0",
|
|
97
|
-
"@aws-sdk/client-cognito-identity-provider": "^3.0.0",
|
|
98
|
-
"@aws-sdk/client-api-gateway": "^3.0.0",
|
|
99
|
-
"@aws-sdk/client-ecs": "^3.0.0",
|
|
100
101
|
"@aws-sdk/lib-dynamodb": "^3.0.0"
|
|
101
102
|
},
|
|
102
103
|
"peerDependenciesMeta": {
|
|
@@ -119,6 +120,6 @@
|
|
|
119
120
|
"optional": true
|
|
120
121
|
}
|
|
121
122
|
},
|
|
122
|
-
"buildDate": "2026-04-
|
|
123
|
+
"buildDate": "2026-04-20T18:31:32.841Z",
|
|
123
124
|
"published": true
|
|
124
125
|
}
|
package/src/server.js
CHANGED
|
@@ -81,9 +81,9 @@ class Server {
|
|
|
81
81
|
{ name: "sqs", class: SQSService, depends: ["lambda"] },
|
|
82
82
|
{ name: "sns", class: SNSService, depends: [] },
|
|
83
83
|
{ name: "eventbridge", class: EventBridgeService, depends: [] },
|
|
84
|
-
{ name: "cognito", class: CognitoService, depends: [] },
|
|
84
|
+
{ name: "cognito", class: CognitoService, depends: ["lambda"] },
|
|
85
85
|
{ name: "ecs", class: ECSService, depends: [] },
|
|
86
|
-
{ name: "apigateway", class: APIGatewayService, depends: ["lambda"] },
|
|
86
|
+
{ name: "apigateway", class: APIGatewayService, depends: ["lambda", "cognito"] },
|
|
87
87
|
{ name: "kms", class: KMSService, depends: [] },
|
|
88
88
|
{ name: "cloudwatch", class: CloudWatchService, depends: [] },
|
|
89
89
|
{ name: "cloudtrail", class: CloudTrailService, depends: [] },
|
|
@@ -15,6 +15,7 @@ class APIGatewayService {
|
|
|
15
15
|
this.simulator = null;
|
|
16
16
|
this.isRunning = false;
|
|
17
17
|
this.lambdaService = dependencies.lambda || null;
|
|
18
|
+
this.cognitoService = dependencies.cognito || null;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
async initialize() {
|
|
@@ -27,6 +28,7 @@ class APIGatewayService {
|
|
|
27
28
|
this.server = new APIGatewayServer(this.port, this.config);
|
|
28
29
|
this.server.simulator = this.simulator;
|
|
29
30
|
this.server.lambdaService = this.lambdaService;
|
|
31
|
+
this.server.cognitoService = this.cognitoService;
|
|
30
32
|
|
|
31
33
|
await this.server.initialize();
|
|
32
34
|
|
|
@@ -64,8 +64,11 @@ class APIGatewayServer {
|
|
|
64
64
|
// Register routes from aws-local-simulator.json config directly
|
|
65
65
|
const apis = this.config.apigateway?.apis || [];
|
|
66
66
|
for (const api of apis) {
|
|
67
|
+
// Build Cognito authorizer middleware for this API if configured
|
|
68
|
+
const cognitoAuthorizer = this._buildCognitoAuthorizer(api.authorizer);
|
|
69
|
+
|
|
67
70
|
for (const endpoint of (api.endpoints || [])) {
|
|
68
|
-
const { path, method, lambdaName, integrationType } = endpoint;
|
|
71
|
+
const { path, method, lambdaName, integrationType, authorizerRequired } = endpoint;
|
|
69
72
|
if (!path || !method) continue;
|
|
70
73
|
|
|
71
74
|
const expressPath = path.replace(/\{([^}]+)\}/g, ':$1');
|
|
@@ -73,7 +76,7 @@ class APIGatewayServer {
|
|
|
73
76
|
|
|
74
77
|
logger.debug(`📡 Registrando rota: ${method} ${path} -> ${lambdaName}`);
|
|
75
78
|
|
|
76
|
-
|
|
79
|
+
const handler = async (req, res) => {
|
|
77
80
|
try {
|
|
78
81
|
const lambdaService = this.lambdaService;
|
|
79
82
|
if (!lambdaService) {
|
|
@@ -92,7 +95,8 @@ class APIGatewayServer {
|
|
|
92
95
|
path: req.path,
|
|
93
96
|
stage: 'local',
|
|
94
97
|
requestId: Math.random().toString(36).substring(7),
|
|
95
|
-
identity: { sourceIp: req.ip }
|
|
98
|
+
identity: { sourceIp: req.ip },
|
|
99
|
+
authorizer: req.cognitoAuthorizer || null
|
|
96
100
|
}
|
|
97
101
|
};
|
|
98
102
|
|
|
@@ -107,11 +111,70 @@ class APIGatewayServer {
|
|
|
107
111
|
logger.error(`Lambda invoke error (${lambdaName}):`, err);
|
|
108
112
|
res.status(500).json({ error: err.message });
|
|
109
113
|
}
|
|
110
|
-
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const middlewares = [];
|
|
117
|
+
if (authorizerRequired && cognitoAuthorizer) {
|
|
118
|
+
middlewares.push(cognitoAuthorizer);
|
|
119
|
+
}
|
|
120
|
+
middlewares.push(handler);
|
|
121
|
+
|
|
122
|
+
const ANY_METHODS = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'];
|
|
123
|
+
if (httpMethod === 'any') {
|
|
124
|
+
for (const m of ANY_METHODS) {
|
|
125
|
+
this.app[m](expressPath, ...middlewares);
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
this.app[httpMethod](expressPath, ...middlewares);
|
|
129
|
+
}
|
|
111
130
|
}
|
|
112
131
|
}
|
|
113
132
|
}
|
|
114
133
|
|
|
134
|
+
_buildCognitoAuthorizer(authorizerConfig) {
|
|
135
|
+
if (!authorizerConfig) return null;
|
|
136
|
+
if (authorizerConfig.type !== 'COGNITO_USER_POOLS') return null;
|
|
137
|
+
|
|
138
|
+
const { userPoolId } = authorizerConfig;
|
|
139
|
+
|
|
140
|
+
return (req, res, next) => {
|
|
141
|
+
const authHeader = req.headers['authorization'] || req.headers['Authorization'];
|
|
142
|
+
if (!authHeader) {
|
|
143
|
+
return res.status(401).json({ message: 'Unauthorized' });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : authHeader;
|
|
147
|
+
|
|
148
|
+
const cognitoSimulator = this.cognitoService?.simulator;
|
|
149
|
+
if (!cognitoSimulator) {
|
|
150
|
+
logger.warn('⚠️ Cognito authorizer configured but Cognito service is not available');
|
|
151
|
+
return res.status(500).json({ message: 'Authorizer unavailable' });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Validate the token belongs to the configured user pool
|
|
155
|
+
const decoded = cognitoSimulator.verifyAccessToken(token);
|
|
156
|
+
if (!decoded) {
|
|
157
|
+
return res.status(401).json({ message: 'Unauthorized' });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// If a specific userPoolId is required, verify it matches
|
|
161
|
+
if (userPoolId && decoded['cognito:username'] !== undefined) {
|
|
162
|
+
const session = cognitoSimulator.accessTokens.get(token);
|
|
163
|
+
if (session && userPoolId && session.UserPoolId !== userPoolId) {
|
|
164
|
+
return res.status(401).json({ message: 'Unauthorized' });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Attach claims to request so Lambda receives them in requestContext.authorizer
|
|
169
|
+
req.cognitoAuthorizer = {
|
|
170
|
+
claims: decoded,
|
|
171
|
+
principalId: decoded.sub
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
next();
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
115
178
|
setupRoutes() {
|
|
116
179
|
// Health check
|
|
117
180
|
this.app.get('/health', (req, res) => {
|
|
@@ -34,6 +34,15 @@ class CognitoService {
|
|
|
34
34
|
injectDependencies(server) {
|
|
35
35
|
const ct = server.getService('cloudtrail');
|
|
36
36
|
if (ct?.simulator) this.simulator.audit.setTrail(ct.simulator);
|
|
37
|
+
|
|
38
|
+
const lambda = server.getService('lambda');
|
|
39
|
+
if (lambda?.simulator) {
|
|
40
|
+
this.simulator.setLambdaSimulator(lambda.simulator);
|
|
41
|
+
// Warn about unregistered triggers now that Lambda is fully initialized
|
|
42
|
+
for (const pool of this.simulator.userPools.values()) {
|
|
43
|
+
this.simulator._warnUnregisteredTriggers(pool);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
37
46
|
}
|
|
38
47
|
|
|
39
48
|
async start() {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const express = require('express');
|
|
6
|
+
const cors = require('cors');
|
|
6
7
|
const logger = require('../../utils/logger');
|
|
7
8
|
|
|
8
9
|
class CognitoServer {
|
|
@@ -16,6 +17,7 @@ class CognitoServer {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
setupMiddlewares() {
|
|
20
|
+
this.app.use(cors());
|
|
19
21
|
this.app.use(express.raw({ type: '*/*', limit: '10mb' }));
|
|
20
22
|
this.app.use((req, res, next) => {
|
|
21
23
|
if (req.body && Buffer.isBuffer(req.body)) {
|
|
@@ -57,25 +59,35 @@ class CognitoServer {
|
|
|
57
59
|
});
|
|
58
60
|
});
|
|
59
61
|
|
|
60
|
-
// User Pool operations
|
|
61
|
-
|
|
62
|
+
// User Pool operations — aceita POST / e POST /:userPoolId (compatibilidade com SDK)
|
|
63
|
+
const cognitoHandler = async (req, res) => {
|
|
62
64
|
const target = req.headers['x-amz-target'];
|
|
63
|
-
logger.info(`Cognito incoming: target=${target} body=${JSON.stringify(req.body)}`);
|
|
65
|
+
logger.info(`Cognito incoming: method=${req.method} path=${req.path} target=${target} body=${JSON.stringify(req.body)}`);
|
|
66
|
+
|
|
64
67
|
if (!target) {
|
|
65
|
-
return res.status(400).json({
|
|
68
|
+
return res.status(400).json({
|
|
69
|
+
__type: 'InvalidParameterException',
|
|
70
|
+
message: 'Missing X-Amz-Target header'
|
|
71
|
+
});
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
try {
|
|
69
75
|
const result = await this.handleRequest(target, req.body || {});
|
|
70
76
|
res.json(result);
|
|
71
77
|
} catch (error) {
|
|
72
|
-
logger.error('Cognito Error:', error);
|
|
78
|
+
logger.error('Cognito Error:', error.message);
|
|
73
79
|
res.status(400).json({
|
|
74
80
|
__type: error.code || 'InternalServerError',
|
|
75
81
|
message: error.message
|
|
76
82
|
});
|
|
77
83
|
}
|
|
78
|
-
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// OPTIONS preflight para CORS
|
|
87
|
+
this.app.options('*', (req, res) => res.sendStatus(204));
|
|
88
|
+
|
|
89
|
+
this.app.post('/', cognitoHandler);
|
|
90
|
+
this.app.post('/:userPoolId', cognitoHandler);
|
|
79
91
|
|
|
80
92
|
// Admin endpoints
|
|
81
93
|
this.setupAdminRoutes();
|
|
@@ -182,6 +194,16 @@ class CognitoServer {
|
|
|
182
194
|
});
|
|
183
195
|
});
|
|
184
196
|
|
|
197
|
+
// Confirma um usuário (útil para dev local)
|
|
198
|
+
this.app.post('/__admin/userpools/:poolId/users/:username/confirm', (req, res) => {
|
|
199
|
+
const user = this.simulator.findUserByUsername(req.params.username, null, req.params.poolId);
|
|
200
|
+
if (!user) return res.status(404).json({ error: 'User not found' });
|
|
201
|
+
user.UserStatus = 'CONFIRMED';
|
|
202
|
+
user.LastModifiedDate = new Date().toISOString();
|
|
203
|
+
this.simulator.persistUsers();
|
|
204
|
+
res.json({ message: `User ${req.params.username} confirmed` });
|
|
205
|
+
});
|
|
206
|
+
|
|
185
207
|
this.app.get('/__admin/userpools/:poolId/users', (req, res) => {
|
|
186
208
|
const pool = this.simulator.userPools.get(req.params.poolId);
|
|
187
209
|
if (!pool) {
|