/**
 * TunnelNotifier — two-channel routing of tunnel lifecycle events to
 * the user.
 *
 * Per spec specs/dev-infrastructure/tunnel-failure-resilience.md Part 3.
 *
 * Strict channel separation (the GPT external review's CRITICAL finding):
 *   - GROUP topic (Dashboard / Lifeline fallback / General fallback):
 *     status text only. NEVER the URL, NEVER the PIN, NEVER a signed
 *     view link. Anyone in the group can read those, which would defeat
 *     the owner-only consent gate.
 *   - OWNER DM (private bot chat with the owner principal): the actual
 *     URL + PIN + signed view links. The only credential-bearing channel.
 *
 * Class-based throttling (verification finding V2 + GPT finding #5):
 *   - action-required: NEVER throttled within an episode. Cross-episode
 *     cooldown applies (consent prompts) but the lifecycle decides that,
 *     not the notifier.
 *   - state-change: light — at most one per (episode, state) pair, and
 *     at most one per 15 minutes within an episode on re-entry of the
 *     same state.
 *   - noise: heavy — flapping episodes collapse into one "tunnel
 *     unstable" message, suppressed for the rest of the episode.
 *
 * Dedup by transition epoch (the lifecycle's monotonic counter): the
 * notifier never emits twice for the same epoch, even if the same
 * event fires through multiple listeners.
 */
import type { TransitionEvent } from './TunnelLifecycle.js';
import type { ProviderName } from './TunnelProvider.js';
/** Message class — drives throttling policy. */
export type NotificationClass = 'action-required' | 'state-change' | 'noise';
/** Channel — drives credential-handling policy. */
export type NotificationChannel = 'group' | 'owner-dm';
export interface NotifierMessage {
    channel: NotificationChannel;
    class: NotificationClass;
    text: string;
    /** Episode this message belongs to (for cross-episode dedup). */
    episodeId: string | null;
    /** Transition epoch — never emit twice for the same epoch. */
    epoch: number;
}
export interface NotifierSink {
    /** Send a status-text message to the group topic (Dashboard preferred, Lifeline/General fallback). */
    sendGroup(text: string): Promise<void>;
    /** Send a credential-bearing message to the owner's private bot DM. */
    sendOwnerDM(text: string): Promise<void>;
}
export interface NotifierClock {
    now(): number;
}
/**
 * Credential snapshot the notifier substitutes into owner-DM messages
 * at compose time. Returned by a `credentialProvider` callback so the
 * notifier never holds stale credentials in its own state.
 */
export interface CredentialSnapshot {
    /** Current public URL (null when no tunnel is active). */
    url: string | null;
    /** Current dashboard PIN (undefined when not configured). */
    pin?: string;
}
export interface TunnelNotifierOptions {
    sink: NotifierSink;
    clock?: NotifierClock;
    /**
     * Returns a fresh credential snapshot. Called each time the notifier
     * composes an owner-DM message that needs URL/PIN substitution.
     * Optional — when absent, owner-DM messages render without
     * credentials (the user gets the explanatory text but no link). This
     * is the back-compat path for tests that don't wire a credential
     * provider.
     */
    credentialProvider?: () => CredentialSnapshot;
    /** Within-episode same-state re-entry throttle (default 15 min). */
    stateChangeMinIntervalMs?: number;
    /** Flap threshold — N connect/drop cycles within an episode → noise-collapse. */
    flapThreshold?: number;
    /**
     * When true, the notifier does NOT emit the owner-DM consent PROMPT
     * for the awaiting-consent transition (the group pointer still
     * fires). Set by the manager when an adapter capable of sending the
     * button-bearing prompt is attached — the manager sends that prompt
     * itself (with the nonce + inline keyboard) so the notifier's plain-
     * text version would be a duplicate.
     */
    suppressConsentDM?: boolean;
}
export declare class TunnelNotifier {
    private readonly sink;
    private readonly clock;
    private readonly credentialProvider;
    private readonly suppressConsentDM;
    private readonly stateChangeMinIntervalMs;
    private readonly flapThreshold;
    private lastEmittedEpoch;
    private currentEpisodeId;
    /** Last-emitted timestamps keyed by `<state>::<channel>` so the group
     * pointer and the owner-DM credential delivery are throttled
     * independently — otherwise the DM gets swallowed after the group
     * pointer fires for the same state-change. */
    private lastEmittedAt;
    /** Track noise-collapse emission per episode. Once emitted, further
     * noise messages in the same episode are suppressed. */
    private noiseEmittedThisEpisode;
    private flapCycles;
    private flapCollapsed;
    constructor(opts: TunnelNotifierOptions);
    /**
     * Drive the notifier from a lifecycle transition. Idempotent on the
     * transition epoch — repeated calls with the same epoch are no-ops.
     */
    onTransition(e: TransitionEvent): Promise<void>;
    /**
     * Compose the user-facing messages for a transition. May return zero,
     * one, or two messages (some transitions emit to BOTH channels, e.g.
     * "back online — link in your DM" + DM with the URL).
     *
     * The DM credential payload is filled in by the manager via a callback
     * (the notifier itself doesn't know URLs/PINs); for now the DM strings
     * are placeholders that the manager substitutes before send. Future:
     * extend onTransition signature to accept the credential payload.
     */
    private composeMessages;
    /**
     * Throttle decision per spec Part 3:
     *   - action-required: NEVER throttled within an episode.
     *   - state-change: at most one per (episode, state, channel) combo
     *     within `stateChangeMinIntervalMs`. The channel scoping is
     *     load-bearing: the group pointer and the owner-DM credential
     *     delivery are TWO messages for the same state-change; they must
     *     both emit, not throttle each other out.
     *   - noise: emits at most once per episode. The flap-collapse
     *     message goes out the first time the threshold is crossed; all
     *     subsequent noise in the same episode is suppressed.
     */
    private allow;
    /** Snapshot of credentials at compose time (safely degrades when no provider). */
    private creds;
    private renderLink;
    private composeRecoveredDM;
    private composeRestoredDM;
    private composeRelayActiveDM;
    private composeConsentPromptDM;
}
export type { ProviderName };
//# sourceMappingURL=TunnelNotifier.d.ts.map