/** Which allowance an alert is about, in the consumer's own words where the plan gave any. */ export interface NotifiedWindow { every: string; label: string | null; used: number; limit: number; percent: number; } export interface NotifiedMember { id: string; email: string | null; } /** What happened. One variant per thing worth telling somebody about. */ export type BillingNotification = /** A member was invited and the invitation record exists. `to` is the invitee. */ { type: "invitation.created"; orgId: string; to: string[]; data: { invitationId: string; email: string; roleSlug: string; acceptUrl: string; organizationId: string; inviterUserId?: string; }; } /** Somebody asked for more than their allowance. `to` is every admin of the workspace. */ | { type: "topup.requested"; orgId: string; to: string[]; data: { requestId: string; member: NotifiedMember; credits: number; /** The window that refused them, when the ask came from one. */ window: NotifiedWindow | null; }; } /** An admin answered — or granted unprompted. `to` is the member it is about. */ | { type: "topup.resolved"; orgId: string; to: string[]; data: { requestId: string | null; member: NotifiedMember; credits: number; outcome: "approved" | "denied" | "granted"; }; } /** Somebody asked to move up a rung the money cannot buy (a seat, a plan). */ | { type: "upgrade.requested"; orgId: string; to: string[]; data: { requestId: string; member: NotifiedMember; kind: "seat" | "plan"; /** The seat type or plan key they asked for. */ target: string; /** * The consumer's own form, and who to answer — carried because a renderer needs * them and looking them up again means the app reading a store the library owns. * Both empty for an ordinary upgrade ask; both present for a quote-only one, which * is the case where somebody has to write back. */ metadata?: Record; contact?: { firstName: string; lastName: string; email: string; }; }; } /** An operator answered. `to` is the admin who asked. */ | { type: "quote.resolved"; orgId: string; to: string[]; data: { quoteId: string; member: NotifiedMember; quote: unknown; }; } /** An allowance crossed a threshold. Fired once per window per threshold per cycle. */ | { type: "usage.threshold"; orgId: string; to: string[]; data: { /** Whose wall it is: one member's pack, or the workspace's own pool/ceiling. */ scope: "member" | "org"; member: NotifiedMember | null; window: NotifiedWindow; /** * The threshold crossed, not the exact percentage. * * `percent` when it is a share of an allowance the plan gives (80, 100). `credits` * when it is the customer's OWN monthly spend alert, where they chose an absolute * figure and a percentage of it would be a number they never typed. */ threshold: number; unit: "percent" | "credits"; }; }; /** An event as a consumer receives it: what happened, plus how to not send it twice. */ export type DeliverableNotification = BillingNotification & { /** Stable across retries and re-fires. Dedupe on this. */ id: string; /** Epoch ms the event was emitted. */ at: number; }; export interface Notifier { deliver(notification: DeliverableNotification): Promise; } /** * Who the event is for, stated as a question rather than an answer. * * A call site knows the workspace and, at most, a member id. Turning that into addresses is * a membership question, answered once in `createEmitter` — off the hot path, because a * metered call must not wait on a WorkOS round trip to send an email. */ export type Audience = /** Every admin of the workspace. "Somebody is asking you for something." */ { kind: "admins"; } /** One member, by id. "Here is the answer to what you asked." */ | { kind: "member"; memberId: string; } /** A literal address — an invitee, who is by definition not a member yet. */ | { kind: "email"; email: string; } /** * The DEPLOYMENT's own staff (`BILLING_OPERATOR_EMAILS`), not the workspace's. * * The one audience on our side of the transaction. An ask to move onto a quote-only plan * is not something the customer's own admins can answer — there is no price yet — so * sending it to them is telling the wrong people about a question only we can settle. */ | { kind: "operators"; }; /** * Fire an event. Returns immediately; failures are swallowed. * * This is the type every call site inside the library holds. `undefined` when no notifier is * configured, which is why every site calls it as `notify?.(…)` — a deployment that wants no * notifications pays nothing, not even the object. */ export type Notify = (notification: BillingNotification & { id: string; audience?: Audience; }) => void; /** `v1,` over `..` — the Svix scheme, because that is what * a consumer's inbound route is most likely to already verify. */ export declare function signNotification(secret: string, id: string, timestamp: number, body: string): string; /** * Verify a signature the way a receiver should: constant-time, and inside a replay window. * * Exported so a consumer's route does not hand-roll it. The one that did (an inbound email * webhook) got it right, and the second one to try would be the one that skipped the * timestamp check. */ export declare function verifyNotification(secret: string, headers: { id?: string | null; timestamp?: string | null; signature?: string | null; }, body: string, opts?: { toleranceSec?: number; now?: number; }): boolean; export interface WebhookNotifierOptions { /** Where to POST. The consumer's own route, usually. */ endpoint: string; /** Shared secret for the signature. Without one the POST is unsigned, which is only ever * right for a localhost endpoint. */ secret?: string; /** Per attempt. Default 5s — a notification must never hold anything open. */ timeoutMs?: number; /** Attempts after the first. Default 2, backing off 250ms then 500ms. */ retries?: number; /** Extra headers (an auth header for a gateway, say). */ headers?: Record; fetchImpl?: typeof fetch; } /** * Deliver over HTTP, signed. * * Shipped rather than left to each consumer because the interesting parts are not the POST: * they are the timeout (a hung endpoint must not hold a metered call open), the retry * (a notification is worth one more try and not ten), and the signature (the receiver has to * be able to tell the library from anyone else who learned the URL). Two consumers writing * that twice is two chances to get the replay window wrong. * * 4xx is NOT retried: the receiver understood and refused, and repeating it just doubles the * refusals. 5xx and network failures are. */ export declare function webhookNotifier(opts: WebhookNotifierOptions): Notifier; /** For a consumer that wants an id and has nothing stable to derive one from. */ export declare function notificationId(prefix: string): string; //# sourceMappingURL=index.d.ts.map