import type { DatabaseSyncLike } from '../../db.js'; import type { Context } from '../../api.js'; /** * GitHub webhook DLQ. Mirrors the Slack DLQ shape (src/connectors/slack/dlq.ts) * but carries GitHub-specific metadata: event_name, delivery_id, signature, * installation_id, repo_full_name. Codex P1 #5 mandates this rich context * so a `hippo gh dlq replay` operator can triage without re-deriving anything * from the raw payload. * * Buckets: * - parse_error — raw_payload was not valid JSON * - unroutable — no tenant resolved for installation_id / repo_full_name * - signature_failed — HMAC did not verify against the active webhook secret * - unhandled — parsed but no handler matched the event */ export type DlqBucket = 'parse_error' | 'unroutable' | 'signature_failed' | 'unhandled'; export interface DlqItem { id: number; tenantId: string; rawPayload: string; error: string; eventName: string | null; deliveryId: string | null; signature: string | null; installationId: string | null; repoFullName: string | null; retryCount: number; receivedAt: string; retriedAt: string | null; bucket: DlqBucket | string; } /** * `tenantId: null` means the connector could not resolve a tenant for the * envelope (unroutable installation/repo). Stored as the sentinel * `'__unroutable__'` so the NOT NULL column is honored — same convention as * Slack DLQ. */ export interface WriteDlqOpts { tenantId: string | null; rawPayload: string; error: string; bucket?: DlqBucket; eventName?: string | null; deliveryId?: string | null; signature?: string | null; installationId?: string | null; repoFullName?: string | null; } export declare function writeToDlq(db: DatabaseSyncLike, opts: WriteDlqOpts): number; export declare function listDlq(db: DatabaseSyncLike, opts: { tenantId: string; limit?: number; }): DlqItem[]; export declare function getDlqEntry(db: DatabaseSyncLike, id: number): DlqItem | null; export interface ReplayDlqOpts { /** Current webhook secret. If omitted, signature check is skipped (force-only path). */ webhookSecret?: string; /** * Previous webhook secret during rotation (v1.3.1 hotfix — claude P1). * Operators rotating GITHUB_WEBHOOK_SECRET would otherwise be forced into * --force on DLQ rows written under the old secret. Plumbed through to * verifyGitHubSignature.previousSecret. */ previousSecret?: string; /** When true, skip signature verification (used for legacy entries after secret rotation). */ force?: boolean; } export type ReplayStatus = 'replayed' | 'parse_error' | 'sig_fail' | 'sig_missing' | 'unhandled' | 'not_found'; export interface ReplayResult { ok: boolean; status: ReplayStatus; memoryId: string | null; retryCount: number; reason?: string; } /** * Hook the webhook route injects to actually re-ingest a row. Decoupling * the dispatch keeps this module free of every event-type handler — the * route already knows how to route an envelope, so it passes that capability * back in. * * v1.3.2 (claude review): the v1.3.1 contract advertised an `idempotencyKey` * field, but the v1.3.1 ingest re-derives the key from the parsed event * itself, so the field was a phantom — any future hook that trusted the * passed-in value would dedupe against a stale key. Field removed. */ export type IngestHook = (ctx: Context, args: { rawPayload: string; eventName: string; deliveryId: string; }) => Promise<{ memoryId: string | null; }>; /** * Replay a DLQ row through the normal ingest path. Behavior: * 1. Fetch row by id. Not found → `not_found`. * 2. If !force and webhookSecret provided, verify signature with the * current secret. Fail → bump retry_count, `sig_fail`. * Missing signature on the row → `sig_missing` (no bump; --force required). * 3. JSON.parse the raw payload. Fail → bump, `parse_error`. * 4. Type-guard the envelope. Fail → bump, `unhandled`. * 5. If an `ingestHook` is supplied, call it and return its memoryId. * If not (dry-run path), bump retry_count and return status `replayed` * with memoryId=null. The webhook route wires the real hook in Task 14. * * Mirrors Slack's "always use current routing" policy: replays use the * deployment state NOW, not at the time of original DLQing. */ export declare function replayDlqEntry(ctx: Context, id: number, opts?: ReplayDlqOpts & { ingestHook?: IngestHook; }): Promise; //# sourceMappingURL=dlq.d.ts.map