infrawise 0.10.3 → 0.11.0

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 CHANGED
@@ -156,7 +156,8 @@ Add to your editor's MCP config:
156
156
  | `postgres_index_suggestions` | Exact `CREATE INDEX` SQL for your actual table |
157
157
  | `suggest_mongo_index` | Exact `createIndex` command for a MongoDB collection + field |
158
158
  | `mysql_index_suggestions` | Exact `ALTER TABLE ADD INDEX` SQL for your MySQL table |
159
- | `get_queue_details` | SQS queues — DLQ status, encryption, message counts |
159
+ | `get_queue_details` | SQS queues — DLQ status, encryption, FIFO type, visibility timeout, message counts |
160
+ | `get_api_routes` | API Gateway APIs (REST, HTTP, WebSocket) — routes, HTTP methods, paths, and Lambda integrations |
160
161
  | `get_topic_details` | SNS topics — subscription counts, protocols, and filter policies (required message attributes per subscription) |
161
162
  | `get_secrets_overview` | Secrets Manager — names and rotation status (values never included) |
162
163
  | `get_parameter_overview` | SSM Parameter Store — names, types, tiers (values never included) |
@@ -274,6 +275,9 @@ s3:
274
275
  kafka:
275
276
  enabled: false
276
277
 
278
+ apiGateway:
279
+ enabled: false
280
+
277
281
  cloudwatchLogs:
278
282
  enabled: false
279
283
  logGroupPrefixes: []
@@ -281,6 +285,9 @@ cloudwatchLogs:
281
285
 
282
286
  analysis:
283
287
  sampleSize: 100
288
+ hotPartitionThreshold: 5
289
+ hotPartitionThresholds:
290
+ high-traffic-table: 12
284
291
  ```
285
292
 
286
293
  ### AWS setup
@@ -334,13 +341,14 @@ Works from AWS APIs, database schema introspection, and IaC files — no depende
334
341
  | DynamoDB schema | Tables, GSIs, partition keys |
335
342
  | PostgreSQL / MySQL schema | Tables, indexes, column types |
336
343
  | MongoDB schema | Collections, indexes |
337
- | SQS | Missing DLQs, unencrypted queues, large backlogs |
344
+ | SQS | Missing DLQs, unencrypted queues, large backlogs, FIFO detection, visibility timeout vs Lambda timeout mismatch |
338
345
  | SNS | Subscription filter policies — required message attributes per subscription |
339
346
  | Kafka (kafkajs) | Producer/consumer topic mapping from code |
340
347
  | Secrets Manager | Missing secret rotation |
341
348
  | Lambda | Default memory (128 MB), high timeouts, triggers (SQS/DynamoDB/Kinesis/EventBridge/S3), missing DLQ on trigger source |
342
349
  | S3 | Public access blocking (verify), missing versioning, missing encryption |
343
350
  | EventBridge | Rules, schedules, event patterns, target Lambda functions |
351
+ | API Gateway | REST, HTTP, and WebSocket APIs — routes, methods, Lambda integrations |
344
352
  | RDS | Publicly accessible, no backups, unencrypted, no deletion protection, single-AZ |
345
353
  | CloudWatch Logs | Log groups with no retention policy |
346
354
  | Terraform / CloudFormation / CDK | IaC drift vs deployed state |
@@ -361,6 +369,9 @@ Uses [ts-morph](https://ts-morph.com/) AST analysis to detect which functions ca
361
369
  | MySQL Full Table Scan | High | Full table scan patterns in MySQL queries |
362
370
  | Missing Mongo Index | Medium | Collections queried without secondary indexes |
363
371
  | Collection Scan | High | `find()` calls without filter predicates |
372
+ | Pipeline: scan in consumer | High / Verify | Full scan inside an event-triggered Lambda handler (High when the lambda-to-code link is IaC-proven, Verify when name-matched) |
373
+ | Pipeline: repeated table access | Medium / Verify | Same table read by 2+ functions in one service pipeline |
374
+ | Pipeline: missing DLQ hop | Medium | Mid-pipeline queue (has producer and consumer) with no Dead Letter Queue |
364
375
 
365
376
  Non-TypeScript/JavaScript projects still get full value from infrastructure-level analyzers — code correlation (function-to-table mapping, N+1 patterns) is skipped.
366
377
 
@@ -409,10 +420,10 @@ src/
409
420
  core/ Config (Zod + YAML), logger (Pino), local cache
410
421
  graph/ Graph engine — nodes, edges, builder
411
422
  adapters/
412
- aws/ DynamoDB, S3, Lambda, SQS/SNS/SSM/Secrets/EventBridge/RDS, CloudWatch
423
+ aws/ DynamoDB, S3, Lambda, SQS/SNS/SSM/Secrets/EventBridge/RDS/APIGateway, CloudWatch
413
424
  db/ PostgreSQL, MySQL, MongoDB
414
425
  iac/ Terraform, CDK, CloudFormation (local file parsing)
415
- analyzers/ 27 rule-based analyzers
426
+ analyzers/ 29 rule-based analyzers
416
427
  context/ Repository scanner (ts-morph AST)
417
428
  server/ Fastify MCP server (@modelcontextprotocol/sdk, Streamable HTTP)
418
429
  cli/ CLI commands (Commander.js)
@@ -1,4 +1,6 @@
1
1
  import { SQSClient, ListQueuesCommand, GetQueueAttributesCommand } from '@aws-sdk/client-sqs';
2
+ import { APIGatewayClient, GetRestApisCommand, GetResourcesCommand, } from '@aws-sdk/client-api-gateway';
3
+ import { ApiGatewayV2Client, GetApisCommand, GetRoutesCommand, GetIntegrationsCommand, } from '@aws-sdk/client-apigatewayv2';
2
4
  import { SNSClient, ListTopicsCommand, GetTopicAttributesCommand, ListSubscriptionsByTopicCommand, GetSubscriptionAttributesCommand, } from '@aws-sdk/client-sns';
3
5
  import { SSMClient, DescribeParametersCommand } from '@aws-sdk/client-ssm';
4
6
  import { SecretsManagerClient, ListSecretsCommand } from '@aws-sdk/client-secrets-manager';
@@ -66,6 +68,7 @@ export async function extractSQSMetadata(cfg = {}) {
66
68
  : undefined;
67
69
  const encrypted = !!(a['KmsMasterKeyId'] || a['SqsManagedSseEnabled'] === 'true');
68
70
  const retentionSeconds = parseInt(a['MessageRetentionPeriod'] ?? '345600', 10);
71
+ const isFifo = name.endsWith('.fifo') || a['FifoQueue'] === 'true';
69
72
  queues.push({
70
73
  name,
71
74
  url,
@@ -73,6 +76,7 @@ export async function extractSQSMetadata(cfg = {}) {
73
76
  hasDLQ: !!dlqArn,
74
77
  dlqArn,
75
78
  encrypted,
79
+ isFifo,
76
80
  visibilityTimeoutSec: parseInt(a['VisibilityTimeout'] ?? '30', 10),
77
81
  retentionDays: Math.round(retentionSeconds / 86400),
78
82
  approximateMessages: parseInt(a['ApproximateNumberOfMessages'] ?? '0', 10),
@@ -382,3 +386,105 @@ export async function extractRDSMetadata(cfg = {}) {
382
386
  export async function validateRDSAccess(cfg = {}) {
383
387
  await new RDSClient(clientConfig(cfg)).send(new DescribeDBInstancesCommand({ MaxRecords: 20 }));
384
388
  }
389
+ // ─── API Gateway ──────────────────────────────────────────────────────────────
390
+ function lambdaNameFromArn(arn) {
391
+ if (!arn)
392
+ return undefined;
393
+ const parts = arn.split(':');
394
+ return parts[parts.length - 1] || undefined;
395
+ }
396
+ export async function extractAPIGatewayMetadata(cfg = {}) {
397
+ const results = [];
398
+ const ccfg = clientConfig(cfg);
399
+ // REST APIs (v1)
400
+ try {
401
+ const restClient = new APIGatewayClient(ccfg);
402
+ let position;
403
+ const restApis = [];
404
+ do {
405
+ const res = await restClient.send(new GetRestApisCommand({ position, limit: 500 }));
406
+ for (const api of res.items ?? []) {
407
+ if (api.id && api.name)
408
+ restApis.push({ id: api.id, name: api.name });
409
+ }
410
+ position = res.position;
411
+ } while (position);
412
+ for (const api of restApis) {
413
+ const routes = [];
414
+ try {
415
+ const resourcesRes = await restClient.send(new GetResourcesCommand({ restApiId: api.id, embed: ['methods'], limit: 500 }));
416
+ for (const resource of resourcesRes.items ?? []) {
417
+ const resourcePath = resource.path ?? '/';
418
+ for (const [method, methodItem] of Object.entries(resource.resourceMethods ?? {})) {
419
+ if (method === 'OPTIONS')
420
+ continue;
421
+ const integration = methodItem
422
+ ?.methodIntegration;
423
+ const lambdaArn = typeof integration?.uri === 'string' ? integration.uri : undefined;
424
+ const lambdaName = lambdaArn
425
+ ? lambdaNameFromArn(lambdaArn.split('/functions/')[1]?.split('/')[0])
426
+ : undefined;
427
+ routes.push({ method, path: resourcePath, lambdaArn, lambdaName });
428
+ }
429
+ }
430
+ }
431
+ catch (err) {
432
+ logger.warn(`API Gateway REST resources failed for ${api.name}: ${err instanceof Error ? err.message : String(err)}`);
433
+ }
434
+ results.push({ name: api.name, id: api.id, type: 'REST', routes });
435
+ }
436
+ }
437
+ catch (err) {
438
+ logger.warn(`API Gateway REST list failed: ${err instanceof Error ? err.message : String(err)}`);
439
+ }
440
+ // HTTP + WebSocket APIs (v2)
441
+ try {
442
+ const v2Client = new ApiGatewayV2Client(ccfg);
443
+ let nextToken;
444
+ const v2Apis = [];
445
+ do {
446
+ const res = await v2Client.send(new GetApisCommand({ NextToken: nextToken, MaxResults: '500' }));
447
+ for (const api of res.Items ?? []) {
448
+ if (api.ApiId && api.Name) {
449
+ v2Apis.push({ id: api.ApiId, name: api.Name, protocolType: api.ProtocolType ?? 'HTTP' });
450
+ }
451
+ }
452
+ nextToken = res.NextToken;
453
+ } while (nextToken);
454
+ for (const api of v2Apis) {
455
+ const apiType = api.protocolType === 'WEBSOCKET' ? 'WEBSOCKET' : 'HTTP';
456
+ const routes = [];
457
+ try {
458
+ const [routesRes, integrationsRes] = await Promise.all([
459
+ v2Client.send(new GetRoutesCommand({ ApiId: api.id, MaxResults: '500' })),
460
+ v2Client.send(new GetIntegrationsCommand({ ApiId: api.id, MaxResults: '500' })),
461
+ ]);
462
+ const integrationMap = new Map();
463
+ for (const integ of integrationsRes.Items ?? []) {
464
+ if (integ.IntegrationId && integ.IntegrationUri) {
465
+ integrationMap.set(integ.IntegrationId, integ.IntegrationUri);
466
+ }
467
+ }
468
+ for (const route of routesRes.Items ?? []) {
469
+ const routeKey = route.RouteKey ?? '';
470
+ const [method, ...pathParts] = routeKey.split(' ');
471
+ const routePath = pathParts.join(' ') || '/';
472
+ const integrationId = route.Target?.replace('integrations/', '');
473
+ const lambdaArn = integrationId ? integrationMap.get(integrationId) : undefined;
474
+ const lambdaName = lambdaArn
475
+ ? lambdaNameFromArn(lambdaArn.split('/functions/')[1]?.split('/')[0])
476
+ : undefined;
477
+ routes.push({ method: method ?? routeKey, path: routePath, lambdaArn, lambdaName });
478
+ }
479
+ }
480
+ catch (err) {
481
+ logger.warn(`API Gateway v2 routes failed for ${api.name}: ${err instanceof Error ? err.message : String(err)}`);
482
+ }
483
+ results.push({ name: api.name, id: api.id, type: apiType, routes });
484
+ }
485
+ }
486
+ catch (err) {
487
+ logger.debug(`API Gateway v2 list failed: ${err instanceof Error ? err.message : String(err)}`);
488
+ }
489
+ return results;
490
+ }
@@ -135,6 +135,7 @@ export async function extractTerraformSchema(repoPath) {
135
135
  schema.lambdas.push({
136
136
  name: tfStr(body, 'function_name') ?? resourceName,
137
137
  runtime: tfStr(body, 'runtime'),
138
+ handler: tfStr(body, 'handler'),
138
139
  source: 'terraform',
139
140
  filePath,
140
141
  });
@@ -299,6 +300,7 @@ function processCFNResources(resources, schema, filePath, source) {
299
300
  schema.lambdas.push({
300
301
  name: cfnStr(props, 'FunctionName') ?? logicalId,
301
302
  runtime: cfnStr(props, 'Runtime'),
303
+ handler: cfnStr(props, 'Handler'),
302
304
  source,
303
305
  filePath,
304
306
  });
@@ -64,6 +64,42 @@ export class LargeQueueBacklogAnalyzer {
64
64
  return findings;
65
65
  }
66
66
  }
67
+ export class VisibilityTimeoutMismatchAnalyzer {
68
+ name = 'VisibilityTimeoutMismatchAnalyzer';
69
+ async analyze(graph) {
70
+ const findings = [];
71
+ const lambdaTimeouts = new Map();
72
+ for (const node of graph.nodes) {
73
+ if (node.type === 'lambda' && node.timeoutSec) {
74
+ lambdaTimeouts.set(node.name, node.timeoutSec);
75
+ }
76
+ }
77
+ for (const node of graph.nodes) {
78
+ if (node.type !== 'queue' || node.visibilityTimeoutSec === undefined)
79
+ continue;
80
+ const triggeringEdge = graph.edges.find((e) => e.type === 'triggers' && e.from === `queue:aws:${node.name}`);
81
+ if (!triggeringEdge)
82
+ continue;
83
+ const lambdaName = triggeringEdge.to.replace('lambda:aws:', '');
84
+ const lambdaTimeout = lambdaTimeouts.get(lambdaName);
85
+ if (lambdaTimeout && node.visibilityTimeoutSec < lambdaTimeout) {
86
+ findings.push({
87
+ severity: 'high',
88
+ issue: `Queue "${node.name}" visibility timeout (${node.visibilityTimeoutSec}s) is less than Lambda "${lambdaName}" timeout (${lambdaTimeout}s)`,
89
+ description: `If the Lambda takes longer than the visibility timeout, SQS will re-deliver the message to another consumer while the original invocation is still running, causing duplicate processing.`,
90
+ recommendation: `Set the visibility timeout for "${node.name}" to at least ${lambdaTimeout * 6}s (6× the Lambda timeout of ${lambdaTimeout}s), per AWS best practice.`,
91
+ metadata: {
92
+ queueName: node.name,
93
+ visibilityTimeoutSec: node.visibilityTimeoutSec,
94
+ lambdaName,
95
+ lambdaTimeoutSec: lambdaTimeout,
96
+ },
97
+ });
98
+ }
99
+ }
100
+ return findings;
101
+ }
102
+ }
67
103
  // ─── Secrets Manager ─────────────────────────────────────────────────────────
68
104
  export class MissingSecretRotationAnalyzer {
69
105
  name = 'MissingSecretRotationAnalyzer';
@@ -90,8 +90,10 @@ export class MissingGSIAnalyzer {
90
90
  export class HotPartitionAnalyzer {
91
91
  name = 'HotPartitionAnalyzer';
92
92
  hotThreshold;
93
- constructor(hotThreshold = 5) {
93
+ tableThresholds;
94
+ constructor(hotThreshold = 5, tableThresholds = {}) {
94
95
  this.hotThreshold = hotThreshold;
96
+ this.tableThresholds = tableThresholds;
95
97
  }
96
98
  async analyze(graph) {
97
99
  const findings = [];
@@ -110,10 +112,11 @@ export class HotPartitionAnalyzer {
110
112
  accessors.add(edge.from);
111
113
  }
112
114
  for (const [tableId, accessors] of tableAccessCount) {
113
- if (accessors.size >= this.hotThreshold) {
114
- const tableNode = graph.nodes.find((n) => n.id === tableId);
115
- if (!tableNode)
116
- continue;
115
+ const tableNode = graph.nodes.find((n) => n.id === tableId);
116
+ if (!tableNode)
117
+ continue;
118
+ const threshold = this.tableThresholds[tableNode.name] ?? this.hotThreshold;
119
+ if (accessors.size >= threshold) {
117
120
  // Also check edge frequency for repeated access patterns
118
121
  let maxFreq = 0;
119
122
  for (const [key, freq] of edgeFrequency) {
@@ -128,6 +131,7 @@ export class HotPartitionAnalyzer {
128
131
  metadata: {
129
132
  tableName: tableNode.name,
130
133
  accessorCount: accessors.size,
134
+ threshold,
131
135
  maxEdgeFrequency: maxFreq,
132
136
  },
133
137
  });
@@ -4,7 +4,8 @@ export { MissingIndexAnalyzer, NplusOneAnalyzer, LargeSelectAnalyzer } from './p
4
4
  export { MissingMySQLIndexAnalyzer, MySQLFullTableScanAnalyzer } from './mysql.js';
5
5
  export { MissingMongoIndexAnalyzer, MongoCollectionScanAnalyzer } from './mongodb.js';
6
6
  export { IaCDriftAnalyzer } from './terraform.js';
7
- export { MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, } from './aws-services.js';
7
+ export { PipelineAnalyzer } from './pipeline.js';
8
+ export { MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, VisibilityTimeoutMismatchAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, } from './aws-services.js';
8
9
  export { RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, } from './rds.js';
9
10
  export async function runAllAnalyzers(graph, analyzers) {
10
11
  const allFindings = [];
@@ -0,0 +1,106 @@
1
+ const STAGE_TOKENS = new Set([
2
+ 'prod',
3
+ 'production',
4
+ 'dev',
5
+ 'development',
6
+ 'staging',
7
+ 'stage',
8
+ 'test',
9
+ 'qa',
10
+ ]);
11
+ const NOISE_TOKENS = new Set(['handler', 'fn', 'func', 'function', 'lambda']);
12
+ export function normalizeName(raw) {
13
+ let s = raw.toLowerCase().trim();
14
+ s = s.split('/').pop() ?? s;
15
+ s = s.replace(/\.(ts|js|mjs|cjs)$/, '');
16
+ let segs = s.split(/[-_.\s]+/).filter(Boolean);
17
+ if (segs.length > 1 && STAGE_TOKENS.has(segs[segs.length - 1] ?? ''))
18
+ segs.pop();
19
+ segs = segs.filter((seg) => !NOISE_TOKENS.has(seg));
20
+ return segs.join('');
21
+ }
22
+ function lambdaNodes(graph) {
23
+ return graph.nodes.filter((n) => n.type === 'lambda');
24
+ }
25
+ function functionNodes(graph) {
26
+ return graph.nodes.filter((n) => n.type === 'function');
27
+ }
28
+ export class HeuristicLinker {
29
+ link(graph) {
30
+ const fns = functionNodes(graph);
31
+ const links = [];
32
+ for (const lam of lambdaNodes(graph)) {
33
+ const target = normalizeName(lam.name);
34
+ if (!target)
35
+ continue;
36
+ const matches = fns.filter((f) => {
37
+ const byName = normalizeName(f.name);
38
+ const byFile = normalizeName(f.file);
39
+ return (byName !== '' && byName === target) || (byFile !== '' && byFile === target);
40
+ });
41
+ if (matches.length !== 1)
42
+ continue;
43
+ const [only] = matches;
44
+ if (!only)
45
+ continue;
46
+ links.push({ lambdaId: lam.id, functionId: only.id, confidence: 'inferred' });
47
+ }
48
+ return links;
49
+ }
50
+ }
51
+ function parseHandler(handler) {
52
+ const lastDot = handler.lastIndexOf('.');
53
+ if (lastDot < 0)
54
+ return { fileBase: '', exportName: '' };
55
+ const exportName = handler.slice(lastDot + 1);
56
+ const filePart = handler.slice(0, lastDot);
57
+ const fileBase = filePart.split('/').pop() ?? filePart;
58
+ return { fileBase, exportName };
59
+ }
60
+ function fileBaseNoExt(file) {
61
+ const base = file.split('/').pop() ?? file;
62
+ return base.replace(/\.(ts|js|mjs|cjs)$/, '');
63
+ }
64
+ export class IaCHandlerLinker {
65
+ iacLambdas;
66
+ constructor(iacLambdas) {
67
+ this.iacLambdas = iacLambdas;
68
+ }
69
+ link(graph) {
70
+ const lambdaIds = new Set(lambdaNodes(graph).map((n) => n.id));
71
+ const fns = functionNodes(graph);
72
+ const links = [];
73
+ for (const il of this.iacLambdas) {
74
+ if (!il.handler)
75
+ continue;
76
+ const lambdaId = `lambda:aws:${il.name}`;
77
+ if (!lambdaIds.has(lambdaId))
78
+ continue;
79
+ const { fileBase, exportName } = parseHandler(il.handler);
80
+ if (!exportName || !fileBase)
81
+ continue;
82
+ const matches = fns.filter((f) => f.name === exportName && fileBaseNoExt(f.file) === fileBase);
83
+ if (matches.length !== 1)
84
+ continue;
85
+ const [only] = matches;
86
+ if (!only)
87
+ continue;
88
+ links.push({ lambdaId, functionId: only.id, confidence: 'proven' });
89
+ }
90
+ return links;
91
+ }
92
+ }
93
+ export class CompositeLinker {
94
+ proven;
95
+ heuristic;
96
+ constructor(proven, heuristic) {
97
+ this.proven = proven;
98
+ this.heuristic = heuristic;
99
+ }
100
+ link(graph) {
101
+ const provenLinks = this.proven.link(graph);
102
+ const covered = new Set(provenLinks.map((l) => l.lambdaId));
103
+ const heur = this.heuristic.link(graph).filter((l) => !covered.has(l.lambdaId));
104
+ return [...provenLinks, ...heur];
105
+ }
106
+ }
@@ -0,0 +1,166 @@
1
+ import { HeuristicLinker, IaCHandlerLinker, CompositeLinker, } from './linkers.js';
2
+ const TRANSPORT_EDGES = new Set(['publishes_to', 'triggers']);
3
+ export class PipelineAnalyzer {
4
+ name = 'PipelineAnalyzer';
5
+ iacLambdas = [];
6
+ setIaCLambdas(lambdas) {
7
+ this.iacLambdas = lambdas;
8
+ }
9
+ async analyze(graph) {
10
+ const links = new CompositeLinker(new IaCHandlerLinker(this.iacLambdas), new HeuristicLinker()).link(graph);
11
+ const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
12
+ return [
13
+ ...detectMissingDlqHop(graph),
14
+ ...detectScanInPipeline(graph, links, nodeById),
15
+ ...detectRepeatedTableAccess(graph, links, nodeById),
16
+ ];
17
+ }
18
+ }
19
+ class UnionFind {
20
+ parent = new Map();
21
+ find(x) {
22
+ const p = this.parent.get(x);
23
+ if (p === undefined) {
24
+ this.parent.set(x, x);
25
+ return x;
26
+ }
27
+ if (p === x)
28
+ return x;
29
+ const root = this.find(p);
30
+ this.parent.set(x, root);
31
+ return root;
32
+ }
33
+ union(a, b) {
34
+ const ra = this.find(a);
35
+ const rb = this.find(b);
36
+ if (ra !== rb)
37
+ this.parent.set(ra, rb);
38
+ }
39
+ }
40
+ function buildComponents(graph, links) {
41
+ const uf = new UnionFind();
42
+ for (const e of graph.edges) {
43
+ if (TRANSPORT_EDGES.has(e.type))
44
+ uf.union(e.from, e.to);
45
+ }
46
+ for (const l of links)
47
+ uf.union(l.lambdaId, l.functionId);
48
+ const inferredRoots = new Set();
49
+ for (const l of links) {
50
+ if (l.confidence === 'inferred')
51
+ inferredRoots.add(uf.find(l.lambdaId));
52
+ }
53
+ return { uf, inferredRoots };
54
+ }
55
+ function detectMissingDlqHop(graph) {
56
+ const hasProducer = new Set();
57
+ const hasConsumer = new Set();
58
+ for (const e of graph.edges) {
59
+ if (e.type === 'publishes_to')
60
+ hasProducer.add(e.to);
61
+ if (e.type === 'triggers')
62
+ hasConsumer.add(e.from);
63
+ }
64
+ const findings = [];
65
+ for (const node of graph.nodes) {
66
+ if (node.type !== 'queue' || node.hasDLQ)
67
+ continue;
68
+ if (!hasProducer.has(node.id) || !hasConsumer.has(node.id))
69
+ continue;
70
+ findings.push({
71
+ severity: 'medium',
72
+ issue: `Queue "${node.name}" sits mid-pipeline without a Dead Letter Queue`,
73
+ description: `Queue "${node.name}" has both a producer and a downstream consumer but no DLQ. A failure in the consumer drops the message silently, breaking the pipeline with no recovery path.`,
74
+ recommendation: `Add a Dead Letter Queue to "${node.name}" with maxReceiveCount 3-5, and alert on DLQ depth so mid-pipeline failures are visible.`,
75
+ metadata: { queueName: node.name, pipelineHop: true },
76
+ });
77
+ }
78
+ return findings;
79
+ }
80
+ function detectScanInPipeline(graph, links, nodeById) {
81
+ const triggeredLambdas = new Set();
82
+ for (const e of graph.edges) {
83
+ if (e.type === 'triggers')
84
+ triggeredLambdas.add(e.to);
85
+ }
86
+ const triggeredFunctionLinks = new Map();
87
+ for (const l of links) {
88
+ if (triggeredLambdas.has(l.lambdaId))
89
+ triggeredFunctionLinks.set(l.functionId, l);
90
+ }
91
+ const findings = [];
92
+ const seen = new Set();
93
+ for (const e of graph.edges) {
94
+ if (e.type !== 'scan')
95
+ continue;
96
+ const link = triggeredFunctionLinks.get(e.from);
97
+ if (!link)
98
+ continue;
99
+ const fn = nodeById.get(e.from);
100
+ const table = nodeById.get(e.to);
101
+ const lambda = nodeById.get(link.lambdaId);
102
+ if (!fn || fn.type !== 'function' || !table || table.type !== 'table')
103
+ continue;
104
+ const dedupe = `${e.from}\0${e.to}`;
105
+ if (seen.has(dedupe))
106
+ continue;
107
+ seen.add(dedupe);
108
+ const inferred = link.confidence === 'inferred';
109
+ const lambdaName = lambda && lambda.type === 'lambda' ? lambda.name : link.lambdaId;
110
+ findings.push({
111
+ severity: inferred ? 'verify' : 'high',
112
+ issue: `Full scan runs inside event-triggered Lambda "${lambdaName}"`,
113
+ description: `Function "${fn.name}" (${fn.file}) performs a full scan on table "${table.name}" and runs as the handler for event-triggered Lambda "${lambdaName}". A scan inside an event-driven consumer executes on every invocation, multiplying read cost with traffic.` +
114
+ (inferred
115
+ ? ` The Lambda-to-code link is inferred by name match (not proven from IaC), so verify it before acting.`
116
+ : ` The Lambda-to-code link is proven from IaC handler configuration.`),
117
+ recommendation: `Replace the scan with a Query using a partition key, or add a GSI for the access pattern. Scans in per-event handlers are the highest-leverage place to fix.`,
118
+ metadata: { function: fn.name, table: table.name, lambda: lambdaName, inferred },
119
+ });
120
+ }
121
+ return findings;
122
+ }
123
+ function detectRepeatedTableAccess(graph, links, nodeById) {
124
+ const { uf, inferredRoots } = buildComponents(graph, links);
125
+ const access = new Map();
126
+ for (const e of graph.edges) {
127
+ if (e.type !== 'query' && e.type !== 'scan')
128
+ continue;
129
+ const fromNode = nodeById.get(e.from);
130
+ const toNode = nodeById.get(e.to);
131
+ if (!fromNode || fromNode.type !== 'function')
132
+ continue;
133
+ if (!toNode || toNode.type !== 'table')
134
+ continue;
135
+ const key = `${uf.find(e.from)}\0${e.to}`;
136
+ const set = access.get(key) ?? new Set();
137
+ set.add(e.from);
138
+ access.set(key, set);
139
+ }
140
+ const findings = [];
141
+ for (const [key, fns] of access) {
142
+ if (fns.size < 2)
143
+ continue;
144
+ const [root, tableId] = key.split('\0');
145
+ const table = nodeById.get(tableId);
146
+ if (!table || table.type !== 'table')
147
+ continue;
148
+ const fnNames = [...fns]
149
+ .map((id) => nodeById.get(id))
150
+ .filter((n) => !!n && n.type === 'function')
151
+ .map((n) => `${n.name} (${n.file})`)
152
+ .sort();
153
+ const inferred = inferredRoots.has(root);
154
+ findings.push({
155
+ severity: inferred ? 'verify' : 'medium',
156
+ issue: `Table "${table.name}" is accessed at multiple stages of one pipeline`,
157
+ description: `Table "${table.name}" is read by ${fns.size} functions linked in the same service pipeline: ${fnNames.join(', ')}. Re-reading the same table across pipeline stages often signals redundant work that could be passed forward in the message payload.` +
158
+ (inferred
159
+ ? ` One or more Lambda-to-code links on this pipeline are inferred by name match (not proven from IaC), so verify the chain before acting.`
160
+ : ''),
161
+ recommendation: `Confirm each access is necessary. Consider carrying the needed fields in the event payload, caching the first read, or consolidating the reads. If the duplication is intentional, no action needed.`,
162
+ metadata: { tableName: table.name, functions: fnNames, inferred },
163
+ });
164
+ }
165
+ return findings;
166
+ }
@@ -8,12 +8,12 @@ import { extractPostgresMetadata } from '../../adapters/db/postgres.js';
8
8
  import { extractMySQLMetadata } from '../../adapters/db/mysql.js';
9
9
  import { extractMongoMetadata } from '../../adapters/db/mongodb.js';
10
10
  import { extractIaCSchema } from '../../adapters/iac/terraform.js';
11
- import { extractSQSMetadata, extractSNSMetadata, extractSSMMetadata, extractSecretsMetadata, extractLambdaMetadata, extractEventBridgeMetadata, extractRDSMetadata, } from '../../adapters/aws/services.js';
11
+ import { extractSQSMetadata, extractSNSMetadata, extractSSMMetadata, extractSecretsMetadata, extractLambdaMetadata, extractEventBridgeMetadata, extractRDSMetadata, extractAPIGatewayMetadata, } from '../../adapters/aws/services.js';
12
12
  import { extractLogsMetadata } from '../../adapters/aws/logs.js';
13
13
  import { extractS3Metadata } from '../../adapters/aws/s3.js';
14
14
  import { scanRepository } from '../../context/index.js';
15
15
  import { buildGraph } from '../../graph/index.js';
16
- import { runAllAnalyzers, IaCDriftAnalyzer, FullTableScanAnalyzer, MissingGSIAnalyzer, HotPartitionAnalyzer, MissingIndexAnalyzer, NplusOneAnalyzer, LargeSelectAnalyzer, MissingMySQLIndexAnalyzer, MySQLFullTableScanAnalyzer, MissingMongoIndexAnalyzer, MongoCollectionScanAnalyzer, MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, } from '../../analyzers/index.js';
16
+ import { runAllAnalyzers, IaCDriftAnalyzer, PipelineAnalyzer, FullTableScanAnalyzer, MissingGSIAnalyzer, HotPartitionAnalyzer, MissingIndexAnalyzer, NplusOneAnalyzer, LargeSelectAnalyzer, MissingMySQLIndexAnalyzer, MySQLFullTableScanAnalyzer, MissingMongoIndexAnalyzer, MongoCollectionScanAnalyzer, MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, VisibilityTimeoutMismatchAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, } from '../../analyzers/index.js';
17
17
  import { printFinding, printSummaryBox, log, printHeader } from '../utils.js';
18
18
  const SEVERITY_ORDER = { high: 3, medium: 2, low: 1, verify: 0 };
19
19
  function buildMarkdownReport(findings, projectName) {
@@ -230,6 +230,20 @@ export async function runAnalyze(options = {}) {
230
230
  chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
231
231
  }
232
232
  }
233
+ // ── API Gateway ───────────────────────────────────────────────────────────────
234
+ if (config.apiGateway?.enabled === true) {
235
+ const s = mkSpinner('Extracting API Gateway routes...');
236
+ try {
237
+ const result = await extractAPIGatewayMetadata(awsCfg);
238
+ servicesMeta.apiGateway = result;
239
+ const routeCount = result.reduce((sum, api) => sum + api.routes.length, 0);
240
+ s.succeed(chalk.green('API Gateway') + chalk.dim(` ${result.length} API(s), ${routeCount} route(s)`));
241
+ }
242
+ catch (err) {
243
+ s.warn(chalk.yellow('API Gateway skipped') +
244
+ chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
245
+ }
246
+ }
233
247
  // ── CloudWatch Logs ──────────────────────────────────────────────────────────
234
248
  if (config.cloudwatchLogs?.enabled) {
235
249
  const s = mkSpinner('Sampling CloudWatch Logs (errors only, max 50 groups)...');
@@ -251,6 +265,7 @@ export async function runAnalyze(options = {}) {
251
265
  }
252
266
  // ── IaC schema (Terraform / CloudFormation / CDK) ────────────────────────────
253
267
  let iacDriftAnalyzer;
268
+ let iacLambdas = [];
254
269
  {
255
270
  const s = mkSpinner('Extracting IaC schema (Terraform / CloudFormation / CDK)...');
256
271
  try {
@@ -267,6 +282,7 @@ export async function runAnalyze(options = {}) {
267
282
  iacSchema.apiGateways.length;
268
283
  iacDriftAnalyzer = new IaCDriftAnalyzer();
269
284
  iacDriftAnalyzer.setIaCSchema(iacSchema);
285
+ iacLambdas = iacSchema.lambdas;
270
286
  s.succeed(chalk.green('IaC schema') + chalk.dim(` ${total} resource(s) across TF/CFN/CDK`));
271
287
  }
272
288
  catch (err) {
@@ -301,9 +317,17 @@ export async function runAnalyze(options = {}) {
301
317
  let findings;
302
318
  {
303
319
  const s = mkSpinner('Running analyzers...');
320
+ const hotPartitionThreshold = config.analysis?.hotPartitionThreshold;
321
+ const hotPartitionThresholds = config.analysis?.hotPartitionThresholds;
322
+ const pipelineAnalyzer = new PipelineAnalyzer();
323
+ pipelineAnalyzer.setIaCLambdas(iacLambdas);
304
324
  const analyzers = [
305
325
  ...(config.dynamodb?.enabled === true
306
- ? [new FullTableScanAnalyzer(), new MissingGSIAnalyzer(), new HotPartitionAnalyzer()]
326
+ ? [
327
+ new FullTableScanAnalyzer(),
328
+ new MissingGSIAnalyzer(),
329
+ new HotPartitionAnalyzer(hotPartitionThreshold, hotPartitionThresholds),
330
+ ]
307
331
  : []),
308
332
  ...(config.postgres?.enabled
309
333
  ? [new MissingIndexAnalyzer(), new NplusOneAnalyzer(), new LargeSelectAnalyzer()]
@@ -319,6 +343,7 @@ export async function runAnalyze(options = {}) {
319
343
  new MissingDLQAnalyzer(),
320
344
  new UnencryptedQueueAnalyzer(),
321
345
  new LargeQueueBacklogAnalyzer(),
346
+ new VisibilityTimeoutMismatchAnalyzer(),
322
347
  ]
323
348
  : []),
324
349
  ...(config.secretsManager?.enabled === true ? [new MissingSecretRotationAnalyzer()] : []),
@@ -347,6 +372,7 @@ export async function runAnalyze(options = {}) {
347
372
  ]
348
373
  : []),
349
374
  ...(iacDriftAnalyzer ? [iacDriftAnalyzer] : []),
375
+ pipelineAnalyzer,
350
376
  ];
351
377
  findings = await runAllAnalyzers(graph, analyzers);
352
378
  s.succeed(chalk.green('Analysis complete') + chalk.dim(` ${findings.length} finding(s)`));
@@ -406,10 +432,12 @@ export async function runCodeRefresh(repoPath, config) {
406
432
  const servicesMeta = cached?.servicesMeta ?? {};
407
433
  // Re-run IaC schema (pure file scan, no AWS calls)
408
434
  let iacDriftAnalyzer;
435
+ let iacLambdas = [];
409
436
  try {
410
437
  const iacSchema = await extractIaCSchema(repoPath);
411
438
  iacDriftAnalyzer = new IaCDriftAnalyzer();
412
439
  iacDriftAnalyzer.setIaCSchema(iacSchema);
440
+ iacLambdas = iacSchema.lambdas;
413
441
  }
414
442
  catch {
415
443
  // IaC scan is best-effort
@@ -423,9 +451,17 @@ export async function runCodeRefresh(repoPath, config) {
423
451
  operations = [];
424
452
  }
425
453
  const graph = buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta, mongoMeta, servicesMeta);
454
+ const hotPartitionThreshold = config.analysis?.hotPartitionThreshold;
455
+ const hotPartitionThresholds = config.analysis?.hotPartitionThresholds;
456
+ const pipelineAnalyzer = new PipelineAnalyzer();
457
+ pipelineAnalyzer.setIaCLambdas(iacLambdas);
426
458
  const analyzers = [
427
459
  ...(config.dynamodb?.enabled === true
428
- ? [new FullTableScanAnalyzer(), new MissingGSIAnalyzer(), new HotPartitionAnalyzer()]
460
+ ? [
461
+ new FullTableScanAnalyzer(),
462
+ new MissingGSIAnalyzer(),
463
+ new HotPartitionAnalyzer(hotPartitionThreshold, hotPartitionThresholds),
464
+ ]
429
465
  : []),
430
466
  ...(config.postgres?.enabled
431
467
  ? [new MissingIndexAnalyzer(), new NplusOneAnalyzer(), new LargeSelectAnalyzer()]
@@ -437,7 +473,12 @@ export async function runCodeRefresh(repoPath, config) {
437
473
  ? [new MissingMongoIndexAnalyzer(), new MongoCollectionScanAnalyzer()]
438
474
  : []),
439
475
  ...(config.sqs?.enabled === true
440
- ? [new MissingDLQAnalyzer(), new UnencryptedQueueAnalyzer(), new LargeQueueBacklogAnalyzer()]
476
+ ? [
477
+ new MissingDLQAnalyzer(),
478
+ new UnencryptedQueueAnalyzer(),
479
+ new LargeQueueBacklogAnalyzer(),
480
+ new VisibilityTimeoutMismatchAnalyzer(),
481
+ ]
441
482
  : []),
442
483
  ...(config.secretsManager?.enabled === true ? [new MissingSecretRotationAnalyzer()] : []),
443
484
  ...(config.cloudwatchLogs?.enabled ? [new MissingLogRetentionAnalyzer()] : []),
@@ -465,6 +506,7 @@ export async function runCodeRefresh(repoPath, config) {
465
506
  ]
466
507
  : []),
467
508
  ...(iacDriftAnalyzer ? [iacDriftAnalyzer] : []),
509
+ pipelineAnalyzer,
468
510
  ];
469
511
  const findings = await runAllAnalyzers(graph, analyzers);
470
512
  writeCache('graph', graph);
@@ -57,6 +57,7 @@ export const InfrawiseConfigSchema = z.object({
57
57
  rds: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
58
58
  s3: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
59
59
  kafka: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
60
+ apiGateway: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
60
61
  cloudwatchLogs: z
61
62
  .object({
62
63
  enabled: z.boolean().optional().default(false),
@@ -67,6 +68,11 @@ export const InfrawiseConfigSchema = z.object({
67
68
  analysis: z
68
69
  .object({
69
70
  sampleSize: z.number().int().positive().optional().default(100),
71
+ hotPartitionThreshold: z.number().int().positive().optional().default(5),
72
+ hotPartitionThresholds: z
73
+ .record(z.string(), z.number().int().positive())
74
+ .optional()
75
+ .default({}),
70
76
  })
71
77
  .optional(),
72
78
  });
@@ -73,10 +73,28 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
73
73
  provider: 'aws',
74
74
  hasDLQ: q.hasDLQ,
75
75
  encrypted: q.encrypted,
76
+ isFifo: q.isFifo,
77
+ visibilityTimeoutSec: q.visibilityTimeoutSec,
76
78
  approximateMessages: q.approximateMessages,
77
79
  retentionDays: q.retentionDays,
78
80
  });
79
81
  }
82
+ for (const api of servicesMeta.apiGateway ?? []) {
83
+ addNode({
84
+ id: `api:aws:${api.id}`,
85
+ type: 'api',
86
+ name: api.name,
87
+ provider: 'aws',
88
+ apiType: api.type,
89
+ routes: api.routes,
90
+ });
91
+ for (const route of api.routes) {
92
+ if (route.lambdaName) {
93
+ const lambdaId = `lambda:aws:${route.lambdaName}`;
94
+ edges.push({ from: `api:aws:${api.id}`, to: lambdaId, type: 'triggers' });
95
+ }
96
+ }
97
+ }
80
98
  for (const t of servicesMeta.sns ?? []) {
81
99
  addNode({
82
100
  id: `topic:aws:${t.name}`,
@@ -402,3 +420,6 @@ export function getEdgeFrequency(graph) {
402
420
  }
403
421
  return freq;
404
422
  }
423
+ export function getAPINodes(graph) {
424
+ return graph.nodes.filter((n) => n.type === 'api');
425
+ }
@@ -8,7 +8,7 @@ import { z } from 'zod';
8
8
  import { logger } from '../core/index.js';
9
9
  const { version } = JSON.parse(readFileSync(join(import.meta.dirname, '../../package.json'), 'utf8'));
10
10
  import { summarizeFindings } from '../analyzers/index.js';
11
- import { getTableNodes, getFunctionNodes, getQueueNodes, getTopicNodes, getSecretNodes, getParameterNodes, getLogGroupNodes, getLambdaNodes, getEventBridgeRuleNodes, getBucketNodes, getScanEdges, getOutgoingEdges, } from '../graph/index.js';
11
+ import { getTableNodes, getFunctionNodes, getQueueNodes, getTopicNodes, getSecretNodes, getParameterNodes, getLogGroupNodes, getLambdaNodes, getEventBridgeRuleNodes, getBucketNodes, getAPINodes, getScanEdges, getOutgoingEdges, } from '../graph/index.js';
12
12
  // ── State ────────────────────────────────────────────────────────────────────
13
13
  let currentGraph = { nodes: [], edges: [] };
14
14
  let currentFindings = [];
@@ -244,7 +244,7 @@ export function createMcpServer() {
244
244
  });
245
245
  }));
246
246
  mcp.registerTool('get_queue_details', {
247
- description: 'Returns all SQS queues with DLQ presence, encryption status, approximate message count, and retention days. Call this when reviewing messaging architecture, investigating a message backlog, or checking DLQ coverage before adding a new consumer. Use get_infra_overview for a quick queue count only.',
247
+ description: 'Returns all SQS queues with DLQ presence, encryption status, FIFO type (isFifo), visibility timeout, approximate message count, and retention days. When isFifo is true, all SendMessage calls must include a MessageGroupId. Call this when reviewing messaging architecture, investigating a message backlog, checking DLQ coverage, or verifying visibility timeout is set correctly relative to Lambda timeout (should be 6× the Lambda timeout). Use get_infra_overview for a quick queue count only.',
248
248
  inputSchema: z.object({}),
249
249
  }, logged('get_queue_details', async () => {
250
250
  const queues = getQueueNodes(currentGraph);
@@ -256,6 +256,8 @@ export function createMcpServer() {
256
256
  provider: q.provider,
257
257
  hasDLQ: q.hasDLQ,
258
258
  encrypted: q.encrypted,
259
+ isFifo: q.isFifo ?? false,
260
+ visibilityTimeoutSec: q.visibilityTimeoutSec,
259
261
  approximateMessages: q.approximateMessages,
260
262
  retentionDays: q.retentionDays,
261
263
  findings: queueFindings
@@ -385,6 +387,24 @@ export function createMcpServer() {
385
387
  })),
386
388
  });
387
389
  }));
390
+ mcp.registerTool('get_api_routes', {
391
+ description: 'Returns all API Gateway APIs (REST, HTTP, WebSocket) with their routes, HTTP methods, paths, and the Lambda function each route invokes. Call this before writing any API handler to understand which Lambda handles a route, or when reviewing API surface area and Lambda integration coverage.',
392
+ inputSchema: z.object({}),
393
+ }, logged('get_api_routes', async () => {
394
+ const apis = getAPINodes(currentGraph);
395
+ return toText({
396
+ total: apis.length,
397
+ apis: apis.map((api) => ({
398
+ name: api.name,
399
+ type: api.apiType,
400
+ routes: (api.routes ?? []).map((r) => ({
401
+ method: r.method,
402
+ path: r.path,
403
+ lambda: r.lambdaName ?? null,
404
+ })),
405
+ })),
406
+ });
407
+ }));
388
408
  mcp.registerTool('get_log_errors', {
389
409
  description: 'Returns recent error pattern summaries from CloudWatch log groups: pattern counts and frequencies grouped by log group. Raw log messages are never returned. Use the optional logGroup filter to scope to one group by name substring. Call this when investigating errors or identifying log groups with no retention policy.',
390
410
  inputSchema: z.object({
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "infrawise",
3
- "version": "0.10.3",
3
+ "version": "0.11.0",
4
4
  "mcpName": "io.github.Sidd27/infrawise",
5
- "description": "CLI-first infrastructure intelligence platform — analyzes DynamoDB, PostgreSQL, MySQL, MongoDB, SQS, SNS, SSM, Secrets Manager, Lambda, S3, CloudWatch Logs and exposes findings as an MCP server for Claude Code",
5
+ "description": "CLI-first infrastructure intelligence platform — analyzes DynamoDB, PostgreSQL, MySQL, MongoDB, SQS, SNS, SSM, Secrets Manager, Lambda, S3, API Gateway, CloudWatch Logs and exposes findings as an MCP server for Claude Code",
6
6
  "keywords": [
7
7
  "mcp",
8
8
  "mcp-server",
@@ -32,7 +32,9 @@
32
32
  "cli",
33
33
  "terraform",
34
34
  "cloudformation",
35
- "cdk"
35
+ "cdk",
36
+ "api-gateway",
37
+ "sqs-fifo"
36
38
  ],
37
39
  "agentskills": {
38
40
  "infrawise": "./AGENTS.md"
@@ -52,7 +54,7 @@
52
54
  "node": ">=24.0.0",
53
55
  "pnpm": ">=9.0.0"
54
56
  },
55
- "packageManager": "pnpm@10.11.0",
57
+ "packageManager": "pnpm@11.6.0",
56
58
  "scripts": {
57
59
  "build": "tsc --noEmit false --outDir dist",
58
60
  "test": "vitest run --passWithNoTests",
@@ -71,6 +73,8 @@
71
73
  "pre-commit": "pnpm format && git add src/ && pnpm lint && pnpm typecheck && pnpm test"
72
74
  },
73
75
  "dependencies": {
76
+ "@aws-sdk/client-api-gateway": "^3.1068.0",
77
+ "@aws-sdk/client-apigatewayv2": "^3.1068.0",
74
78
  "@aws-sdk/client-cloudwatch-logs": "^3.1048.0",
75
79
  "@aws-sdk/client-dynamodb": "^3.1048.0",
76
80
  "@aws-sdk/client-eventbridge": "^3.1051.0",
@@ -121,15 +125,5 @@
121
125
  ],
122
126
  "publishConfig": {
123
127
  "access": "public"
124
- },
125
- "pnpm": {
126
- "overrides": {
127
- "qs": ">=6.15.2",
128
- "vite": "^7"
129
- },
130
- "allowedBuildScripts": [
131
- "puppeteer",
132
- "simple-git-hooks"
133
- ]
134
128
  }
135
129
  }