import type { NormalizedDelivery, ProviderSlug, SignatureScheme, } from '../types/integration.js'; import { normalizeLinear, normalizeRest, normalizeSlack } from './normalizers.js'; import { enrichSlackDelivery } from '../services/slack.service.js'; export type ChallengeResponse = { status: number; body: unknown }; export type ProviderConfig = { slug: ProviderSlug; signatureScheme: SignatureScheme; /** Header carrying the signature. Empty only when the scheme is 'none'. */ signatureHeader: string; timestampHeader?: string; /** Environment variable holding the shared secret when the install has none. */ secretEnvVar?: string; /** Returns a response when the request is a handshake rather than an event. */ challenge?: (body: unknown) => ChallengeResponse | null; /** * Absent until that provider's integration is built. The transport above is * infrastructure; the payload mapping belongs to the integration itself. */ normalize?: (payload: unknown) => NormalizedDelivery; /** * Runs after the ack, inside the retry queue, so it may make network calls. * Receives the raw payload because some providers deliver a reference rather * than content: a Slack reaction names a message without including its text. */ enrich?: ( delivery: NormalizedDelivery, payload: unknown ) => Promise; oauth?: { authorizeUrl: string; tokenUrl: string; scopes: string[]; }; }; const slackChallenge = (body: unknown): ChallengeResponse | null => { if (typeof body !== 'object' || body === null) return null; const candidate = body as { type?: unknown; challenge?: unknown }; if (candidate.type !== 'url_verification') return null; if (typeof candidate.challenge !== 'string') return null; return { status: 200, body: { challenge: candidate.challenge } }; }; export const providers: Record = { rest: { slug: 'rest', signatureScheme: 'none', signatureHeader: '', normalize: normalizeRest, }, linear: { slug: 'linear', signatureScheme: 'linear-sha256', signatureHeader: 'linear-signature', secretEnvVar: 'LINEAR_SIGNING_SECRET', normalize: normalizeLinear, oauth: { authorizeUrl: 'https://linear.app/oauth/authorize', tokenUrl: 'https://api.linear.app/oauth/token', scopes: ['read'], }, }, slack: { slug: 'slack', signatureScheme: 'slack-v0', signatureHeader: 'x-slack-signature', timestampHeader: 'x-slack-request-timestamp', secretEnvVar: 'SLACK_SIGNING_SECRET', challenge: slackChallenge, normalize: normalizeSlack, enrich: enrichSlackDelivery, oauth: { authorizeUrl: 'https://slack.com/oauth/v2/authorize', tokenUrl: 'https://slack.com/api/oauth.v2.access', scopes: ['channels:history', 'reactions:read', 'users:read'], }, }, github: { slug: 'github', signatureScheme: 'github-sha256', signatureHeader: 'x-hub-signature-256', secretEnvVar: 'GITHUB_WEBHOOK_SECRET', oauth: { authorizeUrl: 'https://github.com/login/oauth/authorize', tokenUrl: 'https://github.com/login/oauth/access_token', scopes: ['repo', 'read:discussion'], }, }, jira: { slug: 'jira', signatureScheme: 'none', signatureHeader: '', oauth: { authorizeUrl: 'https://auth.atlassian.com/authorize', tokenUrl: 'https://auth.atlassian.com/oauth/token', scopes: ['read:jira-work', 'offline_access'], }, }, }; export const getProvider = (slug: ProviderSlug): ProviderConfig => providers[slug];