@gugananuvem/aws-local-simulator 1.0.33 → 1.0.34

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 (79) hide show
  1. package/README.md +834 -834
  2. package/aws-config +153 -153
  3. package/bin/aws-local-simulator.js +63 -63
  4. package/package.json +3 -2
  5. package/src/config/config-loader.js +114 -114
  6. package/src/config/default-config.js +79 -79
  7. package/src/config/env-loader.js +68 -68
  8. package/src/index.js +146 -146
  9. package/src/index.mjs +123 -123
  10. package/src/server.js +463 -463
  11. package/src/services/apigateway/index.js +75 -75
  12. package/src/services/apigateway/server.js +607 -607
  13. package/src/services/apigateway/simulator.js +1405 -1405
  14. package/src/services/athena/index.js +75 -75
  15. package/src/services/athena/server.js +101 -101
  16. package/src/services/athena/simulador.js +998 -998
  17. package/src/services/athena/simulator.js +346 -346
  18. package/src/services/cloudformation/index.js +106 -106
  19. package/src/services/cloudformation/server.js +417 -417
  20. package/src/services/cloudformation/simulador.js +1020 -1020
  21. package/src/services/cloudtrail/index.js +84 -84
  22. package/src/services/cloudtrail/server.js +235 -235
  23. package/src/services/cloudtrail/simulador.js +719 -719
  24. package/src/services/cloudwatch/index.js +84 -84
  25. package/src/services/cloudwatch/server.js +366 -366
  26. package/src/services/cloudwatch/simulador.js +1173 -1173
  27. package/src/services/cognito/index.js +79 -79
  28. package/src/services/cognito/server.js +297 -297
  29. package/src/services/cognito/simulator.js +1992 -1761
  30. package/src/services/config/index.js +96 -96
  31. package/src/services/config/server.js +215 -215
  32. package/src/services/config/simulador.js +1260 -1260
  33. package/src/services/dynamodb/index.js +74 -74
  34. package/src/services/dynamodb/server.js +139 -139
  35. package/src/services/dynamodb/simulator.js +1005 -994
  36. package/src/services/dynamodb/sqlite-store.js +722 -0
  37. package/src/services/ecs/index.js +65 -65
  38. package/src/services/ecs/server.js +235 -235
  39. package/src/services/ecs/simulator.js +844 -844
  40. package/src/services/eventbridge/index.js +89 -89
  41. package/src/services/eventbridge/server.js +209 -209
  42. package/src/services/eventbridge/simulator.js +684 -684
  43. package/src/services/index.js +45 -45
  44. package/src/services/kms/index.js +75 -75
  45. package/src/services/kms/server.js +81 -81
  46. package/src/services/kms/simulator.js +344 -344
  47. package/src/services/lambda/handler-loader.js +183 -183
  48. package/src/services/lambda/index.js +81 -81
  49. package/src/services/lambda/route-registry.js +274 -274
  50. package/src/services/lambda/server.js +191 -191
  51. package/src/services/lambda/simulator.js +364 -364
  52. package/src/services/parameter-store/index.js +80 -80
  53. package/src/services/parameter-store/server.js +50 -50
  54. package/src/services/parameter-store/simulator.js +201 -201
  55. package/src/services/s3/index.js +73 -73
  56. package/src/services/s3/server.js +350 -350
  57. package/src/services/s3/simulator.js +568 -568
  58. package/src/services/secret-manager/index.js +80 -80
  59. package/src/services/secret-manager/server.js +51 -51
  60. package/src/services/secret-manager/simulator.js +182 -182
  61. package/src/services/sns/index.js +89 -89
  62. package/src/services/sns/server.js +607 -607
  63. package/src/services/sns/simulator.js +1482 -1482
  64. package/src/services/sqs/index.js +98 -98
  65. package/src/services/sqs/server.js +360 -360
  66. package/src/services/sqs/simulator.js +509 -509
  67. package/src/services/sts/index.js +37 -37
  68. package/src/services/sts/server.js +144 -144
  69. package/src/services/sts/simulator.js +69 -69
  70. package/src/services/xray/index.js +83 -83
  71. package/src/services/xray/server.js +308 -308
  72. package/src/services/xray/simulador.js +994 -994
  73. package/src/template/aws-config-template.js +87 -87
  74. package/src/template/aws-config-template.mjs +90 -90
  75. package/src/template/config-template.json +203 -203
  76. package/src/utils/aws-config.js +91 -91
  77. package/src/utils/cloudtrail-audit.js +129 -129
  78. package/src/utils/local-store.js +83 -83
  79. package/src/utils/logger.js +59 -59
@@ -1,1406 +1,1406 @@
1
- /**
2
- * API Gateway Simulator Core
3
- * Simula REST APIs, HTTP APIs, WebSocket APIs, Routes, Integrations
4
- */
5
-
6
- const crypto = require('crypto');
7
- const { v4: uuidv4 } = require('uuid');
8
- const logger = require('../../utils/logger');
9
- const LocalStore = require('../../utils/local-store');
10
- const path = require('path');
11
- const { URLPattern } = require('urlpattern-polyfill');
12
- const { CloudTrailAudit } = require('../../utils/cloudtrail-audit');
13
-
14
- class APIGatewaySimulator {
15
- constructor(config) {
16
- this.config = config;
17
- this.dataDir = path.join(process.env.AWS_LOCAL_SIMULATOR_DATA_DIR, 'apigateway');
18
- this.store = new LocalStore(this.dataDir);
19
- this.apis = new Map();
20
- this.websocketApis = new Map();
21
- this.deployments = new Map();
22
- this.stages = new Map();
23
- this.resources = new Map();
24
- this.methods = new Map();
25
- this.integrations = new Map();
26
- this.models = new Map();
27
- this.usagePlans = new Map();
28
- this.apiKeys = new Map();
29
- this.domainNames = new Map();
30
- this.audit = new CloudTrailAudit('execute-api.amazonaws.com');
31
- }
32
-
33
- async initialize() {
34
- logger.debug('Inicializando API Gateway Simulator...');
35
- this.loadAPIs();
36
- this._loadStaticAPIs();
37
- this.loadWebSocketAPIs();
38
- this.loadDeployments();
39
- this.loadStages();
40
- this.loadResources();
41
- this.loadMethods();
42
- this.loadIntegrations();
43
- this.loadModels();
44
- this.loadUsagePlans();
45
- this.loadApiKeys();
46
- this.loadDomainNames();
47
-
48
- logger.debug(`✅ API Gateway Simulator inicializado com ${this.apis.size} APIs (${Array.from(this.apis.values()).filter(a => a.isStatic).length} estáticas)`);
49
- }
50
-
51
- _loadStaticAPIs() {
52
- const staticApis = this.config.apigateway?.apis || [];
53
- staticApis.forEach((apiConfig, index) => {
54
- const apiId = `static_${index}`;
55
-
56
- const api = {
57
- id: apiId,
58
- name: apiConfig.name,
59
- description: apiConfig.description || 'Configured in aws-local-simulator.json',
60
- version: 'config',
61
- createdDate: new Date().toISOString(),
62
- isStatic: true,
63
- apiKeySource: 'HEADER',
64
- endpointConfiguration: { types: ['REGIONAL'] },
65
- resources: new Map(),
66
- stages: new Map(),
67
- deployments: new Map(),
68
- models: new Map(),
69
- authorizers: new Map()
70
- };
71
-
72
- // Adiciona recursos a partir dos endpoints configurados
73
- (apiConfig.endpoints || []).forEach((ep, epIndex) => {
74
- const resId = `res_${apiId}_${epIndex}`;
75
- api.resources.set(resId, {
76
- id: resId,
77
- path: ep.path,
78
- pathPart: ep.path.split('/').pop() || '/',
79
- resourceMethods: new Map([[ep.method, {
80
- httpMethod: ep.method,
81
- authorizationType: ep.authorizerRequired ? 'COGNITO_USER_POOLS' : 'NONE',
82
- apiKeyRequired: false,
83
- integration: {
84
- type: ep.integrationType === 'lambda' ? 'AWS_PROXY' : 'HTTP',
85
- uri: ep.lambdaName,
86
- integrationHttpMethod: 'POST'
87
- }
88
- }]])
89
- });
90
- });
91
-
92
- // Adiciona um stage padrão
93
- api.stages.set('local', {
94
- stageName: 'local',
95
- createdDate: new Date().toISOString(),
96
- deploymentId: 'static-deploy'
97
- });
98
-
99
- this.apis.set(apiId, api);
100
- });
101
- }
102
-
103
-
104
- // ============ REST API Operations ============
105
-
106
- createRestApi(params) {
107
- const { name, description, version, apiKeySource, endpointConfiguration, tags } = params;
108
-
109
- const apiId = `api_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
110
- const api = {
111
- id: apiId,
112
- name: name,
113
- description: description || '',
114
- version: version || '1.0',
115
- createdDate: new Date().toISOString(),
116
- apiKeySource: apiKeySource || 'HEADER',
117
- endpointConfiguration: endpointConfiguration || {
118
- types: ['REGIONAL']
119
- },
120
- tags: tags || {},
121
- resources: new Map(),
122
- stages: new Map(),
123
- deployments: new Map(),
124
- models: new Map(),
125
- authorizers: new Map(),
126
- gatewayResponses: new Map(),
127
- documentationParts: new Map()
128
- };
129
-
130
- // Cria recurso raiz
131
- const rootResource = {
132
- id: uuidv4(),
133
- path: '/',
134
- pathPart: '',
135
- parentId: null,
136
- resourceMethods: new Map()
137
- };
138
-
139
- api.resources.set('/', rootResource);
140
-
141
- this.apis.set(apiId, api);
142
- this.persistAPIs();
143
-
144
- logger.debug(`✅ REST API criada: ${name} (${apiId})`);
145
-
146
- return {
147
- id: apiId,
148
- name: api.name,
149
- createdDate: api.createdDate
150
- };
151
- }
152
-
153
- getRestApis() {
154
- return {
155
- items: Array.from(this.apis.values()).map(api => ({
156
- id: api.id,
157
- name: api.name,
158
- description: api.description,
159
- version: api.version,
160
- createdDate: api.createdDate,
161
- apiKeySource: api.apiKeySource,
162
- isStatic: api.isStatic || false,
163
- resourceCount: api.resources.size,
164
- stageCount: api.stages.size
165
- }))
166
- };
167
- }
168
-
169
-
170
- getRestApi(params) {
171
- const { restApiId } = params;
172
- const api = this.apis.get(restApiId);
173
-
174
- if (!api) {
175
- throw new Error(`API ${restApiId} not found`);
176
- }
177
-
178
- return {
179
- id: api.id,
180
- name: api.name,
181
- description: api.description,
182
- version: api.version,
183
- createdDate: api.createdDate,
184
- apiKeySource: api.apiKeySource,
185
- endpointConfiguration: api.endpointConfiguration,
186
- tags: api.tags
187
- };
188
- }
189
-
190
- updateRestApi(params) {
191
- const { restApiId, name, description } = params;
192
- const api = this.apis.get(restApiId);
193
-
194
- if (!api) {
195
- throw new Error(`API ${restApiId} not found`);
196
- }
197
-
198
- if (name !== undefined) api.name = name;
199
- if (description !== undefined) api.description = description;
200
-
201
- if (api.isStatic) {
202
- // Once edited, the API is no longer static and will be persisted
203
- api.isStatic = false;
204
- }
205
-
206
- this.persistAPIs();
207
-
208
- return this.getRestApi({ restApiId });
209
- }
210
-
211
- deleteRestApi(params) {
212
- const { restApiId } = params;
213
-
214
- if (!this.apis.has(restApiId)) {
215
- throw new Error(`API ${restApiId} not found`);
216
- }
217
-
218
- this.apis.delete(restApiId);
219
- this.persistAPIs();
220
-
221
- return {};
222
- }
223
-
224
- // ============ Simplified Dashboard Endpoint Operations ============
225
-
226
- putEndpoint(params) {
227
- const { restApiId, path, method, integrationType, lambdaName, authorizerRequired } = params;
228
- const api = this.apis.get(restApiId);
229
- if (!api) throw new Error(`API ${restApiId} not found`);
230
-
231
- if (api.isStatic) api.isStatic = false;
232
-
233
- // Ensure resource exists
234
- let resource = Array.from(api.resources.values()).find(r => r.path === path);
235
- if (!resource) {
236
- const resourceId = `res_${Date.now()}`;
237
- resource = {
238
- id: resourceId,
239
- path: path,
240
- pathPart: path.split('/').pop() || '/',
241
- parentId: null, // Simplified
242
- resourceMethods: new Map()
243
- };
244
- api.resources.set(resourceId, resource);
245
- }
246
-
247
- // Put method
248
- resource.resourceMethods.set(method.toUpperCase(), {
249
- httpMethod: method.toUpperCase(),
250
- authorizationType: authorizerRequired ? 'COGNITO_USER_POOLS' : 'NONE',
251
- apiKeyRequired: false,
252
- integration: {
253
- type: integrationType === 'lambda' ? 'AWS_PROXY' : 'HTTP',
254
- uri: lambdaName,
255
- integrationHttpMethod: 'POST'
256
- }
257
- });
258
-
259
- this.persistAPIs();
260
- return { resourceId: resource.id, path, method };
261
- }
262
-
263
- deleteEndpoint(params) {
264
- const { restApiId, path, method } = params;
265
- const api = this.apis.get(restApiId);
266
- if (!api) throw new Error(`API ${restApiId} not found`);
267
-
268
- if (api.isStatic) api.isStatic = false;
269
-
270
- const resource = Array.from(api.resources.values()).find(r => r.path === path);
271
- if (resource) {
272
- resource.resourceMethods.delete(method.toUpperCase());
273
- // If no methods left and not root, we could delete the resource, but keeping it is fine.
274
- if (resource.resourceMethods.size === 0 && resource.path !== '/') {
275
- api.resources.delete(resource.id);
276
- }
277
- this.persistAPIs();
278
- }
279
- return {};
280
- }
281
-
282
- // ============ Resource Operations ============
283
-
284
-
285
- createResource(params) {
286
- const { restApiId, parentId, pathPart } = params;
287
- const api = this.apis.get(restApiId);
288
-
289
- if (!api) {
290
- throw new Error(`API ${restApiId} not found`);
291
- }
292
-
293
- const parentResource = api.resources.get(parentId);
294
- if (!parentResource) {
295
- throw new Error(`Parent resource ${parentId} not found`);
296
- }
297
-
298
- const resourceId = uuidv4();
299
- const fullPath = parentResource.path === '/'
300
- ? `/${pathPart}`
301
- : `${parentResource.path}/${pathPart}`;
302
-
303
- const resource = {
304
- id: resourceId,
305
- path: fullPath,
306
- pathPart: pathPart,
307
- parentId: parentId,
308
- resourceMethods: new Map()
309
- };
310
-
311
- api.resources.set(resourceId, resource);
312
- this.persistResources(restApiId);
313
-
314
- logger.debug(`📁 Recurso criado: ${fullPath} (${resourceId})`);
315
-
316
- return {
317
- id: resourceId,
318
- path: resource.path,
319
- parentId: resource.parentId
320
- };
321
- }
322
-
323
- getResources(params) {
324
- const { restApiId } = params;
325
- const api = this.apis.get(restApiId);
326
-
327
- if (!api) {
328
- throw new Error(`API ${restApiId} not found`);
329
- }
330
-
331
- const items = Array.from(api.resources.values()).map(resource => ({
332
- id: resource.id,
333
- path: resource.path,
334
- pathPart: resource.pathPart,
335
- parentId: resource.parentId,
336
- resourceMethods: Object.fromEntries(resource.resourceMethods)
337
- }));
338
-
339
- return { items };
340
- }
341
-
342
- deleteResource(params) {
343
- const { restApiId, resourceId } = params;
344
- const api = this.apis.get(restApiId);
345
-
346
- if (!api) {
347
- throw new Error(`API ${restApiId} not found`);
348
- }
349
-
350
- if (!api.resources.has(resourceId)) {
351
- throw new Error(`Resource ${resourceId} not found`);
352
- }
353
-
354
- // Verifica se tem filhos
355
- const hasChildren = Array.from(api.resources.values()).some(
356
- r => r.parentId === resourceId
357
- );
358
-
359
- if (hasChildren) {
360
- throw new Error('Resource has children');
361
- }
362
-
363
- api.resources.delete(resourceId);
364
- this.persistResources(restApiId);
365
-
366
- return {};
367
- }
368
-
369
- // ============ Method Operations ============
370
-
371
- putMethod(params) {
372
- const { restApiId, resourceId, httpMethod, authorizationType, apiKeyRequired, requestParameters, requestModels, authorizerId } = params;
373
- const api = this.apis.get(restApiId);
374
-
375
- if (!api) {
376
- throw new Error(`API ${restApiId} not found`);
377
- }
378
-
379
- const resource = api.resources.get(resourceId);
380
- if (!resource) {
381
- throw new Error(`Resource ${resourceId} not found`);
382
- }
383
-
384
- const method = {
385
- httpMethod: httpMethod,
386
- authorizationType: authorizationType || 'NONE',
387
- apiKeyRequired: apiKeyRequired || false,
388
- requestParameters: requestParameters || {},
389
- requestModels: requestModels || {},
390
- authorizerId: authorizerId || null,
391
- methodResponses: new Map(),
392
- integration: null
393
- };
394
-
395
- resource.resourceMethods.set(httpMethod, method);
396
- this.persistMethods(restApiId, resourceId);
397
-
398
- logger.debug(`🔧 Método criado: ${httpMethod} ${resource.path}`);
399
-
400
- return {
401
- httpMethod: method.httpMethod,
402
- authorizationType: method.authorizationType,
403
- apiKeyRequired: method.apiKeyRequired
404
- };
405
- }
406
-
407
- getMethod(params) {
408
- const { restApiId, resourceId, httpMethod } = params;
409
- const api = this.apis.get(restApiId);
410
-
411
- if (!api) {
412
- throw new Error(`API ${restApiId} not found`);
413
- }
414
-
415
- const resource = api.resources.get(resourceId);
416
- if (!resource) {
417
- throw new Error(`Resource ${resourceId} not found`);
418
- }
419
-
420
- const method = resource.resourceMethods.get(httpMethod);
421
- if (!method) {
422
- throw new Error(`Method ${httpMethod} not found`);
423
- }
424
-
425
- return {
426
- httpMethod: method.httpMethod,
427
- authorizationType: method.authorizationType,
428
- apiKeyRequired: method.apiKeyRequired,
429
- requestParameters: method.requestParameters,
430
- requestModels: method.requestModels
431
- };
432
- }
433
-
434
- deleteMethod(params) {
435
- const { restApiId, resourceId, httpMethod } = params;
436
- const api = this.apis.get(restApiId);
437
-
438
- if (!api) {
439
- throw new Error(`API ${restApiId} not found`);
440
- }
441
-
442
- const resource = api.resources.get(resourceId);
443
- if (!resource) {
444
- throw new Error(`Resource ${resourceId} not found`);
445
- }
446
-
447
- resource.resourceMethods.delete(httpMethod);
448
- this.persistMethods(restApiId, resourceId);
449
-
450
- return {};
451
- }
452
-
453
- // ============ Integration Operations ============
454
-
455
- putIntegration(params) {
456
- const { restApiId, resourceId, httpMethod, type, integrationHttpMethod, uri, credentials, requestParameters, requestTemplates, passthroughBehavior, timeoutInMillis, cacheNamespace, cacheKeyParameters, contentHandling } = params;
457
- const api = this.apis.get(restApiId);
458
-
459
- if (!api) {
460
- throw new Error(`API ${restApiId} not found`);
461
- }
462
-
463
- const resource = api.resources.get(resourceId);
464
- if (!resource) {
465
- throw new Error(`Resource ${resourceId} not found`);
466
- }
467
-
468
- const method = resource.resourceMethods.get(httpMethod);
469
- if (!method) {
470
- throw new Error(`Method ${httpMethod} not found`);
471
- }
472
-
473
- const integration = {
474
- type: type || 'HTTP',
475
- integrationHttpMethod: integrationHttpMethod,
476
- uri: uri,
477
- credentials: credentials || null,
478
- requestParameters: requestParameters || {},
479
- requestTemplates: requestTemplates || {},
480
- passthroughBehavior: passthroughBehavior || 'WHEN_NO_MATCH',
481
- timeoutInMillis: timeoutInMillis || 29000,
482
- cacheNamespace: cacheNamespace || '',
483
- cacheKeyParameters: cacheKeyParameters || [],
484
- contentHandling: contentHandling || null,
485
- integrationResponses: new Map()
486
- };
487
-
488
- method.integration = integration;
489
- this.persistIntegrations(restApiId, resourceId);
490
-
491
- logger.debug(`🔌 Integração criada: ${type} -> ${uri}`);
492
-
493
- return {
494
- type: integration.type,
495
- integrationHttpMethod: integration.integrationHttpMethod,
496
- uri: integration.uri
497
- };
498
- }
499
-
500
- getIntegration(params) {
501
- const { restApiId, resourceId, httpMethod } = params;
502
- const api = this.apis.get(restApiId);
503
-
504
- if (!api) {
505
- throw new Error(`API ${restApiId} not found`);
506
- }
507
-
508
- const resource = api.resources.get(resourceId);
509
- if (!resource) {
510
- throw new Error(`Resource ${resourceId} not found`);
511
- }
512
-
513
- const method = resource.resourceMethods.get(httpMethod);
514
- if (!method || !method.integration) {
515
- throw new Error(`Integration not found for ${httpMethod}`);
516
- }
517
-
518
- const integration = method.integration;
519
-
520
- return {
521
- type: integration.type,
522
- integrationHttpMethod: integration.integrationHttpMethod,
523
- uri: integration.uri,
524
- credentials: integration.credentials,
525
- requestParameters: integration.requestParameters,
526
- requestTemplates: integration.requestTemplates,
527
- passthroughBehavior: integration.passthroughBehavior,
528
- timeoutInMillis: integration.timeoutInMillis
529
- };
530
- }
531
-
532
- deleteIntegration(params) {
533
- const { restApiId, resourceId, httpMethod } = params;
534
- const api = this.apis.get(restApiId);
535
-
536
- if (!api) {
537
- throw new Error(`API ${restApiId} not found`);
538
- }
539
-
540
- const resource = api.resources.get(resourceId);
541
- if (!resource) {
542
- throw new Error(`Resource ${resourceId} not found`);
543
- }
544
-
545
- const method = resource.resourceMethods.get(httpMethod);
546
- if (method) {
547
- method.integration = null;
548
- this.persistIntegrations(restApiId, resourceId);
549
- }
550
-
551
- return {};
552
- }
553
-
554
- // ============ Integration Response Operations ============
555
-
556
- putIntegrationResponse(params) {
557
- const { restApiId, resourceId, httpMethod, statusCode, selectionPattern, responseParameters, responseTemplates, contentHandling } = params;
558
- const api = this.apis.get(restApiId);
559
-
560
- if (!api) {
561
- throw new Error(`API ${restApiId} not found`);
562
- }
563
-
564
- const resource = api.resources.get(resourceId);
565
- if (!resource) {
566
- throw new Error(`Resource ${resourceId} not found`);
567
- }
568
-
569
- const method = resource.resourceMethods.get(httpMethod);
570
- if (!method || !method.integration) {
571
- throw new Error(`Integration not found for ${httpMethod}`);
572
- }
573
-
574
- const integrationResponse = {
575
- statusCode: statusCode,
576
- selectionPattern: selectionPattern || '',
577
- responseParameters: responseParameters || {},
578
- responseTemplates: responseTemplates || {},
579
- contentHandling: contentHandling || null
580
- };
581
-
582
- method.integration.integrationResponses.set(statusCode, integrationResponse);
583
- this.persistIntegrations(restApiId, resourceId);
584
-
585
- return {
586
- statusCode: integrationResponse.statusCode,
587
- selectionPattern: integrationResponse.selectionPattern
588
- };
589
- }
590
-
591
- // ============ Deployment Operations ============
592
-
593
- createDeployment(params) {
594
- const { restApiId, stageName, stageDescription, description, variables } = params;
595
- const api = this.apis.get(restApiId);
596
-
597
- if (!api) {
598
- throw new Error(`API ${restApiId} not found`);
599
- }
600
-
601
- const deploymentId = uuidv4();
602
- const deployment = {
603
- id: deploymentId,
604
- description: description || '',
605
- createdDate: new Date().toISOString(),
606
- apiId: restApiId
607
- };
608
-
609
- api.deployments.set(deploymentId, deployment);
610
-
611
- if (stageName) {
612
- this.createStage({
613
- restApiId,
614
- stageName,
615
- description: stageDescription,
616
- variables
617
- });
618
- }
619
-
620
- this.persistDeployments(restApiId);
621
-
622
- logger.debug(`🚀 Deployment criado: ${deploymentId} para stage: ${stageName || 'N/A'}`);
623
-
624
- return {
625
- id: deploymentId,
626
- createdDate: deployment.createdDate
627
- };
628
- }
629
-
630
- // ============ Stage Operations ============
631
-
632
- createStage(params) {
633
- const { restApiId, stageName, description, variables, deploymentId, cacheClusterEnabled, cacheClusterSize, tracingEnabled } = params;
634
- const api = this.apis.get(restApiId);
635
-
636
- if (!api) {
637
- throw new Error(`API ${restApiId} not found`);
638
- }
639
-
640
- const stage = {
641
- stageName: stageName,
642
- description: description || '',
643
- createdDate: new Date().toISOString(),
644
- lastUpdatedDate: new Date().toISOString(),
645
- deploymentId: deploymentId,
646
- variables: variables || {},
647
- cacheClusterEnabled: cacheClusterEnabled || false,
648
- cacheClusterSize: cacheClusterSize || null,
649
- tracingEnabled: tracingEnabled || false,
650
- methodSettings: new Map()
651
- };
652
-
653
- api.stages.set(stageName, stage);
654
- this.persistStages(restApiId);
655
-
656
- // Cria endpoint URL
657
- const endpointUrl = `http://localhost:${this.config.ports.apigateway}/${restApiId}/${stageName}`;
658
-
659
- logger.debug(`📡 Stage criado: ${stageName} em ${endpointUrl}`);
660
-
661
- return {
662
- stageName: stage.stageName,
663
- createdDate: stage.createdDate,
664
- deploymentId: stage.deploymentId
665
- };
666
- }
667
-
668
- getStage(params) {
669
- const { restApiId, stageName } = params;
670
- const api = this.apis.get(restApiId);
671
-
672
- if (!api) {
673
- throw new Error(`API ${restApiId} not found`);
674
- }
675
-
676
- const stage = api.stages.get(stageName);
677
- if (!stage) {
678
- throw new Error(`Stage ${stageName} not found`);
679
- }
680
-
681
- return {
682
- stageName: stage.stageName,
683
- description: stage.description,
684
- createdDate: stage.createdDate,
685
- lastUpdatedDate: stage.lastUpdatedDate,
686
- deploymentId: stage.deploymentId,
687
- variables: stage.variables,
688
- methodSettings: Object.fromEntries(stage.methodSettings),
689
- cacheClusterEnabled: stage.cacheClusterEnabled,
690
- tracingEnabled: stage.tracingEnabled
691
- };
692
- }
693
-
694
- updateStage(params) {
695
- const { restApiId, stageName, description, variables, deploymentId, tracingEnabled } = params;
696
- const api = this.apis.get(restApiId);
697
-
698
- if (!api) {
699
- throw new Error(`API ${restApiId} not found`);
700
- }
701
-
702
- const stage = api.stages.get(stageName);
703
- if (!stage) {
704
- throw new Error(`Stage ${stageName} not found`);
705
- }
706
-
707
- if (description !== undefined) stage.description = description;
708
- if (variables !== undefined) stage.variables = { ...stage.variables, ...variables };
709
- if (deploymentId !== undefined) stage.deploymentId = deploymentId;
710
- if (tracingEnabled !== undefined) stage.tracingEnabled = tracingEnabled;
711
-
712
- stage.lastUpdatedDate = new Date().toISOString();
713
- this.persistStages(restApiId);
714
-
715
- return {
716
- stageName: stage.stageName,
717
- description: stage.description,
718
- lastUpdatedDate: stage.lastUpdatedDate
719
- };
720
- }
721
-
722
- deleteStage(params) {
723
- const { restApiId, stageName } = params;
724
- const api = this.apis.get(restApiId);
725
-
726
- if (!api) {
727
- throw new Error(`API ${restApiId} not found`);
728
- }
729
-
730
- api.stages.delete(stageName);
731
- this.persistStages(restApiId);
732
-
733
- return {};
734
- }
735
-
736
- // ============ API Key Operations ============
737
-
738
- createApiKey(params) {
739
- const { name, description, enabled, stageKeys, customerId } = params;
740
-
741
- const apiKeyId = uuidv4();
742
- const apiKey = {
743
- id: apiKeyId,
744
- name: name || '',
745
- description: description || '',
746
- enabled: enabled !== false,
747
- value: crypto.randomBytes(20).toString('hex'),
748
- stageKeys: stageKeys || [],
749
- customerId: customerId || null,
750
- createdDate: new Date().toISOString(),
751
- lastUpdatedDate: new Date().toISOString()
752
- };
753
-
754
- this.apiKeys.set(apiKeyId, apiKey);
755
- this.persistApiKeys();
756
-
757
- logger.debug(`🔑 API Key criada: ${name || apiKeyId}`);
758
-
759
- return {
760
- id: apiKey.id,
761
- name: apiKey.name,
762
- value: apiKey.value,
763
- enabled: apiKey.enabled
764
- };
765
- }
766
-
767
- getApiKeys(params) {
768
- const items = Array.from(this.apiKeys.values()).map(key => ({
769
- id: key.id,
770
- name: key.name,
771
- description: key.description,
772
- enabled: key.enabled,
773
- createdDate: key.createdDate,
774
- lastUpdatedDate: key.lastUpdatedDate
775
- }));
776
-
777
- return { items };
778
- }
779
-
780
- // ============ Usage Plan Operations ============
781
-
782
- createUsagePlan(params) {
783
- const { name, description, apiStages, throttle, quota } = params;
784
-
785
- const usagePlanId = uuidv4();
786
- const usagePlan = {
787
- id: usagePlanId,
788
- name: name,
789
- description: description || '',
790
- apiStages: apiStages || [],
791
- throttle: throttle || {
792
- burstLimit: 100,
793
- rateLimit: 10
794
- },
795
- quota: quota || {
796
- limit: 10000,
797
- period: 'DAY',
798
- offset: 0
799
- },
800
- createdDate: new Date().toISOString(),
801
- lastUpdatedDate: new Date().toISOString()
802
- };
803
-
804
- this.usagePlans.set(usagePlanId, usagePlan);
805
- this.persistUsagePlans();
806
-
807
- logger.debug(`📊 Usage Plan criado: ${name}`);
808
-
809
- return {
810
- id: usagePlan.id,
811
- name: usagePlan.name,
812
- createdDate: usagePlan.createdDate
813
- };
814
- }
815
-
816
- // ============ Request Execution ============
817
-
818
- async executeRequest(apiId, stageName, method, path, headers, body, queryString) {
819
- const api = this.apis.get(apiId);
820
- if (!api) {
821
- return this.createResponse(404, 'API not found');
822
- }
823
-
824
- const stage = api.stages.get(stageName);
825
- if (!stage) {
826
- return this.createResponse(404, 'Stage not found');
827
- }
828
-
829
- // Encontra o recurso e método correspondente
830
- const { resource, methodDef } = this.findMatchingResource(api, method, path);
831
-
832
- if (!resource || !methodDef) {
833
- return this.createResponse(404, 'Resource or method not found');
834
- }
835
-
836
- // Verifica API Key
837
- if (methodDef.apiKeyRequired) {
838
- const apiKey = this.extractApiKey(headers);
839
- if (!this.validateApiKey(apiKey)) {
840
- return this.createResponse(403, 'Forbidden: Invalid API Key');
841
- }
842
- }
843
-
844
- // Verifica throttling
845
- const usagePlan = this.getUsagePlanForApi(apiId, stageName);
846
- if (usagePlan && !this.checkThrottle(usagePlan)) {
847
- return this.createResponse(429, 'Too Many Requests');
848
- }
849
-
850
- // Processa integração
851
- const integration = methodDef.integration;
852
- if (!integration) {
853
- return this.createResponse(500, 'Integration not configured');
854
- }
855
-
856
- // Executa a integração baseada no tipo
857
- const response = await this.executeIntegration(integration, {
858
- method,
859
- path,
860
- headers,
861
- body,
862
- queryString,
863
- resource,
864
- stage
865
- });
866
-
867
- // Aplica resposta da integração
868
- const integrationResponse = this.getMatchingIntegrationResponse(
869
- integration.integrationResponses,
870
- response.statusCode
871
- );
872
-
873
- if (integrationResponse) {
874
- response.headers = { ...response.headers, ...integrationResponse.responseParameters };
875
- response.body = this.applyResponseTemplate(integrationResponse, response.body);
876
- }
877
-
878
- this.audit.record({
879
- eventName: 'Invoke',
880
- readOnly: method === 'GET' || method === 'HEAD',
881
- isDataEvent: true,
882
- resources: [{ ARN: `arn:aws:execute-api:local:000000000000:${apiId}/${stageName}/${method}${path}`, type: 'AWS::APIGateway::Stage' }],
883
- requestParameters: { apiId, stageName, method, path },
884
- });
885
-
886
- return response;
887
- }
888
-
889
- findMatchingResource(api, method, path) {
890
- // Busca recurso que corresponda ao path
891
- const resources = Array.from(api.resources.values());
892
-
893
- // Ordena por path mais específico primeiro
894
- resources.sort((a, b) => b.path.length - a.path.length);
895
-
896
- for (const resource of resources) {
897
- if (this.matchPath(resource.path, path)) {
898
- const methodDef = resource.resourceMethods.get(method);
899
- if (methodDef) {
900
- return { resource, methodDef };
901
- }
902
- }
903
- }
904
-
905
- return { resource: null, methodDef: null };
906
- }
907
-
908
- matchPath(pattern, path) {
909
- // Converte path com parâmetros para regex
910
- // Ex: /users/{userId}/posts/{postId}
911
- const regexPattern = pattern
912
- .replace(/\{([^}]+)\}/g, '([^/]+)')
913
- .replace(/\//g, '\\/');
914
-
915
- const regex = new RegExp(`^${regexPattern}$`);
916
- return regex.test(path);
917
- }
918
-
919
- extractPathParams(pattern, path) {
920
- const paramNames = [];
921
- const regexPattern = pattern
922
- .replace(/\{([^}]+)\}/g, (match, paramName) => {
923
- paramNames.push(paramName);
924
- return '([^/]+)';
925
- })
926
- .replace(/\//g, '\\/');
927
-
928
- const regex = new RegExp(`^${regexPattern}$`);
929
- const match = path.match(regex);
930
-
931
- if (match) {
932
- const params = {};
933
- paramNames.forEach((name, index) => {
934
- params[name] = match[index + 1];
935
- });
936
- return params;
937
- }
938
-
939
- return {};
940
- }
941
-
942
- async executeIntegration(integration, context) {
943
- const { type, uri, integrationHttpMethod, requestTemplates, requestParameters } = integration;
944
-
945
- // Prepara o request
946
- let requestBody = context.body;
947
- let requestHeaders = { ...context.headers };
948
-
949
- // Aplica templates de request
950
- if (requestTemplates && requestTemplates['application/json']) {
951
- const template = requestTemplates['application/json'];
952
- requestBody = this.applyRequestTemplate(template, context);
953
- }
954
-
955
- // Aplica mapeamento de parâmetros
956
- if (requestParameters) {
957
- for (const [key, value] of Object.entries(requestParameters)) {
958
- requestHeaders[key] = this.resolveParameter(value, context);
959
- }
960
- }
961
-
962
- switch(type) {
963
- case 'AWS':
964
- case 'AWS_PROXY':
965
- return this.executeAWSIntegration(integration, context, requestBody, requestHeaders);
966
- case 'HTTP':
967
- case 'HTTP_PROXY':
968
- return this.executeHTTPIntegration(integration, context, requestBody, requestHeaders);
969
- case 'MOCK':
970
- return this.executeMockIntegration(integration);
971
- default:
972
- return this.createResponse(501, `Integration type ${type} not supported`);
973
- }
974
- }
975
-
976
- async executeAWSIntegration(integration, context, body, headers) {
977
- // Simula integração com Lambda
978
- const uriParts = integration.uri.split(':');
979
- const functionName = uriParts[uriParts.length - 1];
980
-
981
- // Aqui poderia chamar o simulador Lambda
982
- logger.debug(`Invoking Lambda: ${functionName}`);
983
-
984
- return this.createResponse(200, {
985
- message: `Lambda ${functionName} invoked`,
986
- event: context
987
- });
988
- }
989
-
990
- async executeHTTPIntegration(integration, context, body, headers) {
991
- const { uri, integrationHttpMethod } = integration;
992
-
993
- logger.debug(`HTTP ${integrationHttpMethod} to ${uri}`);
994
-
995
- // Simula chamada HTTP
996
- try {
997
- const response = await this.makeHttpRequest(uri, integrationHttpMethod, headers, body);
998
- return {
999
- statusCode: response.status,
1000
- headers: response.headers,
1001
- body: response.data
1002
- };
1003
- } catch (error) {
1004
- return this.createResponse(502, 'Bad Gateway');
1005
- }
1006
- }
1007
-
1008
- async makeHttpRequest(url, method, headers, body) {
1009
- // Simulação - em implementação real, usaria axios ou fetch
1010
- return {
1011
- status: 200,
1012
- headers: {},
1013
- data: { message: 'Mock HTTP response' }
1014
- };
1015
- }
1016
-
1017
- executeMockIntegration(integration) {
1018
- // Retorna resposta mock
1019
- const mockResponse = integration.requestTemplates?.mock || {};
1020
- return this.createResponse(200, mockResponse);
1021
- }
1022
-
1023
- getMatchingIntegrationResponse(responses, statusCode) {
1024
- // Busca response que corresponda ao status code
1025
- if (responses.has(statusCode.toString())) {
1026
- return responses.get(statusCode.toString());
1027
- }
1028
-
1029
- // Busca default
1030
- if (responses.has('default')) {
1031
- return responses.get('default');
1032
- }
1033
-
1034
- return null;
1035
- }
1036
-
1037
- applyRequestTemplate(template, context) {
1038
- // Implementação simples de template
1039
- try {
1040
- return JSON.stringify(context);
1041
- } catch (error) {
1042
- return template;
1043
- }
1044
- }
1045
-
1046
- applyResponseTemplate(response, body) {
1047
- const templates = response.responseTemplates;
1048
- if (templates && templates['application/json']) {
1049
- // Aplica template de resposta
1050
- try {
1051
- return JSON.parse(templates['application/json'].replace(/\$\{([^}]+)\}/g, (match, path) => {
1052
- return this.getNestedValue(body, path);
1053
- }));
1054
- } catch (error) {
1055
- return body;
1056
- }
1057
- }
1058
- return body;
1059
- }
1060
-
1061
- resolveParameter(value, context) {
1062
- // Resolve parâmetros como method.request.header.X-Header
1063
- if (value.includes('method.request.')) {
1064
- const parts = value.split('.');
1065
- const type = parts[2]; // header, querystring, path
1066
- const name = parts[3];
1067
-
1068
- switch(type) {
1069
- case 'header':
1070
- return context.headers[name];
1071
- case 'querystring':
1072
- return context.queryString[name];
1073
- case 'path':
1074
- return context.pathParams[name];
1075
- default:
1076
- return null;
1077
- }
1078
- }
1079
- return value;
1080
- }
1081
-
1082
- getNestedValue(obj, path) {
1083
- return path.split('.').reduce((current, key) => current?.[key], obj);
1084
- }
1085
-
1086
- extractApiKey(headers) {
1087
- return headers['x-api-key'] || headers['X-Api-Key'];
1088
- }
1089
-
1090
- validateApiKey(apiKeyValue) {
1091
- if (!apiKeyValue) return false;
1092
-
1093
- for (const key of this.apiKeys.values()) {
1094
- if (key.value === apiKeyValue && key.enabled) {
1095
- return true;
1096
- }
1097
- }
1098
- return false;
1099
- }
1100
-
1101
- getUsagePlanForApi(apiId, stageName) {
1102
- for (const plan of this.usagePlans.values()) {
1103
- const apiStage = plan.apiStages.find(
1104
- as => as.apiId === apiId && as.stage === stageName
1105
- );
1106
- if (apiStage) {
1107
- return plan;
1108
- }
1109
- }
1110
- return null;
1111
- }
1112
-
1113
- checkThrottle(usagePlan) {
1114
- // Implementação simplificada de throttling
1115
- const { rateLimit, burstLimit } = usagePlan.throttle;
1116
- // Aqui poderia implementar contagem de requests
1117
- return true;
1118
- }
1119
-
1120
- createResponse(statusCode, body) {
1121
- return {
1122
- statusCode: statusCode,
1123
- headers: {
1124
- 'Content-Type': 'application/json',
1125
- 'Access-Control-Allow-Origin': '*'
1126
- },
1127
- body: typeof body === 'string' ? body : JSON.stringify(body)
1128
- };
1129
- }
1130
-
1131
- // ============ HTTP API Operations ============
1132
-
1133
- createHttpApi(params) {
1134
- const { name, description, protocolType, routeSelectionExpression, corsConfiguration } = params;
1135
-
1136
- const apiId = `http_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
1137
- const api = {
1138
- id: apiId,
1139
- name: name,
1140
- description: description || '',
1141
- protocolType: protocolType || 'HTTP',
1142
- routeSelectionExpression: routeSelectionExpression || '$request.method $request.path',
1143
- corsConfiguration: corsConfiguration || {
1144
- allowOrigins: ['*'],
1145
- allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
1146
- allowHeaders: ['*'],
1147
- maxAge: 300
1148
- },
1149
- routes: new Map(),
1150
- integrations: new Map(),
1151
- stages: new Map(),
1152
- createdDate: new Date().toISOString()
1153
- };
1154
-
1155
- this.apis.set(apiId, api);
1156
- this.persistAPIs();
1157
-
1158
- logger.debug(`✅ HTTP API criada: ${name} (${apiId})`);
1159
-
1160
- return {
1161
- ApiId: apiId,
1162
- Name: api.name,
1163
- ProtocolType: api.protocolType
1164
- };
1165
- }
1166
-
1167
- createRoute(params) {
1168
- const { apiId, routeKey, authorizationType, target } = params;
1169
- const api = this.apis.get(apiId);
1170
-
1171
- if (!api) {
1172
- throw new Error(`API ${apiId} not found`);
1173
- }
1174
-
1175
- const route = {
1176
- routeKey: routeKey,
1177
- authorizationType: authorizationType || 'NONE',
1178
- target: target,
1179
- createdAt: new Date().toISOString()
1180
- };
1181
-
1182
- api.routes.set(routeKey, route);
1183
- this.persistAPIs();
1184
-
1185
- return { route };
1186
- }
1187
-
1188
- // ============ Persistence ============
1189
-
1190
- loadAPIs() {
1191
- const saved = this.store.read('__apis__');
1192
- if (saved) {
1193
- for (const [id, data] of Object.entries(saved)) {
1194
- // Reconstitui Maps
1195
- data.resources = new Map(Object.entries(data.resources || {}).map(([rid, r]) => {
1196
- r.resourceMethods = new Map(Object.entries(r.resourceMethods || {}));
1197
- return [rid, r];
1198
- }));
1199
- data.stages = new Map(Object.entries(data.stages || {}));
1200
- data.deployments = new Map(Object.entries(data.deployments || {}));
1201
- data.models = new Map(Object.entries(data.models || {}));
1202
- data.authorizers = new Map(Object.entries(data.authorizers || {}));
1203
- this.apis.set(id, data);
1204
- }
1205
- }
1206
- }
1207
-
1208
-
1209
- loadWebSocketAPIs() {
1210
- const saved = this.store.read('__websocket_apis__');
1211
- if (saved) {
1212
- for (const [id, data] of Object.entries(saved)) {
1213
- this.websocketApis.set(id, data);
1214
- }
1215
- }
1216
- }
1217
-
1218
- loadDeployments() {
1219
- const saved = this.store.read('__deployments__');
1220
- if (saved) {
1221
- for (const [id, data] of Object.entries(saved)) {
1222
- this.deployments.set(id, data);
1223
- }
1224
- }
1225
- }
1226
-
1227
- loadStages() {
1228
- const saved = this.store.read('__stages__');
1229
- if (saved) {
1230
- for (const [id, data] of Object.entries(saved)) {
1231
- this.stages.set(id, data);
1232
- }
1233
- }
1234
- }
1235
-
1236
- loadResources() {
1237
- const saved = this.store.read('__resources__');
1238
- if (saved) {
1239
- for (const [id, data] of Object.entries(saved)) {
1240
- this.resources.set(id, data);
1241
- }
1242
- }
1243
- }
1244
-
1245
- loadMethods() {
1246
- const saved = this.store.read('__methods__');
1247
- if (saved) {
1248
- for (const [id, data] of Object.entries(saved)) {
1249
- this.methods.set(id, data);
1250
- }
1251
- }
1252
- }
1253
-
1254
- loadIntegrations() {
1255
- const saved = this.store.read('__integrations__');
1256
- if (saved) {
1257
- for (const [id, data] of Object.entries(saved)) {
1258
- this.integrations.set(id, data);
1259
- }
1260
- }
1261
- }
1262
-
1263
- loadModels() {
1264
- const saved = this.store.read('__models__');
1265
- if (saved) {
1266
- for (const [id, data] of Object.entries(saved)) {
1267
- this.models.set(id, data);
1268
- }
1269
- }
1270
- }
1271
-
1272
- loadUsagePlans() {
1273
- const saved = this.store.read('__usage_plans__');
1274
- if (saved) {
1275
- for (const [id, data] of Object.entries(saved)) {
1276
- this.usagePlans.set(id, data);
1277
- }
1278
- }
1279
- }
1280
-
1281
- loadApiKeys() {
1282
- const saved = this.store.read('__api_keys__');
1283
- if (saved) {
1284
- for (const [id, data] of Object.entries(saved)) {
1285
- this.apiKeys.set(id, data);
1286
- }
1287
- }
1288
- }
1289
-
1290
- loadDomainNames() {
1291
- const saved = this.store.read('__domain_names__');
1292
- if (saved) {
1293
- for (const [id, data] of Object.entries(saved)) {
1294
- this.domainNames.set(id, data);
1295
- }
1296
- }
1297
- }
1298
-
1299
- persistAPIs() {
1300
- const apisObj = {};
1301
- for (const [id, api] of this.apis.entries()) {
1302
- if (api.isStatic) continue;
1303
- apisObj[id] = {
1304
- ...api,
1305
- resources: Object.fromEntries(Array.from(api.resources.entries()).map(([rid, r]) => [rid, { ...r, resourceMethods: Object.fromEntries(r.resourceMethods) }])),
1306
- stages: Object.fromEntries(api.stages),
1307
- deployments: Object.fromEntries(api.deployments),
1308
- models: Object.fromEntries(api.models),
1309
- authorizers: Object.fromEntries(api.authorizers)
1310
- };
1311
- }
1312
- this.store.write('__apis__', apisObj);
1313
- }
1314
-
1315
-
1316
- persistResources(apiId) {
1317
- const api = this.apis.get(apiId);
1318
- if (api) {
1319
- this.persistAPIs();
1320
- }
1321
- }
1322
-
1323
- persistMethods(apiId, resourceId) {
1324
- this.persistResources(apiId);
1325
- }
1326
-
1327
- persistIntegrations(apiId, resourceId) {
1328
- this.persistMethods(apiId, resourceId);
1329
- }
1330
-
1331
- persistStages(apiId) {
1332
- this.persistAPIs();
1333
- }
1334
-
1335
- persistDeployments(apiId) {
1336
- this.persistAPIs();
1337
- }
1338
-
1339
- persistApiKeys() {
1340
- const keysObj = {};
1341
- for (const [id, key] of this.apiKeys.entries()) {
1342
- keysObj[id] = key;
1343
- }
1344
- this.store.write('__api_keys__', keysObj);
1345
- }
1346
-
1347
- persistUsagePlans() {
1348
- const plansObj = {};
1349
- for (const [id, plan] of this.usagePlans.entries()) {
1350
- plansObj[id] = plan;
1351
- }
1352
- this.store.write('__usage_plans__', plansObj);
1353
- }
1354
-
1355
- async reset() {
1356
- this.apis.clear();
1357
- this.websocketApis.clear();
1358
- this.deployments.clear();
1359
- this.stages.clear();
1360
- this.resources.clear();
1361
- this.methods.clear();
1362
- this.integrations.clear();
1363
- this.models.clear();
1364
- this.usagePlans.clear();
1365
- this.apiKeys.clear();
1366
- this.domainNames.clear();
1367
-
1368
- this.persistAPIs();
1369
- this.persistApiKeys();
1370
- this.persistUsagePlans();
1371
-
1372
- logger.debug('API Gateway: Todos os dados resetados');
1373
- }
1374
-
1375
- // ============ Stats ============
1376
-
1377
- getAPIsCount() {
1378
- return this.apis.size;
1379
- }
1380
-
1381
- getDeploymentsCount() {
1382
- let count = 0;
1383
- for (const api of this.apis.values()) {
1384
- count += api.deployments.size;
1385
- }
1386
- return count;
1387
- }
1388
-
1389
- getStagesCount() {
1390
- let count = 0;
1391
- for (const api of this.apis.values()) {
1392
- count += api.stages.size;
1393
- }
1394
- return count;
1395
- }
1396
-
1397
- getResourcesCount() {
1398
- let count = 0;
1399
- for (const api of this.apis.values()) {
1400
- count += api.resources.size;
1401
- }
1402
- return count;
1403
- }
1404
- }
1405
-
1
+ /**
2
+ * API Gateway Simulator Core
3
+ * Simula REST APIs, HTTP APIs, WebSocket APIs, Routes, Integrations
4
+ */
5
+
6
+ const crypto = require('crypto');
7
+ const { v4: uuidv4 } = require('uuid');
8
+ const logger = require('../../utils/logger');
9
+ const LocalStore = require('../../utils/local-store');
10
+ const path = require('path');
11
+ const { URLPattern } = require('urlpattern-polyfill');
12
+ const { CloudTrailAudit } = require('../../utils/cloudtrail-audit');
13
+
14
+ class APIGatewaySimulator {
15
+ constructor(config) {
16
+ this.config = config;
17
+ this.dataDir = path.join(process.env.AWS_LOCAL_SIMULATOR_DATA_DIR, 'apigateway');
18
+ this.store = new LocalStore(this.dataDir);
19
+ this.apis = new Map();
20
+ this.websocketApis = new Map();
21
+ this.deployments = new Map();
22
+ this.stages = new Map();
23
+ this.resources = new Map();
24
+ this.methods = new Map();
25
+ this.integrations = new Map();
26
+ this.models = new Map();
27
+ this.usagePlans = new Map();
28
+ this.apiKeys = new Map();
29
+ this.domainNames = new Map();
30
+ this.audit = new CloudTrailAudit('execute-api.amazonaws.com');
31
+ }
32
+
33
+ async initialize() {
34
+ logger.debug('Inicializando API Gateway Simulator...');
35
+ this.loadAPIs();
36
+ this._loadStaticAPIs();
37
+ this.loadWebSocketAPIs();
38
+ this.loadDeployments();
39
+ this.loadStages();
40
+ this.loadResources();
41
+ this.loadMethods();
42
+ this.loadIntegrations();
43
+ this.loadModels();
44
+ this.loadUsagePlans();
45
+ this.loadApiKeys();
46
+ this.loadDomainNames();
47
+
48
+ logger.debug(`✅ API Gateway Simulator inicializado com ${this.apis.size} APIs (${Array.from(this.apis.values()).filter(a => a.isStatic).length} estáticas)`);
49
+ }
50
+
51
+ _loadStaticAPIs() {
52
+ const staticApis = this.config.apigateway?.apis || [];
53
+ staticApis.forEach((apiConfig, index) => {
54
+ const apiId = `static_${index}`;
55
+
56
+ const api = {
57
+ id: apiId,
58
+ name: apiConfig.name,
59
+ description: apiConfig.description || 'Configured in aws-local-simulator.json',
60
+ version: 'config',
61
+ createdDate: new Date().toISOString(),
62
+ isStatic: true,
63
+ apiKeySource: 'HEADER',
64
+ endpointConfiguration: { types: ['REGIONAL'] },
65
+ resources: new Map(),
66
+ stages: new Map(),
67
+ deployments: new Map(),
68
+ models: new Map(),
69
+ authorizers: new Map()
70
+ };
71
+
72
+ // Adiciona recursos a partir dos endpoints configurados
73
+ (apiConfig.endpoints || []).forEach((ep, epIndex) => {
74
+ const resId = `res_${apiId}_${epIndex}`;
75
+ api.resources.set(resId, {
76
+ id: resId,
77
+ path: ep.path,
78
+ pathPart: ep.path.split('/').pop() || '/',
79
+ resourceMethods: new Map([[ep.method, {
80
+ httpMethod: ep.method,
81
+ authorizationType: ep.authorizerRequired ? 'COGNITO_USER_POOLS' : 'NONE',
82
+ apiKeyRequired: false,
83
+ integration: {
84
+ type: ep.integrationType === 'lambda' ? 'AWS_PROXY' : 'HTTP',
85
+ uri: ep.lambdaName,
86
+ integrationHttpMethod: 'POST'
87
+ }
88
+ }]])
89
+ });
90
+ });
91
+
92
+ // Adiciona um stage padrão
93
+ api.stages.set('local', {
94
+ stageName: 'local',
95
+ createdDate: new Date().toISOString(),
96
+ deploymentId: 'static-deploy'
97
+ });
98
+
99
+ this.apis.set(apiId, api);
100
+ });
101
+ }
102
+
103
+
104
+ // ============ REST API Operations ============
105
+
106
+ createRestApi(params) {
107
+ const { name, description, version, apiKeySource, endpointConfiguration, tags } = params;
108
+
109
+ const apiId = `api_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
110
+ const api = {
111
+ id: apiId,
112
+ name: name,
113
+ description: description || '',
114
+ version: version || '1.0',
115
+ createdDate: new Date().toISOString(),
116
+ apiKeySource: apiKeySource || 'HEADER',
117
+ endpointConfiguration: endpointConfiguration || {
118
+ types: ['REGIONAL']
119
+ },
120
+ tags: tags || {},
121
+ resources: new Map(),
122
+ stages: new Map(),
123
+ deployments: new Map(),
124
+ models: new Map(),
125
+ authorizers: new Map(),
126
+ gatewayResponses: new Map(),
127
+ documentationParts: new Map()
128
+ };
129
+
130
+ // Cria recurso raiz
131
+ const rootResource = {
132
+ id: uuidv4(),
133
+ path: '/',
134
+ pathPart: '',
135
+ parentId: null,
136
+ resourceMethods: new Map()
137
+ };
138
+
139
+ api.resources.set('/', rootResource);
140
+
141
+ this.apis.set(apiId, api);
142
+ this.persistAPIs();
143
+
144
+ logger.debug(`✅ REST API criada: ${name} (${apiId})`);
145
+
146
+ return {
147
+ id: apiId,
148
+ name: api.name,
149
+ createdDate: api.createdDate
150
+ };
151
+ }
152
+
153
+ getRestApis() {
154
+ return {
155
+ items: Array.from(this.apis.values()).map(api => ({
156
+ id: api.id,
157
+ name: api.name,
158
+ description: api.description,
159
+ version: api.version,
160
+ createdDate: api.createdDate,
161
+ apiKeySource: api.apiKeySource,
162
+ isStatic: api.isStatic || false,
163
+ resourceCount: api.resources.size,
164
+ stageCount: api.stages.size
165
+ }))
166
+ };
167
+ }
168
+
169
+
170
+ getRestApi(params) {
171
+ const { restApiId } = params;
172
+ const api = this.apis.get(restApiId);
173
+
174
+ if (!api) {
175
+ throw new Error(`API ${restApiId} not found`);
176
+ }
177
+
178
+ return {
179
+ id: api.id,
180
+ name: api.name,
181
+ description: api.description,
182
+ version: api.version,
183
+ createdDate: api.createdDate,
184
+ apiKeySource: api.apiKeySource,
185
+ endpointConfiguration: api.endpointConfiguration,
186
+ tags: api.tags
187
+ };
188
+ }
189
+
190
+ updateRestApi(params) {
191
+ const { restApiId, name, description } = params;
192
+ const api = this.apis.get(restApiId);
193
+
194
+ if (!api) {
195
+ throw new Error(`API ${restApiId} not found`);
196
+ }
197
+
198
+ if (name !== undefined) api.name = name;
199
+ if (description !== undefined) api.description = description;
200
+
201
+ if (api.isStatic) {
202
+ // Once edited, the API is no longer static and will be persisted
203
+ api.isStatic = false;
204
+ }
205
+
206
+ this.persistAPIs();
207
+
208
+ return this.getRestApi({ restApiId });
209
+ }
210
+
211
+ deleteRestApi(params) {
212
+ const { restApiId } = params;
213
+
214
+ if (!this.apis.has(restApiId)) {
215
+ throw new Error(`API ${restApiId} not found`);
216
+ }
217
+
218
+ this.apis.delete(restApiId);
219
+ this.persistAPIs();
220
+
221
+ return {};
222
+ }
223
+
224
+ // ============ Simplified Dashboard Endpoint Operations ============
225
+
226
+ putEndpoint(params) {
227
+ const { restApiId, path, method, integrationType, lambdaName, authorizerRequired } = params;
228
+ const api = this.apis.get(restApiId);
229
+ if (!api) throw new Error(`API ${restApiId} not found`);
230
+
231
+ if (api.isStatic) api.isStatic = false;
232
+
233
+ // Ensure resource exists
234
+ let resource = Array.from(api.resources.values()).find(r => r.path === path);
235
+ if (!resource) {
236
+ const resourceId = `res_${Date.now()}`;
237
+ resource = {
238
+ id: resourceId,
239
+ path: path,
240
+ pathPart: path.split('/').pop() || '/',
241
+ parentId: null, // Simplified
242
+ resourceMethods: new Map()
243
+ };
244
+ api.resources.set(resourceId, resource);
245
+ }
246
+
247
+ // Put method
248
+ resource.resourceMethods.set(method.toUpperCase(), {
249
+ httpMethod: method.toUpperCase(),
250
+ authorizationType: authorizerRequired ? 'COGNITO_USER_POOLS' : 'NONE',
251
+ apiKeyRequired: false,
252
+ integration: {
253
+ type: integrationType === 'lambda' ? 'AWS_PROXY' : 'HTTP',
254
+ uri: lambdaName,
255
+ integrationHttpMethod: 'POST'
256
+ }
257
+ });
258
+
259
+ this.persistAPIs();
260
+ return { resourceId: resource.id, path, method };
261
+ }
262
+
263
+ deleteEndpoint(params) {
264
+ const { restApiId, path, method } = params;
265
+ const api = this.apis.get(restApiId);
266
+ if (!api) throw new Error(`API ${restApiId} not found`);
267
+
268
+ if (api.isStatic) api.isStatic = false;
269
+
270
+ const resource = Array.from(api.resources.values()).find(r => r.path === path);
271
+ if (resource) {
272
+ resource.resourceMethods.delete(method.toUpperCase());
273
+ // If no methods left and not root, we could delete the resource, but keeping it is fine.
274
+ if (resource.resourceMethods.size === 0 && resource.path !== '/') {
275
+ api.resources.delete(resource.id);
276
+ }
277
+ this.persistAPIs();
278
+ }
279
+ return {};
280
+ }
281
+
282
+ // ============ Resource Operations ============
283
+
284
+
285
+ createResource(params) {
286
+ const { restApiId, parentId, pathPart } = params;
287
+ const api = this.apis.get(restApiId);
288
+
289
+ if (!api) {
290
+ throw new Error(`API ${restApiId} not found`);
291
+ }
292
+
293
+ const parentResource = api.resources.get(parentId);
294
+ if (!parentResource) {
295
+ throw new Error(`Parent resource ${parentId} not found`);
296
+ }
297
+
298
+ const resourceId = uuidv4();
299
+ const fullPath = parentResource.path === '/'
300
+ ? `/${pathPart}`
301
+ : `${parentResource.path}/${pathPart}`;
302
+
303
+ const resource = {
304
+ id: resourceId,
305
+ path: fullPath,
306
+ pathPart: pathPart,
307
+ parentId: parentId,
308
+ resourceMethods: new Map()
309
+ };
310
+
311
+ api.resources.set(resourceId, resource);
312
+ this.persistResources(restApiId);
313
+
314
+ logger.debug(`📁 Recurso criado: ${fullPath} (${resourceId})`);
315
+
316
+ return {
317
+ id: resourceId,
318
+ path: resource.path,
319
+ parentId: resource.parentId
320
+ };
321
+ }
322
+
323
+ getResources(params) {
324
+ const { restApiId } = params;
325
+ const api = this.apis.get(restApiId);
326
+
327
+ if (!api) {
328
+ throw new Error(`API ${restApiId} not found`);
329
+ }
330
+
331
+ const items = Array.from(api.resources.values()).map(resource => ({
332
+ id: resource.id,
333
+ path: resource.path,
334
+ pathPart: resource.pathPart,
335
+ parentId: resource.parentId,
336
+ resourceMethods: Object.fromEntries(resource.resourceMethods)
337
+ }));
338
+
339
+ return { items };
340
+ }
341
+
342
+ deleteResource(params) {
343
+ const { restApiId, resourceId } = params;
344
+ const api = this.apis.get(restApiId);
345
+
346
+ if (!api) {
347
+ throw new Error(`API ${restApiId} not found`);
348
+ }
349
+
350
+ if (!api.resources.has(resourceId)) {
351
+ throw new Error(`Resource ${resourceId} not found`);
352
+ }
353
+
354
+ // Verifica se tem filhos
355
+ const hasChildren = Array.from(api.resources.values()).some(
356
+ r => r.parentId === resourceId
357
+ );
358
+
359
+ if (hasChildren) {
360
+ throw new Error('Resource has children');
361
+ }
362
+
363
+ api.resources.delete(resourceId);
364
+ this.persistResources(restApiId);
365
+
366
+ return {};
367
+ }
368
+
369
+ // ============ Method Operations ============
370
+
371
+ putMethod(params) {
372
+ const { restApiId, resourceId, httpMethod, authorizationType, apiKeyRequired, requestParameters, requestModels, authorizerId } = params;
373
+ const api = this.apis.get(restApiId);
374
+
375
+ if (!api) {
376
+ throw new Error(`API ${restApiId} not found`);
377
+ }
378
+
379
+ const resource = api.resources.get(resourceId);
380
+ if (!resource) {
381
+ throw new Error(`Resource ${resourceId} not found`);
382
+ }
383
+
384
+ const method = {
385
+ httpMethod: httpMethod,
386
+ authorizationType: authorizationType || 'NONE',
387
+ apiKeyRequired: apiKeyRequired || false,
388
+ requestParameters: requestParameters || {},
389
+ requestModels: requestModels || {},
390
+ authorizerId: authorizerId || null,
391
+ methodResponses: new Map(),
392
+ integration: null
393
+ };
394
+
395
+ resource.resourceMethods.set(httpMethod, method);
396
+ this.persistMethods(restApiId, resourceId);
397
+
398
+ logger.debug(`🔧 Método criado: ${httpMethod} ${resource.path}`);
399
+
400
+ return {
401
+ httpMethod: method.httpMethod,
402
+ authorizationType: method.authorizationType,
403
+ apiKeyRequired: method.apiKeyRequired
404
+ };
405
+ }
406
+
407
+ getMethod(params) {
408
+ const { restApiId, resourceId, httpMethod } = params;
409
+ const api = this.apis.get(restApiId);
410
+
411
+ if (!api) {
412
+ throw new Error(`API ${restApiId} not found`);
413
+ }
414
+
415
+ const resource = api.resources.get(resourceId);
416
+ if (!resource) {
417
+ throw new Error(`Resource ${resourceId} not found`);
418
+ }
419
+
420
+ const method = resource.resourceMethods.get(httpMethod);
421
+ if (!method) {
422
+ throw new Error(`Method ${httpMethod} not found`);
423
+ }
424
+
425
+ return {
426
+ httpMethod: method.httpMethod,
427
+ authorizationType: method.authorizationType,
428
+ apiKeyRequired: method.apiKeyRequired,
429
+ requestParameters: method.requestParameters,
430
+ requestModels: method.requestModels
431
+ };
432
+ }
433
+
434
+ deleteMethod(params) {
435
+ const { restApiId, resourceId, httpMethod } = params;
436
+ const api = this.apis.get(restApiId);
437
+
438
+ if (!api) {
439
+ throw new Error(`API ${restApiId} not found`);
440
+ }
441
+
442
+ const resource = api.resources.get(resourceId);
443
+ if (!resource) {
444
+ throw new Error(`Resource ${resourceId} not found`);
445
+ }
446
+
447
+ resource.resourceMethods.delete(httpMethod);
448
+ this.persistMethods(restApiId, resourceId);
449
+
450
+ return {};
451
+ }
452
+
453
+ // ============ Integration Operations ============
454
+
455
+ putIntegration(params) {
456
+ const { restApiId, resourceId, httpMethod, type, integrationHttpMethod, uri, credentials, requestParameters, requestTemplates, passthroughBehavior, timeoutInMillis, cacheNamespace, cacheKeyParameters, contentHandling } = params;
457
+ const api = this.apis.get(restApiId);
458
+
459
+ if (!api) {
460
+ throw new Error(`API ${restApiId} not found`);
461
+ }
462
+
463
+ const resource = api.resources.get(resourceId);
464
+ if (!resource) {
465
+ throw new Error(`Resource ${resourceId} not found`);
466
+ }
467
+
468
+ const method = resource.resourceMethods.get(httpMethod);
469
+ if (!method) {
470
+ throw new Error(`Method ${httpMethod} not found`);
471
+ }
472
+
473
+ const integration = {
474
+ type: type || 'HTTP',
475
+ integrationHttpMethod: integrationHttpMethod,
476
+ uri: uri,
477
+ credentials: credentials || null,
478
+ requestParameters: requestParameters || {},
479
+ requestTemplates: requestTemplates || {},
480
+ passthroughBehavior: passthroughBehavior || 'WHEN_NO_MATCH',
481
+ timeoutInMillis: timeoutInMillis || 29000,
482
+ cacheNamespace: cacheNamespace || '',
483
+ cacheKeyParameters: cacheKeyParameters || [],
484
+ contentHandling: contentHandling || null,
485
+ integrationResponses: new Map()
486
+ };
487
+
488
+ method.integration = integration;
489
+ this.persistIntegrations(restApiId, resourceId);
490
+
491
+ logger.debug(`🔌 Integração criada: ${type} -> ${uri}`);
492
+
493
+ return {
494
+ type: integration.type,
495
+ integrationHttpMethod: integration.integrationHttpMethod,
496
+ uri: integration.uri
497
+ };
498
+ }
499
+
500
+ getIntegration(params) {
501
+ const { restApiId, resourceId, httpMethod } = params;
502
+ const api = this.apis.get(restApiId);
503
+
504
+ if (!api) {
505
+ throw new Error(`API ${restApiId} not found`);
506
+ }
507
+
508
+ const resource = api.resources.get(resourceId);
509
+ if (!resource) {
510
+ throw new Error(`Resource ${resourceId} not found`);
511
+ }
512
+
513
+ const method = resource.resourceMethods.get(httpMethod);
514
+ if (!method || !method.integration) {
515
+ throw new Error(`Integration not found for ${httpMethod}`);
516
+ }
517
+
518
+ const integration = method.integration;
519
+
520
+ return {
521
+ type: integration.type,
522
+ integrationHttpMethod: integration.integrationHttpMethod,
523
+ uri: integration.uri,
524
+ credentials: integration.credentials,
525
+ requestParameters: integration.requestParameters,
526
+ requestTemplates: integration.requestTemplates,
527
+ passthroughBehavior: integration.passthroughBehavior,
528
+ timeoutInMillis: integration.timeoutInMillis
529
+ };
530
+ }
531
+
532
+ deleteIntegration(params) {
533
+ const { restApiId, resourceId, httpMethod } = params;
534
+ const api = this.apis.get(restApiId);
535
+
536
+ if (!api) {
537
+ throw new Error(`API ${restApiId} not found`);
538
+ }
539
+
540
+ const resource = api.resources.get(resourceId);
541
+ if (!resource) {
542
+ throw new Error(`Resource ${resourceId} not found`);
543
+ }
544
+
545
+ const method = resource.resourceMethods.get(httpMethod);
546
+ if (method) {
547
+ method.integration = null;
548
+ this.persistIntegrations(restApiId, resourceId);
549
+ }
550
+
551
+ return {};
552
+ }
553
+
554
+ // ============ Integration Response Operations ============
555
+
556
+ putIntegrationResponse(params) {
557
+ const { restApiId, resourceId, httpMethod, statusCode, selectionPattern, responseParameters, responseTemplates, contentHandling } = params;
558
+ const api = this.apis.get(restApiId);
559
+
560
+ if (!api) {
561
+ throw new Error(`API ${restApiId} not found`);
562
+ }
563
+
564
+ const resource = api.resources.get(resourceId);
565
+ if (!resource) {
566
+ throw new Error(`Resource ${resourceId} not found`);
567
+ }
568
+
569
+ const method = resource.resourceMethods.get(httpMethod);
570
+ if (!method || !method.integration) {
571
+ throw new Error(`Integration not found for ${httpMethod}`);
572
+ }
573
+
574
+ const integrationResponse = {
575
+ statusCode: statusCode,
576
+ selectionPattern: selectionPattern || '',
577
+ responseParameters: responseParameters || {},
578
+ responseTemplates: responseTemplates || {},
579
+ contentHandling: contentHandling || null
580
+ };
581
+
582
+ method.integration.integrationResponses.set(statusCode, integrationResponse);
583
+ this.persistIntegrations(restApiId, resourceId);
584
+
585
+ return {
586
+ statusCode: integrationResponse.statusCode,
587
+ selectionPattern: integrationResponse.selectionPattern
588
+ };
589
+ }
590
+
591
+ // ============ Deployment Operations ============
592
+
593
+ createDeployment(params) {
594
+ const { restApiId, stageName, stageDescription, description, variables } = params;
595
+ const api = this.apis.get(restApiId);
596
+
597
+ if (!api) {
598
+ throw new Error(`API ${restApiId} not found`);
599
+ }
600
+
601
+ const deploymentId = uuidv4();
602
+ const deployment = {
603
+ id: deploymentId,
604
+ description: description || '',
605
+ createdDate: new Date().toISOString(),
606
+ apiId: restApiId
607
+ };
608
+
609
+ api.deployments.set(deploymentId, deployment);
610
+
611
+ if (stageName) {
612
+ this.createStage({
613
+ restApiId,
614
+ stageName,
615
+ description: stageDescription,
616
+ variables
617
+ });
618
+ }
619
+
620
+ this.persistDeployments(restApiId);
621
+
622
+ logger.debug(`🚀 Deployment criado: ${deploymentId} para stage: ${stageName || 'N/A'}`);
623
+
624
+ return {
625
+ id: deploymentId,
626
+ createdDate: deployment.createdDate
627
+ };
628
+ }
629
+
630
+ // ============ Stage Operations ============
631
+
632
+ createStage(params) {
633
+ const { restApiId, stageName, description, variables, deploymentId, cacheClusterEnabled, cacheClusterSize, tracingEnabled } = params;
634
+ const api = this.apis.get(restApiId);
635
+
636
+ if (!api) {
637
+ throw new Error(`API ${restApiId} not found`);
638
+ }
639
+
640
+ const stage = {
641
+ stageName: stageName,
642
+ description: description || '',
643
+ createdDate: new Date().toISOString(),
644
+ lastUpdatedDate: new Date().toISOString(),
645
+ deploymentId: deploymentId,
646
+ variables: variables || {},
647
+ cacheClusterEnabled: cacheClusterEnabled || false,
648
+ cacheClusterSize: cacheClusterSize || null,
649
+ tracingEnabled: tracingEnabled || false,
650
+ methodSettings: new Map()
651
+ };
652
+
653
+ api.stages.set(stageName, stage);
654
+ this.persistStages(restApiId);
655
+
656
+ // Cria endpoint URL
657
+ const endpointUrl = `http://localhost:${this.config.ports.apigateway}/${restApiId}/${stageName}`;
658
+
659
+ logger.debug(`📡 Stage criado: ${stageName} em ${endpointUrl}`);
660
+
661
+ return {
662
+ stageName: stage.stageName,
663
+ createdDate: stage.createdDate,
664
+ deploymentId: stage.deploymentId
665
+ };
666
+ }
667
+
668
+ getStage(params) {
669
+ const { restApiId, stageName } = params;
670
+ const api = this.apis.get(restApiId);
671
+
672
+ if (!api) {
673
+ throw new Error(`API ${restApiId} not found`);
674
+ }
675
+
676
+ const stage = api.stages.get(stageName);
677
+ if (!stage) {
678
+ throw new Error(`Stage ${stageName} not found`);
679
+ }
680
+
681
+ return {
682
+ stageName: stage.stageName,
683
+ description: stage.description,
684
+ createdDate: stage.createdDate,
685
+ lastUpdatedDate: stage.lastUpdatedDate,
686
+ deploymentId: stage.deploymentId,
687
+ variables: stage.variables,
688
+ methodSettings: Object.fromEntries(stage.methodSettings),
689
+ cacheClusterEnabled: stage.cacheClusterEnabled,
690
+ tracingEnabled: stage.tracingEnabled
691
+ };
692
+ }
693
+
694
+ updateStage(params) {
695
+ const { restApiId, stageName, description, variables, deploymentId, tracingEnabled } = params;
696
+ const api = this.apis.get(restApiId);
697
+
698
+ if (!api) {
699
+ throw new Error(`API ${restApiId} not found`);
700
+ }
701
+
702
+ const stage = api.stages.get(stageName);
703
+ if (!stage) {
704
+ throw new Error(`Stage ${stageName} not found`);
705
+ }
706
+
707
+ if (description !== undefined) stage.description = description;
708
+ if (variables !== undefined) stage.variables = { ...stage.variables, ...variables };
709
+ if (deploymentId !== undefined) stage.deploymentId = deploymentId;
710
+ if (tracingEnabled !== undefined) stage.tracingEnabled = tracingEnabled;
711
+
712
+ stage.lastUpdatedDate = new Date().toISOString();
713
+ this.persistStages(restApiId);
714
+
715
+ return {
716
+ stageName: stage.stageName,
717
+ description: stage.description,
718
+ lastUpdatedDate: stage.lastUpdatedDate
719
+ };
720
+ }
721
+
722
+ deleteStage(params) {
723
+ const { restApiId, stageName } = params;
724
+ const api = this.apis.get(restApiId);
725
+
726
+ if (!api) {
727
+ throw new Error(`API ${restApiId} not found`);
728
+ }
729
+
730
+ api.stages.delete(stageName);
731
+ this.persistStages(restApiId);
732
+
733
+ return {};
734
+ }
735
+
736
+ // ============ API Key Operations ============
737
+
738
+ createApiKey(params) {
739
+ const { name, description, enabled, stageKeys, customerId } = params;
740
+
741
+ const apiKeyId = uuidv4();
742
+ const apiKey = {
743
+ id: apiKeyId,
744
+ name: name || '',
745
+ description: description || '',
746
+ enabled: enabled !== false,
747
+ value: crypto.randomBytes(20).toString('hex'),
748
+ stageKeys: stageKeys || [],
749
+ customerId: customerId || null,
750
+ createdDate: new Date().toISOString(),
751
+ lastUpdatedDate: new Date().toISOString()
752
+ };
753
+
754
+ this.apiKeys.set(apiKeyId, apiKey);
755
+ this.persistApiKeys();
756
+
757
+ logger.debug(`🔑 API Key criada: ${name || apiKeyId}`);
758
+
759
+ return {
760
+ id: apiKey.id,
761
+ name: apiKey.name,
762
+ value: apiKey.value,
763
+ enabled: apiKey.enabled
764
+ };
765
+ }
766
+
767
+ getApiKeys(params) {
768
+ const items = Array.from(this.apiKeys.values()).map(key => ({
769
+ id: key.id,
770
+ name: key.name,
771
+ description: key.description,
772
+ enabled: key.enabled,
773
+ createdDate: key.createdDate,
774
+ lastUpdatedDate: key.lastUpdatedDate
775
+ }));
776
+
777
+ return { items };
778
+ }
779
+
780
+ // ============ Usage Plan Operations ============
781
+
782
+ createUsagePlan(params) {
783
+ const { name, description, apiStages, throttle, quota } = params;
784
+
785
+ const usagePlanId = uuidv4();
786
+ const usagePlan = {
787
+ id: usagePlanId,
788
+ name: name,
789
+ description: description || '',
790
+ apiStages: apiStages || [],
791
+ throttle: throttle || {
792
+ burstLimit: 100,
793
+ rateLimit: 10
794
+ },
795
+ quota: quota || {
796
+ limit: 10000,
797
+ period: 'DAY',
798
+ offset: 0
799
+ },
800
+ createdDate: new Date().toISOString(),
801
+ lastUpdatedDate: new Date().toISOString()
802
+ };
803
+
804
+ this.usagePlans.set(usagePlanId, usagePlan);
805
+ this.persistUsagePlans();
806
+
807
+ logger.debug(`📊 Usage Plan criado: ${name}`);
808
+
809
+ return {
810
+ id: usagePlan.id,
811
+ name: usagePlan.name,
812
+ createdDate: usagePlan.createdDate
813
+ };
814
+ }
815
+
816
+ // ============ Request Execution ============
817
+
818
+ async executeRequest(apiId, stageName, method, path, headers, body, queryString) {
819
+ const api = this.apis.get(apiId);
820
+ if (!api) {
821
+ return this.createResponse(404, 'API not found');
822
+ }
823
+
824
+ const stage = api.stages.get(stageName);
825
+ if (!stage) {
826
+ return this.createResponse(404, 'Stage not found');
827
+ }
828
+
829
+ // Encontra o recurso e método correspondente
830
+ const { resource, methodDef } = this.findMatchingResource(api, method, path);
831
+
832
+ if (!resource || !methodDef) {
833
+ return this.createResponse(404, 'Resource or method not found');
834
+ }
835
+
836
+ // Verifica API Key
837
+ if (methodDef.apiKeyRequired) {
838
+ const apiKey = this.extractApiKey(headers);
839
+ if (!this.validateApiKey(apiKey)) {
840
+ return this.createResponse(403, 'Forbidden: Invalid API Key');
841
+ }
842
+ }
843
+
844
+ // Verifica throttling
845
+ const usagePlan = this.getUsagePlanForApi(apiId, stageName);
846
+ if (usagePlan && !this.checkThrottle(usagePlan)) {
847
+ return this.createResponse(429, 'Too Many Requests');
848
+ }
849
+
850
+ // Processa integração
851
+ const integration = methodDef.integration;
852
+ if (!integration) {
853
+ return this.createResponse(500, 'Integration not configured');
854
+ }
855
+
856
+ // Executa a integração baseada no tipo
857
+ const response = await this.executeIntegration(integration, {
858
+ method,
859
+ path,
860
+ headers,
861
+ body,
862
+ queryString,
863
+ resource,
864
+ stage
865
+ });
866
+
867
+ // Aplica resposta da integração
868
+ const integrationResponse = this.getMatchingIntegrationResponse(
869
+ integration.integrationResponses,
870
+ response.statusCode
871
+ );
872
+
873
+ if (integrationResponse) {
874
+ response.headers = { ...response.headers, ...integrationResponse.responseParameters };
875
+ response.body = this.applyResponseTemplate(integrationResponse, response.body);
876
+ }
877
+
878
+ this.audit.record({
879
+ eventName: 'Invoke',
880
+ readOnly: method === 'GET' || method === 'HEAD',
881
+ isDataEvent: true,
882
+ resources: [{ ARN: `arn:aws:execute-api:local:000000000000:${apiId}/${stageName}/${method}${path}`, type: 'AWS::APIGateway::Stage' }],
883
+ requestParameters: { apiId, stageName, method, path },
884
+ });
885
+
886
+ return response;
887
+ }
888
+
889
+ findMatchingResource(api, method, path) {
890
+ // Busca recurso que corresponda ao path
891
+ const resources = Array.from(api.resources.values());
892
+
893
+ // Ordena por path mais específico primeiro
894
+ resources.sort((a, b) => b.path.length - a.path.length);
895
+
896
+ for (const resource of resources) {
897
+ if (this.matchPath(resource.path, path)) {
898
+ const methodDef = resource.resourceMethods.get(method);
899
+ if (methodDef) {
900
+ return { resource, methodDef };
901
+ }
902
+ }
903
+ }
904
+
905
+ return { resource: null, methodDef: null };
906
+ }
907
+
908
+ matchPath(pattern, path) {
909
+ // Converte path com parâmetros para regex
910
+ // Ex: /users/{userId}/posts/{postId}
911
+ const regexPattern = pattern
912
+ .replace(/\{([^}]+)\}/g, '([^/]+)')
913
+ .replace(/\//g, '\\/');
914
+
915
+ const regex = new RegExp(`^${regexPattern}$`);
916
+ return regex.test(path);
917
+ }
918
+
919
+ extractPathParams(pattern, path) {
920
+ const paramNames = [];
921
+ const regexPattern = pattern
922
+ .replace(/\{([^}]+)\}/g, (match, paramName) => {
923
+ paramNames.push(paramName);
924
+ return '([^/]+)';
925
+ })
926
+ .replace(/\//g, '\\/');
927
+
928
+ const regex = new RegExp(`^${regexPattern}$`);
929
+ const match = path.match(regex);
930
+
931
+ if (match) {
932
+ const params = {};
933
+ paramNames.forEach((name, index) => {
934
+ params[name] = match[index + 1];
935
+ });
936
+ return params;
937
+ }
938
+
939
+ return {};
940
+ }
941
+
942
+ async executeIntegration(integration, context) {
943
+ const { type, uri, integrationHttpMethod, requestTemplates, requestParameters } = integration;
944
+
945
+ // Prepara o request
946
+ let requestBody = context.body;
947
+ let requestHeaders = { ...context.headers };
948
+
949
+ // Aplica templates de request
950
+ if (requestTemplates && requestTemplates['application/json']) {
951
+ const template = requestTemplates['application/json'];
952
+ requestBody = this.applyRequestTemplate(template, context);
953
+ }
954
+
955
+ // Aplica mapeamento de parâmetros
956
+ if (requestParameters) {
957
+ for (const [key, value] of Object.entries(requestParameters)) {
958
+ requestHeaders[key] = this.resolveParameter(value, context);
959
+ }
960
+ }
961
+
962
+ switch(type) {
963
+ case 'AWS':
964
+ case 'AWS_PROXY':
965
+ return this.executeAWSIntegration(integration, context, requestBody, requestHeaders);
966
+ case 'HTTP':
967
+ case 'HTTP_PROXY':
968
+ return this.executeHTTPIntegration(integration, context, requestBody, requestHeaders);
969
+ case 'MOCK':
970
+ return this.executeMockIntegration(integration);
971
+ default:
972
+ return this.createResponse(501, `Integration type ${type} not supported`);
973
+ }
974
+ }
975
+
976
+ async executeAWSIntegration(integration, context, body, headers) {
977
+ // Simula integração com Lambda
978
+ const uriParts = integration.uri.split(':');
979
+ const functionName = uriParts[uriParts.length - 1];
980
+
981
+ // Aqui poderia chamar o simulador Lambda
982
+ logger.debug(`Invoking Lambda: ${functionName}`);
983
+
984
+ return this.createResponse(200, {
985
+ message: `Lambda ${functionName} invoked`,
986
+ event: context
987
+ });
988
+ }
989
+
990
+ async executeHTTPIntegration(integration, context, body, headers) {
991
+ const { uri, integrationHttpMethod } = integration;
992
+
993
+ logger.debug(`HTTP ${integrationHttpMethod} to ${uri}`);
994
+
995
+ // Simula chamada HTTP
996
+ try {
997
+ const response = await this.makeHttpRequest(uri, integrationHttpMethod, headers, body);
998
+ return {
999
+ statusCode: response.status,
1000
+ headers: response.headers,
1001
+ body: response.data
1002
+ };
1003
+ } catch (error) {
1004
+ return this.createResponse(502, 'Bad Gateway');
1005
+ }
1006
+ }
1007
+
1008
+ async makeHttpRequest(url, method, headers, body) {
1009
+ // Simulação - em implementação real, usaria axios ou fetch
1010
+ return {
1011
+ status: 200,
1012
+ headers: {},
1013
+ data: { message: 'Mock HTTP response' }
1014
+ };
1015
+ }
1016
+
1017
+ executeMockIntegration(integration) {
1018
+ // Retorna resposta mock
1019
+ const mockResponse = integration.requestTemplates?.mock || {};
1020
+ return this.createResponse(200, mockResponse);
1021
+ }
1022
+
1023
+ getMatchingIntegrationResponse(responses, statusCode) {
1024
+ // Busca response que corresponda ao status code
1025
+ if (responses.has(statusCode.toString())) {
1026
+ return responses.get(statusCode.toString());
1027
+ }
1028
+
1029
+ // Busca default
1030
+ if (responses.has('default')) {
1031
+ return responses.get('default');
1032
+ }
1033
+
1034
+ return null;
1035
+ }
1036
+
1037
+ applyRequestTemplate(template, context) {
1038
+ // Implementação simples de template
1039
+ try {
1040
+ return JSON.stringify(context);
1041
+ } catch (error) {
1042
+ return template;
1043
+ }
1044
+ }
1045
+
1046
+ applyResponseTemplate(response, body) {
1047
+ const templates = response.responseTemplates;
1048
+ if (templates && templates['application/json']) {
1049
+ // Aplica template de resposta
1050
+ try {
1051
+ return JSON.parse(templates['application/json'].replace(/\$\{([^}]+)\}/g, (match, path) => {
1052
+ return this.getNestedValue(body, path);
1053
+ }));
1054
+ } catch (error) {
1055
+ return body;
1056
+ }
1057
+ }
1058
+ return body;
1059
+ }
1060
+
1061
+ resolveParameter(value, context) {
1062
+ // Resolve parâmetros como method.request.header.X-Header
1063
+ if (value.includes('method.request.')) {
1064
+ const parts = value.split('.');
1065
+ const type = parts[2]; // header, querystring, path
1066
+ const name = parts[3];
1067
+
1068
+ switch(type) {
1069
+ case 'header':
1070
+ return context.headers[name];
1071
+ case 'querystring':
1072
+ return context.queryString[name];
1073
+ case 'path':
1074
+ return context.pathParams[name];
1075
+ default:
1076
+ return null;
1077
+ }
1078
+ }
1079
+ return value;
1080
+ }
1081
+
1082
+ getNestedValue(obj, path) {
1083
+ return path.split('.').reduce((current, key) => current?.[key], obj);
1084
+ }
1085
+
1086
+ extractApiKey(headers) {
1087
+ return headers['x-api-key'] || headers['X-Api-Key'];
1088
+ }
1089
+
1090
+ validateApiKey(apiKeyValue) {
1091
+ if (!apiKeyValue) return false;
1092
+
1093
+ for (const key of this.apiKeys.values()) {
1094
+ if (key.value === apiKeyValue && key.enabled) {
1095
+ return true;
1096
+ }
1097
+ }
1098
+ return false;
1099
+ }
1100
+
1101
+ getUsagePlanForApi(apiId, stageName) {
1102
+ for (const plan of this.usagePlans.values()) {
1103
+ const apiStage = plan.apiStages.find(
1104
+ as => as.apiId === apiId && as.stage === stageName
1105
+ );
1106
+ if (apiStage) {
1107
+ return plan;
1108
+ }
1109
+ }
1110
+ return null;
1111
+ }
1112
+
1113
+ checkThrottle(usagePlan) {
1114
+ // Implementação simplificada de throttling
1115
+ const { rateLimit, burstLimit } = usagePlan.throttle;
1116
+ // Aqui poderia implementar contagem de requests
1117
+ return true;
1118
+ }
1119
+
1120
+ createResponse(statusCode, body) {
1121
+ return {
1122
+ statusCode: statusCode,
1123
+ headers: {
1124
+ 'Content-Type': 'application/json',
1125
+ 'Access-Control-Allow-Origin': '*'
1126
+ },
1127
+ body: typeof body === 'string' ? body : JSON.stringify(body)
1128
+ };
1129
+ }
1130
+
1131
+ // ============ HTTP API Operations ============
1132
+
1133
+ createHttpApi(params) {
1134
+ const { name, description, protocolType, routeSelectionExpression, corsConfiguration } = params;
1135
+
1136
+ const apiId = `http_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
1137
+ const api = {
1138
+ id: apiId,
1139
+ name: name,
1140
+ description: description || '',
1141
+ protocolType: protocolType || 'HTTP',
1142
+ routeSelectionExpression: routeSelectionExpression || '$request.method $request.path',
1143
+ corsConfiguration: corsConfiguration || {
1144
+ allowOrigins: ['*'],
1145
+ allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
1146
+ allowHeaders: ['*'],
1147
+ maxAge: 300
1148
+ },
1149
+ routes: new Map(),
1150
+ integrations: new Map(),
1151
+ stages: new Map(),
1152
+ createdDate: new Date().toISOString()
1153
+ };
1154
+
1155
+ this.apis.set(apiId, api);
1156
+ this.persistAPIs();
1157
+
1158
+ logger.debug(`✅ HTTP API criada: ${name} (${apiId})`);
1159
+
1160
+ return {
1161
+ ApiId: apiId,
1162
+ Name: api.name,
1163
+ ProtocolType: api.protocolType
1164
+ };
1165
+ }
1166
+
1167
+ createRoute(params) {
1168
+ const { apiId, routeKey, authorizationType, target } = params;
1169
+ const api = this.apis.get(apiId);
1170
+
1171
+ if (!api) {
1172
+ throw new Error(`API ${apiId} not found`);
1173
+ }
1174
+
1175
+ const route = {
1176
+ routeKey: routeKey,
1177
+ authorizationType: authorizationType || 'NONE',
1178
+ target: target,
1179
+ createdAt: new Date().toISOString()
1180
+ };
1181
+
1182
+ api.routes.set(routeKey, route);
1183
+ this.persistAPIs();
1184
+
1185
+ return { route };
1186
+ }
1187
+
1188
+ // ============ Persistence ============
1189
+
1190
+ loadAPIs() {
1191
+ const saved = this.store.read('__apis__');
1192
+ if (saved) {
1193
+ for (const [id, data] of Object.entries(saved)) {
1194
+ // Reconstitui Maps
1195
+ data.resources = new Map(Object.entries(data.resources || {}).map(([rid, r]) => {
1196
+ r.resourceMethods = new Map(Object.entries(r.resourceMethods || {}));
1197
+ return [rid, r];
1198
+ }));
1199
+ data.stages = new Map(Object.entries(data.stages || {}));
1200
+ data.deployments = new Map(Object.entries(data.deployments || {}));
1201
+ data.models = new Map(Object.entries(data.models || {}));
1202
+ data.authorizers = new Map(Object.entries(data.authorizers || {}));
1203
+ this.apis.set(id, data);
1204
+ }
1205
+ }
1206
+ }
1207
+
1208
+
1209
+ loadWebSocketAPIs() {
1210
+ const saved = this.store.read('__websocket_apis__');
1211
+ if (saved) {
1212
+ for (const [id, data] of Object.entries(saved)) {
1213
+ this.websocketApis.set(id, data);
1214
+ }
1215
+ }
1216
+ }
1217
+
1218
+ loadDeployments() {
1219
+ const saved = this.store.read('__deployments__');
1220
+ if (saved) {
1221
+ for (const [id, data] of Object.entries(saved)) {
1222
+ this.deployments.set(id, data);
1223
+ }
1224
+ }
1225
+ }
1226
+
1227
+ loadStages() {
1228
+ const saved = this.store.read('__stages__');
1229
+ if (saved) {
1230
+ for (const [id, data] of Object.entries(saved)) {
1231
+ this.stages.set(id, data);
1232
+ }
1233
+ }
1234
+ }
1235
+
1236
+ loadResources() {
1237
+ const saved = this.store.read('__resources__');
1238
+ if (saved) {
1239
+ for (const [id, data] of Object.entries(saved)) {
1240
+ this.resources.set(id, data);
1241
+ }
1242
+ }
1243
+ }
1244
+
1245
+ loadMethods() {
1246
+ const saved = this.store.read('__methods__');
1247
+ if (saved) {
1248
+ for (const [id, data] of Object.entries(saved)) {
1249
+ this.methods.set(id, data);
1250
+ }
1251
+ }
1252
+ }
1253
+
1254
+ loadIntegrations() {
1255
+ const saved = this.store.read('__integrations__');
1256
+ if (saved) {
1257
+ for (const [id, data] of Object.entries(saved)) {
1258
+ this.integrations.set(id, data);
1259
+ }
1260
+ }
1261
+ }
1262
+
1263
+ loadModels() {
1264
+ const saved = this.store.read('__models__');
1265
+ if (saved) {
1266
+ for (const [id, data] of Object.entries(saved)) {
1267
+ this.models.set(id, data);
1268
+ }
1269
+ }
1270
+ }
1271
+
1272
+ loadUsagePlans() {
1273
+ const saved = this.store.read('__usage_plans__');
1274
+ if (saved) {
1275
+ for (const [id, data] of Object.entries(saved)) {
1276
+ this.usagePlans.set(id, data);
1277
+ }
1278
+ }
1279
+ }
1280
+
1281
+ loadApiKeys() {
1282
+ const saved = this.store.read('__api_keys__');
1283
+ if (saved) {
1284
+ for (const [id, data] of Object.entries(saved)) {
1285
+ this.apiKeys.set(id, data);
1286
+ }
1287
+ }
1288
+ }
1289
+
1290
+ loadDomainNames() {
1291
+ const saved = this.store.read('__domain_names__');
1292
+ if (saved) {
1293
+ for (const [id, data] of Object.entries(saved)) {
1294
+ this.domainNames.set(id, data);
1295
+ }
1296
+ }
1297
+ }
1298
+
1299
+ persistAPIs() {
1300
+ const apisObj = {};
1301
+ for (const [id, api] of this.apis.entries()) {
1302
+ if (api.isStatic) continue;
1303
+ apisObj[id] = {
1304
+ ...api,
1305
+ resources: Object.fromEntries(Array.from(api.resources.entries()).map(([rid, r]) => [rid, { ...r, resourceMethods: Object.fromEntries(r.resourceMethods) }])),
1306
+ stages: Object.fromEntries(api.stages),
1307
+ deployments: Object.fromEntries(api.deployments),
1308
+ models: Object.fromEntries(api.models),
1309
+ authorizers: Object.fromEntries(api.authorizers)
1310
+ };
1311
+ }
1312
+ this.store.write('__apis__', apisObj);
1313
+ }
1314
+
1315
+
1316
+ persistResources(apiId) {
1317
+ const api = this.apis.get(apiId);
1318
+ if (api) {
1319
+ this.persistAPIs();
1320
+ }
1321
+ }
1322
+
1323
+ persistMethods(apiId, resourceId) {
1324
+ this.persistResources(apiId);
1325
+ }
1326
+
1327
+ persistIntegrations(apiId, resourceId) {
1328
+ this.persistMethods(apiId, resourceId);
1329
+ }
1330
+
1331
+ persistStages(apiId) {
1332
+ this.persistAPIs();
1333
+ }
1334
+
1335
+ persistDeployments(apiId) {
1336
+ this.persistAPIs();
1337
+ }
1338
+
1339
+ persistApiKeys() {
1340
+ const keysObj = {};
1341
+ for (const [id, key] of this.apiKeys.entries()) {
1342
+ keysObj[id] = key;
1343
+ }
1344
+ this.store.write('__api_keys__', keysObj);
1345
+ }
1346
+
1347
+ persistUsagePlans() {
1348
+ const plansObj = {};
1349
+ for (const [id, plan] of this.usagePlans.entries()) {
1350
+ plansObj[id] = plan;
1351
+ }
1352
+ this.store.write('__usage_plans__', plansObj);
1353
+ }
1354
+
1355
+ async reset() {
1356
+ this.apis.clear();
1357
+ this.websocketApis.clear();
1358
+ this.deployments.clear();
1359
+ this.stages.clear();
1360
+ this.resources.clear();
1361
+ this.methods.clear();
1362
+ this.integrations.clear();
1363
+ this.models.clear();
1364
+ this.usagePlans.clear();
1365
+ this.apiKeys.clear();
1366
+ this.domainNames.clear();
1367
+
1368
+ this.persistAPIs();
1369
+ this.persistApiKeys();
1370
+ this.persistUsagePlans();
1371
+
1372
+ logger.debug('API Gateway: Todos os dados resetados');
1373
+ }
1374
+
1375
+ // ============ Stats ============
1376
+
1377
+ getAPIsCount() {
1378
+ return this.apis.size;
1379
+ }
1380
+
1381
+ getDeploymentsCount() {
1382
+ let count = 0;
1383
+ for (const api of this.apis.values()) {
1384
+ count += api.deployments.size;
1385
+ }
1386
+ return count;
1387
+ }
1388
+
1389
+ getStagesCount() {
1390
+ let count = 0;
1391
+ for (const api of this.apis.values()) {
1392
+ count += api.stages.size;
1393
+ }
1394
+ return count;
1395
+ }
1396
+
1397
+ getResourcesCount() {
1398
+ let count = 0;
1399
+ for (const api of this.apis.values()) {
1400
+ count += api.resources.size;
1401
+ }
1402
+ return count;
1403
+ }
1404
+ }
1405
+
1406
1406
  module.exports = APIGatewaySimulator;