/**
 * SecretDrop — Secure secret submission from user to agent.
 *
 * When an agent needs a secret (API key, password, token), it creates a
 * one-time-use, time-limited URL. The user opens the link in a browser,
 * submits the secret via a clean form, and the agent receives it directly —
 * never passing through Telegram or any chat history.
 *
 * Security properties:
 * - One-time use: token destroyed after successful submission
 * - Time-limited: expires after configurable TTL (default 15 minutes)
 * - In-memory only: pending requests and secrets never touch disk
 * - CSRF protection: form includes a hidden CSRF token
 * - Rate-limited: max submissions per IP
 * - XSS-safe: all rendered content is escaped
 * - The URL token IS the auth — no login or bearer token needed
 */
export interface SecretField {
    /** Field identifier (e.g., "api_key", "password") */
    name: string;
    /** Human-readable label shown in the form */
    label: string;
    /** Whether to mask the input (default: true) */
    masked?: boolean;
    /** Placeholder text */
    placeholder?: string;
}
export interface SecretRequest {
    /** Unique token for the URL */
    token: string;
    /** CSRF token embedded in the form */
    csrfToken: string;
    /** What's being requested (shown in the form header) */
    label: string;
    /** Optional description/reason shown to the user */
    description?: string;
    /** Fields to collect (defaults to a single "secret" field) */
    fields: SecretField[];
    /** Telegram topic to notify on receipt */
    topicId?: number;
    /** When this request was created */
    createdAt: number;
    /** When this request expires (ms since epoch) */
    expiresAt: number;
    /** Callback fired when the secret is received */
    onReceive?: (values: Record<string, string>) => void;
    /** Agent name (shown in the form) */
    agentName: string;
    /**
     * Sealed-handoff sender authentication (R1a). When set, the submission MUST
     * carry a `_sig` field — an Ed25519 signature, by the pinned sender key, over
     * the canonical message {@link canonicalSubmitMessage} (token + declared field
     * values). A submission that is unsigned, signed by the wrong key, or whose
     * values were tampered after signing is REJECTED before storage. This closes
     * the "first-POST-wins" race on a write-only URL: possession of the URL is no
     * longer sufficient — the submitter must prove possession of the sender key.
     * Omitted for ordinary (human) Secret Drops, which stay unchanged.
     */
    senderVerification?: {
        /** Raw 32-byte Ed25519 public key of the expected sender, hex-encoded. */
        senderPubKeyHex: string;
    };
}
export interface SecretSubmission {
    /** The values submitted by the user */
    values: Record<string, string>;
    /** When the submission was received */
    receivedAt: string;
    /** The request label */
    label: string;
    /** The topic to notify */
    topicId?: number;
}
export interface CreateSecretRequestOptions {
    /** What's being requested — shown as the form title */
    label: string;
    /** Why it's needed — shown as description in the form */
    description?: string;
    /** Fields to collect. Defaults to single masked "secret" field */
    fields?: SecretField[];
    /** Telegram topic to notify on receipt */
    topicId?: number;
    /** TTL in milliseconds (default: 15 minutes) */
    ttlMs?: number;
    /** Callback when secret is received */
    onReceive?: (values: Record<string, string>) => void;
    /** Sealed-handoff sender authentication (R1a) — see {@link SecretRequest.senderVerification}. */
    senderVerification?: {
        /** Raw 32-byte Ed25519 public key of the expected sender, hex-encoded. */
        senderPubKeyHex: string;
    };
}
/**
 * Canonical message an R1a sealed-handoff sender signs (and the server verifies):
 * the one-time `token`, then each DECLARED field as `name=value`, keys sorted, all
 * joined by newlines. Deterministic on both sides so the Ed25519 signature matches
 * exactly. Binding the token prevents replaying a signature to a different request;
 * binding the values prevents post-signing tampering. The `_sig` field itself is
 * never part of the signed message (it carries the signature, not signed content).
 */
export declare function canonicalSubmitMessage(token: string, declaredValues: Record<string, string>): Buffer;
/**
 * Sender-side counterpart to R1a: build a signed sealed-handoff submission body.
 * Given the one-time `token`, the declared field values, and the sender's raw
 * 32-byte Ed25519 private seed (hex), returns the values plus a `_sig` that the
 * receiver's {@link SecretDrop.submit} verifies against the pinned sender key.
 * The sender POSTs the returned object as the submission body. Keeping the signer
 * and verifier in one module guarantees they share {@link canonicalSubmitMessage}.
 */
export declare function buildSignedSubmission(token: string, declaredValues: Record<string, string>, senderEd25519SeedHex: string): Record<string, string>;
/**
 * Event emitted when a submission has been sitting in `received` for longer
 * than {@link STUCK_CONSUMER_GRACE_MS} without being explicitly consumed.
 * Consumers register a listener via {@link SecretDrop.onStuckConsumer}.
 *
 * Added 2026-05-20 after the topic-10873 incident: a buggy bridge consumer
 * called the (then-destructive) retrieve endpoint, failed to extract the
 * value, and lost the secret silently. The hardening makes retrieval
 * non-destructive by default; this event surfaces the case where a
 * non-destructive retrieve never reaches an explicit consume — a visible
 * cue that the consumer chain broke.
 */
export interface StuckConsumerEvent {
    token: string;
    label: string;
    topicId?: number;
    receivedAt: string;
    minutesUntilCleanup: number;
}
export declare class SecretDrop {
    private pending;
    private received;
    private receivedTimers;
    /** Absolute cleanup deadline (ms-since-epoch) per stored submission — the
     *  sliding idle window can extend up to, but never past, this value. */
    private receivedDeadline;
    private stuckTimers;
    private stuckConsumerListeners;
    private cleanupTimer;
    private agentName;
    constructor(agentName: string);
    /**
     * Register a listener for stuck-consumer events. Listeners are invoked
     * when a submission has been in `received` longer than the grace period
     * (60s) without being explicitly consumed. Multiple listeners may
     * register — all are invoked. Listener errors are caught and logged so
     * a single bad listener cannot block the others.
     */
    onStuckConsumer(listener: (event: StuckConsumerEvent) => void): void;
    /**
     * Create a new secret request. Returns the token for URL construction.
     */
    create(options: CreateSecretRequestOptions): {
        token: string;
    };
    /**
     * Get a pending request by token. Returns null if expired or not found.
     */
    getPending(token: string): SecretRequest | null;
    /**
     * Submit a secret. Validates CSRF, consumes the request (one-time use),
     * fires the callback, and stores the submission for retrieval.
     * Returns the submission on success, null on failure.
     */
    submit(token: string, csrfToken: string, values: Record<string, string>): SecretSubmission | null;
    /**
     * (Re)arm the idle cleanup timer for a stored submission. Fires after
     * {@link RECEIVED_IDLE_TTL_MS} of no activity, but never later than the
     * per-token absolute deadline in {@link receivedDeadline}. Called on submit
     * and on every {@link peekReceived}, so the window SLIDES with activity —
     * an in-use secret cannot be deleted out from under its consumer, while the
     * absolute cap still bounds total in-memory lifetime.
     */
    private armReceivedCleanup;
    /**
     * Retrieve a received submission WITHOUT consuming it. Each call SLIDES the
     * idle cleanup window (see {@link armReceivedCleanup}), so a submission that
     * is actively being retrieved stays available — capped only by the absolute
     * {@link RECEIVED_ABSOLUTE_MAX_MS} deadline. Safe to call repeatedly; this is
     * the path polling/multi-step consumers should use. Consume explicitly via
     * {@link consumeReceived} only after the handoff has actually succeeded.
     *
     * @returns the submission or null if not present (never seen, already
     *   consumed, or already cleaned up past the absolute cap).
     */
    peekReceived(token: string): SecretSubmission | null;
    /**
     * Retrieve AND consume a submission. The submission is removed from
     * the store before the function returns. Use this when the caller is
     * confident the value has been successfully handed off (parsed, stored,
     * forwarded, etc.) and a re-read would be wrong.
     *
     * @returns the submission or null if not present.
     */
    consumeReceived(token: string): SecretSubmission | null;
    /**
     * @deprecated Use {@link peekReceived} for non-destructive reads or
     * {@link consumeReceived} for one-shot semantics. Preserved on the
     * public surface for callers that have not yet migrated; behaves
     * identically to {@link consumeReceived} so existing code keeps
     * working until migrated.
     */
    getReceived(token: string): SecretSubmission | null;
    /**
     * List all pending requests (for the agent's management API).
     */
    listPending(): Array<{
        token: string;
        label: string;
        topicId?: number;
        createdAt: number;
        expiresAt: number;
        expired: boolean;
    }>;
    /**
     * Cancel a pending request.
     */
    cancel(token: string): boolean;
    /**
     * Remove expired requests.
     */
    private cleanup;
    /**
     * Render the secret submission form as self-contained HTML.
     */
    renderForm(request: SecretRequest): string;
    /**
     * Render an expired/not-found page.
     */
    renderExpiredPage(): string;
    /**
     * Shutdown — clean up timers and listeners.
     */
    shutdown(): void;
}
//# sourceMappingURL=SecretDrop.d.ts.map