/**
 * Per-box hosted webhook receivers: config + daemon host (RUSH-2548).
 *
 * `~/.agents/daemon/webhooks.yaml` declares which signed webhook receivers THIS
 * box hosts — bundle, local port, rate limit, and an optional public Tailscale
 * Funnel port. The daemon's `webhook-receiver` service reads it and binds one
 * receiver per entry, drawing each receiver's signing secret from the broker /
 * machine-local file store (no `AGENTS_SECRETS_PASSPHRASE`, no `nohup`). An
 * absent or empty file hosts nothing, so an unconfigured box binds nothing.
 *
 * This is per-box operational state (a public receiver runs on exactly one box),
 * so it is deliberately NOT part of the fleet-synced device config — it mirrors
 * `services.yaml` / daemon-services.ts, not `agents config`.
 */
import { type WebhookSecrets } from './triggers/webhook.js';
import { type FunnelPort } from './funnel.js';
export declare const DEFAULT_WEBHOOK_PORT = 8787;
export declare const DEFAULT_WEBHOOK_RATE_LIMIT = 60;
/** Optional public exposure for a hosted receiver via Tailscale Funnel. */
export interface HostedReceiverFunnel {
    /** Public HTTPS port (Funnel allows 443 / 8443 / 10000). */
    publicPort: FunnelPort;
}
/** One receiver this box hosts. */
export interface HostedReceiverConfig {
    /** Secrets bundle holding GITHUB_WEBHOOK_SECRET, LINEAR_WEBHOOK_SECRET, and/or
     *  SLACK_SIGNING_SECRET (a Slack receiver also carries SLACK_BOT_TOKEN when the
     *  agent replies with the Slack Web API rather than `agents send`). */
    bundle: string;
    /** Local bind port (default 8787). */
    port?: number;
    /** Accepted deliveries per source per minute (default 60). */
    rateLimit?: number;
    /** Public Funnel exposure; omit to bind localhost only. */
    funnel?: HostedReceiverFunnel;
}
export interface DaemonWebhooksConfig {
    receivers: HostedReceiverConfig[];
}
/** Path to `~/.agents/daemon/webhooks.yaml`. */
export declare function getDaemonWebhooksConfigPath(): string;
/**
 * Read the hosted-receivers config. A missing or malformed file yields an empty
 * receiver list — never throws — so a box with no config hosts nothing.
 */
export declare function readDaemonWebhooksConfig(): DaemonWebhooksConfig;
/** Write the hosted-receivers config, creating the daemon config dir if needed. */
export declare function writeDaemonWebhooksConfig(cfg: DaemonWebhooksConfig): void;
/** The port a receiver binds — its identity, since one port hosts one receiver. */
export declare function hostedReceiverPort(receiver: HostedReceiverConfig): number;
/**
 * Declare a receiver on this box, replacing any existing entry on the same port.
 * Port is the identity because two receivers cannot bind one port — a second
 * `add` on a port is an edit, never a silently ignored duplicate.
 */
export declare function addHostedReceiver(receiver: HostedReceiverConfig): DaemonWebhooksConfig;
/**
 * Drop the receiver bound to `port`. Returns the removed entry, or null when
 * nothing was declared there. The caller is responsible for taking down any
 * public Funnel the removed entry declared — leaving it up would keep a public
 * `https://<host>.ts.net` route pointed at a port nothing serves.
 */
export declare function removeHostedReceiver(port: number): HostedReceiverConfig | null;
/**
 * Resolve a receiver's signing secrets from its bundle. Reads `agentOnly` (no
 * Touch ID — the daemon is a background service, SEC-13): a broker-held or
 * file-store bundle resolves silently; a locked bundle THROWS the actionable
 * unlock message, which fails the receiver LOUD rather than binding with no
 * verifiable signature. Throws when neither webhook secret is present.
 */
export declare function resolveReceiverSecrets(bundle: string): WebhookSecrets;
export interface HostedWebhookReceivers {
    /** Number of receivers actually bound. */
    count: number;
    /** Stop every hosted receiver. Idempotent. */
    close(): Promise<void>;
}
type Logger = (level: string, message: string) => void;
/**
 * Start every receiver declared in `webhooks.yaml`. A receiver that cannot start
 * — an unreadable secret (locked bundle, no webhook secret) or a failed bind
 * (the port is already taken by a foreground `agents webhooks serve`, another
 * daemon, or anything else) — is skipped with a loud WARN and does NOT take the
 * others, or the daemon, down. Returns a handle that stops them all.
 *
 * This is async because a bind failure is only observable asynchronously:
 * `server.listen()` surfaces EADDRINUSE as an `'error'` event, so a `try/catch`
 * around the start call never sees it and the event reaches the process-level
 * `uncaughtException` handler (`index.ts`), which exits 1 for the supervisor to
 * restart — a crash loop that would take the secrets broker, scheduler,
 * monitors, browser IPC, and self-heal down with it. `waitForListening` is what
 * turns that into one skipped receiver.
 */
export declare function startHostedWebhookReceivers(opts: {
    log: Logger;
    /**
     * How a receiver's signing secrets are resolved. Defaults to
     * `resolveReceiverSecrets` (the real broker read). Injectable for the same
     * reason `FireWebhookOptions.dispatch` is — so a test can exercise the bind
     * path against real sockets without a machine-local secrets bundle.
     */
    resolveSecrets?: (bundle: string) => WebhookSecrets;
}): Promise<HostedWebhookReceivers>;
export {};
