BJ-6: BitbucketAdapter + webhook route#
Summary#
Add a BitbucketAdapter (mirroring GitHubAdapter) that normalises 7 Bitbucket webhook events into DispatchEvents, plus a POST /webhooks/bitbucket route. Secret verification uses URL-embedded ?secret= and an IP allowlist (Bitbucket raw webhooks have no HMAC).
Files#
| Path | Action | Purpose |
|---|---|---|
src/webhook/adapters/bitbucket.adapter.ts | create | PlatformAdapter for Bitbucket: verifySignature() + normalize() for 7 event types |
src/webhook/adapters/bitbucket.adapter.spec.ts | create | Unit tests per event type, secret + IP allowlist paths, bot self-filter |
src/webhook/adapters/platform-adapter.interface.ts | modify | Extend verifySignature with optional context?: { clientIp?: string; query?: Record<string,string> } so adapters can inspect non-header auth |
src/webhook/adapters/github.adapter.ts | modify | Accept (and ignore) the new optional context arg to preserve interface parity |
src/webhook/webhook.controller.ts | modify | Add @Post('bitbucket') handler; pass req.ip + req.query to verifySignature; use tenantResolver.resolveFromWebhook('bitbucket', …) |
src/webhook/webhook.module.ts | modify | Provide BitbucketAdapter and register it in onModuleInit() next to GitHubAdapter |
src/webhook/dto/dispatch-event.dto.ts | modify | Extend DispatchEventType with 'pr_updated' | 'approved' | 'changes_requested' | 'push' (new types required by the mapping) |
src/config/config.schema.ts | modify | Add optional BITBUCKET_WEBHOOK_SECRET (required when route used) and optional BITBUCKET_WEBHOOK_IP_ALLOWLIST (comma-separated CIDRs/IPs) |
.env.example | modify | Document the two new env vars |
CLAUDE.md | modify | Add BITBUCKET_WEBHOOK_SECRET + BITBUCKET_WEBHOOK_IP_ALLOWLIST to the env table (AGENTS.md rule) |
Steps#
- Extend
PlatformAdapter.verifySignaturewith an optionalcontextarg; updateGitHubAdapterto match the new shape (no behavior change). - Extend
DispatchEventTypeunion with the four new values. - Implement
BitbucketAdapter—platform = 'bitbucket'; constructor injectsConfigService+PLATFORM_AUTH_PROVIDER(for bot self-filter); cachebotUsernameat construction. verifySignature:timingSafeEqualthecontext.query.secretagainstBITBUCKET_WEBHOOK_SECRET; ifBITBUCKET_WEBHOOK_IP_ALLOWLISTis set, rejectcontext.clientIpoutside the list; throwUnauthorizedExceptionon failure. Add a block comment noting Bitbucket has no HMAC.normalize: switch onx-event-keyheader and map each of the 7 events to aDispatchEvent. ExtractissueNumberfrom PR branch / title / body via a helper that mirrorsGitHubAdapter.extractIssueNumber.- For
pullrequest:comment_created, skip bot-authored comments, require/agentsde(or legacy/agent) via the same directive regex. - Wire controller: add
@Post('bitbucket'), buildcontext = { clientIp: req.ip, query: req.query as Record<string,string> }, call adapter, persistWebhookDeliveryEntitywithsource: 'bitbucket', resolve tenant with'bitbucket'. - Register
BitbucketAdapterinWebhookModuleproviders andonModuleInit(). - Add config schema entries and
.env.examplelines; updateCLAUDE.mdenv table. - Write unit tests: valid/invalid secret, IP allow/deny, all 7 event keys, unknown
x-event-key→{ event: null }, bot self-filter,/agentsdedirective on PR comment.
Verification#
npm run test— all unit tests pass,BitbucketAdapterspec green.npm run lint— zero warnings.npm run build— clean (confirms newDispatchEventTypevalues compile across dispatch/phase consumers).
Risks#
- Interface change ripples: extending
verifySignaturesignature touches every adapter. Mitigated by makingcontextoptional soGitHubAdapteris unaffected. - New
DispatchEventTypevalues: downstreamswitchstatements (dispatch.service,phase-router) may not handle them yet — acceptable for BJ-6 (Wave 2 handler wiring lands in later BJ-* tickets), but the build must still pass.
Open Questions#
- Should
BITBUCKET_WEBHOOK_SECRETberequiredinconfig.schemawhen absent, or remain optional (route returns 401 if the env var is unset)? Assumption: optional — matches the lazy-enablement pattern used by GitHub App vars.