/**
 * Public webhook trigger receiver for routines.
 *
 * A routine may declare a `trigger` block instead of (or alongside) a cron
 * `schedule` (see `JobConfig.trigger` in `../routines.ts`). This module turns
 * incoming GitHub or Linear webhooks into the set of routines they should fire,
 * and dispatches those routines through the exact same path a cron fire uses
 * (`executeJobDetached`).
 *
 * The matching logic is pure, so it can be unit-tested without a daemon or HTTP
 * server. The listener adds the public-ingress requirements: raw-body HMAC
 * verification, idempotency, source allow-listing, and rate limiting.
 */
import * as http from 'http';
import type { IncomingHttpHeaders } from 'http';
import type { JobConfig, RunMeta, WebhookContext } from '../scheduling/routines.js';
import { type FiredHandler } from './handlers.js';
export type WebhookSource = 'github' | 'linear' | 'slack';
export interface IncomingWebhook {
    /** Delivery source, derived from `/hooks/<source>` or one-shot command flags. */
    source: WebhookSource;
    /** Source event name: GitHub header event, Linear payload `type`, or the Slack
     *  event subtype (`app_mention`) / slash command (`/agents`). */
    event: string;
    /** Decoded request body (JSON for GitHub/Linear/Slack-events; the normalized
     *  {@link SlackPayload} for a Slack slash command's form body). */
    payload: Record<string, unknown>;
}
export type GithubWebhook = IncomingWebhook & {
    source: 'github';
};
export type LinearWebhook = IncomingWebhook & {
    source: 'linear';
};
export type SlackWebhook = IncomingWebhook & {
    source: 'slack';
};
/** Read `repository.full_name` (`owner/name`) from a webhook payload, if present. */
export declare function webhookRepo(payload: Record<string, unknown>): string | null;
/**
 * Extract every candidate branch a webhook payload references, per event type.
 * A trigger's `branch` matches if it equals any of these. Different events
 * carry the branch in different places:
 *   - push:          `ref` (refs/heads/<b>)
 *   - pull_request:  base + head refs of the PR
 *   - workflow_run:  `workflow_run.head_branch`
 *   - issue_comment: no branch (comments aren't branch-scoped)
 */
export declare function webhookBranches(event: string, payload: Record<string, unknown>): string[];
export declare function linearAction(payload: Record<string, unknown>): string | null;
export declare function linearTeamKey(payload: Record<string, unknown>): string | null;
export declare function linearLabels(payload: Record<string, unknown>): string[];
export declare function githubAction(payload: Record<string, unknown>): string | null;
export declare function githubLabels(payload: Record<string, unknown>): string[];
/** True when a single job's trigger matches the given webhook. Pure. */
export declare function jobMatchesWebhook(job: JobConfig, webhook: IncomingWebhook): boolean;
/**
 * Pure matcher: given a set of jobs and an incoming webhook, return the jobs
 * whose `trigger` matches. Jobs without a trigger (schedule-only routines) are
 * never selected — proving time-based jobs are unaffected by webhook delivery.
 */
export declare function matchJobsToWebhook(jobs: JobConfig[], webhook: IncomingWebhook): JobConfig[];
/** Options for firing webhook-matched jobs (dispatch is injectable for tests). */
export interface FireWebhookOptions {
    /** Job source. Defaults to all persisted routines (`listJobs()`). */
    jobs?: JobConfig[];
    /**
     * How to dispatch a matched job. Defaults to `executeJobDetached` — the SAME
     * path a cron fire uses (see `daemon.ts`). Injectable so tests can assert
     * matching without spawning real agent processes.
     */
    dispatch?: (config: JobConfig) => Promise<RunMeta>;
    /** Matched job names that already completed for this delivery. */
    skipJobNames?: ReadonlySet<string>;
    /** Called immediately after a single matched job dispatch succeeds. */
    onJobFired?: (job: JobConfig, fired: FiredJob) => void;
    /** Optional webhook context used to expand `{{...}}` placeholders in prompts. */
    context?: WebhookContext;
}
/** Result of firing one matched job. */
export interface FiredJob {
    jobName: string;
    runId: string;
}
export declare class WebhookDispatchError extends Error {
    readonly fired: FiredJob[];
    readonly failures: {
        jobName: string;
        error: Error;
    }[];
    constructor(message: string, fired: FiredJob[], failures: {
        jobName: string;
        error: Error;
    }[]);
}
/**
 * Match an incoming webhook against the persisted routines and fire each match
 * through the cron dispatch path. Returns one entry per fired job.
 */
export declare function fireWebhookJobs(webhook: IncomingWebhook, options?: FireWebhookOptions): Promise<FiredJob[]>;
export declare function verifyGithubSignature(headers: IncomingHttpHeaders, rawBody: Buffer, secret: string): boolean;
export declare function verifyLinearSignature(headers: IncomingHttpHeaders, rawBody: Buffer, secret: string): boolean;
export declare function verifyLinearTimestamp(payload: Record<string, unknown>, now?: number, toleranceMs?: number): boolean;
/**
 * Verify a Slack request signature (the `v0` scheme). Slack signs the base
 * string `v0:${timestamp}:${rawBody}` with the app's signing secret and sends
 * the hex digest as `X-Slack-Signature: v0=<hex>`, alongside the unix
 * `X-Slack-Request-Timestamp`. The timestamp is BOTH part of the signed base
 * string AND checked for freshness here — a request older than `toleranceSec`
 * (default 5 min) is rejected, so a captured, correctly-signed body cannot be
 * replayed later. Fails closed on any missing/malformed header.
 */
export declare function verifySlackSignature(headers: IncomingHttpHeaders, rawBody: Buffer, secret: string, now?: number, toleranceSec?: number): boolean;
/**
 * Normalized fields extracted from a Slack slash-command or Events API delivery.
 * This is the `payload` of a Slack {@link IncomingWebhook}: transport-level
 * parsing lives here, message *semantics* (splitting the mention text into an
 * agent / project / prompt) live in `handlers.ts` `buildWebhookContext`.
 */
export interface SlackPayload extends Record<string, unknown> {
    /** `url_verification` | `event_callback` | `slash_command`. */
    type: string;
    /** Echoed back for the one-time Events API URL-verification handshake. */
    challenge?: string;
    /** Slack event id (Events API) — the dedup key when present. */
    event_id?: string;
    /** Event subtype, e.g. `app_mention` (Events API). */
    event_type?: string;
    /** Raw message text (the mention body, or the slash-command text). */
    text?: string;
    /** Channel id (`C0…`) the message arrived in. */
    channel?: string;
    /** Thread to reply into: an existing thread's parent ts, else the message ts. */
    thread_ts?: string;
    /** Invoking user id (`U0…`). */
    user?: string;
    /** Slash command name, e.g. `/agents`. */
    command?: string;
    /** Slash-command `response_url` (valid ~30 min, 5 uses). */
    response_url?: string;
    /** Slack team / workspace id. */
    team?: string;
}
/**
 * Parse a Slack delivery body into a normalized {@link SlackPayload}. Slack
 * sends slash commands as `application/x-www-form-urlencoded` and Events API
 * deliveries (including the `url_verification` handshake) as
 * `application/json` — this is the one place that content-type branch lives.
 */
export declare function parseSlackBody(contentType: string | undefined, rawBody: Buffer): SlackPayload;
/** The `event` name a Slack delivery fires under: the slash command, or the event subtype. */
export declare function slackEventName(payload: SlackPayload): string;
export interface WebhookSecrets {
    github?: string;
    linear?: string;
    slack?: string;
}
export interface DeliveryStore {
    seen(id: string): boolean;
    mark(id: string): void;
    completedJobs(id: string): ReadonlySet<string>;
    markJob(id: string, jobName: string): void;
}
export declare function createMemoryDeliveryStore(maxEntries?: number): DeliveryStore;
/**
 * A durable, disk-backed delivery store. Unlike `createMemoryDeliveryStore`,
 * seen delivery ids survive a process restart and are bounded by AGE, not by a
 * fixed entry count — so a captured valid delivery cannot re-fire after a
 * restart or after count-based LRU eviction would have dropped it.
 *
 * `retentionMs` doubles as the replay-acceptance window: a delivery whose id is
 * still on record (younger than the window) is rejected as a duplicate; entries
 * older than the window are pruned (keeping the file bounded) since a webhook
 * source will not legitimately retry a delivery that old.
 */
export declare function createFileDeliveryStore(filePath: string, retentionMs?: number): DeliveryStore;
export interface RateLimiter {
    take(key: string): boolean;
}
export declare function createMemoryRateLimiter(limit: number, windowMs: number): RateLimiter;
/**
 * Resolve once a receiver is actually accepting connections, REJECT if the bind
 * failed. `server.listen()` reports a bind failure (EADDRINUSE, EACCES) as an
 * asynchronous `'error'` event, never a throw — so a `try/catch` around
 * `startWebhookServer` cannot see it, and without this the event reaches Node's
 * default handler and takes the whole process down. Every caller that hosts a
 * receiver MUST await this rather than assuming the return means "bound".
 */
export declare function waitForListening(server: http.Server): Promise<void>;
/** Options for the local webhook http listener. */
export interface WebhookServerOptions {
    port?: number;
    host?: string;
    /** HMAC signing secrets keyed by source. */
    secrets: WebhookSecrets;
    /** Override the fire options (mainly for tests). */
    fire?: FireWebhookOptions;
    /**
     * Called the moment a delivery's matches are known — BEFORE any routine or
     * handler is dispatched. Matching is a pure, synchronous lookup (no I/O), so
     * this fires immediately after the ack, regardless of how long the matched
     * work then takes to run. This is the right hook for a "webhook X fired Y"
     * log line: a `run.command` handler that shells out a long agent session
     * (`exec()`, which only resolves on process exit) used to hold that log back
     * for as long as the session ran — `ps` would show the agent already running
     * while the log still implied nothing had matched (RUSH-2722). Names only
     * (no runId/exitCode yet — those aren't known until dispatch settles).
     */
    onMatch?: (webhook: IncomingWebhook, matchedJobNames: string[], matchedHandlerNames: string[]) => void;
    /**
     * Called after a delivery has fully settled — every matched routine and
     * handler dispatched (and, for `run.command` handlers, exited). Because the
     * receiver acks the HTTP response BEFORE dispatch (see `startWebhookServer`),
     * this fires strictly after the response has been written, and is the only
     * way a caller observes the dispatch outcome (runId/exitCode/output). It is
     * NOT the right hook for "did this webhook match" logging — use `onMatch`.
     */
    onDelivery?: (webhook: IncomingWebhook, fired: FiredJob[], handlers: FiredHandler[]) => void;
    /**
     * Called when settling an already-acked delivery threw. The HTTP status can
     * no longer carry the failure, so this is the loud path — the daemon host and
     * `agents webhooks serve` both log it. Never swallowed silently.
     */
    onDeliveryError?: (webhook: IncomingWebhook, error: Error) => void;
    deliveryStore?: DeliveryStore;
    rateLimiter?: RateLimiter;
    rateLimitPerMinute?: number;
    maxBodyBytes?: number;
    /** Per-IP ingress throttle applied BEFORE the body read (bad-sig flood guard). */
    ipRateLimiter?: RateLimiter;
    /** Per-source-IP requests/minute allowed through to the body read. Default 120. */
    ipRateLimitPerMinute?: number;
    /** Max concurrent TCP connections the receiver accepts. Default 256. */
    maxConnections?: number;
}
/**
 * Start a localhost-bound receiver. It accepts only:
 *   POST /hooks/github  with X-Hub-Signature-256
 *   POST /hooks/linear  with Linear-Signature + fresh webhookTimestamp
 *   POST /hooks/slack   with X-Slack-Signature + fresh X-Slack-Request-Timestamp
 *
 * **The ack is asynchronous (RUSH-2548).** Once a delivery has passed signature
 * verification, freshness, dedup, and rate limiting, the receiver writes
 * `202 {ok:true, accepted:true}` IMMEDIATELY and dispatches the matched routines
 * and handlers afterwards. Dispatch starts an agent run and takes 15-20s, which
 * exceeds Linear's delivery timeout — holding the socket open across it made
 * every real delivery log a timeout + retry on Linear's side. Nothing about the
 * dedup ledger changes: the `<source>:<delivery-id>` key is still what makes a
 * retry a no-op, per-job `markJob` still lets a retry finish only the matches
 * that failed, and the delivery is marked complete only after it settles.
 *
 * A retry that lands WHILE the first is still settling is answered as a
 * duplicate from an in-flight set, since `deliveryStore.seen` only reports
 * completed deliveries and would otherwise let a mid-flight retry double-fire.
 *
 * `onMatch` fires as soon as matching is known, before any dispatch; `onDelivery`
 * fires only once every matched routine/handler has settled — see their docs on
 * `WebhookServerOptions`. A caller that wants a prompt "this fired" log uses
 * `onMatch`, not `onDelivery` (RUSH-2722).
 *
 * Returns the underlying server so callers can `close()` it.
 */
export declare function startWebhookServer(options: WebhookServerOptions): http.Server;
