@@ -115,6 +115,20 @@ export interface TaskApiProps {
115115 */
116116 readonly webhookTable ?: dynamodb . ITable ;
117117
118+ /**
119+ * The DynamoDB platform API key table. When provided, API key management
120+ * endpoints are created and the webhook management endpoints accept an API
121+ * key (with `webhooks:manage`) in addition to a Cognito JWT.
122+ */
123+ readonly apiKeyTable ?: dynamodb . ITable ;
124+
125+ /**
126+ * Number of days to retain revoked API key records before DynamoDB TTL
127+ * deletes them.
128+ * @default 30
129+ */
130+ readonly apiKeyRetentionDays ?: number ;
131+
118132 /**
119133 * ARN of the orchestrator Lambda alias. When set, the create-task handler
120134 * async-invokes the orchestrator after writing the task record.
@@ -198,10 +212,13 @@ export interface TaskApiProps {
198212 * - GET /tasks/{task_id} → getTask (Cognito)
199213 * - DELETE /tasks/{task_id} → cancelTask (Cognito)
200214 * - GET /tasks/{task_id}/events → getTaskEvents (Cognito)
201- * - POST /webhooks → createWebhook (Cognito)
202- * - GET /webhooks → listWebhooks (Cognito)
203- * - DELETE /webhooks/{webhook_id} → deleteWebhook (Cognito)
215+ * - POST /webhooks → createWebhook (Cognito, or JWT/API-key when apiKeyTable set )
216+ * - GET /webhooks → listWebhooks (Cognito, or JWT/API-key when apiKeyTable set )
217+ * - DELETE /webhooks/{webhook_id} → deleteWebhook (Cognito, or JWT/API-key when apiKeyTable set )
204218 * - POST /webhooks/tasks → webhookCreateTask (REQUEST authorizer)
219+ * - POST /api-keys → createApiKey (Cognito)
220+ * - GET /api-keys → listApiKeys (Cognito)
221+ * - DELETE /api-keys/{key_id} → deleteApiKey (Cognito)
205222 */
206223export class TaskApi extends Construct {
207224 /**
@@ -1008,6 +1025,95 @@ export class TaskApi extends Construct {
10081025 }
10091026 }
10101027
1028+ // --- Platform API key infrastructure (only when apiKeyTable is provided) ---
1029+ //
1030+ // Default: webhook management routes stay Cognito-only. When an API key
1031+ // table is wired, a unified REQUEST authorizer replaces Cognito on those
1032+ // routes so they accept EITHER a Cognito JWT OR a `webhooks:manage` API key
1033+ // (issue #376). Key *creation* stays Cognito-gated.
1034+ let webhookMgmtAuthOptions : apigw . MethodOptions = cognitoAuthOptions ;
1035+
1036+ if ( props . apiKeyTable ) {
1037+ const apiKeyEnv : Record < string , string > = {
1038+ API_KEY_TABLE_NAME : props . apiKeyTable . tableName ,
1039+ API_KEY_RETENTION_DAYS : String ( props . apiKeyRetentionDays ?? DEFAULT_WEBHOOK_RETENTION_DAYS ) ,
1040+ } ;
1041+
1042+ // --- Unified authorizer: Cognito JWT OR platform API key ---
1043+ const apiKeyAuthorizerFn = new lambda . NodejsFunction ( this , 'ApiKeyAuthorizerFn' , {
1044+ entry : path . join ( handlersDir , 'api-key-authorizer.ts' ) ,
1045+ handler : 'handler' ,
1046+ runtime : Runtime . NODEJS_24_X ,
1047+ architecture : Architecture . ARM_64 ,
1048+ environment : {
1049+ API_KEY_TABLE_NAME : props . apiKeyTable . tableName ,
1050+ API_KEY_REQUIRED_SCOPE : 'webhooks:manage' ,
1051+ USER_POOL_ID : this . userPool . userPoolId ,
1052+ APP_CLIENT_ID : this . appClient . userPoolClientId ,
1053+ } ,
1054+ bundling : commonBundling ,
1055+ } ) ;
1056+ props . apiKeyTable . grantReadData ( apiKeyAuthorizerFn ) ;
1057+
1058+ // No fixed identity source: a request carries EITHER Authorization OR
1059+ // X-API-Key, never a guaranteed one. Multiple identity sources are AND-ed
1060+ // and would short-circuit to 401 before the Lambda runs, so we leave them
1061+ // empty and disable caching (the Lambda decides on every request).
1062+ const webhookMgmtAuthorizer = new apigw . RequestAuthorizer ( this , 'ApiKeyOrJwtAuthorizer' , {
1063+ handler : apiKeyAuthorizerFn ,
1064+ identitySources : [ ] ,
1065+ resultsCacheTtl : Duration . seconds ( 0 ) ,
1066+ } ) ;
1067+
1068+ webhookMgmtAuthOptions = {
1069+ authorizer : webhookMgmtAuthorizer ,
1070+ authorizationType : apigw . AuthorizationType . CUSTOM ,
1071+ requestValidator,
1072+ } ;
1073+
1074+ // --- API key management Lambdas (Cognito-authenticated — minting a key
1075+ // requires a real interactive user) ---
1076+ const createApiKeyFn = new lambda . NodejsFunction ( this , 'CreateApiKeyFn' , {
1077+ entry : path . join ( handlersDir , 'create-api-key.ts' ) ,
1078+ handler : 'handler' ,
1079+ runtime : Runtime . NODEJS_24_X ,
1080+ architecture : Architecture . ARM_64 ,
1081+ environment : apiKeyEnv ,
1082+ bundling : commonBundling ,
1083+ } ) ;
1084+
1085+ const listApiKeysFn = new lambda . NodejsFunction ( this , 'ListApiKeysFn' , {
1086+ entry : path . join ( handlersDir , 'list-api-keys.ts' ) ,
1087+ handler : 'handler' ,
1088+ runtime : Runtime . NODEJS_24_X ,
1089+ architecture : Architecture . ARM_64 ,
1090+ environment : apiKeyEnv ,
1091+ bundling : commonBundling ,
1092+ } ) ;
1093+
1094+ const deleteApiKeyFn = new lambda . NodejsFunction ( this , 'DeleteApiKeyFn' , {
1095+ entry : path . join ( handlersDir , 'delete-api-key.ts' ) ,
1096+ handler : 'handler' ,
1097+ runtime : Runtime . NODEJS_24_X ,
1098+ architecture : Architecture . ARM_64 ,
1099+ environment : apiKeyEnv ,
1100+ bundling : commonBundling ,
1101+ } ) ;
1102+
1103+ props . apiKeyTable . grantReadWriteData ( createApiKeyFn ) ;
1104+ props . apiKeyTable . grantReadData ( listApiKeysFn ) ;
1105+ props . apiKeyTable . grantReadWriteData ( deleteApiKeyFn ) ;
1106+
1107+ const apiKeys = this . api . root . addResource ( 'api-keys' ) ;
1108+ apiKeys . addMethod ( 'POST' , new apigw . LambdaIntegration ( createApiKeyFn ) , cognitoAuthOptions ) ;
1109+ apiKeys . addMethod ( 'GET' , new apigw . LambdaIntegration ( listApiKeysFn ) , cognitoAuthOptions ) ;
1110+
1111+ const apiKeyById = apiKeys . addResource ( '{key_id}' ) ;
1112+ apiKeyById . addMethod ( 'DELETE' , new apigw . LambdaIntegration ( deleteApiKeyFn ) , cognitoAuthOptions ) ;
1113+
1114+ allFunctions . push ( apiKeyAuthorizerFn , createApiKeyFn , listApiKeysFn , deleteApiKeyFn ) ;
1115+ }
1116+
10111117 // --- Webhook endpoints (only when webhookTable is provided) ---
10121118 if ( props . webhookTable ) {
10131119 const webhookEnv : Record < string , string > = {
@@ -1146,12 +1252,26 @@ export class TaskApi extends Construct {
11461252 } ;
11471253
11481254 // --- API resource tree: /webhooks ---
1255+ // Management routes use the unified authorizer when an API key table is
1256+ // wired (JWT or `webhooks:manage` key), else fall back to Cognito-only.
11491257 const webhooks = this . api . root . addResource ( 'webhooks' ) ;
1150- webhooks . addMethod ( 'POST' , new apigw . LambdaIntegration ( createWebhookFn ) , cognitoAuthOptions ) ;
1151- webhooks . addMethod ( 'GET' , new apigw . LambdaIntegration ( listWebhooksFn ) , cognitoAuthOptions ) ;
1258+ const createWebhookMethod = webhooks . addMethod ( 'POST' , new apigw . LambdaIntegration ( createWebhookFn ) , webhookMgmtAuthOptions ) ;
1259+ const listWebhooksMethod = webhooks . addMethod ( 'GET' , new apigw . LambdaIntegration ( listWebhooksFn ) , webhookMgmtAuthOptions ) ;
11521260
11531261 const webhookById = webhooks . addResource ( '{webhook_id}' ) ;
1154- webhookById . addMethod ( 'DELETE' , new apigw . LambdaIntegration ( deleteWebhookFn ) , cognitoAuthOptions ) ;
1262+ const deleteWebhookMethod = webhookById . addMethod ( 'DELETE' , new apigw . LambdaIntegration ( deleteWebhookFn ) , webhookMgmtAuthOptions ) ;
1263+
1264+ // When the unified authorizer is in use these methods are CUSTOM, not
1265+ // Cognito — suppress COG4 (the authorizer still enforces a Cognito JWT
1266+ // or a scoped API key; issue #376).
1267+ if ( props . apiKeyTable ) {
1268+ NagSuppressions . addResourceSuppressions ( [ createWebhookMethod , listWebhooksMethod , deleteWebhookMethod ] , [
1269+ {
1270+ id : 'AwsSolutions-COG4' ,
1271+ reason : 'Webhook management uses a unified REQUEST authorizer accepting a Cognito JWT or a scoped platform API key — by design for headless automation (issue #376)' ,
1272+ } ,
1273+ ] ) ;
1274+ }
11551275
11561276 const webhookTasks = webhooks . addResource ( 'tasks' ) ;
11571277 const webhookTasksMethod = webhookTasks . addMethod ( 'POST' , new apigw . LambdaIntegration ( webhookCreateTaskFn ) , webhookAuthOptions ) ;
0 commit comments