Files
iios/packages/iios-adapter-sdk/src/webhook-adapter.ts
T
maaz519 d70c7ea47b feat(sdk): P5.1 @insignia/iios-adapter-sdk — ChannelAdapter contract + HMAC webhook adapter + sandbox + fixtures
Contract (capability incl. requiresFollowupFetch seam, verifySignature, normalize,
fetch?, send), HMAC-SHA256 sign/verify (timing-safe), reference WebhookAdapter
(normalize→IngestInteractionRequest), SandboxSink (no network), email/whatsapp
fixtures. CJS build (consumed by the NestJS service). 4 tests; boundary updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:37:55 +05:30

58 lines
2.0 KiB
TypeScript

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<string, string | undefined>, 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<SendResult> {
return SandboxSink.simulate(cmd);
}
}