import { type IncomingMessage, type ServerResponse } from "node:http";
import { type Json } from "./protocol.js";
/**
 * sudo with no terminal, answered from the phone.
 *
 * `sudo -A` runs `SUDO_ASKPASS` (`<bridge>/askpass`, a two-line script the
 * Hook installs) with its prompt as the argument and reads the password from
 * its stdout. The script runs the bundle's `askpass`, which asks the Hook over
 * agent.sock and holds the connection. The password must reach sudo and no
 * one else, so before asking the phone the Hook checks the whole chain
 * (`verifyAsker`): the asker is this Hook's node running its bundle with no
 * flags, its parent is the installed script, that script's parent is a sudo
 * running as root (euid 0, which no process of the owner's can fake), and the
 * asker's stdout is a pipe no other process of the owner's reads. The
 * connection is bound to that process too: on macOS its other end must be
 * held by the asker alone; on Linux the Hook writes the password straight into
 * the asker's stdout (`/proc/<pid>/fd/1`), so a connection that names another
 * process's pid gets nothing. It reads
 * that sudo's command line itself and pushes the request to the phone; `GET /v1/sudo` and the overview socket's `sudo` frame
 * list it. `POST /v1/sudo/answer` writes the password into the held
 * connection and forgets the request: the password is never logged, stored,
 * or kept past that write. Deny, a timeout, no reachable phone, or the asker
 * going away ends the request, and askpass exits non-zero, so sudo fails.
 */
export declare const SUDO_HOLD_MS: number;
/** Held askpass requests at once; more are refused. */
export declare const MAX_SUDO_PENDING = 8;
/** How long after a password is handed over the Hook waits to see whether
 * sudo asks again, before calling it accepted. */
export declare const SUDO_OUTCOME_MS: number;
export type SudoOutcome = "accepted" | "rejected" | "unknown";
export declare const ASKPASS_FILE = "askpass";
export declare const askpassPath: () => string;
export declare const askpassInstalled: () => boolean;
/** SUDO_ASKPASS for a launch, when this computer's Hook installed its helper. */
export declare const askpassEnv: () => Record<string, string>;
/** sudo runs SUDO_ASKPASS with no arguments of its own, so the helper is a
 * script: the node that installed the Hook, the current bundle, `askpass`.
 * It stays node's parent (no exec) so the Hook can see it in the chain, and
 * drops what would load other code into node. */
export declare function askpassScript(node: string, bundle: string): string;
export declare function installAskpass(node: string, bundle: string): Promise<void>;
export declare function removeAskpass(): Promise<void>;
export interface SudoSession {
    source?: string;
    label?: string;
    server?: string;
    workspace?: string;
    tab?: string;
    pane?: string;
}
/** What the phone sees. Never carries a password. */
export interface SudoRequestView {
    /** `account` is whose password sudo asks for (the invoking user); `user` is who the command runs as. */
    id: string;
    computer: string;
    command: string;
    account?: string;
    user?: string;
    cwd?: string;
    session?: SudoSession;
    askedAt: string;
    expiresAt: string;
}
/** What askpass gets: the password to print, word that the Hook wrote it into
 * askpass's stdout itself (Linux), or why not. */
export type SudoReply = {
    password: string;
} | {
    delivered: true;
} | {
    error: string;
    status: number;
};
/** The command a sudo process will run, without sudo's own options, and the
 * user it runs as. Falls back to the whole line when there is no command
 * (`sudo -v`, `sudo -s`). */
export declare function sudoCommand(argv: string[]): {
    command: string;
    user?: string;
};
export interface ProcessRecord {
    ppid: number;
    /** Effective user id. */
    euid: number;
    /** Executable name (`comm`). */
    name: string;
    /** Arguments, split on NULs on Linux and on spaces on macOS. */
    argv: string[];
    /** The whole command line as ps prints it. */
    line: string;
    /** Environment names, where the platform shows them (Linux). */
    env?: string[];
    /** When it started (`ps -o lstart`), to tell it from a later process with its pid. */
    started?: string;
}
/** One process's parent, effective uid, name and arguments. */
export declare function readProcess(pid: number): Promise<ProcessRecord | undefined>;
/** Whether `pid`'s stdout is a pipe that no process of the owner's other than
 * `allowed` holds. sudo, running as root, is never visible here. */
export declare function stdoutReaders(pid: number, allowed: number[]): Promise<"sudo" | "other" | "unknown">;
/** macOS: whether the other end of the Hook's connection `hookFd` is held by
 * `pid` and no other process of the owner's. */
export declare function connectionHolders(pid: number, hookFd: number | undefined): Promise<"asker" | "other" | "unknown">;
/** Linux: a process's start time, to tell it from a later one with its pid. */
export declare function processStart(pid: number): Promise<string | undefined>;
export interface AskerCheck {
    processes: (pid: number) => Promise<ProcessRecord | undefined>;
    stdout: (pid: number, allowed: number[]) => Promise<"sudo" | "other" | "unknown">;
    /** Whether the connection that asked belongs to the asker alone. */
    connection: (pid: number) => Promise<"asker" | "other" | "unknown">;
    /** This Hook's node and the bundle paths askpass may run from. */
    node: string;
    bundles: string[];
    script: string;
    unprivilegedSudo?: boolean;
}
/** The chain from askpass up to sudo, or why it is refused. */
export declare function verifyAsker(pid: number, check: AskerCheck): Promise<{
    sudo: ProcessRecord;
    sudoPid: number;
} | {
    refused: string;
}>;
export interface SudoBrokerOptions {
    computer: () => string;
    push?: {
        readonly available: boolean;
        notifySudo(value: SudoRequestView): Promise<boolean>;
    };
    /** Label and agent for the pane that asked, when the Hook can place it. */
    describe?: (place: {
        server: string;
        workspace: string;
        tab: string;
        pane: string;
    }) => Promise<Pick<SudoSession, "source" | "label"> | undefined>;
    label?: (dispatchId: string) => Promise<string | undefined>;
    /** Reads a process, to see whether sudo is still running; tests replace it. */
    processes?: (pid: number) => Promise<ProcessRecord | undefined>;
    /** The askpass chain check and how the password then reaches it; tests replace it. */
    verify?: (pid: number, hookFd: number | undefined) => Promise<Verified | {
        refused: string;
    }>;
    holdMs?: number;
    outcomeMs?: number;
    /** Whose password sudo asks for; this Hook's user. */
    account?: string;
    now?: () => number;
}
export interface Verified {
    sudo: ProcessRecord;
    sudoPid: number;
    deliver: (password: string) => Promise<SudoReply>;
}
export declare class SudoBroker {
    private options;
    private pending;
    private events;
    private watchers;
    private tries;
    constructor(options: SudoBrokerOptions);
    private get now();
    list(): SudoRequestView[];
    /** Overview sockets that asked for sudo frames. Each counts as a phone
     * that can answer while it is open. */
    subscribe(listener: (requests: SudoRequestView[]) => void): () => void;
    private changed;
    /** An askpass call: checks the caller, builds the request, holds it until
     * an answer, a timeout, or `cancel` (the caller went away). */
    ask(body: Json, respond: (reply: SudoReply) => void, hookFd?: number): Promise<{
        cancel: () => void;
    }>;
    /** The phone's answer. Single use: the request is gone once answered.
     * `outcome` resolves to whether sudo took the password: rejected when the
     * same sudo asks again, accepted when it has not within `outcomeMs` (or
     * is still running then), unknown when it gave up or the Hook cannot tell. */
    answer(id: string, answer: {
        password: string;
    } | {
        deny: true;
    }): false | {
        outcome: Promise<SudoOutcome | undefined>;
    };
    private watchOutcome;
    close(): void;
    /** The agent.sock `/sudo` route. */
    handle(req: IncomingMessage, res: ServerResponse): Promise<void>;
}
/** A password as sudo reads it from askpass: one line, nothing it would cut. */
export declare function sudoAnswer(data: Json): {
    id: string;
    answer: {
        password: string;
    } | {
        deny: true;
    };
    outcome: boolean;
};
export declare class SudoAnswerError extends Error {
    constructor();
}
/** The bundle's `askpass` command, run by sudo through `<bridge>/askpass`.
 * Prints the password and exits 0, or explains on stderr and exits 1. */
export declare function askpass(prompt: string | undefined, env?: NodeJS.ProcessEnv): Promise<number>;
/** `phren sudo <command...>`: `sudo -A` with this Hook's askpass, so the
 * password comes from the phone even with no terminal (a `!` command, an
 * agent's shell). Exits with sudo's status. */
export declare function runSudo(args: string[]): Promise<number>;
