/** * Webhook event types sent by SEO Faster. * * The union is intentionally OPEN (`| (string & {})`) so that: * - TypeScript still autocompletes the known event types * - New event types added on the backend don't break consumers with * exhaustive `switch (event)` statements — they hit the default branch * and ack with 200 instead of failing compilation * * Backend currently fires: * article.published, article.updated, article.unpublished, article.deleted, * article.slug_changed, article.translation_completed, list.updated, * article.generation_started, article.generation_completed, * article.generation_failed * * Generation events are SEO Faster dashboard internals; client SDKs * typically ignore them. The other seven are content-facing. */ type WebhookEventType = 'article.published' | 'article.updated' | 'article.unpublished' | 'article.deleted' | 'article.slug_changed' | 'article.translation_completed' | 'list.updated' | (string & {}); /** * Featured image data in webhook payload */ interface WebhookFeaturedImage { url: string; alt: string; } /** * Article metadata sent in webhook payloads. * * Some events ride extra fields merged into `data` via the backend's * `meta.extra` mechanism: * - article.slug_changed → oldSlug, newSlug, locale (already in base) * - list.updated → affectedSlugs, action ('published'|'deleted'|...) * * Full content is NOT included — fetch via the API client if needed. */ interface WebhookArticleData { /** MongoDB document ID */ id: string; /** URL-friendly slug */ slug: string; /** Article title */ title: string; /** Content locale (e.g., 'en', 'es', 'de') */ locale: string; /** Article category */ category: string; /** Publication status */ status: 'draft' | 'published' | 'approved' | 'archived' | 'pending_review' | 'rejected' | string; /** Featured image (if available) */ featuredImage: WebhookFeaturedImage | null; /** When the article was published */ publishedAt: string | null; /** When the article was last updated */ updatedAt: string; /** Present on article.slug_changed */ oldSlug?: string; /** Present on article.slug_changed */ newSlug?: string; /** Present on list.updated (e.g. bulk publish) */ affectedSlugs?: string[]; /** Present on list.updated — e.g. 'published' | 'deleted' */ action?: string; } /** * Workspace info in webhook payload */ interface WebhookWorkspace { id: string; slug: string; } /** What changed in an article.updated event */ type WebhookChangeType = 'content' | 'image' | 'metadata' | 'translation' | 'state' | 'full'; /** Who initiated the change */ type WebhookSource = 'admin' | 'ai' | 'batch' | 'schedule' | 'api' | 'system'; /** * Full webhook payload structure. * * `version` was added in Aℓ.1 (currently 1). Future schema bumps will * increment it; SDK code should treat the field as optional for backward * compatibility with older backends. */ interface WebhookPayload { /** Payload schema version (added in Aℓ.1). Optional for older backends. */ version?: number; /** Event type that triggered the webhook */ event: WebhookEventType; /** Workspace that owns the content */ workspace: WebhookWorkspace; /** Article metadata + event-specific extras */ data: WebhookArticleData; /** Unix timestamp (seconds) when the event occurred */ timestamp: number; /** What part of the article changed (only on article.updated) */ changeType?: WebhookChangeType; /** Names of fields that changed, e.g. ['title','content.html'] */ changedFields?: string[]; /** Who initiated the change (admin user, AI job, batch op, …) */ source?: WebhookSource; } /** * Cache configuration for automatic invalidation. * * Both builder callbacks receive the full payload as a second arg so they * can inspect event-specific fields (oldSlug/newSlug on slug_changed, * affectedSlugs on list.updated, changeType on article.updated). */ interface CacheConfig { /** * Generate paths to revalidate using revalidatePath(). * @example (article) => [`/${article.locale}/blog/${article.slug}`, `/${article.locale}/blog`] */ revalidatePaths?: (article: WebhookArticleData, payload?: WebhookPayload) => string[]; /** * Generate tags to revalidate using revalidateTag(). * @example (article) => [`article-${article.slug}`, `articles-${article.locale}`] */ revalidateTags?: (article: WebhookArticleData, payload?: WebhookPayload) => string[]; /** * Next.js 16 revalidateTag profile. Default 'max' = stale-while-revalidate * (next visitor sees the cached page instantly while a fresh render * happens in background — no first-visit latency penalty after webhook). * Use 'expire' if editors expect the next view to show the new version * even at the cost of one slow request. */ revalidateMode?: 'max' | 'expire'; } /** * Configuration for the webhook handler. * * Every callback is optional. If `cache` is provided, the handler runs * cache invalidation BEFORE the custom callback. Any throw inside the * cache step is caught + forwarded to `onError` and does NOT crash the * handler — the next-cache infra occasionally hiccups and a 500 here would * trigger backend retries that all fail identically. */ interface WebhookHandlerConfig { /** * Your webhook secret(s) from SEO Faster dashboard. * * Pass an array to support zero-downtime secret rotation: every secret * in the list is tried during verification, and the request validates * if ANY matches. Typical pattern is `[newSecret, oldSecret]` during * the 24h grace window after a rotation (matches backend Aℓ.11). */ secret: string | string[]; /** * Declarative cache invalidation config (recommended). * Automatically calls revalidatePath() and revalidateTag() for all events. */ cache?: CacheConfig; onArticlePublished?: (article: WebhookArticleData, payload: WebhookPayload) => Promise | void; onArticleUpdated?: (article: WebhookArticleData, payload: WebhookPayload) => Promise | void; onArticleUnpublished?: (article: WebhookArticleData, payload: WebhookPayload) => Promise | void; onArticleDeleted?: (article: WebhookArticleData, payload: WebhookPayload) => Promise | void; onSlugChanged?: (article: WebhookArticleData, payload: WebhookPayload) => Promise | void; onArticleTranslationCompleted?: (article: WebhookArticleData, payload: WebhookPayload) => Promise | void; onListUpdated?: (article: WebhookArticleData, payload: WebhookPayload) => Promise | void; /** * Catch-all error hook — fired on any throw inside the cache step or * any per-event callback. Use to forward to Sentry / log aggregation. * Errors thrown by `onError` itself are swallowed; the handler still * acks with 200 to prevent backend retry storms. */ onError?: (err: unknown, request: Request) => Promise | void; /** * Allow-list of locales this client actually serves (B.9). * * SEOFaster generates content in 19+ locales; chatfaster (for example) * only renders en/ar/de. Without this filter, every `article.updated` * for a `ko` article still runs cache invalidation that does nothing — * wasted work + log noise. With `supportedLocales: ['en','ar','de']`, * events for other locales are acked with 200 and skip the rest of the * handler entirely. * * Locale check looks at `payload.data.locale`. Events without a locale * (e.g. workspace-level events the SDK may not even know about yet) * pass through unfiltered. */ supportedLocales?: string[]; } /** * Result of signature verification */ interface VerificationResult { valid: boolean; error?: string; } declare function verifyWebhookSignature(payload: string, signature: string, secret: string | string[], timestamp?: string | null, maxAge?: number): Promise; declare function getDefaultCacheTags(article: WebhookArticleData, payload?: WebhookPayload): string[]; /** * Generate default paths to revalidate for an article. * Covers common blog URL patterns. * * @param article - Article data from webhook * @returns Array of paths to revalidate * * @example * ```typescript * export const POST = createWebhookHandler({ * secret: process.env.SEOFASTER_WEBHOOK_SECRET!, * cache: { * revalidatePaths: getDefaultCachePaths, * }, * }, { revalidatePath, revalidateTag }); * ``` */ /** * One-shot helper that runs the right invalidation for ANY event type * using the SDK's default tag/path builders (B.13). * * Lets a client drop the entire `cache: { revalidateTags, revalidatePaths }` * block from `createWebhookHandler` config: * * ```ts * import { createWebhookHandler, revalidateForEvent } from * '@codepark-apps/seofaster-nextjs/webhook'; * import { revalidatePath, revalidateTag } from 'next/cache'; * * export const POST = createWebhookHandler({ * secret: process.env.SEOFASTER_WEBHOOK_SECRET!, * onArticleUpdated: (a, p) => revalidateForEvent(p, { revalidatePath, revalidateTag }), * onArticlePublished:(a, p) => revalidateForEvent(p, { revalidatePath, revalidateTag }), * onArticleDeleted: (a, p) => revalidateForEvent(p, { revalidatePath, revalidateTag }), * onSlugChanged: (a, p) => revalidateForEvent(p, { revalidatePath, revalidateTag }), * onListUpdated: (a, p) => revalidateForEvent(p, { revalidatePath, revalidateTag }), * // ... etc. * }); * ``` * * Each call invalidates the union of `getDefaultCachePaths(payload.data, payload)` * and `getDefaultCacheTags(payload.data, payload)` — which already cover slug * rename, list.updated affected slugs, and the sitemap tag. */ declare function revalidateForEvent(payload: WebhookPayload, fns: RevalidationFunctions, opts?: { mode?: 'max' | 'expire'; }): void; declare function getDefaultCachePaths(article: WebhookArticleData, payload?: WebhookPayload): string[]; /** * Revalidation functions injected from Next.js * Must be provided when using cache config * Supports both Next.js 14/15 and Next.js 16 APIs */ interface RevalidationFunctions { revalidatePath: (path: string, ...args: any[]) => any; revalidateTag: (tag: string, ...args: any[]) => any; } /** * Create a webhook handler for Next.js App Router * * @param config - Handler configuration with secret and event callbacks * @param revalidationFns - Optional revalidation functions from next/cache (required if using cache config) * @returns A POST handler function for use in route.ts * * @example * ```typescript * // app/api/seofaster-webhook/route.ts * import { createWebhookHandler } from '@codepark-apps/seofaster-nextjs/webhook'; * import { revalidatePath, revalidateTag } from 'next/cache'; * * // Option 1: Declarative cache config (recommended) * export const POST = createWebhookHandler({ * secret: process.env.SEOFASTER_WEBHOOK_SECRET!, * cache: { * revalidatePaths: (article) => [ * `/${article.locale}/blog/${article.slug}`, * `/${article.locale}/blog`, * ], * revalidateTags: (article) => [ * `article-${article.slug}-${article.locale}`, * `articles-${article.locale}`, * 'articles-all', * ], * }, * }, { revalidatePath, revalidateTag }); * * // Option 2: Custom callbacks for more control * export const POST = createWebhookHandler({ * secret: process.env.SEOFASTER_WEBHOOK_SECRET!, * onArticlePublished: async (article) => { * revalidatePath(`/${article.locale}/blog/${article.slug}`); * }, * }); * ``` */ declare function createWebhookHandler(config: WebhookHandlerConfig, revalidationFns?: RevalidationFunctions): (request: Request) => Promise; export { type CacheConfig, type RevalidationFunctions, type VerificationResult, type WebhookArticleData, type WebhookChangeType, type WebhookEventType, type WebhookFeaturedImage, type WebhookHandlerConfig, type WebhookPayload, type WebhookSource, type WebhookWorkspace, createWebhookHandler, getDefaultCachePaths, getDefaultCacheTags, revalidateForEvent, verifyWebhookSignature };