// Channel-adapter / DI ports the host app implements and injects via // defineModule({ adapters }). Consumed by the internal dispatch engine // (internal/plan* and internal/deliver*) and the event executors. /** Recipient profile resolved by the user-management adapter at dispatch time, with one `addresses` entry per channelId the recipient owns. */ export interface RecipientProfile { userId: string; locale: string; addresses: Record; } // Recipient rows (User) live in the host app's namespace, so the DB handle the // adapter reads from is opaque to this module — typed `unknown` so the adapter // implementation narrows it to its own Kysely instance. The caller's handle is // passed through so recipients created earlier in the same uncommitted // transaction (e.g. invitation bootstrap users) are visible to the lookup. type RecipientLookupDB = unknown; /** Port the host app implements to resolve a {@link RecipientProfile} for a userId, or `null` when the user cannot be resolved. */ export interface ResolveRecipientFn { (db: RecipientLookupDB, userId: string): Promise; } /** Channel adapter the host app implements to deliver a Notification to the in-app inbox channel. */ export interface InAppAdapter { send: (args: { /** Id of the Notification row being delivered. */ notificationId: string; /** Stable per-row dedup token; forward it to the provider so a redrain re-send collapses to one delivery. */ idempotencyKey: string; /** Channel-specific recipient address resolved from the {@link RecipientProfile}. */ recipientAddress: string; /** Rendered subject. */ subject: string; /** Rendered plain-text body. */ body: string; }) => Promise< | { ok: true } | { ok: false; /** Stable failure category (audited). */ errorClass: string; /** Human-readable failure detail (audited). */ errorDetail: string; } >; } /** Channel adapter the host app implements to deliver a Notification to the EMAIL channel. */ export interface EmailAdapter { send: (args: { /** Id of the Notification row being delivered. */ notificationId: string; /** Stable per-row dedup token; forward it to the provider so a redrain re-send collapses to one delivery. */ idempotencyKey: string; /** Channel-specific recipient address resolved from the {@link RecipientProfile}. */ recipientAddress: string; /** Rendered subject. */ subject: string; /** Rendered plain-text body. */ body: string; /** Rendered HTML body, or `null` when none was rendered. */ htmlBody: string | null; }) => Promise< | { ok: true; /** Provider message id returned on success. */ messageId: string; } | { ok: false; /** Stable failure category (audited). */ errorClass: string; /** Human-readable failure detail (audited). */ errorDetail: string; } >; } /** Channel adapter the host app implements to post a message to a shared DESTINATION surface (e.g. a Slack channel), independent of any recipient set. */ export interface DestinationAdapter { send: (args: { /** Id of the DESTINATION channel this binding posts on. */ channelId: string; /** Stable per-binding dedup token; forward it to providers that support idempotency so a redrain re-send collapses to one delivery. Slack `chat.postMessage` has no such parameter, so destination delivery is at-least-once. */ idempotencyKey: string; /** Provider-side reference of the shared surface to post to (e.g. a Slack channel id). */ externalChannelRef: string; /** Rendered subject. */ subject: string; /** Rendered plain-text body. */ body: string; /** Optional extra context the adapter may use when composing the message. */ metadata?: Record; }) => Promise< | { ok: true; /** Provider message id returned on success. */ externalMessageId?: string; /** Redacted provider response persisted on the delivery log. */ providerResponse?: string; } | { ok: false; /** Stable failure category (audited). */ errorClass: string; /** Human-readable failure detail (audited). */ errorDetail: string; /** Redacted provider response persisted on the delivery log. */ providerResponse?: string; } >; } /** Bundle of channel-delivery ports the host app injects via `defineModule({ adapters })`. */ export interface DispatchAdapters { /** Required adapter for the in-app inbox channel. */ inApp: InAppAdapter; /** Optional EMAIL adapter; when omitted, EMAIL-channel pairs fail as audited `CHANNEL_ADAPTER_FAILED`. */ email?: EmailAdapter; /** Required port resolving a recipient profile from a userId. */ resolveRecipient: ResolveRecipientFn; /** Optional DESTINATION adapter (Slack/Teams); when omitted, the DESTINATION stage is a no-op. */ destination?: DestinationAdapter; /** Optional clock override so tests can pin the dayBucket boundary. */ now?: () => Date; }