import { RetryStrategy as RetryStrategy_dwxcm3 } from "@workkit/errors"; type ChannelName = string; type DeliveryStatus = "queued" | "sent" | "delivered" | "read" | "failed" | "bounced" | "skipped" | "duplicate"; type DispatchMode = "live" | "test"; interface ChannelTemplate

{ /** * The template body. Adapters interpret it differently: * - `email`: `string` HTML, or a React Email element rendered via the * optional `@react-email/render` peer. * - `whatsapp`: typically a template id or full template object. * - others: as the adapter documents. * * Typed as `unknown` so adapters can accept their own narrower shape * without forcing every other adapter to deal with it. */ template?: unknown; variables?: (payload: P) => Record; props?: (payload: P) => unknown; attachments?: (payload: P) => Array<{ filename?: string; r2Key: string; type?: string; }>; title?: (payload: P) => string; body?: (payload: P) => string; deepLink?: (payload: P) => string; } interface AdapterSendArgs

{ userId: string; notificationId: string; channel: ChannelName; address: string; template: ChannelTemplate

; payload: P; deliveryId: string; mode: DispatchMode; } interface AdapterSendResult { providerId?: string; status: Exclude; error?: string; /** * Optional. Whether the failure should be retried. Adapters that catch * a `WorkkitError` populate this from `WorkkitError.retryable`; other * adapters can leave it undefined. See ADR-002. */ retryable?: boolean; /** * Optional. Recommended backoff strategy for the failure. Adapters that * catch a `WorkkitError` populate this from `WorkkitError.retryStrategy`. * Consumers / queue policy can opt into reading this field; today it is * not yet acted on by `createNotifyConsumer` (see ADR-002 follow-ups). */ retryStrategy?: RetryStrategy_dwxcm3; } interface WebhookEvent { channel: ChannelName; providerId: string; status: Extract; at: number; raw?: unknown; } interface Adapter

{ send(args: AdapterSendArgs

): Promise; parseWebhook?(req: Request): Promise; verifySignature?(req: Request, secret: string): Promise; } /** Minimal D1-shape we depend on. Matches @cloudflare/workers-types' D1Database. */ interface NotifyD1 { prepare(query: string): NotifyPreparedStatement; batch(statements: NotifyPreparedStatement[]): Promise; } interface NotifyPreparedStatement { bind(...values: unknown[]): NotifyPreparedStatement; first>(): Promise; all>(): Promise<{ results?: T[]; }>; run(): Promise<{ success?: boolean; meta?: { changes?: number; }; }>; } interface StopMatchOptions { extraKeywords?: ReadonlyArray; } declare function isStopKeyword(text: string, options?: StopMatchOptions): boolean; /** Exposed for tests + consumer introspection. */ declare function defaultStopKeywords(): ReadonlyArray; /** * In-memory marketing-pause flag, single-Worker-isolate scope. * * Quality-rating webhooks call `pause()` to halt `category: marketing` sends * pending operator review. Multi-isolate fan-out is a v2 concern (Durable * Object). For v1, Meta's quality metric is slow-moving — a 30s propagation * gap across isolates is acceptable. * * Audit hook receives every transition so callers can persist + alert. */ interface MarketingPauseAuditEvent { at: number; state: "paused" | "resumed"; reason: string; } type MarketingPauseAuditHook = (event: MarketingPauseAuditEvent) => void | Promise; declare class MarketingPauseRegistry { private paused; private reason; private auditHook; constructor(options?: { auditHook?: MarketingPauseAuditHook; }); isPaused(): boolean; pauseReason(): string | undefined; pause(reason: string): Promise; resume(reason: string): Promise; } declare function assertE164(value: string): string; declare function isE164(value: string): boolean; /** * Optional cipher hook for storing phone numbers at rest. Caller supplies * AES-GCM (or any symmetric cipher) `encrypt`/`decrypt` callbacks. The * default is identity — phones stored as plain E.164. Switching to a real * cipher later does not require a schema migration since the column is * declared TEXT (Base64-friendly). */ interface PhoneCipher { encrypt(plain: string): Promise; decrypt(cipher: string): Promise; } declare const identityCipher: PhoneCipher; type WhatsAppCategory = "marketing" | "transactional" | "authentication"; interface WaTemplateRef { name: string; language: string; variables?: ReadonlyArray; category?: WhatsAppCategory; } interface WaMediaRef { mediaId: string; mimeType?: string; } interface WaSendArgs { toE164: string; template?: WaTemplateRef; sessionText?: string; media?: WaMediaRef; } interface WaSendResult { providerId: string; } interface WaUploadArgs { bytes: Uint8Array; mimeType: string; filename?: string; } /** * Inbound message events parsed from a webhook. The adapter inspects the * text body for STOP keywords and the type for quality-rating updates. */ interface WaInboundMessage { from: string; text?: string; at: number; raw?: unknown; } interface WaQualityAlert { level: "low" | "medium" | "high" | "flagged"; at: number; raw?: unknown; } /** * Provider events: a union of delivery `WebhookEvent`s (mapped to the * notify-core webhook shape), inbound messages, and account-quality * alerts. The adapter routes each variant. */ type WaProviderEvent = { kind: "delivery"; event: WebhookEvent; } | { kind: "inbound"; message: WaInboundMessage; } | { kind: "quality"; alert: WaQualityAlert; }; /** * Pluggable provider interface. `metaWaProvider` is the reference impl; * `twilioWaProvider` and `gupshupWaProvider` are stubs. */ interface WaProvider { readonly name: "meta" | "twilio" | "gupshup"; send(args: WaSendArgs): Promise; uploadMedia(args: WaUploadArgs): Promise; parseWebhook(req: Request): Promise; verifySignature(req: Request, secret: string): Promise; /** * Meta requires a one-shot `GET ?hub.mode=subscribe&hub.challenge=…&hub.verify_token=…` * handshake on webhook setup. Providers that don't need this can return null. */ handleVerificationChallenge(req: Request, verifyToken: string): Response | null; } interface WhatsAppPayload { [key: string]: unknown; } interface R2BucketLike { get(key: string): Promise<{ body: ReadableStream | null; arrayBuffer(): Promise; etag?: string; httpMetadata?: { contentType?: string; }; } | null>; } /** * Pluggable check for "is the recipient on the DND registry?". Adapter * only invokes this for `category: "marketing"` templates. Returning true * suppresses the send and is reported through the adapter's existing * failure result path (`AdapterSendResult.status` does not include * `skipped`). */ type DndChecker = (phoneE164: string) => Promise; /** * Caller-provided opt-out hook. Invoked when an inbound message matches a * STOP/UNSUBSCRIBE keyword (after webhook signature verification). * `notificationId: null` because STOPs are global by intent. */ type WaOptOutHook = (userId: string, channel: "whatsapp", notificationId: null, reason: "inbound-stop") => Promise; interface WhatsAppTemplateRef { name: string; language: string; variables?: (payload: WhatsAppPayload) => ReadonlyArray; category: WhatsAppCategory; media?: (payload: WhatsAppPayload) => { r2Key: string; mimeType?: string; } | undefined; } interface WhatsAppAdapterOptions { provider: WaProvider; db: NotifyD1; bucket?: R2BucketLike; cipher?: PhoneCipher; pauseRegistry?: MarketingPauseRegistry; dndCheck?: DndChecker; optOutHook?: WaOptOutHook; stopKeywords?: StopMatchOptions; /** Optional resolver: webhook payload `from` (E.164) → your internal userId. */ userIdFromPhone?: (phoneE164: string) => Promise; /** Force template send even when inside the 24h session window (rare; defaults false). */ forceTemplate?: boolean; } declare function whatsappAdapter(options: WhatsAppAdapterOptions): Adapter; interface MetaWaProviderOptions { accessToken: string; phoneNumberId: string; apiUrl?: string; graphVersion?: string; } declare function metaWaProvider(options: MetaWaProviderOptions): WaProvider; interface TwilioWaProviderOptions { accountSid: string; authToken: string; fromNumber: string; apiUrl?: string; } /** * Stub. The provider interface is fixed so a real implementation can drop * in without touching the adapter or any caller code. */ declare function twilioWaProvider(_options: TwilioWaProviderOptions): WaProvider; interface GupshupWaProviderOptions { apiKey: string; appName: string; apiUrl?: string; } /** Stub. Provider interface is stable. */ declare function gupshupWaProvider(_options: GupshupWaProviderOptions): WaProvider; interface OptInProof { userId: string; phoneE164: string; optedInAt: number; method: string; sourceUrl?: string; ipHash?: string; userAgent?: string; revokedAt?: number; revokeReason?: string; } interface RecordOptInArgs { userId: string; phoneE164: string; method: string; sourceUrl?: string; ipHash?: string; userAgent?: string; } interface OptInDeps { db: NotifyD1; cipher?: PhoneCipher; now?: () => number; } declare function recordOptIn(deps: OptInDeps, args: RecordOptInArgs): Promise; declare function revokeOptIn(deps: OptInDeps, userId: string, reason: string): Promise; declare function isOptedIn(deps: OptInDeps, userId: string): Promise; declare function getOptInProof(deps: OptInDeps, userId: string): Promise; interface SessionWindowDeps { db: NotifyD1; now?: () => number; } /** * Record an inbound message — used to determine whether the recipient is * inside the WhatsApp 24h customer-service window. */ declare function recordInbound(deps: SessionWindowDeps, args: { userId: string; at?: number; text?: string; }): Promise; /** * True iff the most recent inbound message from `userId` is within the WA * 24h window. Returns false when no inbound has been recorded. */ declare function withinSessionWindow(deps: SessionWindowDeps, userId: string): Promise; declare const SESSION_WINDOW_MS: number; interface MediaCacheDeps { db: NotifyD1; now?: () => number; } interface CachedMedia { mediaId: string; mimeType?: string; bytes?: number; uploadedAt: number; expiresAt?: number; } declare function cacheKey(provider: string, r2Key: string, etag: string): string; declare function getCached(deps: MediaCacheDeps, key: string): Promise; declare function putCached(deps: MediaCacheDeps, key: string, args: { provider: string; mediaId: string; mimeType?: string; bytes?: number; ttlMs?: number; }): Promise; declare function purgeExpiredMedia(deps: MediaCacheDeps): Promise<{ deleted: number; }>; declare const DEFAULT_MEDIA_TTL_MS: number; interface ForgetWhatsAppResult { optInRowsDeleted: number; mediaCacheRowsDeleted: number; inboundLogRowsDeleted: number; } /** * Cascade-delete a user's WhatsApp footprint from D1: opt-in proof, * inbound message log. Media-cache rows are NOT user-keyed (they're keyed * by R2 etag) so we leave them; the global TTL purge handles eviction. * * Caller should also invoke `@workkit/notify`'s `forgetUser` to drop the * preferences/opt-out/delivery-record rows in the same transaction. */ declare function forgetWhatsAppUser(db: NotifyD1, userId: string): Promise; /** * D1 schema for `@workkit/notify/whatsapp`. Run alongside `ALL_MIGRATIONS` * from `@workkit/notify` and `INAPP_MIGRATION_SQL` from * `@workkit/notify/inapp`. */ declare const WA_OPTIN_MIGRATION_SQL: string; declare const WA_MEDIA_CACHE_MIGRATION_SQL: string; declare const WA_INBOUND_LOG_MIGRATION_SQL: string; declare const WA_ALL_MIGRATIONS: ReadonlyArray; import { ConfigError, ValidationError } from "@workkit/errors"; declare class OptInRequiredError extends ValidationError { constructor(userId: string, channel?: string); } declare class TemplateNotApprovedError extends ConfigError { constructor(reason: string); } declare class WhatsAppPhoneFormatError extends ValidationError { constructor(value: string); } declare class WhatsAppWebhookSignatureError extends ValidationError { constructor(reason: string); } declare class MarketingPausedError extends ValidationError { constructor(notificationId: string); } export { withinSessionWindow, whatsappAdapter, twilioWaProvider, revokeOptIn, recordOptIn, recordInbound, putCached, purgeExpiredMedia, metaWaProvider, isStopKeyword, isOptedIn, isE164, identityCipher, gupshupWaProvider, getOptInProof, getCached, forgetWhatsAppUser, defaultStopKeywords, cacheKey, assertE164, WhatsAppWebhookSignatureError, WhatsAppTemplateRef, WhatsAppPhoneFormatError, WhatsAppPayload, WhatsAppCategory, WhatsAppAdapterOptions, WaUploadArgs, WaTemplateRef, WaSendResult, WaSendArgs, WaQualityAlert, WaProviderEvent, WaProvider, WaOptOutHook, WaMediaRef, WaInboundMessage, WA_OPTIN_MIGRATION_SQL, WA_MEDIA_CACHE_MIGRATION_SQL, WA_INBOUND_LOG_MIGRATION_SQL, WA_ALL_MIGRATIONS, TwilioWaProviderOptions, TemplateNotApprovedError, StopMatchOptions, SESSION_WINDOW_MS, RecordOptInArgs, PhoneCipher, OptInRequiredError, OptInProof, OptInDeps, MetaWaProviderOptions, MarketingPausedError, MarketingPauseRegistry, MarketingPauseAuditHook, MarketingPauseAuditEvent, GupshupWaProviderOptions, ForgetWhatsAppResult, DndChecker, DEFAULT_MEDIA_TTL_MS, CachedMedia };