/**
 * TunnelLifecycle — single-owner state machine for the tunnel layer.
 *
 * Per spec specs/dev-infrastructure/tunnel-failure-resilience.md Part 2.
 *
 * This module owns the detect → attempt → fall-back → notify → self-heal
 * lifecycle. The existing server.ts startup-retry ladder, background-
 * retry ladder, and Lifeline failure message are retired in favor of
 * routing all retry through here.
 *
 * Design properties:
 *   - Single-writer transitions: every state change goes through
 *     `transition(expectedFrom, to)`, a compare-and-swap that REJECTS
 *     any transition whose expectedFrom doesn't match the current
 *     state. This prevents the error+exit double-handler race that the
 *     concurrency reviewer surfaced.
 *   - Monotonic epoch: each guarded transition carries a `transition.id`
 *     for downstream notification dedup. Notifications are emitted ONLY
 *     from inside the guarded mutation, tagged with the epoch.
 *   - Episode model: each contiguous failure→recovery cycle is an
 *     `episode` with its own id and one-time consent nonce. All consent
 *     state binds to (episodeId, provider, ownerId, chatId, messageId,
 *     issuedAt).
 *
 * This module is provider-agnostic and notification-channel-agnostic.
 * Providers are injected as a pool; the notifier is injected as a sink.
 * Both responsibilities live OUTSIDE this module per the spec's
 * separation of concerns.
 */
import { EventEmitter } from 'node:events';
import type { ProviderFailureReason, ProviderName } from './TunnelProvider.js';
export type TunnelLifecycleState = 
/** No tunnel running and not currently trying. */
'idle'
/** A start attempt is in flight on some provider. */
 | 'starting'
/** A provider is up; URL is being served. */
 | 'active'
/** A Tier-1 provider failed and we are backing off / trying the next one. */
 | 'retrying'
/** All Tier-1 providers exhausted; awaiting owner consent for a Tier-2 relay. */
 | 'awaiting-consent'
/** A Tier-2 relay provider is up; URL being served via the relay. */
 | 'relay-active'
/** Cloudflare recovery probe verifying Tier-1 is back; switch-back imminent. */
 | 'self-healing'
/** All providers exhausted (or consent declined); background self-heal is the only retry mechanism. */
 | 'exhausted';
export interface Episode {
    /** Stable per-failure-cycle id. Resets each time the manager enters `starting` from `idle`/`active`. */
    episodeId: string;
    /** When this episode began (ISO timestamp). */
    startedAt: string;
    /** Number of Tier-1 providers attempted in this episode. */
    tier1Attempts: number;
    /** Last classified failure reason (the most recent provider failure). */
    lastFailureReason: ProviderFailureReason | null;
    /** Which provider names have been attempted in this episode (in order). */
    attemptedProviders: ProviderName[];
}
export interface ConsentRecord {
    /** Tied to the episode that issued the prompt. */
    episodeId: string;
    /** Which Tier-2 provider the consent authorizes. Single-use, per-provider. */
    provider: 'localtunnel' | 'bore';
    /** Owner principal id (Telegram user id). Non-owner clicks are rejected. */
    ownerId: string;
    /** Telegram chat the consent was issued in (DM chat id). */
    chatId: string;
    /** Telegram message id of the inline-button prompt. */
    messageId: string;
    /** 128-bit CSPRNG nonce carried in callback_data. Atomic compare-and-delete on use. */
    nonce: string;
    /** Issued-at (ms epoch). Combined with consentTimeoutMs to expire. */
    issuedAt: number;
}
export interface ConsentCooldownState {
    /** Count of consecutive declines/timeouts feeding the exponential backoff. */
    consecutiveRefusals: number;
    /** When the cooldown was last extended (ms epoch). */
    lastExtendedAt: number;
    /** Until when the cooldown is active (ms epoch). 0 = no cooldown. */
    activeUntil: number;
}
export interface PersistedTunnelState {
    /** Schema version for safe future migrations. */
    version: 1;
    /** Last observed state. Used by the boot-recovery path. */
    lastState: TunnelLifecycleState;
    /** Last active URL (if any) — for diagnostic only; never trusted as live. */
    lastUrl: string | null;
    /** Name of the provider that was active when state was persisted. */
    activeProvider: ProviderName | null;
    /**
     * Set to true the moment a Tier-2 relay-active state is entered. Cleared
     * only after `authToken`/PIN rotation succeeds. If the agent crashes
     * mid-relay-episode, this flag tells the boot-recovery path "rotate
     * BEFORE accepting any API traffic on the new boot" — per spec Part 6.
     */
    rotationPending: boolean;
    /** Cross-episode consent cooldown counter. */
    consentCooldown: ConsentCooldownState;
    /** Current episode, if any. */
    episode: Episode | null;
    /** Saved-at timestamp (ISO). */
    savedAt: string;
}
export interface TransitionEvent {
    /** Monotonic per-manager-instance epoch — never reset. Used by notifiers for dedup. */
    epoch: number;
    /** State BEFORE this transition. */
    from: TunnelLifecycleState;
    /** State AFTER this transition. */
    to: TunnelLifecycleState;
    /** Episode in effect at the time of the transition. */
    episode: Episode | null;
    /** Last classified failure (when relevant). */
    lastFailureReason: ProviderFailureReason | null;
    /** When (ms epoch). */
    at: number;
}
export type TunnelLifecycleEvents = {
    /** Fires on every successful guarded transition. */
    transition: (e: TransitionEvent) => void;
};
/** Classify a provider failure based on the Error's `message`. */
export declare function classifyFailure(err: unknown): ProviderFailureReason;
/** Generate a fresh 128-bit CSPRNG nonce as hex (32 chars). */
export declare function generateNonce(): string;
/** Generate a fresh episode id (16-byte hex, no PII). */
export declare function generateEpisodeId(): string;
export declare function isValidTransition(from: TunnelLifecycleState, to: TunnelLifecycleState): boolean;
export declare class TunnelLifecycle extends EventEmitter {
    private _state;
    private _epoch;
    private _episode;
    private _lastFailureReason;
    private _activeProvider;
    private _consentCooldown;
    private _rotationPending;
    get state(): TunnelLifecycleState;
    get epoch(): number;
    get episode(): Episode | null;
    get activeProvider(): ProviderName | null;
    get rotationPending(): boolean;
    get consentCooldown(): ConsentCooldownState;
    get lastFailureReason(): ProviderFailureReason | null;
    /**
     * Compare-and-swap transition. Returns true if the transition was
     * accepted; false if the current state didn't match `expectedFrom`
     * (caller MUST treat false as a race — the lifecycle moved
     * underneath them — and re-read state).
     *
     * `to` must be reachable from `expectedFrom` per VALID_TRANSITIONS;
     * an invalid pair throws (programming error, not a runtime race).
     */
    transition(expectedFrom: TunnelLifecycleState, to: TunnelLifecycleState, ctx?: {
        episode?: Episode | null;
        activeProvider?: ProviderName | null;
        lastFailureReason?: ProviderFailureReason | null;
        rotationPending?: boolean;
    }): boolean;
    /** Begin a new failure-recovery episode. Called when entering `starting` from `idle` or `active`. */
    startEpisode(): Episode;
    /** Close out the current episode (called when returning to `active` or `idle`). */
    endEpisode(): void;
    /** Record a Tier-1 provider failure attempt against the current episode. */
    recordAttempt(provider: ProviderName, reason: ProviderFailureReason): void;
    /** Apply an exponential consent cooldown after a decline / timeout. */
    recordConsentRefusal(): ConsentCooldownState;
    /** Clear cooldown — called on explicit owner opt-in or fresh post-cooldown episode. */
    clearConsentCooldown(): void;
    /** True if the consent prompt is currently suppressed by the cooldown. */
    isConsentSuppressed(now?: number): boolean;
    /** Snapshot current state for persistence to tunnel.json. */
    snapshot(): PersistedTunnelState;
    /**
     * Restore from a persisted snapshot. Called at boot ONLY for the
     * cooldown counter + rotationPending flag — the state itself is NOT
     * resumed (every boot starts at `idle` and re-enters `starting` from
     * scratch). The rotationPending flag triggers a rotation-before-
     * traffic in the boot-recovery path; rotation clears the flag.
     */
    restoreFrom(snapshot: PersistedTunnelState): void;
    /** Set the rotation-pending flag (entered on `relay-active`; cleared after rotation). */
    setRotationPending(value: boolean): void;
    /** Used by callers (manager) that need to validate a consent-reply against the active state. */
    isAwaitingConsent(): boolean;
}
//# sourceMappingURL=TunnelLifecycle.d.ts.map