@aetherionfw/infra 1.0.0 → 1.1.1

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 ADDED
@@ -0,0 +1,45 @@
1
+ # @aetherionfw/infra
2
+
3
+ > CDKTF (Cloud Development Kit for Terraform) builder for the Aetherion Serverless Framework.
4
+
5
+ Aetherion is a modern, TypeScript-first serverless framework designed to bring the developer experience of frameworks like NestJS to AWS serverless architectures.
6
+
7
+ ## Overview
8
+
9
+ This package is responsible for reading the metadata generated by your `@aetherionfw/core` decorators and automatically synthesizing the corresponding AWS infrastructure using Terraform (via CDKTF).
10
+
11
+ Features:
12
+ - Automatic **Lambda Function** provisioning (1:1 Architecture)
13
+ - Automatic **API Gateway** (HTTP/REST) wiring with resources, methods, and integrations
14
+ - **DynamoDB**, **RDS**, **S3**, **SQS**, **EventBridge**, and **KMS** integrations
15
+ - Automatic **IAM Permissions** resolution
16
+ - **VPC** configuration support
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @aetherionfw/infra cdktf constructs @cdktf/provider-aws
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ You rarely need to interact with this package manually. The Aetherion CLI handles the synthesis process for you. However, the entry point looks like this:
27
+
28
+ ```typescript
29
+ import { App } from 'cdktf';
30
+ import { FrameworkStack } from '@aetherionfw/infra';
31
+ import '../src/app.module'; // Import your app module to register metadata
32
+
33
+ const app = new App();
34
+ // This automatically synthesizes all Lambdas, API Gateways, etc.
35
+ new FrameworkStack(app, 'aetherion-dev');
36
+ app.synth();
37
+ ```
38
+
39
+ ## Documentation
40
+
41
+ For full documentation, visit [https://github.com/CrisD3v/Aetherion-framework](https://github.com/CrisD3v/Aetherion-framework).
42
+
43
+ ## License
44
+
45
+ MIT
@@ -1,5 +1,25 @@
1
1
  import { Construct } from 'constructs';
2
2
  import { TerraformStack } from 'cdktf';
3
3
  export declare class FrameworkStack extends TerraformStack {
4
+ /**
5
+ * Resolves and loads `aetherion.config.ts` from the current working directory.
6
+ * Falls back to safe defaults if no config file is found.
7
+ */
8
+ private static loadConfig;
9
+ /** All created Lambda functions indexed by their function name */
10
+ private lambdaFunctions;
11
+ /** All created Cognito User Pools indexed by their props.name */
12
+ private cognitoPools;
4
13
  constructor(scope: Construct, id: string);
14
+ private buildRestApiGateway;
15
+ private buildHttpApiGateway;
16
+ private wireRestApiRoute;
17
+ private wireHttpApiRoute;
18
+ /**
19
+ * Parses a path like `/users/{id}/orders` and creates intermediate
20
+ * API Gateway resources, reusing already-created segments.
21
+ */
22
+ private getOrCreateRestResource;
23
+ private getOrCreateAuthorizer;
24
+ private createCorsOptionsMethod;
5
25
  }
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.FrameworkStack = void 0;
4
37
  const cdktf_1 = require("cdktf");
@@ -16,15 +49,79 @@ const iam_role_policy_1 = require("@cdktf/provider-aws/lib/iam-role-policy");
16
49
  const db_instance_1 = require("@cdktf/provider-aws/lib/db-instance");
17
50
  const lambda_function_1 = require("@cdktf/provider-aws/lib/lambda-function");
18
51
  const lambda_event_source_mapping_1 = require("@cdktf/provider-aws/lib/lambda-event-source-mapping");
52
+ const lambda_permission_1 = require("@cdktf/provider-aws/lib/lambda-permission");
53
+ const api_gateway_rest_api_1 = require("@cdktf/provider-aws/lib/api-gateway-rest-api");
54
+ const api_gateway_resource_1 = require("@cdktf/provider-aws/lib/api-gateway-resource");
55
+ const api_gateway_method_1 = require("@cdktf/provider-aws/lib/api-gateway-method");
56
+ const api_gateway_integration_1 = require("@cdktf/provider-aws/lib/api-gateway-integration");
57
+ const api_gateway_deployment_1 = require("@cdktf/provider-aws/lib/api-gateway-deployment");
58
+ const api_gateway_stage_1 = require("@cdktf/provider-aws/lib/api-gateway-stage");
59
+ const api_gateway_authorizer_1 = require("@cdktf/provider-aws/lib/api-gateway-authorizer");
60
+ const api_gateway_method_response_1 = require("@cdktf/provider-aws/lib/api-gateway-method-response");
61
+ const api_gateway_integration_response_1 = require("@cdktf/provider-aws/lib/api-gateway-integration-response");
62
+ const apigatewayv2_api_1 = require("@cdktf/provider-aws/lib/apigatewayv2-api");
63
+ const apigatewayv2_integration_1 = require("@cdktf/provider-aws/lib/apigatewayv2-integration");
64
+ const apigatewayv2_route_1 = require("@cdktf/provider-aws/lib/apigatewayv2-route");
65
+ const apigatewayv2_stage_1 = require("@cdktf/provider-aws/lib/apigatewayv2-stage");
19
66
  const core_1 = require("@aetherionfw/core");
67
+ const path = __importStar(require("path"));
68
+ const fs = __importStar(require("fs"));
20
69
  class FrameworkStack extends cdktf_1.TerraformStack {
70
+ /**
71
+ * Resolves and loads `aetherion.config.ts` from the current working directory.
72
+ * Falls back to safe defaults if no config file is found.
73
+ */
74
+ static loadConfig() {
75
+ const configPath = path.resolve(process.cwd(), 'aetherion.config.ts');
76
+ const configJsPath = path.resolve(process.cwd(), 'aetherion.config.js');
77
+ let resolvedPath = null;
78
+ if (fs.existsSync(configPath))
79
+ resolvedPath = configPath;
80
+ else if (fs.existsSync(configJsPath))
81
+ resolvedPath = configJsPath;
82
+ if (!resolvedPath) {
83
+ console.warn('[Aetherion] No aetherion.config.ts found. Using default AWS provider settings.\n' +
84
+ ' Run `aetherion init` to scaffold a config file, or create aetherion.config.ts manually.');
85
+ return {
86
+ accountId: process.env.AWS_ACCOUNT_ID ?? '',
87
+ region: process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? 'us-east-1',
88
+ profile: process.env.AWS_PROFILE,
89
+ };
90
+ }
91
+ try {
92
+ // Use require for .js, ts-node/register path for .ts
93
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
94
+ const mod = require(resolvedPath);
95
+ const config = mod.default ?? mod;
96
+ return {
97
+ accountId: process.env.AWS_ACCOUNT_ID ?? config.accountId,
98
+ region: process.env.AWS_REGION ?? config.region,
99
+ profile: process.env.AWS_PROFILE ?? config.profile,
100
+ };
101
+ }
102
+ catch (err) {
103
+ console.error(`[Aetherion] Failed to load aetherion.config.ts: ${err.message}`);
104
+ process.exit(1);
105
+ throw new Error('unreachable'); // satisfy TypeScript return type
106
+ }
107
+ }
108
+ /** All created Lambda functions indexed by their function name */
109
+ lambdaFunctions = new Map();
110
+ /** All created Cognito User Pools indexed by their props.name */
111
+ cognitoPools = new Map();
21
112
  constructor(scope, id) {
22
113
  super(scope, id);
114
+ const config = FrameworkStack.loadConfig();
23
115
  new provider_1.AwsProvider(this, 'AWS', {
24
- region: 'us-east-1',
116
+ region: config.region,
117
+ ...(config.profile ? { profile: config.profile } : {}),
118
+ ...(config.accountId ? { allowedAccountIds: [config.accountId] } : {}),
25
119
  });
26
120
  const registry = core_1.MetadataRegistry.getInstance();
121
+ // ────────────────────────────────────────────
27
122
  // 1. Build Infra Resources
123
+ // ────────────────────────────────────────────
124
+ const apiGatewayConfigs = new Map();
28
125
  const infraClasses = registry.getInfraClasses();
29
126
  for (const target of infraClasses) {
30
127
  const resources = registry.getInfraResources(target);
@@ -34,7 +131,6 @@ class FrameworkStack extends cdktf_1.TerraformStack {
34
131
  case 'S3Bucket':
35
132
  new s3_bucket_1.S3Bucket(this, res.name, {
36
133
  bucket: res.props.name,
37
- // Minimal abstracted configuration mapped to raw CDKTF
38
134
  });
39
135
  break;
40
136
  case 'DynamoTable':
@@ -64,7 +160,6 @@ class FrameworkStack extends cdktf_1.TerraformStack {
64
160
  });
65
161
  break;
66
162
  case 'CloudFrontDistribution':
67
- // Simplified mapping for a highly complex resource
68
163
  new cloudfront_distribution_1.CloudfrontDistribution(this, res.name, {
69
164
  enabled: true,
70
165
  origin: [{
@@ -83,11 +178,13 @@ class FrameworkStack extends cdktf_1.TerraformStack {
83
178
  restrictions: { geoRestriction: { restrictionType: 'none' } },
84
179
  });
85
180
  break;
86
- case 'CognitoUserPool':
87
- new cognito_user_pool_1.CognitoUserPool(this, res.name, {
181
+ case 'CognitoUserPool': {
182
+ const pool = new cognito_user_pool_1.CognitoUserPool(this, res.name, {
88
183
  name: res.props.name,
89
184
  });
185
+ this.cognitoPools.set(res.props.name, pool);
90
186
  break;
187
+ }
91
188
  case 'Vpc':
92
189
  new vpc_1.Vpc(this, res.name, {
93
190
  cidrBlock: res.props.cidr,
@@ -99,7 +196,7 @@ class FrameworkStack extends cdktf_1.TerraformStack {
99
196
  case 'IamRole':
100
197
  new iam_role_1.IamRole(this, res.name, {
101
198
  name: res.props.name,
102
- assumeRolePolicy: res.props.assumedBy, // Assume this is a JSON string passed in
199
+ assumeRolePolicy: res.props.assumedBy,
103
200
  });
104
201
  break;
105
202
  case 'RdsInstance':
@@ -107,23 +204,27 @@ class FrameworkStack extends cdktf_1.TerraformStack {
107
204
  engine: res.props.engine,
108
205
  instanceClass: res.props.size,
109
206
  dbName: res.props.dbName,
110
- skipFinalSnapshot: true, // safe default for dev frameworks
207
+ skipFinalSnapshot: true,
111
208
  });
112
209
  break;
210
+ case 'ApiGateway':
211
+ // Collect for later processing (after Lambdas are created)
212
+ apiGatewayConfigs.set(res.props.name, { infraResource: res, props: res.props });
213
+ break;
113
214
  default:
114
215
  console.warn(`Unknown infra resource type: ${res.type}`);
115
216
  }
116
217
  }
117
218
  }
219
+ // ────────────────────────────────────────────
118
220
  // 2. Build Lambdas and IAM Policies (1:1 Lambda per Handle Architecture)
221
+ // ────────────────────────────────────────────
119
222
  const controllers = registry.getControllers();
120
223
  for (const [target, controllerMeta] of controllers) {
121
224
  const handles = registry.getHandles(target);
122
225
  const iamPermissions = registry.getIamPermissions(target);
123
- const routes = registry.getRoutes(target);
124
226
  const sqsTriggers = registry.getSqsTriggers(target);
125
227
  if (handles.length === 0) {
126
- // Fallback or skip if no methods are decorated with @Handle
127
228
  continue;
128
229
  }
129
230
  for (const handle of handles) {
@@ -145,7 +246,6 @@ class FrameworkStack extends cdktf_1.TerraformStack {
145
246
  // 2b. Attach IAM Policies (Method overrides Class)
146
247
  let handlePerms = iamPermissions.find(p => p.methodName === methodName);
147
248
  if (!handlePerms) {
148
- // Fallback to class-level permissions
149
249
  handlePerms = iamPermissions.find(p => !p.methodName);
150
250
  }
151
251
  if (handlePerms) {
@@ -178,26 +278,344 @@ class FrameworkStack extends cdktf_1.TerraformStack {
178
278
  memorySize: handle.memorySize || controllerMeta.memorySize || 128,
179
279
  timeout: handle.timeout || controllerMeta.timeout || 3,
180
280
  role: role.arn,
181
- filename: 'dummy.zip', // CDKTF requires a deployment package
281
+ filename: 'dummy.zip',
182
282
  handler: 'index.handler',
183
283
  environment: {
184
284
  variables: {
185
- AETHERION_TARGET_CLASS: controllerMeta.lambdaName, // Storing for debug
285
+ AETHERION_TARGET_CLASS: controllerMeta.lambdaName,
186
286
  AETHERION_TARGET_METHOD: methodName,
187
287
  }
188
288
  }
189
289
  });
290
+ this.lambdaFunctions.set(lambdaName, lambdaFunction);
190
291
  // 2d. Check for SQS Triggers targeting this handle
191
292
  const handleTriggers = sqsTriggers.filter(t => t.methodName === methodName);
192
293
  for (const trigger of handleTriggers) {
193
294
  console.log(`Linking SQS Trigger ${trigger.queueName} to ${lambdaName}`);
194
295
  new lambda_event_source_mapping_1.LambdaEventSourceMapping(this, `${lambdaName}-${trigger.queueName}-mapping`, {
195
296
  functionName: lambdaFunction.arn,
196
- eventSourceArn: `arn:aws:sqs:us-east-1:123456789012:${trigger.queueName}`, // mock
297
+ eventSourceArn: `arn:aws:sqs:us-east-1:123456789012:${trigger.queueName}`,
197
298
  });
198
299
  }
199
300
  }
200
301
  }
302
+ // ────────────────────────────────────────────
303
+ // 3. Build API Gateways and wire Routes
304
+ // ────────────────────────────────────────────
305
+ const apiRefs = new Map();
306
+ // 3a. Create or import each API Gateway
307
+ for (const [apiName, config] of apiGatewayConfigs) {
308
+ const props = config.props;
309
+ console.log(`Building API Gateway: ${apiName} (${props.type})`);
310
+ if (props.type === 'REST') {
311
+ this.buildRestApiGateway(apiName, props, apiRefs);
312
+ }
313
+ else {
314
+ this.buildHttpApiGateway(apiName, props, apiRefs);
315
+ }
316
+ }
317
+ // 3b. Wire controllers to their API Gateways
318
+ for (const [target, controllerMeta] of controllers) {
319
+ if (!controllerMeta.apiGateway)
320
+ continue;
321
+ const apiRef = apiRefs.get(controllerMeta.apiGateway);
322
+ if (!apiRef) {
323
+ console.warn(`API Gateway "${controllerMeta.apiGateway}" not found for controller "${controllerMeta.lambdaName}"`);
324
+ continue;
325
+ }
326
+ const routes = registry.getRoutes(target);
327
+ const handles = registry.getHandles(target);
328
+ for (const route of routes) {
329
+ // Find the matching handle for this route to get the lambda name
330
+ const handle = handles.find(h => h.methodName === route.methodName);
331
+ if (!handle)
332
+ continue;
333
+ const lambdaName = `${controllerMeta.lambdaName}-${route.methodName}`;
334
+ const lambdaFn = this.lambdaFunctions.get(lambdaName);
335
+ if (!lambdaFn)
336
+ continue;
337
+ console.log(`Wiring ${route.method} ${route.path} → ${lambdaName}`);
338
+ if (apiRef.metadata.type === 'REST') {
339
+ this.wireRestApiRoute(apiRef, route, lambdaFn, lambdaName);
340
+ }
341
+ else {
342
+ this.wireHttpApiRoute(apiRef, route, lambdaFn, lambdaName);
343
+ }
344
+ }
345
+ }
346
+ // 3c. Create Deployments and Stages
347
+ for (const [apiName, apiRef] of apiRefs) {
348
+ const stageName = apiRef.metadata.stageName || 'dev';
349
+ if (apiRef.metadata.type === 'REST' && apiRef.restApi && !apiRef.metadata.existingApiId) {
350
+ const deployment = new api_gateway_deployment_1.ApiGatewayDeployment(this, `${apiName}-deployment`, {
351
+ restApiId: apiRef.restApi.id,
352
+ lifecycle: {
353
+ createBeforeDestroy: true,
354
+ },
355
+ });
356
+ new api_gateway_stage_1.ApiGatewayStage(this, `${apiName}-stage`, {
357
+ restApiId: apiRef.restApi.id,
358
+ deploymentId: deployment.id,
359
+ stageName,
360
+ });
361
+ console.log(`Created REST API deployment: ${apiName} → stage "${stageName}"`);
362
+ }
363
+ else if (apiRef.metadata.type === 'HTTP' && apiRef.httpApi) {
364
+ new apigatewayv2_stage_1.Apigatewayv2Stage(this, `${apiName}-stage`, {
365
+ apiId: apiRef.httpApi.id,
366
+ name: stageName,
367
+ autoDeploy: true,
368
+ });
369
+ console.log(`Created HTTP API stage: ${apiName} → "${stageName}"`);
370
+ }
371
+ }
372
+ }
373
+ // ────────────────────────────────────────────
374
+ // REST API Gateway Builder
375
+ // ────────────────────────────────────────────
376
+ buildRestApiGateway(apiName, props, apiRefs) {
377
+ if (props.existingApiId) {
378
+ // Import existing REST API
379
+ console.log(`Importing existing REST API: ${props.existingApiId}`);
380
+ apiRefs.set(apiName, {
381
+ metadata: props,
382
+ rootResourceId: props.existingRootResourceId || '',
383
+ resourceMap: new Map(),
384
+ authorizerMap: new Map(),
385
+ methodIds: [],
386
+ });
387
+ }
388
+ else {
389
+ // Create new REST API
390
+ const restApi = new api_gateway_rest_api_1.ApiGatewayRestApi(this, apiName, {
391
+ name: props.name,
392
+ description: props.description || `API Gateway for ${props.name}`,
393
+ });
394
+ apiRefs.set(apiName, {
395
+ metadata: props,
396
+ restApi,
397
+ rootResourceId: restApi.rootResourceId,
398
+ resourceMap: new Map(),
399
+ authorizerMap: new Map(),
400
+ methodIds: [],
401
+ });
402
+ }
403
+ }
404
+ // ────────────────────────────────────────────
405
+ // HTTP API Gateway Builder
406
+ // ────────────────────────────────────────────
407
+ buildHttpApiGateway(apiName, props, apiRefs) {
408
+ if (props.existingApiId) {
409
+ console.log(`Importing existing HTTP API: ${props.existingApiId}`);
410
+ apiRefs.set(apiName, {
411
+ metadata: props,
412
+ rootResourceId: '',
413
+ resourceMap: new Map(),
414
+ authorizerMap: new Map(),
415
+ methodIds: [],
416
+ });
417
+ }
418
+ else {
419
+ const corsConfig = props.corsEnabled !== false ? {
420
+ allowOrigins: props.corsOrigins || ['*'],
421
+ allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
422
+ allowHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date', 'X-Api-Key'],
423
+ } : undefined;
424
+ const httpApi = new apigatewayv2_api_1.Apigatewayv2Api(this, apiName, {
425
+ name: props.name,
426
+ protocolType: 'HTTP',
427
+ description: props.description || `HTTP API for ${props.name}`,
428
+ corsConfiguration: corsConfig,
429
+ });
430
+ apiRefs.set(apiName, {
431
+ metadata: props,
432
+ httpApi,
433
+ rootResourceId: '',
434
+ resourceMap: new Map(),
435
+ authorizerMap: new Map(),
436
+ methodIds: [],
437
+ });
438
+ }
439
+ }
440
+ // ────────────────────────────────────────────
441
+ // REST API Route Wiring
442
+ // ────────────────────────────────────────────
443
+ wireRestApiRoute(apiRef, route, lambdaFn, lambdaName) {
444
+ const restApi = apiRef.restApi;
445
+ const restApiId = restApi ? restApi.id : apiRef.metadata.existingApiId;
446
+ // Build hierarchical resources for the path
447
+ const resourceId = this.getOrCreateRestResource(apiRef, route.path, restApiId);
448
+ // Determine authorization type
449
+ let authorizationType = 'NONE';
450
+ let authorizerId;
451
+ if (route.authorizer) {
452
+ const authorizer = this.getOrCreateAuthorizer(apiRef, route.authorizer, restApiId);
453
+ if (authorizer) {
454
+ authorizationType = 'COGNITO_USER_POOLS';
455
+ authorizerId = authorizer.id;
456
+ }
457
+ }
458
+ const methodId = `${lambdaName}-${route.method}`;
459
+ // Create Method
460
+ const method = new api_gateway_method_1.ApiGatewayMethod(this, methodId, {
461
+ restApiId,
462
+ resourceId,
463
+ httpMethod: route.method.toUpperCase(),
464
+ authorization: authorizationType,
465
+ authorizerId,
466
+ });
467
+ apiRef.methodIds.push(methodId);
468
+ // Create Integration (AWS_PROXY)
469
+ new api_gateway_integration_1.ApiGatewayIntegration(this, `${methodId}-integration`, {
470
+ restApiId,
471
+ resourceId,
472
+ httpMethod: method.httpMethod,
473
+ type: 'AWS_PROXY',
474
+ integrationHttpMethod: 'POST',
475
+ uri: lambdaFn.invokeArn,
476
+ });
477
+ // Grant API Gateway permission to invoke Lambda
478
+ new lambda_permission_1.LambdaPermission(this, `${methodId}-permission`, {
479
+ statementId: `AllowAPIGateway-${methodId}`,
480
+ action: 'lambda:InvokeFunction',
481
+ functionName: lambdaFn.functionName,
482
+ principal: 'apigateway.amazonaws.com',
483
+ });
484
+ // CORS: Create OPTIONS method if CORS is enabled
485
+ if (apiRef.metadata.corsEnabled !== false) {
486
+ this.createCorsOptionsMethod(apiRef, route.path, restApiId, resourceId, lambdaName);
487
+ }
488
+ }
489
+ // ────────────────────────────────────────────
490
+ // HTTP API Route Wiring
491
+ // ────────────────────────────────────────────
492
+ wireHttpApiRoute(apiRef, route, lambdaFn, lambdaName) {
493
+ const httpApi = apiRef.httpApi;
494
+ const apiId = httpApi ? httpApi.id : apiRef.metadata.existingApiId;
495
+ const integrationId = `${lambdaName}-${route.method}-int`;
496
+ // Create Integration
497
+ const integration = new apigatewayv2_integration_1.Apigatewayv2Integration(this, integrationId, {
498
+ apiId,
499
+ integrationType: 'AWS_PROXY',
500
+ integrationUri: lambdaFn.invokeArn,
501
+ payloadFormatVersion: '2.0',
502
+ });
503
+ // Create Route
504
+ const routeKey = `${route.method.toUpperCase()} ${route.path}`;
505
+ new apigatewayv2_route_1.Apigatewayv2Route(this, `${lambdaName}-${route.method}-route`, {
506
+ apiId,
507
+ routeKey,
508
+ target: `integrations/${integration.id}`,
509
+ });
510
+ // Grant API Gateway permission to invoke Lambda
511
+ new lambda_permission_1.LambdaPermission(this, `${lambdaName}-${route.method}-permission`, {
512
+ statementId: `AllowHTTPAPI-${lambdaName}-${route.method}`,
513
+ action: 'lambda:InvokeFunction',
514
+ functionName: lambdaFn.functionName,
515
+ principal: 'apigateway.amazonaws.com',
516
+ });
517
+ }
518
+ // ────────────────────────────────────────────
519
+ // Hierarchical Resource Builder (REST API)
520
+ // ────────────────────────────────────────────
521
+ /**
522
+ * Parses a path like `/users/{id}/orders` and creates intermediate
523
+ * API Gateway resources, reusing already-created segments.
524
+ */
525
+ getOrCreateRestResource(apiRef, path, restApiId) {
526
+ if (path === '/')
527
+ return apiRef.rootResourceId;
528
+ const segments = path.split('/').filter(Boolean);
529
+ let currentParentId = apiRef.rootResourceId;
530
+ let currentPath = '';
531
+ for (const segment of segments) {
532
+ currentPath += `/${segment}`;
533
+ if (apiRef.resourceMap.has(currentPath)) {
534
+ currentParentId = apiRef.resourceMap.get(currentPath).id;
535
+ continue;
536
+ }
537
+ const resource = new api_gateway_resource_1.ApiGatewayResource(this, `resource-${currentPath.replace(/[/{}]/g, '-')}`, {
538
+ restApiId,
539
+ parentId: currentParentId,
540
+ pathPart: segment,
541
+ });
542
+ apiRef.resourceMap.set(currentPath, resource);
543
+ currentParentId = resource.id;
544
+ }
545
+ return currentParentId;
546
+ }
547
+ // ────────────────────────────────────────────
548
+ // Cognito Authorizer Builder (REST API)
549
+ // ────────────────────────────────────────────
550
+ getOrCreateAuthorizer(apiRef, authorizerName, restApiId) {
551
+ // Reuse existing authorizer if already created for this API
552
+ if (apiRef.authorizerMap.has(authorizerName)) {
553
+ return apiRef.authorizerMap.get(authorizerName);
554
+ }
555
+ // Look up the Cognito User Pool by name
556
+ const pool = this.cognitoPools.get(authorizerName);
557
+ if (!pool) {
558
+ console.warn(`Authorizer "${authorizerName}" references a Cognito User Pool that was not found in @Infra resources.`);
559
+ return undefined;
560
+ }
561
+ const authorizer = new api_gateway_authorizer_1.ApiGatewayAuthorizer(this, `${authorizerName}-authorizer`, {
562
+ name: `${authorizerName}-cognito-authorizer`,
563
+ restApiId,
564
+ type: 'COGNITO_USER_POOLS',
565
+ providerArns: [pool.arn],
566
+ });
567
+ apiRef.authorizerMap.set(authorizerName, authorizer);
568
+ return authorizer;
569
+ }
570
+ // ────────────────────────────────────────────
571
+ // CORS OPTIONS Method (REST API)
572
+ // ────────────────────────────────────────────
573
+ createCorsOptionsMethod(apiRef, path, restApiId, resourceId, lambdaName) {
574
+ const corsId = `${lambdaName}-OPTIONS-${path.replace(/[/{}]/g, '-')}`;
575
+ // Avoid duplicate OPTIONS methods on the same resource
576
+ if (apiRef.methodIds.includes(corsId))
577
+ return;
578
+ apiRef.methodIds.push(corsId);
579
+ const origins = apiRef.metadata.corsOrigins?.join(',') || '*';
580
+ const headers = 'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token';
581
+ const methods = 'GET,POST,PUT,PATCH,DELETE,OPTIONS';
582
+ const optionsMethod = new api_gateway_method_1.ApiGatewayMethod(this, corsId, {
583
+ restApiId,
584
+ resourceId,
585
+ httpMethod: 'OPTIONS',
586
+ authorization: 'NONE',
587
+ });
588
+ new api_gateway_integration_1.ApiGatewayIntegration(this, `${corsId}-integration`, {
589
+ restApiId,
590
+ resourceId,
591
+ httpMethod: optionsMethod.httpMethod,
592
+ type: 'MOCK',
593
+ requestTemplates: {
594
+ 'application/json': '{"statusCode": 200}',
595
+ },
596
+ });
597
+ new api_gateway_method_response_1.ApiGatewayMethodResponse(this, `${corsId}-response`, {
598
+ restApiId,
599
+ resourceId,
600
+ httpMethod: optionsMethod.httpMethod,
601
+ statusCode: '200',
602
+ responseParameters: {
603
+ 'method.response.header.Access-Control-Allow-Headers': true,
604
+ 'method.response.header.Access-Control-Allow-Methods': true,
605
+ 'method.response.header.Access-Control-Allow-Origin': true,
606
+ },
607
+ });
608
+ new api_gateway_integration_response_1.ApiGatewayIntegrationResponse(this, `${corsId}-int-response`, {
609
+ restApiId,
610
+ resourceId,
611
+ httpMethod: optionsMethod.httpMethod,
612
+ statusCode: '200',
613
+ responseParameters: {
614
+ 'method.response.header.Access-Control-Allow-Headers': `'${headers}'`,
615
+ 'method.response.header.Access-Control-Allow-Methods': `'${methods}'`,
616
+ 'method.response.header.Access-Control-Allow-Origin': `'${origins}'`,
617
+ },
618
+ });
201
619
  }
202
620
  }
203
621
  exports.FrameworkStack = FrameworkStack;
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "@aetherionfw/infra",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "CDKTF Builder for Aetherion Framework",
5
5
  "main": "dist/main.js",
6
6
  "dependencies": {
7
7
  "cdktf": "^0.20.0",
8
8
  "constructs": "^10.3.0",
9
9
  "@cdktf/provider-aws": "^19.0.0",
10
- "@aetherionfw/core": "1.0.0"
10
+ "@aetherionfw/core": "1.1.1"
11
11
  },
12
12
  "devDependencies": {
13
+ "@types/node": "^20.0.0",
13
14
  "typescript": "^5.5.4"
14
15
  },
15
16
  "publishConfig": {
@@ -22,6 +23,11 @@
22
23
  ],
23
24
  "author": "CrisD3v",
24
25
  "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/CrisD3v/Aetherion-framework.git",
29
+ "directory": "packages/infra"
30
+ },
25
31
  "scripts": {
26
32
  "build": "tsc",
27
33
  "synth": "cdktf synth",