import type { Kysely } from 'kysely'; import type { Database, WebhookEndpointRow } from '../db/schema.js'; import { type WebhookEvent } from './events.js'; export declare class WebhookEndpointError extends Error { readonly code: 'not_found' | 'invalid_url' | 'invalid_events'; name: string; constructor(message: string, code?: 'not_found' | 'invalid_url' | 'invalid_events'); } export interface WebhookEndpoint extends Omit { events: WebhookEvent[]; } /** * Where a delivery may be sent. * * `https` or a loopback `http`, and nothing else. The payload names unpublished items by title and * path, so plain `http` to a public host puts an editorial calendar on the wire in clear — nearly * always a typo rather than a decision, and the kind that looks like it is working. Loopback stays * open because `npm run dev` and a receiver on the same machine are real, and neither leaves the * host. * * Only an admin can reach this, which is what settles the SSRF question rather than a host allowlist: * anyone who can create an endpoint can already read every draft through the admin, so a URL aimed * at internal infrastructure tells them nothing they could not already see. */ export declare function validateWebhookUrl(input: string): URL; export interface CreatedWebhookEndpoint { endpoint: WebhookEndpointSummary; /** * The signing secret, for the one screen that shows it. * * Unlike an API key this *is* readable from the row afterwards, and the admin still shows it once * and then offers rotation instead. Holding it does not mean displaying it: a reveal control is a * live credential on screen behind an unattended session, and the recovery — rotate, paste the new * one — is the same two minutes as looking it up would have been. */ secret: string; } export declare function createWebhookEndpoint(db: Kysely, input: { label: string; url: string; events: string[]; userId?: string | null; }): Promise; /** * An endpoint with the secret taken out — what a screen renders and what a route serialises. * * A type rather than a convention, because the failure is silent and total: `selectAll()` on this * table returns a live signing secret, and a route that hands it back puts one in browser history, * in `Referer`, and in every access log between here and the admin's laptop. The same reasoning * that keeps a minted API key out of a query string, one step earlier — and unlike a key, this value * cannot be revoked by looking at it, only rotated. */ export type WebhookEndpointSummary = Omit; export declare function redactWebhookEndpoint(endpoint: WebhookEndpoint): WebhookEndpointSummary; /** * Every endpoint, without secrets. * * Redacted **by construction** rather than by each caller remembering: this is the function the * screens and the REST route use, and the only ones that need the secret are the dispatcher and the * test send, which reach for it explicitly. */ export declare function listWebhookEndpoints(db: Kysely): Promise; export declare function getWebhookEndpoint(db: Kysely, id: string): Promise; export declare function updateWebhookEndpoint(db: Kysely, id: string, input: { label?: string; url?: string; events?: string[]; active?: boolean; }): Promise; /** * Mint a new secret and forget the old one. * * The only recovery from a lost or leaked secret, and it is immediate by design: a grace period * where both verify would mean a leaked secret staying valid for exactly as long as the convenience * is worth, which is the wrong side of that trade for a value one paste replaces. */ export declare function rotateWebhookSecret(db: Kysely, id: string): Promise; /** * Delete an endpoint, taking its deliveries with it. * * A real delete, where an API key is only ever revoked, and the difference is what the row is * evidence of. A key names a principal that acted, so audit entries point at it and it has to stay * resolvable; an endpoint is a destination, and an audit entry about one carries its label already. * Pausing covers the case revocation covers — see `active`. */ export declare function deleteWebhookEndpoint(db: Kysely, id: string): Promise; /** Whether this endpoint asked for this event. */ export declare function matchesEvent(endpoint: WebhookEndpoint, event: WebhookEvent): boolean; /** * Every endpoint that is switched on. * * The subscription narrowing happens in JS rather than as a `like '%…%'` over `events`, and not for * tidiness: a substring match reads `item.publish` as a prefix of `item.published` and would fire * the wrong subscription. The set is single digits on any real deployment, so the filter is free. * * `active` *is* in SQL, because that one is exact and is what makes the common case — nothing * configured, which is every deployment until somebody sets one up — a single indexed miss. */ export declare function activeWebhookEndpoints(db: Kysely): Promise; /** The endpoints one event has to reach. */ export declare function endpointsForEvent(db: Kysely, event: WebhookEvent): Promise;