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 SseSubscriber {
userId: string;
close(): void;
push(payload: string): void;
}
/**
* In-memory SSE registry. Single Worker isolate scope — multi-isolate
* fan-out belongs to a future Durable-Object-backed adapter.
*/
declare class SseRegistry {
private map;
add(sub: SseSubscriber): void;
remove(sub: SseSubscriber): void;
count(userId: string): number;
push(userId: string, event: string): void;
disconnectUser(userId: string): void;
}
interface SseHandlerOptions {
db: NotifyD1;
registry: SseRegistry;
auth: (req: Request) => Promise<{
userId: string;
} | null>;
originAllowlist?: ReadonlyArray;
maxConnPerUser?: number;
heartbeatMs?: number;
}
/**
* Construct an SSE handler. `(req: Request) => Promise`. Auth is
* **required** at construction — there is no anonymous default. Origin
* allowlist defends against cross-origin EventSource scraping when set.
*/
declare function createSseHandler(opts: SseHandlerOptions): (req: Request) => Promise;
interface InAppPayload {
[key: string]: unknown;
}
interface InAppAdapterOptions {
db: NotifyD1;
registry?: SseRegistry;
/** Maximum body length in characters. Default 2000. */
maxBodyChars?: number;
/** Allowed deep-link URL schemes. Default `["https:"]`. */
allowedSchemes?: ReadonlyArray;
}
declare function inAppAdapter(options: InAppAdapterOptions): Adapter;
interface InAppNotificationRow {
id: string;
notificationId: string;
title: string;
body: string;
deepLink: string | null;
metadata: Record | null;
createdAt: number;
readAt: number | null;
dismissedAt: number | null;
}
interface FeedOptions {
userId: string;
cursor?: string | null;
limit?: number;
includeRead?: boolean;
includeDismissed?: boolean;
}
interface FeedPage {
items: InAppNotificationRow[];
nextCursor: string | null;
}
declare function feed(db: NotifyD1, opts: FeedOptions): Promise;
interface MarkReadOptions {
userId: string;
ids?: string[];
markAll?: boolean;
}
declare function markRead(db: NotifyD1, opts: MarkReadOptions, now?: number): Promise<{
updated: number;
}>;
declare function dismiss(db: NotifyD1, opts: {
userId: string;
ids: string[];
}, now?: number): Promise<{
updated: number;
}>;
declare function unreadCount(db: NotifyD1, userId: string): Promise;
interface SafeLinkOptions {
allowedSchemes?: ReadonlyArray;
/**
* Allow relative paths that start with `/` (e.g. `/briefs/r1`). Default true.
* Protocol-relative URLs (`//host/...`) are always rejected; bare paths
* without a leading `/` are not accepted either.
*/
allowRelative?: boolean;
}
/**
* Sanity-check a deep-link URL: only schemes in the allowlist (default
* `https:`) are permitted; relative paths (`/foo`) are allowed by default.
* `javascript:`, `data:`, `file:` always rejected.
*
* Returns the input unchanged on success; throws `UnsafeLinkError` otherwise.
*/
declare function safeLink(value: string, options?: SafeLinkOptions): string;
interface ForgetInAppResult {
rowsDeleted: number;
}
/**
* Cascade-delete a user's in-app notification feed AND drop any active
* SSE subscriptions for that user. Call alongside `@workkit/notify`'s
* `forgetUser` for the full GDPR/DPDP cascade.
*/
declare function forgetInAppUser(db: NotifyD1, userId: string, registry?: SseRegistry): Promise;
/**
* D1 schema additions for `@workkit/notify/inapp`. Run once during your
* migration setup, alongside `ALL_MIGRATIONS` from `@workkit/notify`.
*/
declare const INAPP_MIGRATION_SQL: string;
import { ValidationError } from "@workkit/errors";
declare class BodyTooLongError extends ValidationError {
constructor(actual: number, cap: number);
}
declare class UnsafeLinkError extends ValidationError {
constructor(value: string, reason: string);
}
export { unreadCount, safeLink, markRead, inAppAdapter, forgetInAppUser, feed, dismiss, createSseHandler, UnsafeLinkError, SseSubscriber, SseRegistry, SseHandlerOptions, SafeLinkOptions, MarkReadOptions, InAppPayload, InAppNotificationRow, InAppAdapterOptions, INAPP_MIGRATION_SQL, ForgetInAppResult, FeedPage, FeedOptions, BodyTooLongError };