/** * terminal-notice.ts, routing a terminal inbound-mail failure to the OWNER. * * docs/inbound-email.md §3.4b: *"A terminal state is announced, not merely * recorded... Silent permanent death is the failure this entire round exists to * eliminate."* * * Everything needed for that already existed and none of it was joined up. The * watcher reaches a terminal verdict, builds an `InboundMailTerminalFailure` * carrying the exact remedial step, and announces it once per transition. The * supervisor forwards it. The composition root holds * `deliverStructuredNotice`, the same port arriving mail is announced through *, and used it for arriving mail and not for the one condition that means no * mail will ever arrive again: the only consumer of `terminalFailure` was * `logger.error`. A log line is a record, not an announcement, and this is the * capability whose whole promise is that the owner does not have to be watching * for it to be told. * * Two properties this file is responsible for. * * **Once per transition, not once per probe.** The watcher already suppresses * a repeat of the same verdict, but it is not the only thing that can produce a * terminal failure, a supervisor whose run loop dies produces one too, so the * latch lives here as well, keyed on the condition rather than on the wording: * a server that phrases the same refusal differently each hour must not become * an hourly alarm. Recovering to any non-`insufficient` state re-arms it, so * the SECOND time the credential is refused the owner is told again. * * **Structure, never rendered text.** The notice is built from structured * fields by `renderInboundMailStoppedNotice`: our reason and our fix as * `literal` spans, the server's own wording as an `untrusted` one. Nothing here * ever holds a channel-formatted string, so the escaping stays where the * channel is known (§7.2). */ import type { StructuredNotice } from '../inbound-notice.js'; import type { InboundCapabilityTransition, InboundMailObserver, InboundMailTerminalFailure } from './ports.js'; /** * Whether a notice actually reached the owner. * * Never a bare boolean and never `void`, for the reason the whole file exists: * the delivery port RESOLVES a refusal rather than rejecting it, so a caller * that only catches sees every refusal as a success. `void` would be worse * still, a port that says nothing forces this module to guess, and the guess * that used to be made here was "it went out". * * Structurally compatible with the daemon layer's `SurfaceNoticeDelivery`, * which is what the composition root binds, without importing across the * layer boundary. */ export type InboundNoticeDelivery = { readonly delivered: true; } | { readonly delivered: false; /** Which guard refused, when the delivery layer named one. */ readonly reason?: string | undefined; /** The transport's own message, when there was one. */ readonly error?: string | undefined; }; export interface InboundTerminalFailureAnnouncerOptions { /** * Where the owner is reached. Takes the STRUCTURE, so the composition root's * delivery helper picks the channel and its escaper. * * Its result is READ, and this is the correction that matters most in this * file. It used to be `Promise`, documented as "deliberately * unread" on the reasoning that delivery disclosure belonged to the delivery * layer. That reasoning had a hole in it: `deliverStructuredNotice` reports a * refusal by RESOLVING `{ delivered: false, reason }`, not by rejecting, so * the `.catch()` this module relied on never fired. With no route binding * configured, the ordinary state of a fresh install, the owner was not * told, the latch was set anyway, every later occurrence was suppressed, and * the log line recorded `announced: true`. A log asserting the owner was told * when they were not, for the one condition that means no mail will ever * arrive again. * * The send is still not awaited by the observer, which stays synchronous: the * result is inspected in a continuation, so a status transition is never * blocked on somebody's phone. */ readonly send: (notice: StructuredNotice) => Promise; /** Overridable for tests that assert on what was logged. */ readonly log?: ((message: string, fields: Record) => void) | undefined; } /** An observer that announces terminal failures and re-arms on recovery. */ export interface InboundTerminalFailureAnnouncer extends InboundMailObserver { terminalFailure(failure: InboundMailTerminalFailure): void; stateChanged(transition: InboundCapabilityTransition): void; } /** * Build the observer the composition root passes to `InboundMailSupervisor`. * * Synchronous, because `InboundMailObserver` is synchronous: it is a report * sink and the reporting path must never be able to hold up the watcher. The * send is started and its failure is logged rather than propagated, a * notification route being down must not become a second failure on top of the * one being reported. */ export declare function createInboundTerminalFailureAnnouncer(options: InboundTerminalFailureAnnouncerOptions): InboundTerminalFailureAnnouncer; //# sourceMappingURL=terminal-notice.d.ts.map