import type { IngestInteractionRequest } from '@insignia/iios-contracts'; import type { AdapterCapability, ChannelAdapter, NormalizeContext, OutboundCommandInput, SendResult } from './types'; import { hmacVerify } from './hmac'; import { SandboxSink } from './sandbox'; /** The canonical webhook payload the reference adapter understands. */ export interface WebhookPayload { eventId: string; from: string; displayName?: string; threadRef?: string; subject?: string; text: string; occurredAt?: string; } /** * Reference HMAC webhook adapter — proves the whole anti-corruption pipeline. * New providers implement the same ChannelAdapter contract with their own * signature scheme + payload mapping. */ export class WebhookAdapter implements ChannelAdapter { readonly channelType: string; readonly capability: AdapterCapability = { canReceive: true, canSend: true, supportsThreads: true, requiresFollowupFetch: false, }; constructor(channelType = 'WEBHOOK') { this.channelType = channelType; } verifySignature(rawBody: string, headers: Record, secret: string): boolean { const sig = headers['x-iios-signature'] ?? headers['X-Iios-Signature']; return hmacVerify(rawBody, secret, typeof sig === 'string' ? sig : undefined); } normalize(payload: unknown, ctx: NormalizeContext): IngestInteractionRequest { const p = payload as WebhookPayload; return { scope: ctx.scope, channel: { type: this.channelType, externalChannelId: this.channelType.toLowerCase() }, source: { handleKind: 'EXTERNAL_VISITOR', externalId: p.from, displayName: p.displayName ?? p.from }, kind: 'MESSAGE', thread: { externalThreadId: p.threadRef ?? `wh_${p.from}`, subject: p.subject }, parts: [{ kind: 'TEXT', bodyText: p.text }], occurredAt: p.occurredAt ?? new Date().toISOString(), providerEventId: p.eventId, }; } async send(cmd: OutboundCommandInput): Promise { return SandboxSink.simulate(cmd); } }