/** * The FR-019 approval round trip: inbound `approval/requested` or * `approval/request` → the consumer's handler → outbound `approval/decide` * (spec 14990 T031, FR-53910-1, tdd SS5.4). * * Split out of `session.ts` rather than added to it: the router owns a handler, * a per-stage latch, and a decision-shaping guard chain that have nothing to do * with folding a view, and `Session` was already the package's largest module. * * Two frames carry a request in: the `approval/requested` notification and the * enrolled `approval/request` server request (`RequestReceipt`), which the host * sends beside it and re-sends after a resume (tdd SS5.6). A client-owned * connection routes both here (ADR 42116 D18), and the per-stage latch decides * each stage once, whichever arrives first. The decision goes out as the * `approval/decide` command FR-019 names. */ import type { Connection } from "../connection/connection.js"; import type { DeepReadonly } from "../fold/session-fold.js"; import type { ApprovalDecideParams, ApprovalRequestParams, ApprovalUpdatedParams } from "../msp.js"; /** * What an approval handler answers with: a choice the SERVER offered, and * optional feedback. Composed from the generated decide params, so "the client * picks a `choiceId`" stays one fact rather than two that can drift. * * D-006 is select-never-create, and this TYPE cannot enforce it — `choiceId` is * a `string` on the wire — so the router checks the answer against the * request's own `availableChoices` before the frame is built. */ export type ApprovalDecisionInput = Readonly>; /** FR-019: the consumer's decision callback. May be sync or async. */ export type ApprovalHandler = (request: ApprovalRequestParams) => ApprovalDecisionInput | Promise; /** * Why one approval round trip did not complete. * * REPORTED, not thrown: the trip is driven from a notification, so a throw * would escape into the consumer's pump with nothing to catch it — the same * hazard `Session.apply` already refuses a malformed frame for. Each kind names * whose repair it is (the consumer's handler; the consumer's choice, for * `unofferedChoice` and `feedbackNotAccepted`; the host), because the repair * differs. */ export type ApprovalFailure = { readonly kind: "unofferedChoice"; readonly approvalId: string; readonly choiceId: string; readonly availableChoiceIds: readonly string[]; } /** * The handler attached `feedback` to a choice whose `acceptsFeedback` is not * true. The host would refuse that decision, so it is not sent; the offered * reject (the `denied` choice, else the `abort` one) is sent next, so the * approval closes. With neither offered, nothing is sent and the approval * stays open; a failure to send the reject is reported as `submitFailed`. * Next time, answer without feedback, or pick one of `feedbackChoiceIds`, the * offered choices that accept it. */ | { readonly kind: "feedbackNotAccepted"; readonly approvalId: string; readonly choiceId: string; readonly feedbackChoiceIds: readonly string[]; } | { readonly kind: "handlerThrew"; readonly approvalId: string; readonly error: unknown; } | { readonly kind: "submitFailed"; readonly approvalId: string; readonly error: unknown; }; /** Receives each `ApprovalFailure`; the failure's `kind` names what went wrong and whose repair it is. */ export type ApprovalFailureHandler = (failure: ApprovalFailure) => void; export declare class ApprovalRouter { #private; constructor(sessionId: string, connection: Connection | undefined); /** * Register the decision callback (FR-019). * * REPLACES any previous handler rather than adding to a list: two handlers * would race to decide one approval and only one decision can win, so the * loser's would be silently discarded. */ onApproval(handler: ApprovalHandler): void; /** Observe round trips that did not complete. See {@link ApprovalFailure}. */ onApprovalError(handler: ApprovalFailureHandler): void; /** * An `approval/requested` the fold kept pending, or a routed `approval/request` * (FR-53910-1). Returns the decision round trip to await, or `undefined` when * this client authors nothing for it. * * No handler at all is the supported default posture, NOT a degraded one * (D-008): the client runs under the server's own default-deny, and this SDK * parks nothing — no queued decision, no synthesized denial, no local hold. */ requested(params: ApprovalRequestParams): Promise | undefined; /** * An `approval/updated` folded onto a STILL-PENDING approval (#36949). * * A multi-stage approval advances `currentRequirementId` through this frame * ALONE on the notification plane — the SS5.6 item-3 re-issue on stage * advance is the `approval/request` server request, which reaches this * router only through `Session.apply` (a `MuseClient`-owned connection * delivers it; a bare `Connection` answers it `-32601`) — so waiting for a * re-issued `approval/requested` notification leaves the new stage undecided * and the turn pending forever. * * The update alone carries none of the request-shape members the handler is * typed on, but the fold retains the original request and every identity * member is stable across the stages of ONE approval, so the merged request * below is honest (`REFRESH_FROM_*` above pins the split). Feeding it * through {@link requested} reuses the per-stage latch, so an update whose * requirement was already decided — and any later re-issued request for the * stage this update decided — authors nothing. */ updated(requested: DeepReadonly, params: ApprovalUpdatedParams): Promise | undefined; }