Skip to content
32 changes: 30 additions & 2 deletions cdk/src/constructs/jira-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Runtime, Architecture } from 'aws-cdk-lib/aws-lambda';
import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import { NagSuppressions } from 'cdk-nag';
import { Construct } from 'constructs';
Expand All @@ -35,8 +36,17 @@ import { JiraWorkspaceRegistryTable } from './jira-workspace-registry-table';
/** Default task-record retention used for TTL computation (days). */
const DEFAULT_TASK_RETENTION_DAYS = 90;

/** Webhook-processor Lambda timeout (seconds). */
const WEBHOOK_PROCESSOR_TIMEOUT_SECONDS = 30;
/**
* Webhook-processor Lambda timeout (seconds). The processor is invoked
* asynchronously (not behind the API Gateway 30s deadline), so it can run
* longer than the receiver. #577 added serial authenticated download +
* Bedrock screening of up to 10 Jira attachments; the per-attachment fetch
* timeout alone (10s) can sum past 60s across a full batch, and a mid-loop
* kill would orphan objects and force an idempotent retry. 300s covers the
* worst-case serial batch with headroom. (A future optimization could
* parallelize the download/screen loop and lower this again.)
*/
const WEBHOOK_PROCESSOR_TIMEOUT_SECONDS = 300;

/**
* Marker key embedded in the auto-generated stack-wide webhook-secret
Expand Down Expand Up @@ -87,6 +97,14 @@ export interface JiraIntegrationProps {
/** Bedrock Guardrail version for input screening. */
readonly guardrailVersion?: string;

/**
* S3 bucket for task attachment storage. Required for the webhook processor
* to fetch, screen, and store Jira `media` file attachments at task-admission
* time (issue #577). When omitted, issues carrying supported file attachments
* are rejected with a Jira comment rather than silently dropping them.
*/
readonly attachmentsBucket?: s3.IBucket;

/** Task retention in days for TTL computation. */
readonly taskRetentionDays?: number;

Expand Down Expand Up @@ -206,6 +224,9 @@ export class JiraIntegration extends Construct {
createTaskEnv.GUARDRAIL_ID = props.guardrailId;
createTaskEnv.GUARDRAIL_VERSION = props.guardrailVersion;
}
if (props.attachmentsBucket) {
createTaskEnv.ATTACHMENTS_BUCKET_NAME = props.attachmentsBucket.bucketName;
}

// --- Cognito Authorizer (for /jira/link) ---
const cognitoAuthorizer = new apigw.CognitoUserPoolsAuthorizer(this, 'JiraCognitoAuthorizer', {
Expand Down Expand Up @@ -280,6 +301,13 @@ export class JiraIntegration extends Construct {
],
}));
}
// The processor downloads Jira `media` attachments, screens them, and
// writes the cleaned bytes to the attachments bucket before creating the
// task (#577). ReadWrite mirrors the confirm-uploads path (Put + Get for
// multipart/versioned writes).
if (props.attachmentsBucket) {
props.attachmentsBucket.grantReadWrite(webhookProcessorFn);
}

// --- Webhook receiver (verifies HMAC, dedups, invokes processor) ---
const webhookFn = new lambda.NodejsFunction(this, 'WebhookFn', {
Expand Down
24 changes: 24 additions & 0 deletions cdk/src/constructs/task-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,18 @@ export class TaskApi extends Construct {
textTransformations: [{ priority: 0, type: 'NONE' }],
},
},
{
// Jira issue payloads include the full issue field set.
// Attachment metadata can push them over 8 KB. The
// receiver HMAC-verifies the raw body and the priority-4
// rule below still rate-limits this route.
byteMatchStatement: {
fieldToMatch: { uriPath: {} },
positionalConstraint: 'EXACTLY',
searchString: '/v1/jira/webhook',
textTransformations: [{ priority: 0, type: 'NONE' }],
},
},
{
// GitHub deployment_status webhook (preview-deploy
// screenshot pipeline). The full payload (workflow run
Expand Down Expand Up @@ -376,6 +388,18 @@ export class TaskApi extends Construct {
},
},
},
{
notStatement: {
statement: {
byteMatchStatement: {
fieldToMatch: { uriPath: {} },
positionalConstraint: 'EXACTLY',
searchString: '/v1/jira/webhook',
textTransformations: [{ priority: 0, type: 'NONE' }],
},
},
},
},
{
notStatement: {
statement: {
Expand Down
Loading
Loading