Plan — BJ-7: JiraAdapter + webhook route#
Summary#
Add a JiraAdapter that normalizes Jira webhooks into DispatchEvent, expose POST /webhooks/jira, and extract HMAC-SHA256 into a shared helper so both adapters share one crypto path. Targets rc/atlassian-integration (Wave 2).
Files#
| Path | Action | Notes |
|---|---|---|
src/webhook/crypto.ts | create | verifyHmacSha256(rawBody, signatureHeader, secret, prefix) — timingSafeEqual, throws UnauthorizedException |
src/webhook/adapters/github.adapter.ts | modify | Replace inline HMAC block (lines 30–45) with verifyHmacSha256(..., 'sha256=') |
src/webhook/adapters/github.adapter.spec.ts | modify | Keep existing coverage; no behavioural change |
src/webhook/adapters/jira.adapter.ts | create | Implements PlatformAdapter (platform='jira'); switches on body.webhookEvent |
src/webhook/adapters/jira.adapter.spec.ts | create | Covers 5 event types + signature + unknown-event ignore |
src/webhook/webhook.controller.ts | modify | Add @Post('jira') handler mirroring handleGitHub shape, reads JIRA_WEBHOOK_SECRET, signature header x-hub-signature |
src/webhook/webhook.controller.spec.ts | modify | Add cases: 401 on bad signature, 200 + dispatch on valid payload, 200 + ignored on unknown event |
src/webhook/webhook.module.ts | modify | Provide JiraAdapter; register in onModuleInit() |
src/webhook/dto/dispatch-event.dto.ts | modify | Add 'issue_updated' and 'link_created' to DispatchEventType; add 'jira' to platform union (coordinate with #540) |
src/config/config.schema.ts | modify | JIRA_WEBHOOK_SECRET: Joi.string().optional() |
.env.example | modify | Document JIRA_WEBHOOK_SECRET |
CLAUDE.md | modify | Note JIRA_WEBHOOK_SECRET in env-var table |
Steps#
- Extract
verifyHmacSha256intosrc/webhook/crypto.tsand refactorGitHubAdapter.verifySignatureto delegate. - Extend
DispatchEventTypewithissue_updatedandlink_created; add'jira'to theplatformunion; update.env.example,config.schema.ts, andCLAUDE.mdforJIRA_WEBHOOK_SECRET. - Implement
JiraAdapterwith the 5 event mappings (jira:issue_created→intake,jira:issue_updated→issue_updatedwithchangeloginspection,jira:issue_deleted→task_cancelled,comment_created|comment_updated→issue_comment,jira:issuelink_created→link_created); unknown event returns{ event: null, reason }. - Add
POST /webhooks/jiratoWebhookController: read raw body, calladapter.verifySignaturewith headerx-hub-signatureandJIRA_WEBHOOK_SECRET, persistWebhookDeliveryEntitywithsource='jira', resolve tenant viaTenantResolverService.resolveFromWebhook('jira', ...), dispatch. - Register
JiraAdapterinWebhookModuleproviders andonModuleInit(). - Write
jira.adapter.spec.ts(event mapping, signature, graceful unknown) and extendwebhook.controller.spec.tswith jira-route coverage; runnpm run testandnpm run lint.
Verification#
npm run testpasses; new specs cover all 5 event types plus bad/missing signature and unknownwebhookEvent.npm run lintpasses with zero warnings;npm run buildsucceeds.GitHubAdapterbehaviour unchanged — its existing spec continues to pass after crypto extraction.
Risks#
- #540 (BJ-0) not yet merged — it also adds
'jira'to platform unions. Mitigation: include the union extension here ifrc/atlassian-integrationstill lacks it at implementation time; rebase drops the duplicate once #540 lands. - Tenant resolution for Jira —
TenantResolverService.resolveFromWebhookmay not yet support'jira'. Mitigation: fall back to{ org: <configured>, source: 'jira' }and open a follow-up if the resolver needs Jira-specific logic.
Open Questions#
- Should
jira:issue_updatedwithout achangelogfield emit anissue_updateddispatch, or be ignored? Plan assumes emit (carrycommentBody=undefined, no label info) per the issue's "graceful" guidance.