feat(iios): email adapter dedup fix + smoke + verification
CI / build (push) Failing after 2m8s

Adds adapter-owned externalEventId extraction to the ChannelAdapter contract
(WebhookAdapter→eventId, EmailAdapter→Message-ID) and uses it in InboundService,
so email replays dedup on Message-ID (previously only payload.eventId worked).
Adds an email-dedup spec + smoke-email.mjs proving inbound → normalized, a reply
joins the same email thread, bad signature rejected, and outbound EMAIL SENT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 20:34:26 +05:30
parent f0afca89e3
commit f0ea1ca553
6 changed files with 113 additions and 1 deletions
@@ -43,6 +43,11 @@ export class EmailAdapter implements ChannelAdapter {
return hmacVerify(rawBody, secret, typeof sig === 'string' ? sig : undefined);
}
externalEventId(payload: unknown): string | undefined {
const id = (payload as EmailInboundPayload)?.messageId;
return typeof id === 'string' ? id : undefined;
}
normalize(payload: unknown, ctx: NormalizeContext): IngestInteractionRequest {
const p = payload as EmailInboundPayload;
// A reply's thread root is the first Message-ID in its chain → same IIOS thread as the original.
+2
View File
@@ -34,6 +34,8 @@ export interface ChannelAdapter {
channelType: string;
capability: AdapterCapability;
verifySignature(rawBody: string, headers: Record<string, string | undefined>, secret: string): boolean;
/** Provider-native id used to dedup replays/retries at the inbound edge (e.g. eventId, Message-ID). */
externalEventId?(payload: unknown): string | undefined;
normalize(payload: unknown, ctx: NormalizeContext): IngestInteractionRequest;
fetch?(ref: string): Promise<unknown>;
send(cmd: OutboundCommandInput): Promise<SendResult>;
@@ -37,6 +37,11 @@ export class WebhookAdapter implements ChannelAdapter {
return hmacVerify(rawBody, secret, typeof sig === 'string' ? sig : undefined);
}
externalEventId(payload: unknown): string | undefined {
const id = (payload as WebhookPayload)?.eventId;
return typeof id === 'string' ? id : undefined;
}
normalize(payload: unknown, ctx: NormalizeContext): IngestInteractionRequest {
const p = payload as WebhookPayload;
return {