/**
 * TunnelManager — single owner of the tunnel lifecycle.
 *
 * Per spec specs/dev-infrastructure/tunnel-failure-resilience.md.
 *
 * Rewritten on top of the foundation modules:
 *   - TunnelProvider — backend abstraction; concrete providers under
 *     src/tunnel/Cloudflare*Provider.ts.
 *   - TunnelLifecycle — single-writer CAS-guarded state machine.
 *   - TunnelNotifier — two-channel routing (group / owner-DM) of
 *     transition events.
 *
 * The manager is the SOLE owner of the detect → attempt → fall-back →
 * notify → self-heal lifecycle. The previous server.ts startup-retry
 * ladder + background-retry ladder + Lifeline failure message are
 * retired in favor of routing all retry through here (one backoff
 * engine, not two).
 *
 * Scope of this rewrite (PR 2 of the chain):
 *   - Tier-1 provider pool (Cloudflare named → quick) with internal
 *     backoff between retries within an episode.
 *   - Post-start reachability probe (HTTP /health through the public
 *     URL) before declaring `active` — prevents broadcasting a "back
 *     online" link that doesn't actually serve traffic.
 *   - Backward-compatible public API: start(), stop(), forceStop(),
 *     enableAutoReconnect(), disableAutoReconnect(), getExternalUrl(),
 *     url/isRunning/state. Plus the existing events.
 *   - Notifier sink optional; when telegram adapter is plumbed,
 *     transition events route to the group topic.
 *
 * Out of scope for THIS PR (future PRs in the chain):
 *   - Tier-2 consent flow + relay providers (PR 4).
 *   - Owner-DM channel + inline-button consent UX (PR 3).
 *   - authToken/PIN rotation + boot recovery (PR 5).
 *   - Self-heal probe with N-consecutive-success stability gate (PR 6).
 *   - The /tunnel route (PR 7).
 *
 * The lifecycle state machine already supports these states; the
 * manager simply doesn't transition into them yet in PR 2.
 */
import { EventEmitter } from 'node:events';
import { type PersistedTunnelState } from './TunnelLifecycle.js';
import type { TunnelProvider, ProviderName, ProviderFailureReason } from './TunnelProvider.js';
import type { NotifierSink } from './TunnelNotifier.js';
export interface TunnelConfig {
    /** Whether tunnel is enabled. */
    enabled: boolean;
    /** Tunnel type: 'quick' (ephemeral, no account) or 'named' (persistent, requires token). */
    type: 'quick' | 'named';
    /** Cloudflare tunnel token (named, token-auth). */
    token?: string;
    /** Config file path (named, config-file-auth). */
    configFile?: string;
    /** Public hostname for named tunnels. */
    hostname?: string;
    /** Local port to tunnel to. */
    port: number;
    /** State directory for persisting tunnel.json. */
    stateDir: string;
    /** Tier-2 relay providers to offer, in consent order. Default ['localtunnel']. */
    relayProviders?: ('localtunnel' | 'bore')[];
    /** Master switch for Tier-2 relays. Default true. false = Cloudflare-only (no consent offered). */
    relaysEnabled?: boolean;
    /** 'ask' (default) prompts before a relay; 'never' = Cloudflare-only. */
    relayConsent?: 'ask' | 'never';
    /** Consent-prompt timeout in ms. Default 900000 (15 min). */
    consentTimeoutMs?: number;
}
export interface TunnelState {
    url: string | null;
    type: 'quick' | 'named';
    startedAt: string | null;
    connectionId?: string;
    connectionLocation?: string;
}
export interface TunnelEvents {
    url: (url: string) => void;
    connected: (info: {
        id: string;
        ip: string;
        location: string;
    }) => void;
    disconnected: () => void;
    error: (error: Error) => void;
    stopped: () => void;
}
/** Optional injections for testability. */
export interface TunnelManagerInjections {
    providers?: TunnelProvider[];
    notifierSink?: NotifierSink;
    fetch?: typeof fetch;
}
/**
 * Minimal duck-typed interface for the messaging adapter the manager
 * uses for user-facing notifications. The real implementation is
 * `TelegramAdapter` but we don't import that type here to keep this
 * module decoupled from the messaging layer.
 */
export interface TunnelMessagingAdapter {
    sendToTopic(topicId: number, text: string): Promise<unknown>;
    sendToOwnerDM(text: string): Promise<unknown>;
    getDashboardTopicId(): number | undefined;
    getLifelineTopicId(): number | undefined;
    /**
     * Send the consent prompt to the owner with approve/decline inline
     * buttons carrying the nonce. Returns the message id or null on
     * failure. Optional — when the adapter doesn't implement it, the
     * manager falls back to sending the consent prompt as plain text
     * via sendToOwnerDM (degraded: the owner would reply in words,
     * which PR 6 doesn't wire — so the button path is the supported
     * one).
     */
    sendOwnerConsentPrompt?(text: string, nonce: string): Promise<number | null>;
    /** Register the grant/decline callback handler (inline-button clicks). */
    setTunnelConsentHandler?(fn: ((action: 'grant' | 'decline', nonce: string) => Promise<string>) | null): void;
}
export declare class TunnelManager extends EventEmitter {
    private readonly config;
    private readonly stateFile;
    private readonly lifecycle;
    private readonly providers;
    private notifier;
    private readonly fetcher;
    private currentHandle;
    private currentProviderName;
    private _legacyState;
    private _autoReconnect;
    private _stopped;
    private _startPromise;
    private _backoffAttempt;
    private _backoffTimer;
    private _postExhaustedTimer;
    /** Self-heal probe (Part 5): runs while relay-active to detect Tier-1 recovery. */
    private _selfHealTimer;
    private _selfHealSuccesses;
    /** Guards performSwitchBack against re-entrancy (probe tick vs. stop). */
    private _switchingBack;
    /**
     * Pending consent record — populated when the manager enters
     * `awaiting-consent` and cleared on grant / decline / timeout / stop.
     * The nonce is the CSPRNG token sent to the owner; matching it on
     * `grantConsent()` is the security-load-bearing check.
     */
    private _pendingConsent;
    /** Adapter ref captured by attachTelegram — used to send the button prompt. */
    private _consentAdapter;
    /**
     * Credential-rotation callback (wired by server.ts via
     * setCredentialRotator). Owns the WHAT of rotation — regenerate the
     * dashboard PIN + authToken, persist them, and DM the owner the new
     * PIN — while the manager owns the WHEN (every terminal exit from
     * relay-active + boot-recovery). Decoupled from config/messaging so
     * this module stays free of those imports. See
     * specs/dev-infrastructure/tunnel-failure-resilience.md Part 6.
     */
    private _rotateCredentials;
    constructor(config: TunnelConfig, injections?: TunnelManagerInjections);
    get url(): string | null;
    get isRunning(): boolean;
    get state(): TunnelState;
    /** Additive new accessor — lifecycle snapshot. */
    get lifecycleState(): PersistedTunnelState;
    /**
     * Start the tunnel. Drives the full Tier-1 ladder internally — the
     * caller does NOT wrap with its own retry loop (the old server.ts
     * startup ladder is RETIRED).
     *
     * Resolves with the URL of the first provider that reaches `active`.
     * Rejects when all Tier-1 providers fail and backoff is exhausted.
     */
    start(): Promise<string>;
    stop(): Promise<void>;
    /** Force-stop with escalation. Providers internally do SIGINT → SIGKILL. */
    forceStop(_timeoutMs?: number): Promise<void>;
    /**
     * Wire the credential-rotation callback (called by server.ts). The
     * callback regenerates the dashboard PIN + authToken, persists them,
     * and DMs the owner the new PIN. Wired early in startup so both
     * boot-recovery and runtime relay-episode-end can rotate.
     */
    setCredentialRotator(fn: (() => Promise<void>) | null): void;
    /**
     * Rotate credentials if a rotation is pending. Idempotent and safe to
     * call on any stop; the `rotationPending` flag (set on relay-active
     * entry, persisted to tunnel.json) gates it so non-relay stops are
     * no-ops. Returns true iff a rotation was actually performed.
     *
     * Called from:
     *   - `stop()` — relay-active → idle (operator stop / shutdown).
     *   - boot-recovery (`recoverPendingRotation`) — the agent died
     *     mid-relay-episode; rotate before the server accepts traffic.
     *   - self-heal switch-back (PR 8) — relay-active → active.
     *
     * Mandatory-rotation invariant (spec Part 6): the flag is cleared
     * ONLY after the rotator resolves, so a crash mid-rotation re-attempts
     * on next boot. If no rotator is wired (misconfiguration), we log
     * loudly and clear the flag rather than loop forever — but this path
     * should never happen in a correctly-wired server.
     */
    runCredentialRotation(reason: string): Promise<boolean>;
    /**
     * Boot-recovery: if the persisted state shows a relay episode was in
     * flight when the agent last died (rotationPending restored from
     * tunnel.json), rotate BEFORE the server accepts any API traffic.
     * server.ts awaits this before `server.start()`.
     */
    recoverPendingRotation(): Promise<boolean>;
    enableAutoReconnect(): void;
    disableAutoReconnect(): void;
    getExternalUrl(localPath: string): string | null;
    /**
     * Attach a messaging adapter so the manager can route lifecycle
     * transitions to the user. Called by `server.ts` after the telegram
     * adapter is constructed (the tunnel itself is constructed earlier
     * in startup so it can boot before messaging is wired). Safe to call
     * once; subsequent calls replace the active notifier.
     *
     * Channel routing:
     *   - Group messages → Dashboard topic (falls back to Lifeline if
     *     Dashboard isn't ensured yet).
     *   - Owner DM messages → `sendToOwnerDM` on the adapter (the
     *     adapter handles "no owner configured" / "owner hasn't DM'd
     *     the bot yet" failure modes itself).
     *
     * The credentialProvider returns the current URL + dashboard PIN
     * at compose time. The notifier substitutes them into owner-DM
     * messages; the credentials NEVER appear in group messages.
     */
    attachTelegram(adapter: TunnelMessagingAdapter, dashboardPin: () => string | undefined): void;
    private buildDefaultPool;
    private doStart;
    private driveTier1;
    private exhaustedOrBackoff;
    /** First Tier-2 provider that reports available, or null. */
    private findAvailableTier2;
    /** First Tier-1 provider (Cloudflare named/quick), or null. */
    private firstTier1Provider;
    private startSelfHealProbe;
    private stopSelfHealProbe;
    /**
     * One self-heal probe tick. Public so tests can drive the stability
     * gate deterministically (no real multi-minute waits — the spec's
     * Part 8 testability requirement). The production timer calls this on
     * a fixed cadence.
     *
     * Each tick attempts to establish a Tier-1 tunnel and verify
     * reachability. Consecutive successes accrue; a single failure resets
     * the counter (so Cloudflare flapping never triggers a premature
     * switch). On the Nth consecutive success the freshly-verified handle
     * is PROMOTED via an atomic new-then-old switch-back — so the public
     * URL never points at a dead tunnel mid-switch.
     *
     * Returns the outcome for assertions/telemetry.
     */
    runSelfHealCheck(): Promise<'switched' | 'progress' | 'reset' | 'inactive'>;
    /**
     * Atomic switch-back from a Tier-2 relay to a recovered Tier-1 tunnel.
     * `newHandle` is already started AND reachability-verified by the
     * probe. Order (spec Part 5): swap `_state.url` to the new URL in one
     * synchronous assignment and emit 'url' BEFORE tearing the relay down,
     * so getExternalUrl() never returns a dead URL. Relay teardown is the
     * provider's stop() (forceful escalation is the provider contract).
     * On reaching `active`, the relay episode has terminally ended → rotate
     * credentials (PR 7), since the relay operator saw the old PIN/links.
     */
    private performSwitchBack;
    /**
     * Internal — called after the lifecycle transitions to
     * `awaiting-consent`. Generates the one-time nonce, stores the
     * pending consent record, and arms the timeout.
     */
    private requestConsent;
    /** Owner-facing consent prompt text. Honest about third-party exposure + rotation cost. */
    private consentPromptText;
    private clearPendingConsent;
    /**
     * Public — called by the consent UX layer (Telegram callback handler
     * in PR 6) after the owner approves. Validates the nonce matches the
     * pending consent record, starts the Tier-2 provider, and transitions
     * to `relay-active`. Returns true on success, false if the nonce
     * didn't match (replay, race, stale click) or the state moved beyond
     * awaiting-consent.
     */
    grantConsent(nonce: string): Promise<boolean>;
    /**
     * Public — called by the consent UX layer when the owner declines.
     * Validates the nonce, applies the cross-episode cooldown, and
     * transitions to `exhausted` + background retry.
     */
    declineConsent(nonce: string): boolean;
    private recordConsentDecline;
    /**
     * Public — the active pending-consent record (or null). Used by the
     * consent UX layer (PR 6) to know what nonce to embed in the inline
     * button.
     */
    get pendingConsent(): {
        episodeId: string;
        provider: ProviderName;
        nonce: string;
        issuedAt: number;
    } | null;
    /**
     * Background retry — runs the bounded exponential ladder off the
     * start() promise so initial start() resolves/rejects fast. After
     * the ladder exhausts, hands off to the indefinite post-exhausted
     * placeholder (the PR 2 self-heal; PR 6 replaces with the spec's
     * N-consecutive-success probe).
     */
    private scheduleBackgroundRetry;
    private clearBackoffTimer;
    private clearPostExhaustedTimer;
    /**
     * Minimum-viable post-exhausted retry (PR 2 self-heal placeholder).
     * Schedules a single low-frequency probe; on completion (success or
     * failure) it re-arms itself, so the agent keeps trying to recover
     * even after the bounded startup-reconnect ladder gives up. PR 6
     * replaces this with the spec's N-consecutive-success stability gate.
     */
    private schedulePostExhaustedRetry;
    /** HTTP probe through the public URL — confirms the link actually serves. */
    private probeReachability;
    private persist;
    private restorePersisted;
}
export type { ProviderFailureReason };
//# sourceMappingURL=TunnelManager.d.ts.map