import { EventEmitter } from "node:events"; import { connect } from "@nats-io/transport-node"; import { type SecretStoreIdentity } from "./secret-store.js"; import type { EpVerbTarget, EpAttributedReply } from "./endpoint-verbs.js"; import type { AgentCard, ChannelConfig, ControlReply, ControlRequest, ControlRequestInit, EndpointRef, Part, Presence, PresenceStatus, AttentionMode, ChannelMode, CotalMessage, DeliveryClass, MembershipSnapshot } from "./types.js"; import { type DeliveryLeaseInfo, type ManagerLeaseInfo } from "./lease.js"; export declare const DEFAULT_SERVER = "nats://127.0.0.1:4222"; /** Space joined when none is given on the CLI (the `cotal-` cmux tab, etc.). */ export declare const DEFAULT_SPACE = "main"; /** How many channel filters one multi-filter consumer create may carry. * * A create names every requested channel in one request, so the request grows with the channel * count and the CLIENT's request timeout is what gives way, not the broker: the create is never * refused, it just does not answer. Measured on an isolated broker, one message per channel, * `limit=5`: 70 filters answer in 43ms, 1,000 in 246ms, 5,000 in 5,345ms, and 10,000 does not * answer at all, failing `timeout` after 5,023ms. Note 5,000 SUCCEEDED while taking longer than * 10,000 took to fail, which is what says the ceiling is on the create request rather than on the * read: past roughly 5s the create itself is what times out. * * 1,000 is chosen from that sweep rather than from the failure point: it is a fifth of the largest * count that still answered, and it answers in a quarter second, so a batch stays far away from * both the timeout and the response-deadline budget the dashboard has to share with its DM read. * A space with the 69 chat channels this was built for is still ONE read, so the round-trip claim * in #1210 is unchanged at that size. */ export declare const MULTI_FILTER_BATCH = 1000; /** How many filter batches may be in flight at once. Bounded because the point of #1210 was to stop * issuing one read per channel: a space large enough to need batches must not get the fan-out back * under another name. */ export declare const MULTI_FILTER_READ_CONCURRENCY = 4; export interface EndpointOptions { /** The collaboration to join. */ space: string; /** Identity. `id` is generated if omitted. */ card: Omit & { id?: string; }; /** This incarnation's lifecycle UID (SPEC §13.1), minted ONCE per lifecycle by the provisioning * authority (manager/CLI: `mintLifecycleUid()`) and PERSISTED with the agent — never re-minted per * process, or a supervised restart would abandon the durable inbox. REQUIRED to bind/create the * lifecycle-keyed messaging durables (`dm_…-`, `dlv_…-`, `chathist_…-`) — an * endpoint without one (a pure operator/daemon connection) can not consume DM/chat history. */ lifecycleUid?: string; /** The accepted-row token of this credential's issuance (SPEC 13.15). Set when the launcher * minted the credential as an issuance: after connecting, the endpoint reads the generation the * ISSUER bound under this token (a broker-enforced per-key read) and pins it into every caller * rail it forms. A token that resolves to no row, or to another incarnation's reference, fails * the connect: the rails would be refused at the broker anyway, and a silent legacy fallback is * exactly what the versioned rail exists to rule out. */ acceptedToken?: string; servers?: string; /** Connection token (soft-shared auth). Mutually exclusive with user/pass. */ token?: string; /** Username/password auth (both required together). */ user?: string; pass?: string; /** NATS user creds file *content* (JWT + nkey seed), or a SOURCE that mints/reads a fresh copy. * When set, the endpoint authenticates as that user and adopts the creds' identity as its card.id. * * A STRING is the ordinary static cred. A FUNCTION is the STANDING-RENEWAL seam (D5 slice 5) for * bounded-lifetime standing creds: the endpoint fetches before the first connect, re-fetches at * 75% of each JWT's lifetime (then swaps the connection onto the fresh cred with a controlled * reconnect) and on rebuilds, and every (re)connect attempt presents the freshest copy — the * broker's expiry-close is only the backstop for a missed swap. * The two source shapes are the renewal classes: a seed-holder self-remints (the manager's * supervisor), a seed-less daemon re-reads its manager-reminted creds file (delivery). A source * requires explicit `card.id` (the pinned identity); every fetched cred MUST carry that same nkey * or the endpoint fails loud — renewal may never silently swap identity. A fetch failure is * emitted as a "warning" event and retried; the connection stays up until its current JWT expires, * so a dead reminter is loud without instantly dropping the mesh. Node rethrows unhandled "error" * events, so a retry notice must not use that channel (#891). Once the cached cred expires, * the endpoint refuses to present it to the broker and keeps retrying the source with backoff. */ creds?: string | (() => Promise); /** USER-MODE auth: a validated Cotal user bearer (the JWT from `cotal login` → the IdP bridge), or a * SOURCE that mints a fresh one. When set, the endpoint connects through the auth callout — presenting * {@link sentinelCreds} + the bearer, the broker mints its scoped data-account JWT. * * A STRING is a one-shot bearer for connection-lifetime ≤ bearer-lifetime callers (the CLI): the * owner+actor PRINCIPAL is derived from it (`sub`, `act.actor`), and the connection dies at the * bearer-bound JWT expiry. A FUNCTION is a bearer source for long-lived endpoints (spawned agents): * the endpoint fetches before the first connect, re-fetches ahead of each expiry and on rebuilds, and * every (re)connect attempt presents the freshest token — so reconnects outlive any single bearer. * A source requires explicit `card.owner` + `card.actor` (there is no bearer to derive them from at * construction); every fetched bearer MUST carry that same principal or the endpoint fails loud. * A fetch failure is emitted as a "warning" event and retried — the connection stays up until its * current JWT expires, so a dead auth service surfaces loudly without instantly dropping the mesh. * Node rethrows unhandled "error" events, so a retry notice must not use that channel (#891). * Mutually exclusive with `creds`/`token`/`user`/`pass`; requires `sentinelCreds`. */ bearer?: string | (() => Promise); /** The shared, deny-all auth-account sentinel creds presented alongside {@link bearer} so the connect * lands in the callout account (`createCalloutAuth().sentinelCreds`). Powerless on its own. */ sentinelCreds?: string; /** Require a TLS connection to the server. */ tls?: boolean; /** Channels to subscribe to; the first concrete one is the default broadcast target. Omitted or * empty ⇒ NO channels: the endpoint joins nothing, and {@link CotalEndpoint.multicast} refuses * a call with no explicit channel rather than picking one. */ channels?: string[]; /** Presence heartbeat interval (ms). */ heartbeatMs?: number; /** Presence liveness window (ms); a peer is considered gone after this. */ ttlMs?: number; /** Publish our own presence (default true). */ registerPresence?: boolean; /** Track the roster of peers (default true). */ watchPresence?: boolean; /** Open + watch the channel registry (default true). Independent of {@link watchPresence}: a * presence-only supervisor sets this false to track the roster WITHOUT opening the channel-registry * cache — so its cred needs no channel-registry read grant (residual 2). No effect when `consume` is * true: the join-time replay decision reads the registry, so a consumer always opens it. */ watchChannels?: boolean; /** Create inbound stream consumers (DM / chat / anycast). Default true; a pure observer sets false. */ consume?: boolean; /** Initial per-channel attention overrides to publish in presence from the first heartbeat (the * connector's file-default seed). Mirror only — never read back into delivery. */ channelModes?: Record; /** How long an unacked (un-surfaced) message waits before redelivery (ms). */ ackWaitMs?: number; /** Retire this instance's durable consumers after it's been gone this long (ms). */ inactiveThresholdMs?: number; } /** A peer subscribed to a channel — broker truth (a chat-stream consumer) joined with * presence for liveness. `live: false` is a stale ghost: the durable lingers (reconnect * grace) but presence says the peer is gone/offline. */ export interface ChannelMember { id: string; name: string; role?: string; live: boolean; } /** Trust state of this endpoint's local presence roster. `fresh` remains for older consumers: * unpopulated is deliberately false so they fail toward unknown rather than treating a partial * reconnect snapshot as an authoritative absence verdict. */ export type PresenceView = { state: "current"; fresh: true; } | { state: "unpopulated"; fresh: false; } | { state: "stale"; fresh: false; staleSince: number; }; /** Raw NATS transport liveness for the endpoint's CURRENT connection epoch. This is deliberately * separate from the `connection` event, which means the full Cotal bind is ready. */ export interface TransportState { connected: boolean; /** The server nats.js named for this edge. Omitted when the runtime supplied none. */ server?: string; } /** A value or a promise of it — the Plane-3 `aclFor` reads the durable ACL registry FRESH per entry * (async), so the reader/fan-out call sites await it. */ type MaybePromise = T | Promise; /** One page of a mediated history read: the newest `limit` messages, oldest-first WITHIN the page. * * `complete` is the honest-truncation signal and the reason this is not just `CotalMessage[]`: * `true` means the page reaches the start of the channel's RETAINED history (nothing older is on * the stream to read), `false` means older messages exist behind it. A caller that cannot tell * those apart renders "there is more" as "this is the beginning of the conversation". It says * nothing about messages already aged out by retention — no reader can see those. */ export type HistoryPage = { items: CotalMessage[]; complete: boolean; }; /** The NEWEST prefix-from-the-end of `items` whose serialized size fits `budget` bytes, order * preserved. Returns `[]` when not even the newest single message fits — the caller must refuse * loudly there rather than serve an empty page, which would read as "no history". * * Measured in ENCODED bytes, not `string.length`: a page of multi-byte text would otherwise be * undercounted and still overflow the broker. Same discipline as `assertFactFits`. */ export declare function fitHistoryPage(items: CotalMessage[], budget: number): CotalMessage[]; export declare class CotalEndpoint extends EventEmitter { readonly card: AgentCard; readonly space: string; readonly channels: string[]; private readonly servers; private readonly token?; private readonly user?; private readonly pass?; /** The creds source, when standing renewal is on (bounded supervisor/daemon creds); undefined for * a static string. Mirrors {@link bearerSource} exactly — same fetch-ahead + pin + retry shape. */ private readonly credsSource?; /** The freshest creds — what every (re)connect attempt presents. Static callers set it once. */ private currentCreds?; private credsTimer?; /** True in user mode (bearer string OR source) — gates the callout-shaped connect. */ private readonly userMode; /** The bearer source, when auth refreshes (spawned agents); undefined for a one-shot string. */ private readonly bearerSource?; /** The freshest bearer — what every (re)connect attempt presents. */ private currentBearer?; private bearerTimer?; /** Arms against the credential authenticated on the current wire. nats-core 3.4.0 discards the * promise returned by its async transport-close continuation; if that continuation enters the * reconnect dial loop with an expired JWT, its terminal auth error becomes an unhandled rejection. * Cotal knows the JWT expiry, so it disables the library reconnect before the broker closes the * transport. The endpoint's observed `closed()` supervisor remains responsible for rebuilding. */ private authExpiryReconnectTimer?; private readonly sentinelCreds?; private readonly tls; private readonly heartbeatMs; private readonly ttlMs; private readonly doRegister; private readonly doWatch; private readonly doWatchChannels; private readonly doConsume; private readonly ackWaitMs; private readonly inactiveThresholdMs; private nc?; private js?; private jsm?; private kv?; private channelKv?; /** The presence/channel-registry watches' own handles. Each is an ORDERED push consumer: its idle * heartbeat monitor lives on a JS timer independent of the connection, so a drain that doesn't * `.stop()` it first leaves the monitor to fire into a closing connection every 30s, throwing * `DrainingConnectionError` out of `reset()` on a timer nothing awaits. */ private presenceWatchIter?; private channelWatchIter?; /** Plane-3 durable-membership registry KV — lazily opened by the privileged delivery daemon (or a * short-lived provisioner). */ private membersKv?; private aclKv?; private deliveryKv?; private managerLeaseKv?; /** Our revision of the per-space daemon-credential renewal lease (#1634), or undefined when we do * not hold it. Cleared with the bound KV handle: a revision from a dead connection is not a lease * we can prove we still hold. */ private daemonRenewalLeaseRevision?; private membershipFeedKv?; /** Caller-owned membership watches survive a connection rebuild as INTENT. Their iterators are * connection-scoped and are stopped/re-created around the epoch swap. */ private readonly membershipFeedWatches; /** The live `ctl.delivery` serve subscription (delivery daemon) — re-created on every (re)connect by * {@link armDeliveryControl}; tracked so the stale one is dropped on reconnect. */ private deliveryServeSub?; private deliveryAdminServeSub?; /** When set, this endpoint hosts the Plane-3 fan-out writer + trusted reader (the server-side delivery * daemon). `aclFor` maps an owner id to its current read ACL (`allowSubscribe`) for the reader's * re-authorization — read FRESH per entry from the durable ACL registry KV, hence async. */ /** True once {@link quiescePlane3} has stopped serving this shard pending an ownership answer. * Guards {@link armPlane3} so a RECONNECT cannot silently resume serving mid-question. */ private plane3Quiesced; private plane3?; /** Live local cache of the channel registry (key = channel token), kept by a KV watch. */ private readonly channelConfigs; private channelDefaults; /** Per-subscription join watermark: the stream frontier captured when a channel was joined. * The tail ack-drops chat messages with `seq <= watermark` (suppresses pre-join history for * a lagging joiner + dedups the backfill overlap). Keyed by the subscription pattern (may be * wildcard), so the drop matches every concrete channel the pattern subsumes. */ private readonly joinSeq; /** Serializes history reads ({@link collectHistory}): they share the fixed per-instance * `chathist_` consumer, so overlapping reads would delete/recreate it under one another. */ private histLock; private readonly subs; private readonly streamMsgs; /** Per-channel native core subscriptions (SPEC v0.3) — the manager-free live read path for boot + * runtime channels (there is no per-instance chat durable). Keyed by channel so leave unsubscribes * just one. */ private readonly chatSubs; /** Channels whose core-sub the broker refused (async sub.allow violation) — read by the * broker-confirmed join: a denied subscribe is NOT a successful join (SPEC conformance #13). */ private readonly chatSubDenied; /** Channels this session has a Plane-3 durable backstop for (per-channel join GENERATION, from * durableJoin, so leave passes it back for the stale-leave guard). A durable channel's core-sub is * NOT coverage-dropped — it stays a live wake-hint, dedup-coalesced with the Plane-3 durable copy by * id-dedup. Drives the durable-state surface + routes leave to `durableLeave`. PERSISTS across * reconnect (like `this.channels`): the membership record + the `dlv_` durable are persistent so * the backstop survives a reconnect on its own; the agent can't re-read the privileged members KV, * so this in-memory mirror is kept, not rebuilt. Cleared only on full stop. */ private readonly plane3Channels; /** Channels whose live sub was REFUSED while they held a Plane-3 durable membership, whose §7 * tombstone has not yet confirmed (channel → join generation). {@link closeRefusedMembership} retries * the tombstone until it lands; until then this is a `durable-unclosed` state surfaced via * {@link pendingDurableLeaves} (the connector shows it in `cotal_channels`, never as ordinary * absence). Persists across reconnect; cleared on tombstone success or full stop. */ private readonly pendingDurableLeave; /** Boot durable channels whose self-join hasn't yet established a membership (daemon down/absent at * first connect, or a transient `durable:false`). {@link reconcileBootJoin} retries with capped * backoff until the membership exists or the channel is left — so a first-connect daemon outage * self-heals on recovery instead of leaving the channel silently live-only. Surfaced to the connector * via {@link hasDurableMembership} (a joined durable channel NOT yet a member renders degraded). */ private readonly pendingBootJoins; /** Chat-join subjects currently being broker-confirmed. An out-of-ACL subscribe among these trips an * EXPECTED async permission violation that joinChannel turns into a clean throw, so watchStatus * suppresses it rather than surfacing a spurious connection error. */ private readonly confirmingChatSubs; /** True until the first successful connect completes its boot backfill — distinguishes first-connect * (backfill the boot channels' history) from a reconnect (reopen the core-subs, no re-backfill). * Persists across reconnect (NOT connection-scoped). Replaces the legacy chat-durable consumed-cursor * signal now that there is no per-instance chat durable. */ private firstConnect; private heartbeatTimer?; private sweepTimer?; /** #1356: when the presence bucket started refusing writes; undefined once one succeeds. */ private presenceWriteFailingSince?; /** #1356: the broker's last refusal message, kept alongside the start time for diagnosis. */ private lastPresenceWriteError?; private readonly roster; /** Resolves when the current presence watch has consumed its complete initial KV snapshot. */ private presenceSnapshot; /** False from connection reset until the watch marks the last entry in its initial replay. */ private presenceSnapshotPopulated; /** * Observer-local age of the last presence-KV delivery (any key, including DEL/PURGE). Distinct * from each peer's `ts`: that is the publisher's heartbeat. Whole-bucket silence past TTL is * the observer going deaf, not N simultaneous deaths, and sweep must not treat it as the * latter (#1045). */ private lastPresenceWatchAt; /** Last emitted presence-view state. Suppresses duplicate `presence-view` events. */ private presenceViewState; /** A presence-watch rebind in flight (see {@link rebindStalePresenceWatch}); one at a time. */ private presenceRebind?; /** Bumped by every connection-scoped teardown and by {@link stop}. A presence bind that was * awaiting the broker when the epoch moved belongs to a retired epoch: it releases the * iterator it got and installs nothing (see {@link startPresenceWatch}). */ private presenceEpoch; /** The current watch was bound onto a bucket with NO keys (see {@link markPresenceBucketEmpty}). * Such a watch cannot deliver until someone writes, so its silence is not staleness. */ private presenceWatchEmpty; /** Wall-clock of the last rebind attempt, so a bucket that is silent because it is EMPTY (or a * broker that keeps refusing the consumer create) is retried once per TTL, not per sweep tick. */ private presenceRebindAt; private status; private activity?; /** Mirror of the connector's authoritative attention state, published in presence (advisory). The * endpoint never reads these back into delivery — they exist only to broadcast. */ private attentionMode?; private channelModes?; private stopped; /** In-flight rebuild (drain+rebind) — serializes manual reconnect, the supervisor's * closed(), and reestablishLoop so only ONE rebuild runs at a time (a second trigger * coalesces onto the shared promise, never starts a parallel connectAndBind). */ private rebuildPromise?; /** True only during the null window of a rebuild (this.nc unset) — user-facing ops then * throw a "reconnecting" message instead of the misleading "endpoint not started". */ private reconnecting; /** One reestablishLoop at a time; concurrent triggers coalesce via rebuild(). */ private reestablishing; /** Interruptible backoff for reestablishLoop — reconnect()/stop() resolves this to retry * now instead of awaiting the full retryMs. */ private backoffResolve?; private backoffTimer?; private readonly retryMs; /** Consecutive failed rebuilds, driving {@link nextRetryDelayMs}. Reset on every success. */ private retryAttempt; /** The connection's authenticated nkey — dev: the creds' identity; user mode: the per-connection * ephemeral. Distinct from the {@link owner}+{@link actor} principal: it names the CONNECTION (the * broker-authenticated user), and scopes the private reply inbox (`_INBOX_`) + the credId * equality check. The principal (owner+actor) is what the WIRE grammar and every per-agent key use. */ private readonly connId; /** This endpoint's owner token (principal half 1) — `"local"` in the dev default. */ private readonly owner; /** This endpoint's actor token (principal half 2) — the connection id in the dev default. */ private readonly actor; /** True when {@link actor} was SELF-MINTED at construction — a fresh random token, because the * card declared no actor and no id and no creds named one. Such a principal differs on every * restart, so nothing can be granted to it in advance and nothing durable may be keyed on it. * Exposed via {@link actorIsEphemeral} so a caller deriving a per-agent resource name can refuse * the mode instead of silently keying on a value that will not survive the process. */ readonly actorIsEphemeral: boolean; /** This incarnation's lifecycle UID (opts.lifecycleUid) — see {@link EndpointOptions.lifecycleUid}. */ private readonly ownLifecycleUid?; private readonly acceptedToken?; /** The issuer-bound generation, learned once per connection from the accepted row. */ private issuedGeneration?; /** Per-endpoint-name {@link resolveService} cache for {@link invokeService} — dropped on a * `failed-precondition` currency refusal (the described incarnation was superseded). */ private readonly resolvedServices; /** How many calls {@link invokeService} has silently recovered from a bind refusal (§13.2) — the * class-queue splits this endpoint hit and survived. * * Counted because it is recovered: handling the split is what makes it invisible, so this is the * only evidence the split rate exists. Always on, never behind a flag — a counter you have to * enable is not there when the thing you needed it for happened. */ private splitsRecovered; /** This endpoint's wire principal (owner + actor tokens, §13.2) — what its minted grant rows * pin. Public so a caller can build owner-mode target blocks for {@link invokeService}. */ get principal(): { owner: string; actor: string; }; /** Class-queue splits this endpoint has hit and silently survived ({@link splitsRecovered}). * Pull it, or listen for `split-recovered` — the event can be missed, the count cannot. */ get splitRecoveryCount(): number; /** The endpoint's own lifecycle UID, REQUIRED for every lifecycle-keyed messaging resource; absent * ⇒ loud refusal naming the operation (the hard cut of SPEC §13.1 — no alias-keyed fallback). */ private requireLifecycleUid; constructor(opts: EndpointOptions); ref(): EndpointRef; /** True on any AUTHED broker (static creds OR user-mode bearer) — the gate every open-vs-auth * branch keys on: authed endpoints OPEN pre-created streams/KVs and BIND pre-provisioned * durables (creates are denied to agents); only the open dev broker lazy-creates. */ private get authed(); start(): Promise; /** How far ahead of the current bearer's `exp` a refresh fires, and how soon a FAILED refresh * retries. The margin must clear a reconnect window (nats.js retries use the sync token getter, * so whatever `currentBearer` holds is what every attempt presents). */ private static readonly BEARER_REFRESH_MARGIN_MS; private static readonly BEARER_RETRY_MS; /** * A condition the endpoint is already surviving. Node rethrows `error` when no listener is * attached, so emitting retry notices on `error` killed hosts that the endpoint intended to * keep running (#891). `warning` is observable and never fatal without a listener. */ private emitRecoverable; /** Fetch a fresh bearer from the source, pin its principal to ours, arm the next refresh. On a * fetch/principal failure: THROWS when `initial` (start() must fail loud before first connect); * otherwise emits "warning" and retries — the live connection keeps working until its current JWT * expiry, so a dead auth service is loud without instantly dropping the mesh. */ private refreshBearer; private armBearerRefresh; /** How soon a FAILED creds refresh retries. Successful refreshes schedule by lifetime fraction * (75% of iat→exp), not a fixed margin — standing creds span hours to days, bearers minutes. */ private static readonly CREDS_RETRY_MS; /** THE ONE PLACE a credential is cleared for presentation to a broker. * * The property is unconditional — this endpoint never presents a credential it has already * decoded as expired — so it is a property of the SUPPLY, not of any one dial site. It used to * live inside {@link bindConnection}, which only `start()` and `doRebuild` reach; the * authenticator nats.js re-evaluates on ITS OWN reconnects read the cache directly and so * presented whatever was last fetched, expired included. Two such reconnects exist and neither * passes through bindConnection: the one the broker forces at JWT `exp`, and an ALREADY RUNNING * dial loop from an earlier drop that crosses `exp` while it retries (the pre-expiry * reconnect fence flips a policy flag nats-core only reads when it observes a NEW drop, so it * cannot stop a loop already in flight). * * Putting the refusal here instead means a future caller cannot miss it: the only way to reach a * dial is through {@link credsForWire}, and the one presentation that does not read the cache * (the adoption preflight, which presents a fresh CANDIDATE) calls this same function on it. * * An unbounded credential (no numeric `exp`) is presentable: bounded lifetimes are the renewal * seam's concern, and a cred with no expiry has none to be past. */ private static presentableCreds; /** The cached credential, checked. Handed to nats.js as the authenticator's source on EVERY auth * mode (renewed or static), so each (re)connect attempt — ours or the library's — re-reads a * CHECKED value. A refusal throws out of the authenticator, which nats-core turns into a closed * connection rather than a CONNECT carrying dead material; the endpoint's own supervisor then * rebuilds on capped backoff, and {@link bindConnection} re-fetches from the source on each of * those attempts, so a renewal that starts working recovers the endpoint without presenting * anything expired in the meantime. Deliberately side-effect free: kicking the renewal timer * from here would retry the source once per dial attempt, which is the flat load on a dead * broker that {@link RETRY_BACKOFF_CAP_MS} exists to prevent. */ private credsForWire; /** Refuses a fetched generation there is nothing to renew FROM, so the caller's failure posture * ({@link CREDS_RETRY_MS}) applies instead of the renewal schedule (issue #1523). * * A source that hands back the SAME generation past its own renewal point has not re-signed yet: * that is a missed remint, not a candidate. Adopting it re-arms from a non-positive delay, which * {@link armCredsRefresh} floors to 1s, which fetches again, which is the same generation — a 1s * read loop against a store that is already having a bad day, for the JWT's remaining 25% of life, * with the 60s backoff that exists for exactly this bypassed because the fetch did not FAIL. * * The membership feed's rw-cred renewal already refuses it this way (`membership-feed.ts`, * `adoptRwCreds`: "the rw source still holds the previous generation past its renewal point"); * this is the same rule on the endpoint's `delivery.creds` seam, so the two renewal paths answer * a dead source identically. * * An ALREADY-EXPIRED generation is refused whatever it is: the delay is non-positive for it too, * but it is dead rather than merely due, so it is named separately and refused even when the * source keeps returning a different one. */ private static assertRenewableGeneration; /** The disposable-preflight connect bound for the EXPLICIT reload proof (D5 class-2 adoption). A * rogue or unreachable candidate must resolve well UNDER the manager's delivery-admin request * bound, so this stays a few seconds and never blocks the responder. */ private static readonly PREFLIGHT_MS; /** How long the resident wire swap is deferred past the reply on the EXPLICIT reload path. The * delivery-admin responder rides THIS connection, so `nc.reconnect()` must not run until the * reply has flushed on the still-live old cred; the old cred stays valid until its exp, so a short * deferral is safe. */ private static readonly RESIDENT_SWAP_DEFER_MS; /** The daemon-side deadline for the WHOLE prove-then-adopt transaction (source fetch + preflight + * commit), kept strictly BELOW the manager's delivery-admin request bound so a slow or hung hosted * store returns a structured component failure rather than an ambiguous client timeout, and a late * fetch can never commit after the caller gave up. */ private static readonly RELOAD_DEADLINE_MS; /** Fetch fresh creds from the source, pin their identity to ours, cache them, and arm the next * refresh at 75% of the new JWT's lifetime. THROWS on fetch/pin failure — the callers decide the * failure posture (loud-and-retry for the timer, a structured error reply for an explicit * {@link reloadCreds}). This is the PASSIVE-BACKSTOP fetch; the explicit auditable path * ({@link reloadCreds}) does its own fetch so it can preflight the candidate on a disposable * connection BEFORE mutating this live cache. */ private fetchFreshCreds; /** Swap the live connection onto the freshest cached cred with a controlled `nc.reconnect()` * (nats.js re-evaluates the creds getter per attempt). Swapping now, instead of waiting for the * broker to close the connection at `exp`, means the wire never carries a near-dead JWT and the * operator never sees a spurious "authentication expired". After the reconnect succeeds, re-arm * the expiry fence for the credential now authenticated on the wire. */ private swapConnectionOntoFreshCreds; /** nats-core keeps its reconnect switch on the protocol handler. This pinned internal shape is the * same last-resort surface used by {@link closeFailedBind}; there is no public API for changing the * reconnect policy of an existing connection. Disabling it does not disable Cotal self-heal: the * `nc.closed()` supervisor below rebuilds the endpoint with freshly checked auth material. */ private disableLibraryReconnect; /** Close a connection whose library reconnect has already been disabled. `drain()` flushes with a * PING and waits for the matching PONG; nats-core only rejects that waiter inside reconnect * `prepare()`, so a half-open socket with reconnect=false leaves drain pending until the 2-minute * ping interval times out. `close()` tears the transport down without that round-trip, which is * the same public path {@link closeFailedBind} already uses when there is no graceful delivery * contract left. */ private closeWithoutLibraryReconnect; /** Disable nats-core reconnect shortly before the JWT authenticated on this wire expires. The small * lead makes the policy change precede the broker's expiry close even when both timers wake in the * same event-loop turn. A credential adoption does not move this fence until the resident reconnect * is requested, so the old wire remains protected during the prove-then-adopt window. */ private armAuthExpiryReconnectFence; /** The connectAndBind PRE-CONNECT fetch: pull the freshest source cred and pin it into * {@link currentCreds} so the connect() that immediately follows presents it — that connect IS the * proof, so NO preflight and NO live swap here. THROWS when `initial` (no cred to start from), else * emits and retries. The LIVE-SWAP paths — the 75% timer {@link renewCredsOnTimer} and the explicit * {@link reloadCreds} — preflight the candidate on a disposable connection instead, so they never * reconnect the live connection onto an unproven cred. */ private refreshCreds; /** Serializes every prove-then-adopt transaction (the 75% timer and the explicit reload) so a timer * tick and an explicit reload can never interleave their fetch/preflight/commit — the design's * single-flight requirement. Runs `fn` after any in-flight transaction settles, whatever its * outcome; the internal chain never rejects. */ private credsTxn; private runCredsTxn; /** Race `p` against a `ms` deadline so a slow/hung SecretStore fetch (or preflight) cannot exceed * the daemon transaction bound. The underlying promise is not cancellable, so callers also FENCE a * late commit by re-checking the deadline before mutating {@link currentCreds}. */ private static withDeadline; /** The one PROVE-then-adopt transaction shared by the 75% timer and the explicit reload (run under * {@link runCredsTxn}). Fetch (deadline-bounded) → identity-pin → optional `expected` fingerprint → * PREFLIGHT the candidate on a disposable connection → fence a late commit against the deadline → * commit {@link currentCreds} → arm the next 75% timer. Because currentCreds is written ONLY after * the preflight proves broker acceptance, the resident authenticator getter can never present an * unproven cred (not even on an incidental reconnect), and a rejected candidate leaves the resident * connection untouched on BOTH the timer and explicit paths. THROWS (structured) on any failure. */ private adoptFreshCreds; /** The 75%-of-lifetime renewal timer tick: prove + adopt + swap the LIVE connection. Preflights (it * reconnects a live connection, so the candidate must be broker-proven first) and is serialized * with the explicit reload. Never throws — a failed renewal logs and retries while the old cred * stays live until its expiry. */ private renewCredsOnTimer; /** EXPLICIT credential reload — the auditable adoption step of D5 class-2 standing renewal (served * to the renewal owner via the delivery-admin rail). The PROOF-OF-RECORD is a DISPOSABLE PREFLIGHT * connection that presents exactly the candidate: it succeeds only when the BROKER accepts this * re-signed generation, and it runs BEFORE the live cache is touched. So a cred the broker refuses * throws HERE (structured), the resident connection — and the delivery-admin rail this very reply * rides — is never disturbed, and "file re-signed" can never masquerade as "daemon adopted". * `expected` is the renewal owner's generation token (SHA-256 of the JWT it re-signed): a re-read * that does not match is rejected before the preflight even runs. On success the candidate becomes * the resident connection's next-presented cred; the wire swap is NOT forced here — the caller * ({@link handleDeliveryAdmin}) schedules it AFTER the aggregate reply, because the responder rides * this connection and reconnecting before the reply flushes would strand it (see * adoption-false-green.smoke.ts). Returns the BROKER-ACCEPTED generation's window (identity/iat/exp): * the resident wire swap is best-effort and self-healing (the 75% timer + the broker's expiry-close), * NOT witnessed, so the caller must claim broker acceptance, not verified resident reauth. NEVER a * fingerprint. */ reloadCreds(expected?: string): Promise<{ identity: string; iat?: number; exp?: number; }>; /** Arm the resident wire swap for the delivery-admin `reloadCreds` path. Called by * {@link handleDeliveryAdmin} ONLY after BOTH component proofs have settled and immediately before * the reply is returned+responded — never from inside {@link reloadCreds} while the aggregate is * still open, or a slow co-component proof would let `nc.reconnect()` reconnect the admin rail * before the reply flushes and silently strand it. The short deferral lets the reply flush on the * still-live old cred first. Best-effort — the proof was the preflight; a transient swap failure is * retried by the 75% timer and the broker's own expiry-close, both presenting the adopted candidate. */ private scheduleResidentSwap; private armCredsRefresh; /** Open the connection and bind everything that hangs off it: status watch, presence * watch + heartbeat, channel registry, and the durable consumers. Re-runnable — a * reconnect calls it again after {@link clearConnectionScoped}; every binding is * idempotent (durables bind by name, JetStream dedups by msgID, KV opens are idempotent). */ private connectAndBind; private bindConnection; /** Tear down everything {@link connectAndBind} (re)creates, so a rebind can't leak a * second heartbeat, double-pump a consumer, or keep stale roster ghosts. Caller-owned * subs (tap/serve) are left alone — they aren't rebuilt here. */ private clearConnectionScoped; /** A transport can connect before a later JetStream/KV/subscription bind fails. The caller will * retry the whole start, so release that partial epoch first instead of overwriting `nc` and * leaving one established socket behind per retry. `close()` is deliberate: a failed bind has no * graceful delivery contract to drain, and cleanup itself must not extend the retry failure. * * A throwing `close()` is not hypothetical: nats.js `NatsConnectionImpl.close()` awaits the * protocol close, which itself flushes outbound + tears down subscription state; any of those * that reject leaves the underlying TCP transport ESTABLISHED even though every client handle * on this endpoint has been cleared. The next `start()` on the same object then dials a fresh * connection while the old socket stays live on the broker (`/varz.connections` climbs once per * failed attempt). The recovery path is layered, from most graceful to most surgical, matching * nats-core's public surface (nats.d.ts: `close`, `drain`, `isClosed`; protocol.d.ts owns the * `transport` handle whose `close(err?)` is the socket teardown; transport.d.ts exposes a * synchronous `disconnect()` for the case where even close rejects). We only touch a private * (`nc.protocol.transport`) when the public API has already refused, and the ORIGINAL bind * error is re-raised at every layer. The close error is diagnostic noise. The bind error is * what the caller must see. * * The delivery-serve subs and `this.subs` array are handles created by a successful * `armDeliveryControl` on a PREVIOUS epoch; a bind failure on the retry never rewrites them * because arming is a late step. Left in place they point at a dead protocol and survive into * the next retry. The reviewer flagged this as an untested handle class. Zero them here so a * retry starts with the same empty state as a first attempt. */ private closeFailedBind; /** If stop() ran during a rebuild's `await connectAndBind`, the just-bound connection + * heartbeat + supervisor would be left live on a stopped endpoint. Tear that fresh * connection back down and report it. Reads `this.nc` in its own scope (a bare `this.nc` * in doRebuild narrows to `never` via TS inlining connectAndBind's assignment). Returns * true iff it tore something down (caller bails out of the rebuild). */ private tearDownIfStopped; /** Watch for a terminal close (nats.js has exhausted its own reconnect) and rebuild. * Our own stop()/drain also resolves closed(), so the `stopped` guard keeps a clean * shutdown from re-establishing. The identity guard (`this.nc !== nc`) no-ops a STALE * supervisor — one whose connection reconnect()/rebuild already replaced — so only a * close of the CURRENT connection triggers a rebuild. The rebuild itself is serialized * with the manual path via {@link rebuild}. */ private superviseConnection; /** Single serialized rebuild: drain the old connection and rebind via {@link connectAndBind}, * guarded so concurrent triggers (manual {@link reconnect}, the supervisor's closed(), the * retry loop) coalesce onto ONE in-flight rebuild instead of racing two connectAndBinds and * leaking a connection. Returns the shared promise; a second caller gets the in-flight one. */ private rebuild; /** The transition: stop the connection-scoped timers FIRST (so nothing live touches * this.nc during the null window), drop the connection refs, close the old nc, then * rebind + re-arm the supervisor on the fresh connection. clearConnectionScoped is * idempotent, so connectAndBind's own call here is a noop. */ private doRebuild; /** The ceiling {@link nextRetryDelayMs} grows to. A failure that outlives a couple of retries is * a standing one (a down broker, a credential nothing can renew), and retrying it every few * seconds forever is what turns one stuck client into a permanent flat load on the broker and its * auth callout. */ private static readonly RETRY_BACKOFF_CAP_MS; /** The wait before the next rebuild attempt: {@link retryMs}, doubling per consecutive failure up * to {@link RETRY_BACKOFF_CAP_MS}. The FIRST retry still waits exactly retryMs, so a transient * drop recovers as fast as it always did; only a failure that repeats gets paced. */ private nextRetryDelayMs; /** Rebuild with backoff until it sticks or we're stopped. Interruptible: a manual * {@link reconnect} kicks the backoff so the next attempt runs immediately instead of * awaiting the full delay. One loop at a time ({@link reestablishing}); concurrent * triggers coalesce via {@link rebuild}. */ private reestablishLoop; /** Cut an in-flight reestablish backoff short so the next attempt runs immediately, and * clear its timer so it can't fire later on a stopped/restarted loop. */ private kickBackoff; /** Manual reconnect: tear down the current connection and rebuild, WITHOUT the permanent * stop (stopped/stopping stay false). Serialized with the self-heal supervisor via * {@link rebuild}, and interruptible — if a backoff is in flight, kick it so the attempt * is now, not in retryMs. Throws if stopped. On failure, leaves {@link reestablishLoop} * running in the background so the endpoint never stays dead, and rethrows so the caller * can report it. */ reconnect(): Promise; /** The presence epoch moves first: a bind still awaiting the broker must find it moved * before any await below gives it a window to install a watch on a stopped endpoint * (see {@link startPresenceWatch}). */ stop(): Promise; /** Multicast: broadcast to everyone on a channel. */ multicast(text: string, opts?: { channel?: string; parts?: Part[]; replyTo?: string; contextId?: string; mentions?: string[]; }): Promise; /** The broker's live `max_payload` — the CEILING a frame is measured against, not a budget for a * caller's own payload. * * Exposed because the connection is private and callers outside core (a connector assembling a * batched payload) otherwise have no way to learn the ceiling except by failing a publish. * Throws rather than guessing a default: a wrong ceiling is worse than none, because it splits * either too eagerly or too late and both look like working code. * * THIS ALONE CANNOT SIZE A MESSAGE. The envelope this endpoint adds after the publish call, and * the client's own headers, are charged against the same ceiling and the caller never sees them. * Use {@link encodedSize}, which measures what will actually be sent. */ get maxPayload(): number; /** * Verify the PRECONDITION {@link multicastExpecting} depends on: that the chat stream evaluates * the subject expectation BEFORE the `Nats-Msg-Id` dedup cache. **Throws if it cannot be * guaranteed.** Call before the first serialized append on a given endpoint. * * **The ordering follows the stream's REPLICATION FACTOR, not the deployment.** A standalone R1 * stream and an R1 stream inside a real 3-node cluster both refuse a stale expectation with a CAS * error; only an R3 stream evaluates dedup first and answers a retry with `duplicate: true`. A * check written against cluster size would pass on exactly the deployment that breaks. * * Every stream Cotal creates is `num_replicas: 1`, from the same canonical config the restore path * uses, so the property holds by construction today. This exists because "by construction" is an * observation until something checks it: nothing in the wire contract reserves the replica factor. * A caller appending under a stale assumption does not fail loudly; it accepts a retry as success * and drops a message. * * Evidence is `smoke:cas-preflight-cluster`, which records the server version it measured against * rather than naming one here — the suites resolve `nats-server` from `PATH`, so a hardcoded * provenance ages into a claim about a machine that no longer exists. * * @throws if the stream is unreadable (no `STREAM.INFO` grant, or absent) or reports more than * one replica. Never degrades to a warning: the failure it prevents is silent. */ assertExpectationSemantics(): Promise; /** The envelope {@link multicastExpecting} publishes, built in ONE place so that a frame and any * measurement of that frame cannot describe different messages. The fields this adds — `ts`, * `space`, `from`, `channel` and the normalized `mentions` — are exactly the ones a caller * holding only its parts cannot account for. */ private casEnvelope; /** * The bytes this frame will ACTUALLY put on the wire, to compare against {@link maxPayload}. * * Caller-side arithmetic is wrong in the dangerous direction: a split sized against the caller's * own payload produces a frame the broker REJECTS, and a rejected truncation makes the loss silent * again — the failure splitting exists to prevent. * * It lives on the surface that BUILDS the envelope so measurement and construction cannot drift * apart unnoticed: it shares {@link casEnvelope} with the publish path, sets the same two headers, * and lets the client's own encoder encode them rather than re-implementing the wire format. * `frame-size.smoke.ts` binary-searches a real broker's ceiling and requires this number to land * on it exactly. * * `expectedLastSubjectSeq` is a parameter because it is a header VALUE: sizing at 0 and publishing * at 123456 differ by five bytes. * * Residual: `ts` is re-stamped at publish, so the two differ in value — not in length until * epoch-millis needs a 14th digit. */ encodedSize(opts: { channel: string; parts: Part[]; id: string; expectedLastSubjectSeq: number; mentions?: string[]; replyTo?: string; contextId?: string; }): number; /** * Multicast with an OPTIMISTIC-CONCURRENCY expectation and a caller-chosen dedup id, returning * the `PubAck` fields instead of discarding them. The serialized-append primitive: two writers * racing one subject cannot interleave, because the loser's expectation no longer holds. * * **Why a separate method rather than options on {@link multicast}.** `multicast` mints a fresh * `id` per call and drops the ack; both are right for ordinary chat and both are fatal to a * caller that must retry an append idempotently. Keeping them apart means no existing caller * changes behaviour, and the stricter validation below applies only where a caller opted in. * * - `id` becomes the JetStream `Nats-Msg-Id`, so the SAME id may be republished on retry and the * server dedups it within the stream's duplicate window. It is validated rather than trusted: * it lands in a wire header, and the dedup cache is **stream-wide**, so a caller-supplied id is * both an injection surface and a way to suppress another publisher's message. * - `expectedLastSubjectSeq` is the sequence this publisher believes is the subject's tip; `0` * means "the subject must be empty". A mismatch throws, and the throw stays classifiable by the * already-public {@link isCasLoss} — the error is deliberately **not wrapped**, since wrapping * would hide the `err_code` that classification reads. * * @throws if the endpoint is not live, the channel is not concrete, `id` is malformed, `parts` is * empty, or `expectedLastSubjectSeq` is not a non-negative safe integer. */ multicastExpecting(opts: { channel: string; parts: Part[]; id: string; expectedLastSubjectSeq: number; mentions?: string[]; replyTo?: string; contextId?: string; }): Promise<{ message: CotalMessage; ack: { seq: number; duplicate: boolean; }; }>; /** Unicast: direct message to one specific instance. */ unicast(instanceId: string, text: string, opts?: { parts?: Part[]; replyTo?: string; contextId?: string; }): Promise; /** Anycast: deliver to ANY one instance of a service (role) — queue-group load balancing. */ anycast(service: string, text: string, opts?: { parts?: Part[]; replyTo?: string; contextId?: string; }): Promise; /** Subscribe to a read-only observer feed. Defaults to the whole space; an observer under * auth must pass `chatWildcard(space)` since its `sub.allow` only covers chat (DM/anycast * stay confidential), and an admin must tap the messaging planes individually * (`chat`/`inst`/`svc` — its enumerated `sub.allow` excludes the v0.4 endpoint rails, * SPEC 13.9/13.11), otherwise the space-wildcard subscribe is denied and the feed dies. */ tap(handler: (subject: string, msg: CotalMessage | undefined) => void, opts?: { subject?: string; }): void; /** Serve control requests for a service. Returns the subscription so a caller that re-registers on * reconnect (the delivery daemon) can drop the stale one. `boundReply` is REQUIRED for any service * whose responder holds a wildcard publish grant over the service subtree (the delivery daemon's * `ctl.delivery.*.reply.>`): without it, an authenticated caller could set its reply target to a * PEER's reply lane (`ctl.delivery..reply.`) and turn the responder into a confused * deputy — the broker does NOT permission-check the requester's embedded reply subject. With it, a * reply is published only when `m.reply` is under the AUTHENTICATED request subject * (`${m.subject}.reply.…`), binding the reply to the broker-policed sender token. The manager's three * lifecycle tiers ALSO require it as of closure (i): they reply on bounded `ctl...reply.…` * (the manager cred holds the wildcard `ctl..*.reply.>` pub — exactly the confused-deputy * condition above), so do NOT drop `boundReply` on them. */ serveControl(service: string, handler: (req: ControlRequest) => Promise | ControlReply, opts?: { boundReply?: boolean; }): import("@nats-io/transport-node").Subscription; /** Send a control request to a service and await its reply (client side). Like {@link requestDelivery}, * the reply rides a BOUNDED subject UNDER the request subject (`ctl...reply.`), not * the per-id `_INBOX` — closure (i): this frees the manager's permission set from needing a position-1 * inbox wildcard, so its publish surface can be an exact self-scoped allow-list (no message forging). * `noMux` lets us name the reply subject while keeping NoResponders detection. The random suffix is * defense-in-depth (a predictable suffix would let a peer target an in-flight named reply sub). The * reply sits under the sender's OWN request subject, so the responder's `boundReply` guard accepts it. * * CUTOVER (not backward-compatible): an agent cred minted BEFORE closure (i) lacks the * `ctl...reply.>` sub grant — it can still publish the request but cannot subscribe the * reply, so its control calls (spawn/despawn/purge/definePersona, self-stop) hang. The per-user-auth * atomic cutover re-mints every agent; if this change ships ahead of that, agents must be RESPAWNED. */ requestControl(service: string, req: ControlRequestInit, timeoutMs?: number): Promise; /** This endpoint's v0.4 caller triple (§13.2) — the identity its minted ep-rail rows pin. The * owner/actor principal is mode-correct by construction (static: DEV_OWNER + the connection * identity; user mode: the bearer's callout-derived pair — 1c.2c), and the lifecycle UID is the * launcher-supplied incarnation the rows are keyed on (ledger-consistent: the §13.1 presence * lifecycle-proof refuses a divergent uid before any publish). */ private serviceCaller; /** GENERIC v0.4 service invoke over this endpoint's own connection (P2 item 1, 1c.2b): resolve * the named endpoint's registered surface — describe, §13.7 store fetch, digest-verified * recompile ({@link resolveService}; cached per endpoint name) — and invoke one command. The * resolve is describe-bound currency, and a call that reaches the wrong incarnation is recovered * two ways depending on WHO caught it — the difference between knowing the command did not run * and only knowing someone answered: * - the RESPONDER fenced it on the request's `bind` (§13.2, an `ok:false` reply marked * {@link replyRefusedBeforeEffect}): the command did not run, so the bind is dropped and the * call re-issued ONCE for any command. If that re-issue cannot be resolved, the refusal * surfaces — still saying the command did not run — naming the resolve failure as why the * repair could not be attempted. * - this CLIENT caught it on the reply ({@link respondedButUnbound}: a different instance, * `failed-precondition`; the same instance at any other epoch, `expired`), which is what a * responder too old to know the field produces. A live instance received and answered it, so * the bind is dropped but the call is re-issued only for a command on the * {@link isRepeatSafeCommand} allowlist; anything else surfaces, since a second attempt could * duplicate its effect. * Errors from the responder come back structurally on the attributed reply * (`reply.ok === false`); transport/validation refusals throw {@link EpEnvelopeError}. */ invokeService(endpoint: string, command: string, args?: Record, opts?: { target?: EpVerbTarget; deadlineMs?: number; follow?: boolean; }): Promise; /** Send a durable-membership request to the SERVER-SIDE delivery daemon (`ctl.delivery`) and await its * reply. Unlike {@link requestControl}, the reply rides a subject UNDER `ctl.delivery..>` (not the * per-id `_INBOX`), so the scoped delivery cred can answer without broad inbox-publish — see * CONTROL_DELIVERY. `noMux` lets us name the reply subject while keeping NoResponders detection (so a * caller can fail-closed vs. degrade to live-only when no daemon is present). */ private requestDelivery; /** Send a PRIVILEGED delivery-admin request to the server-side delivery daemon and await its reply * (the D5 rail-split: `reloadCreds` now, the eviction executor next). Same bounded-reply shape as * {@link requestDelivery}; the cred layer is the real gate — only the manager's supervisor profile * holds the request-publish grant, so an agent calling this gets a broker denial, not a handler * refusal. NoResponders (no daemon) surfaces as the thrown request error — callers decide whether * that degrades (renewal falls back to the daemon's 75% re-read backstop) or fails. */ requestDeliveryAdmin(op: string, args: Record, timeoutMs?: number): Promise; getRoster(): Presence[]; /** * Trust state of THIS observer's presence watch, not of any peer. `unpopulated` means the * current watch has not completed its initial snapshot, so {@link getRoster} may be partial and * cannot support an absence verdict. `stale` means the whole bucket has been silent past the * liveness window, so the roster is last-known as of `staleSince`. `fresh` is false for both * unsafe states so consumers written before `state` was added degrade in the safe direction. */ presenceView(): PresenceView; /** Wait until the current presence watch has consumed its initial KV snapshot. An empty bucket * emits no watch entry, so the timeout keeps a genuinely empty mesh bounded and is reported * distinctly from snapshot completion. */ waitForPresenceSnapshot(timeoutMs?: number): Promise<"snapshot" | "timeout">; setActivity(activity: string): Promise; setStatus(status: PresenceStatus): Promise; /** Publish the agent's global attention mode into presence (advisory observability). Mirror only — * delivery decisions stay in the connector's authoritative state. */ setAttention(attention: AttentionMode): Promise; /** Publish the agent's per-channel attention overrides into presence (advisory). An empty map drops * the field. Mirror only — never read back into delivery. */ setChannelModes(modes: Record): Promise; /** Overlay the host's live model and optional variant onto the card's display-only metadata, then * republish presence. For connectors that learn their actual selection only after launch (e.g. * Claude Code's `SessionStart` hook). The mutated card is read live by every later publish, so even * a pre-connect call surfaces on the first presence write. */ setCardModel(model: string, variant?: string): Promise; /** This channel's registry config from the live local cache (undefined if unset). */ getChannelConfig(channel: string): ChannelConfig | undefined; /** Effective replay-on-join policy for a channel: per-channel override ?? space default ?? * true. Reads the live cache, so it reflects runtime registry edits. */ channelReplay(channel: string): boolean; /** Effective replay window for a channel (per-channel override ?? space default), or undefined * for the full retained window. Only meaningful when {@link channelReplay} is true. */ channelReplayWindow(channel: string): string | undefined; /** Effective delivery class for a channel (per-channel override ?? space default ?? "durable"), * from the live watch cache — drives the non-gating delivery-health surface (only durable-class * channels have a Plane-3 backstop to report on). */ channelDeliveryClass(channel: string): DeliveryClass; /** The channels this endpoint is currently subscribed to (live — reflects join/leave). */ joinedChannels(): string[]; /** * Join a channel mid-session: open a native core subscription (manager-free live read, broker- * confirmed against `sub.allow`), capture the stream frontier as the join watermark, backfill its * history if replay is on, and — for a `durable`-class channel when a delivery daemon is present — * request a Plane-3 durable backstop (via `ctl.delivery`). Idempotent: re-joining is a no-op (no * re-backfill). Returns the backfill count + whether the durable backstop is active (+ a `reason` * when a durable channel couldn't get one). */ joinChannel(channel: string): Promise<{ joined: boolean; backfilled: number; durable: boolean; reason?: string; }>; /** Leave a channel mid-session — MANAGER-FREE for the live read: close the core subscription. For a * Plane-3 durable channel, the membership is tombstoned FIRST at the leave cursor (SPEC §7: leave is * a hard read boundary for the backstop — a pre-leave entry stays deliverable, `seq > leaveCursor` is * denied). FAIL-CLOSED: if the tombstone can't be confirmed the call throws and the leave is NOT * applied (live sub stays up, local mirror intact) so the caller can retry — never close the live * read while the backstop keeps delivering. */ leaveChannel(channel: string): Promise<{ left: boolean; }>; /** One coherent channel model for dashboards: every channel that has messages OR a registry * entry (configured-but-empty), each tagged with its {@link ChannelConfig}. Works even on * observer endpoints (no consumers needed). */ listChannels(): Promise<{ channel: string; messages: number; config?: ChannelConfig; }[]>; /** * Who is a durable member of a channel — read from the privileged members registry (Plane-3), * joined with presence for liveness (a member whose peer is gone but lingering shows `live:false`, * not a phantom). Only CURRENT, ACTIVATED members (non-tombstoned, and past activation catch-up — a * join still completing or that failed catch-up reported durable:false and stays hidden here until * confirmed, so this surface never overstates membership). A wildcard registry channel would count for * the concrete channels it subsumes, but durable membership is per-concrete-channel, so records are * concrete. `live`-class channels carry no durable record — membership there is the live core-sub, * not tracked here. Privileged read (the members KV is manager-write/read; agents hold no grant), so * it is served by the manager, not an agent capability. */ channelMembers(channel: string): Promise; channelMembers(): Promise>; /** Lazily open the DERIVED membership FEED KV (`cotal_membership_`; admin/observer read, the * delivery daemon writes it) — the display-only who-is-subscribed view. Distinct from the authoritative * {@link membersRegistry} (`cotal_members_`, the Plane-3 durable-membership source of truth): the * two names look alike, so this one is explicitly "feed". Read-only here; agents hold no grant. */ private membershipFeedRegistry; /** * Snapshot the broker-sourced channel-membership feed (admin/observer read): every agent's * `{live, durable}` record plus `asOf` — the feed's freshness heartbeat (epoch ms of the daemon's last * successful poll, from the reserved {@link MEMBERSHIP_FEED_KEY}). `live` patterns are kept as-is * (wildcards preserved); the consumer expands them against the channel registry. `asOf` is undefined * when the feed has never been written (no daemon → the dashboard degrades to traffic-only). */ readMembership(): Promise; /** Watch the membership feed for changes (admin/observer): `onChange` fires on every KV entry, * including the initial replay — the caller debounces + re-reads {@link readMembership}. The async * stop handle resolves only after its ordered broker consumer is deleted. Best-effort: a feed the * cred can't read (or absent) surfaces as an `error` event and the dashboard keeps its last snapshot. */ watchMembership(onChange: () => void): Promise<{ stop(): Promise; }>; /** Bind one caller-owned membership-watch intent to the CURRENT connection. Arming is serialized * per intent, so a public watch call cannot race a reconnect rearm into two consumers. */ private armMembershipWatch; /** Delete identity retained across a failed/closed-epoch consumer object using the CURRENT connection. */ private deleteRetainedMembershipConsumer; private finishMembershipWatchStop; /** Delete one membership-watch consumer, swallowing ONLY already-gone. */ private deleteMembershipConsumer; /** Stop the local iterator AND delete its ordered consumer. The admin/observer grant already holds * the bucket-scoped consumer-delete row, so a reconnect leaves no five-minute predecessor. */ private disarmMembershipWatch; /** Rebind every live membership-watch intent after a connection rebuild. */ private rearmMembershipWatches; /** Fetch recent messages from a channel's JetStream backlog. `signal` cancels the active pull and * reclaims its ephemeral consumer before the promise rejects. */ channelHistory(channel: string, opts?: { limit?: number; signal?: AbortSignal; }): Promise; /** * The newest `limit` chat messages across MANY channels at once, oldest-first within the page, * each tagged with the channel the BROKER delivered it on. * * **One read, not one per channel.** The CHAT stream already interleaves every channel into one * sequence space, so "the newest N across these channels" is the tail of ONE stream, and a * consumer takes a SET of filter subjects. The dashboard's activity feed used to answer this by * calling {@link channelHistory} once per channel and merging: each of those is a widening probe * loop, so the cost carried two multipliers (a probe loop per channel, and a fan-out across every * channel). Counted on the wire over a seeded corpus of 69 chat channels and 24 event channels at * limit 200: 2524 broker requests and about 8.0 MB transferred to return a 143,401-byte page, * against 143 requests and about 0.91 MB here. With no link cost the counts and the page size * repeat exactly across runs; the byte totals move by tens of bytes. `pnpm smoke:web-activity-read-cost` reproduces the * second column and a frozen copy of the fan-out shape; the first is that same suite run against * `544a974b7` (Cotal #1210). * * **The broker does the filtering, so the wire carries only what is asked for.** A channel left * out of `channels` costs nothing: its messages are never delivered, so a space whose volume is * dominated by channels the caller does not want stays cheap. That is the same "filter before the * fetch" property the per-channel fan-out had, kept rather than traded away. * * **The channel comes from the SUBJECT, never from the payload.** A message claims a `channel` * field, and this method ignores it: the tag is derived from the subject the broker routed the * message on, the same derivation {@link listChannels} uses to name a channel in the first place. * * **Concrete channels only.** Filter subjects on one consumer may not overlap, and a wildcard * channel subsumes its own subtree, so a wildcard here is refused rather than silently dropped or * silently double-counted. * * **Observer/admin credentials only, and the broker is what says so.** A multi-filter create * cannot encode its filter in the API subject, so it rides the bare * `$JS.API.CONSUMER.CREATE.` row that only the read-only dashboard profiles hold. An agent * credential pins the filter into the subject per channel and is denied here by the broker, which * is the correct answer: this method reads across channels, and an agent's read ACL is per * channel. */ multiChannelHistory(channels: readonly string[], opts?: { limit?: number; signal?: AbortSignal; batch?: number; }): Promise<{ channel: string; msg: CotalMessage; }[]>; /** Read a channel's recent history THROUGH THE DELIVERY DAEMON instead of through a consumer this * connection creates itself — the mediated read of SPEC's "Mediated reads (normative)" rule (no raw * consumer / `DIRECT.GET` / `STREAM.MSG.GET` for an untrusted holder; the trusted reader serves it * onto the caller's own confined rail). `items` is shape-identical to what {@link channelHistory} * returns — the same `CotalMessage[]`, the same newest-N selection, the same oldest-first order * within the page — so a caller migrates by reading `.items` and nothing else changes. The return * is WRAPPED rather than bare precisely because of `complete`: a bare array cannot say whether * older history remains behind it. * * **Why this exists when `channelHistory` already works:** authorization. A consumer pins its * authorization at CREATE time, so a caller whose read ACL is revoked mid-scroll keeps being served * by the consumer it already holds. The mediator re-reads authorization on EVERY call (live * registry row ∩ mint-time ceiling — SPEC §9.6), so a revocation stops the very next read and a * registry-only widen cannot exceed the effective credential. That is the whole point of the verb; * a mediator that cached the ACL would be a rename of the path it replaces. * * **The caller never names itself.** The daemon takes the principal from the broker-authenticated * request subject ({@link serveControl} fail-closes when the payload `from` disagrees), so there is * no caller-supplied identity to forge. * * A channel outside the caller's read ACL THROWS. It must never come back as an empty page: "you * may not read this" and "there is nothing here" are different answers and only one is safe to * render as an empty conversation. * * Chat channels only — DM history is deliberately not served here (it is god-view-only today, a * different authorization model, and one handler with two authz paths is the wrong shape on the * surface where a mistake exposes private messages). */ readHistory(channel: string, opts?: { limit?: number; }): Promise; /** Fetch recent DMs (any sender→any recipient) from the space's DM backlog. `signal` cancels the * active pull and reclaims its ephemeral consumer. God-view only: * a normal agent/observer's ACL denies CONSUMER.CREATE on DM_, so this throws-and- * skips for them — only an `admin`-profile cred can read it. */ dmHistory(opts?: { limit?: number; signal?: AbortSignal; }): Promise; /** * The `limit` MOST RECENT messages matching `subject`, oldest-first within the page. * * **This used to return the OLDEST N.** `js.consumers.get(stream, {...})` builds an ORDERED * consumer, which defaults to `DeliverPolicy.StartSequence` with `opt_start_seq: 1` — the very * beginning of the stream. Capping the fetch at `limit` therefore took the first N messages ever * sent, while this method is documented as "recent" and every caller (the dashboard feed, the * agent-facing history tools) presents the result as the latest. Confirmed live against a * 123-message channel: `limit=10` returned the ten oldest, not the ten newest. * * Fixing it by draining from the start and keeping the tail would be correct and ruinous: it * transfers the entire backlog to show one screen. Instead, find the newest matching sequence and * consume a WINDOW ending there, widening geometrically until the window holds `limit` matches. * A filtered subject's sequences are non-contiguous (other channels interleave in the same * stream), so the window cannot be computed arithmetically. A FAILED attempt holds fewer than a * page by definition, so wasted transfer stays page-sized and geometric growth keeps the number of * attempts logarithmic. A channel with fewer than `limit` matches used to keep widening until * sequence 1 and drain the stream's whole retained set (#840). The walk now stops at the * subject's FIRST matching sequence (the mirror of the last-matching ceiling), probed on the * same CREATE surface, and only after a short drain so a dense page never pays for the floor. * * `before` pages toward the past: pass the `seq` of the oldest message you already have. */ private streamHistory; /** The oldest stream sequence matching `subject`, or 0 when the subject has no messages. * Mirror of {@link lastMatchingSeq}: same CREATE/INFO/NEXT/DELETE surface, `DeliverPolicy.All` * instead of `Last`, first delivered seq instead of last. Read credentials already hold this. */ private firstMatchingSeq; /** The newest stream sequence matching `subject`, or 0 when the subject has no messages. * * One ordered consumer at `DeliverPolicy.Last` with this subject's filter: its `num_pending` * (available from the create, before anything is delivered) is 0 for an empty subject, and * otherwise one message carries the sequence. Same CREATE/INFO/NEXT/DELETE surface `drainWindow` * already uses, so no broker authority is added. */ private lastMatchingSeq; /** Drain every message matching `subject` with sequence in `[start, ceiling]`, oldest-first. * One ephemeral ordered consumer, one batched pull — `AckPolicy.None`, so no per-message ack * round trip. Fetches exactly the pending count so it returns as soon as the window is * delivered rather than blocking for the pull's full expiry. */ private drainWindow; /** * Surface the connection's async status errors on our `error` event. NATS reports * publish permission violations *only* here (subscription/request ones too), never on * the failing call — so without this an over-tight ACL silently drops the agent's * traffic and it just looks "absent". We annotate permission denials explicitly so a * denial is never mistaken for absence (which already has a benign cause: MCP reconnect). */ private watchStatus; /** The error message for a guard that finds the endpoint unbound: "reconnecting" during a * rebuild's null window OR an inter-retry backoff (so a concurrent op reports the real * reason, not "not started" — `reestablishing` spans the whole retry loop incl. backoff), * else "endpoint not started" (genuine pre-start). */ private notLiveMsg; private publishMsg; /** Create the three backing streams for this space (idempotent). Open-mode lazy create; * the same definitions are used by `cotal up` at privileged setup. */ private ensureStreams; /** * Privileged: pre-create an agent's DM inbox durable (auth mode), so the agent can BIND * it without holding CONSUMER.CREATE on DM_. The creator sets the filter to * inst..* — the agent never gets to choose it, which is what stops a peer from * creating a durable filtered to someone else's inbox. Idempotent (byte-identical config), * safe to call again on manager restart. The caller must be permissive on DM_. */ provisionDmInbox(owner: string, actor: string, lifecycleUid: string): Promise; /** Idempotent-PER-LIFECYCLE create of a `dm_--` durable with its ACTIVATION FRONTIER * (SPEC §8). Info-first: an existing durable (a manager-restart re-provision of the SAME uid, or * the same lifecycle's own restart) is kept as-is, preserving the ORIGINAL frontier — the * activation moment never moves. A fresh lifecycle captures the DM stream's current `last_seq` and * starts delivery at frontier+1, so a same-alias successor inherits none of the predecessor's * pending DMs (its filter is the shared alias subject `inst.>`; the FRONTIER, not the subject, is * the cut). * * HONESTY (panel-locked): the no-gap guarantee (a DM published between the capture and the create * lands ABOVE the frontier and is delivered) holds ONLY under ONE provisioner per lifecycle uid — * the manager provisions sequentially, which satisfies it. Under CONCURRENT same-uid provisioners * (a split-brain manager), a higher-frontier winner excludes the loser's N+1..M capture window: * the lost-race branch below keeps the winner's durable unconditionally. No per-uid serialization * or persisted-frontier machinery is added in this slice; concurrent same-uid provisioning is * out-of-contract (at-least-once best-effort). */ private ensureDmDurable; /** * Privileged: pre-create an agent's bind-only Plane-3 DELIVER durable (`dlv_`, filtered to * `dlv.`), so the agent can BIND its per-member durable handoff without holding CONSUMER.CREATE * on the DLV stream. Same bind-only model as {@link provisionDmInbox}: the creator sets the filter, * the agent never does. The trusted reader transfers re-authorized copies onto `dlv.`; the agent * acks them via native JetStream (SPEC §8). Idempotent. The caller must be permissive on DLV. */ provisionDlvInbox(owner: string, actor: string, lifecycleUid: string): Promise; /** * Privileged: pre-create a role's shared TASK work-queue durable (auth mode), so agents * of that role can BIND it without holding CONSUMER.CREATE on TASK_. The creator * sets the filter to svc..* — agents never choose it, which stops cross-role drain. * Idempotent per role. The caller must be permissive on TASK_. */ provisionTaskQueue(role: string): Promise; /** Lazily open the privileged members registry KV (delivery daemon / open-mode self). */ private membersRegistry; /** Lazily open the durable read-ACL registry KV. Privileged write (the manager records an agent's * ACL at mint); the delivery daemon reads it fresh per durable entry to re-authorize. */ private aclRegistry; /** Privileged ({@link DurableProvisioner}): record an agent's read ACL in the durable registry at * provision/mint time — the same act as baking it into the JWT, persisted so the server-side * delivery daemon can re-authorize the agent's durable entries and validate its runtime * durable-joins without holding any in-memory ledger. Written ATOMICALLY ({@link writeAclRecord}), * so a present record is always complete (`[]` = known no-read, never a half-write). */ commitAcl(targetId: string, lifecycleUid: string, allowSubscribe: string[]): Promise; /** * Raise the mint-time ACL ceiling. Provision/remint only — see {@link reissueAcl}. * Process discipline: call only in the same act that bakes `allowSubscribe` into the JWT; the * write is not crypto-bound to credential bytes. */ reissueAcl(targetId: string, lifecycleUid: string, allowSubscribe: string[]): Promise; /** The server-side delivery daemon's fresh-per-entry ACL read: one LIFECYCLE's current read ACL * (`allowSubscribe`) from the durable registry (exact key `..`), or `undefined` * if no record (an unknown lifecycle — the reader DEFERS, never drops). A present `[]` (known * no-read) returns `[]` (the reader DROPS). */ aclForOwner(owner: string, lifecycleUid: string): Promise; /** Resolve an ALIAS to its single live lifecycle-keyed ACL row (`readAclForAlias`): the daemon's * authz seam for callers that arrive with alias identity only (a `ctl.delivery` durable-join). * THROWS {@link AmbiguousAclAlias} on two live rows — first-match would let a stale lifecycle * authorize the successor (SPEC 13.1: at most one live lifecycle per alias). */ aclForAlias(principal: string): Promise<{ allowSubscribe: string[]; issuedAllowSubscribe: string[]; lifecycleUid: string; } | undefined>; /** Lazily open the delivery lease/readiness KV (pre-created at `cotal up`; bind, never create). */ private deliveryRegistry; /** This endpoint instance's LEASE INCARNATION: which run of this principal a lease row was written * by. Minted per construction and never re-derived, because the credential cannot supply it, the * daemon's cred is a file on disk that every restart re-reads, so `card.id` is stable across * processes by design. See {@link DeliveryLeaseInfo.incarnation}. */ private readonly leaseIncarnation; private encodeLease; /** Is this shard's lease row one THIS ENDPOINT INSTANCE wrote? The question a daemon whose renew * just failed has to answer before it decides whether it still owns the shard. * * Both halves are required. `holder` alone is not sufficient (a successor daemon re-reading the * same creds file presents the same principal, so its row would read as ours), and `incarnation` * alone is not sufficient either, it is a bare uuid with no claim to the principal, so a row * bearing ours but a foreign holder is not something we should ever adopt. A row with NO * incarnation predates the field and cannot be proven ours, which is the safe reading: it leads * to the takeover path rather than to serving on someone else's claim. */ ownsDeliveryLease(info: DeliveryLeaseInfo): boolean; /** Acquire the single-flight delivery lease for a shard via an ATOMIC CAS create, marked NOT-ready. * THROWS if a live lease exists — a loud refusal-to-bind (the daemon exits), never a retry, so two * daemons can't split a durable's delivery. A crashed holder's lease auto-expires (bucket TTL), * freeing a re-acquire. Acquired BEFORE binding (single-flight gate); {@link markDeliveryLeaseReady} * flips it ready AFTER the loops + `ctl.delivery` are bound. Returns the lease revision. */ acquireDeliveryLease(shardIndex: number): Promise; /** Flip the held lease to READY (CAS `kv.update`) AFTER `startPlane3` has bound the loops + the * `ctl.delivery` responder — so "lease ready" proves the responder is up, not just that the slot was * claimed. Returns the new revision. */ markDeliveryLeaseReady(shardIndex: number, revision: number): Promise; /** Flip the held lease back to NOT-ready, the counterpart to {@link markDeliveryLeaseReady}, for a * holder that has UNBOUND its loops and control responder but has not given up the shard. * * `ready` is a claim about the RESPONDER, not about the row's existence: `ensureDelivery` waits on * it and the channel-health surface reports it. A daemon that goes quiet to re-check its ownership * still holds the key, so without this the space would be told a responder is up while nothing is * bound, a readiness lie of exactly the kind #1318 is about, just pointed the other way. Keeping * the row (rather than deleting it) is deliberate: the shard is still claimed, so no third daemon * should be invited in; what is being withdrawn is only the claim to be answering. */ markDeliveryLeaseNotReady(shardIndex: number, revision: number): Promise; /** Renew the held lease (CAS `kv.update` against `revision`, keeping `ready:true`) to refresh it before * the bucket TTL expires it. Returns the new revision. Throws if the revision moved (lost the lease — * the daemon should exit). */ renewDeliveryLease(shardIndex: number, revision: number): Promise; /** Release the held lease on clean shutdown so a replacement daemon re-acquires immediately (best * effort, a crash just lets the bucket TTL expire it). * * THE REVISION IS WHAT MAKES THIS A RELEASE RATHER THAN A DELETE. An unconditional delete removes * whatever row is there, and by shutdown time the row is not necessarily still ours: the exit * paths that matter most are precisely the ones where another daemon has taken the shard, so the * departing process would delete the REPLACEMENT's lease on its way out and leave the shard with * no holder at all. Passing the revision this endpoint last owned makes the delete a compare-and- * swap (`previousSeq` becomes JetStream's `ExpectedLastSubjectSequence`), so a row that has moved * on is left alone. * * THE ARGUMENT IS REQUIRED, AND EXPLICITLY NULLABLE RATHER THAN OPTIONAL. `undefined` means "this * process no longer holds a revision it can argue for", which is the takeover paths' honest * answer and correctly releases nothing, the bucket TTL is the crash-safe authority and expires * a genuinely stale row. But if that were the DEFAULT, every existing `releaseDeliveryLease(0)` * call site would keep compiling and silently stop releasing: the same omission hole * `standaloneConnectOpts` closed by deleting its `= {}`. Measured, not theorised, this landed as * a red in `smoke:delivery-lease`, where a caller that genuinely held the lease released nothing * and the next acquire was refused. A caller must now say which it means. */ releaseDeliveryLease(shardIndex: number, revision: number | undefined): Promise; /** Read a shard's delivery lease (the daemon-availability signal), or `undefined` if none is live. * READ-ONLY surface — drives Component 6's `cotal_channels` delivery-health field (an agent reads it * under its own cred, which holds lease-bucket read but no write). */ readDeliveryLease(shardIndex: number): Promise; /** The lease row AND the KV revision it is at. The revision is the CAS token every renew and the * CAS release are argued against, so a caller re-establishing ownership after a failed renew * needs the BROKER's sequence, not the one it last cached: a renew can fail with its write * already applied (a lost reply, a reconnect mid-request), which leaves the cached revision one * behind forever and every subsequent CAS refused over a sequence this process itself moved , * read as somebody else's takeover, which is the #1318 misreading in a second costume. */ readDeliveryLeaseEntry(shardIndex: number): Promise<{ info: DeliveryLeaseInfo; revision: number; } | undefined>; /** Ensure + bind the manager singleton-lease bucket. Mirrors the presence-bucket pattern (connectAndBind): * AUTH mode OPENs the bucket pre-created at `cotal up` (the scoped `supervisor` cred holds no * STREAM.CREATE — and `open` binds direct=false, so the CAS-conflict `kv.get` inside `acquire` rides * STREAM.MSG.GET, the verb the supervisor grants, never DIRECT.GET). OPEN mode (no creds) create-firsts: * `Kvm.open` binds LAZILY — it does NOT verify the stream exists or throw when it's missing (a fresh * bucket then fails 'stream not found' on the first write), so `create` is the ensure-exists call (it * makes the bucket or, when another endpoint already did, throws and we bind the existing one). Either * way the per-KEY CAS create stays the only single-flight gate, so a lost bucket-create race never reads * as "lease held". */ private managerLeaseRegistry; /** Take or keep the per-SPACE daemon-credential renewal lease (#1634), returning whether THIS * instance now holds it. One atomic CAS `create` per pass: it succeeds for whoever arrives first * and throws for everyone else, so exactly one manager remints even when several share the * daemon's store. The holder re-`update`s its own key by revision, which both keeps it and proves * it never lost it. Losing the CAS is an ordinary outcome (a peer holds it) and returns false; it * never fails a start. A crashed holder's key TTL-expires with the bucket, so the next pass hands * the lease to a survivor with no operator step. */ holdDaemonRenewalLease(instanceId: string): Promise; /** Release the renewal lease on a clean stop so a peer takes over at once rather than at the TTL. * CAS-guarded, so a lease we already lost is never deleted out from under its new holder. */ releaseDaemonRenewalLease(): Promise; private encodeDaemonRenewalLease; private encodeManagerLease; /** Acquire THIS logical instance's liveness lease via ATOMIC CAS create on its own per-instance key * ({@link managerLeaseKey}). THROWS only if that SAME instance id already holds a live key (a same-root * concurrent double-start, or a restart racing the crashed predecessor's not-yet-expired key) — a loud * refusal, never a retry. A DIFFERENT instance (second workspace root ⇒ different id) creates its OWN * key and coexists (P2 item 3 demotion). A crashed holder's key auto-expires (bucket TTL). Returns the * lease revision (for renew). */ acquireManagerLease(info: Omit): Promise; /** Renew THIS instance's held key (CAS update against `revision`) before the bucket TTL expires it. * Throws if the revision moved (lost the lease). Returns the new revision. */ renewManagerLease(info: Omit, revision: number): Promise; /** Read THIS instance's OWN lease key, keyed (not the `lease.*` sweep {@link readManagerLease} does). * * `undefined` means the key IS NOT THERE — a definite absence, established by a completed read. * A read that could not be completed THROWS instead, so a caller can tell "it is gone" from "I could * not find out". That distinction is the whole point of the method: a renew that got no answer has * proved nothing, and only a definite answer here may be acted on. */ readOwnManagerLease(instanceId: string): Promise<{ info: ManagerLeaseInfo; revision: number; } | undefined>; /** Release THIS instance's key on clean shutdown so a same-id restart re-acquires immediately. CAS-guarded * by `revision`: if we already LOST it (renew gap) the stored revision has moved, the conditional delete * no-ops. Keyed per instance, so a release NEVER touches a sibling manager's key (security pin 6). */ releaseManagerLease(instanceId: string, revision?: number): Promise; /** Read a live manager liveness lease, or undefined if NONE (no manager instance holds the space). A * presence/existence check for the CLI's `spawn -f` reuse and `waitLeaseGone`, which only need "is any * manager here". Open-only — never creates the bucket, so a probe that finds no manager leaves none * behind. (Instance-precise enumeration for the class scatter comes from the registration records KV * in 3b-4, not this liveness bucket.) * * MULTI-INSTANCE EXACT, and it has to be read that way rather than as a point get. Several managers * may hold one space, each renewing its own `lease.`. A single `last_by_subj` over * `lease.*` returns the newest message under the wildcard REGARDLESS OF KEY — so a stopping peer's * DEL tombstone, being newest, answered "no manager here" while a sibling was alive and renewing. * Only an explicit `kv.delete` writes that tombstone: a manager whose lease TTL-expires is removed by * limits and leaves nothing behind, so the poisoning case was the ORDINARY one (stop a manager * cleanly, then `spawn -f`), not the crash. Enumerating live entries collapses to the greatest * revision PER KEY and drops keys whose final state is a marker, so a peer's DEL can only retire that * peer's own key and can never mask a live one. */ readManagerLease(): Promise; /** Privileged: one owner's NON-TOMBSTONED durable memberships as `{channel, generation, activated}` — * the server-side delivery daemon serves this to a connecting agent (the `listMemberships` op on * `ctl.delivery`). The agent seeds its leave mirror from the ACTIVATED ones (the confirmed backstops), * but the non-activated ones are returned too so `leaveChannel` can discover + close a record that * still routes under the pure-interval predicate (a crash-stuck pending activation) — without reading * the privileged KV itself. */ ownerMemberships(owner: string, lifecycleUid: string): Promise<{ channel: string; generation: number; activated: boolean; }[]>; /** Effective delivery class read AUTHORITATIVELY from the registry KV (not the watch cache) — so a * `live`→`durable` flip is seen by fan-out without a cache-propagation gap (red-team MED-3). */ private deliveryClassFresh; /** Collision-safe `@mention` → owner-id resolution: a name that resolves to exactly one present * peer wins; 0 or >1 matches drop (never fan a directed durable copy to an unrelated same-named * bystander — red-team LOW; SPEC §4 unique instance id). */ private resolveOwnerByName; /** Publish one fan-out entry into a member LIFECYCLE's mixed inbox (`dinbox...`, SPEC * §13.1: fan-out addresses the member row's RECORDED lifecycle, never the alias's current * occupant), idempotent via `Nats-Msg-Id` (`::`) so a catch-up copy * and a racing fan-out copy collapse. The `principal` is the member's owner+actor dot-form. */ private publishDinbox; /** The fan-out consumer's delivered stream-seq — the activation-fence upper bound (red-team * BLOCKER-1: the shared fan-out cursor advances independently of the stream frontier). */ private fanoutDeliveredSeq; /** * Privileged durable-JOIN write (v3: the delivery daemon calls this from its `ctl.delivery` handler * after validating channel ⊆ the caller's read ACL): capture `joinCursor`, commit a `durable-active` * record (CAS + generation bump), then ACTIVATION CATCH-UP idempotently copies `(joinCursor, fence]` * into the owner inbox where `fence = max(frontier, fanoutDelivered)` — fan-out owns `seq > fence`. * Idempotent against a timeout-retry (an already-activated membership no-ops). Returns `{durable:false}` * (honest degrade) only if the catch-up window was evicted. * * Runs on the daemon (which hosts the fan-out/reader loops + the members KV), so catch-up + the * activation fence read are in-process — no cross-process cursor read. */ durableJoinFor(owner: string, channel: string, lifecycleUid: string): Promise<{ durable: boolean; reason?: string; generation?: number; }>; /** Privileged durable-LEAVE write: tombstone the membership at `leaveCursor = frontier` so the * backstop denies `seq > leaveCursor` while a pre-leave entry stays deliverable (SPEC §7 interval). */ durableLeaveFor(owner: string, channel: string, lifecycleUid: string, expectedGeneration?: number): Promise; /** Idempotently copy the eligible chat messages in `(fromSeqExcl, toSeqIncl]` for `channel` into the * owner inbox, via a DEDICATED per-(owner,join) ephemeral consumer (NOT the agent-scoped * `chathist_`/`histLock` — red-team HIGH-8). `evicted` ⇒ the oldest eligible seq aged out under * `discard=Old` (the start seq could not be served), a durable shortfall the caller surfaces. */ private catchupCopy; /** Start the Plane-3 fan-out writer + trusted reader on THIS (privileged, server-side delivery-daemon) * endpoint, AND serve the `ctl.delivery` control service (runtime durable join/leave/list). `aclFor` * maps an owner id to its current read ACL for the reader's re-authorization — read FRESH per entry * from the durable ACL registry (async). Call once after connect; idempotent durable creation lets it * resume on a daemon restart. Both the JS loops AND the `ctl.delivery` subscription are (re)bound by * {@link armPlane3} on EVERY (re)connect — a reconnect drains the old connection, so re-binding both * is required, not optional (the responder would otherwise be lost on a broker blip). */ startPlane3(aclFor: (owner: string, lifecycleUid: string) => MaybePromise, opts?: { reloadMembershipCreds?: (expected?: string) => Promise; evictPrincipal?: (principal: string) => Promise; planeConnLiveness?: (query: unknown) => Promise; principalLiveness?: (principal: string) => Promise; reloadStoreIdentity?: () => SecretStoreIdentity; }): Promise; /** Serve one runtime durable-membership control request (the server-side delivery daemon). The caller * id is the authenticated subject sender ({@link serveControl} fail-closes on a mismatch). Validation * is against the durable ACL registry — the SAME KV the reader re-auths against (single source of * truth, no in-memory ledger to drift). */ /** Whether an ALREADY-DISPATCHED unit of Plane-3 work may still take effect. * * Unsubscribing stops NEW work; it cannot recall work already in flight. A handler that entered * before {@link quiescePlane3} and awaited broker I/O inside it resumes AFTER the freeze, and a * reviewer traced the ordering that makes that a split rather than a latency blip: the loser * accepts a unit and awaits, the loser is descheduled, the successor acquires the shard and flips * its lease READY, then the loser resumes and answers or acks for a shard it no longer holds. * Subscription counts and parked-pull readings cannot see it, because the effect is the reply and * the ack rather than the binding. * * So each work path re-asks HERE, at the point of effect, after its awaits and before it acts. * The lease keeps one ROW, not one SERVER; this is the half that keeps one server. An unacked * message is not lost by refusing: a stopped consumer redelivers it to whoever holds the shard * next, which is the same property quiescing already relies on. */ private plane3MayAct; private handleDeliveryControl; /** Validate the channel ARG shape only: non-blank, valid, concrete (NO ACL check, that is op-specific). * Returns the channel on success or a ControlReply error to short-circuit. */ private checkDurableChannelArg; /** JOIN requires the channel be within the caller's CURRENT read ACL (you can't durable-subscribe a * channel you may not read). */ private deliveryJoin; /** LEAVE must NOT require current-ACL coverage. Leave fires precisely when the ACL was narrowed/revoked * (a refused live sub → {@link closeRefusedMembership}); gating the tombstone on the current ACL would * loop forever and leave the SPEC §7 boundary open (the membership could resume if the ACL is later * restored). The guards are: authenticated caller (serveControl), concrete channel, a finite generation * (the join epoch — without it a stale/replayed leave could tombstone a newer rejoin), and an EXISTING * own membership; `durableLeaveFor` → `tombstoneMember` then enforces the generation match. */ private deliveryLeave; /** Serve one MEDIATED HISTORY READ (`readHistory`, the client side is {@link readHistory}). The daemon * holds the consumer so the caller does not have to: this is a privilege reduction, not a new * capability, and it is the shape SPEC's "Mediated reads (normative)" rule asks for. * * THE ONE INVARIANT THIS METHOD EXISTS FOR: authorization is read FRESH on every call and never * cached across calls. A consumer pins its authorization when it is created, so a revoked caller * keeps being served by a consumer it already holds; re-reading here is what makes a revocation * stop the very NEXT read. Cache this and the verb loses its only advantage over the raw consumer * path. * * AUTHORITY (SPEC §9.6): "current read ACL" is the effective broker-accepted credential. The * durable registry is a live mirror of that credential, not a second grant source. History * authorizes against `allowSubscribe ∩ issuedAllowSubscribe` — the live row (so revocation via * plain commitAcl still stops the next read) intersected with the mint-time ceiling (so a * registry widen without a remint cannot grant what the JWT does not). Raising the ceiling is * {@link reissueAcl}, which is what provision does when it bakes the list into the JWT. * * The caller arrives as an alias (the control subject carries owner+actor, never a uid) and is the * AUTHENTICATED subject sender — `serveControl` has already fail-closed on any payload that names a * different principal, so `caller` cannot be self-asserted. */ private deliveryReadHistory; /** Bytes a control reply may occupy: the broker's `max_payload` less headroom for the `ControlReply` * envelope wrapped around the items. Read from the live server info rather than assumed, since an * operator can raise or lower it. */ private payloadBudget; /** Stop serving Plane-3 WITHOUT tearing down the connection, so a daemon that has just learned its * lease may no longer be its own can stop acting on the shard while it finds out for certain. * * A COMPARE-AND-SWAP KEEPS ONE LEASE ROW; IT DOES NOT KEEP ONE SERVER. That distinction is the * reason this exists, and it was a review finding. When a renew fails, the daemon re-reads the * key and may then re-acquire it, and across that read-then-create it was still consuming the * fan-out durable, still running the inbox reader, and still answering ctl.delivery. If a * replacement acquired the shard in that window, both processes served the same durables until * the loser's create was refused and its teardown finished. The old code did not have this * window, because it began shutting down on the first renew failure; treating that failure as a * question instead of a verdict is right, but asking the question while still serving is not. * * So the daemon goes quiet FIRST and re-arms only once it has proof: `held` on a re-read, or a * won atomic create. `unknown` stays quiet, the whole point is that not being able to ask is not * permission to keep acting. Quiescing costs delivery latency for a few seconds; the alternative * costs a SPLIT durable, which is a correctness failure rather than an availability one. * * Deliberately not `stop()`: the connection, the lease KV handles and the control rails must stay * up, because the daemon still has to ask the broker who owns the shard. */ quiescePlane3(): Promise; /** Whether Plane-3 is currently quiesced: unbound, and refusing to re-bind until ownership is proven. * Read-only. The flag is the daemon's own answer to "am I serving this shard", so a cell that asserts * a recovery worked can check the endpoint's state rather than inferring it from a log line. */ plane3IsQuiesced(): boolean; /** Resume serving Plane-3 after {@link quiescePlane3}, once ownership has been re-established. * Idempotent, and a no-op when the daemon was never quiesced. * * The flag is cleared ONLY after every binding is up, and restored if any of them throws. Clearing * it first looks equivalent and is not: `armPlane3` binds in four stages (`manager`, the two control * responders, the fan-out consumer, the reader), so a failure at any one leaves the endpoint recorded * as un-quiesced while some of those are missing. From there every later `rearmPlane3` returns at the * `!plane3Quiesced` guard WITHOUT attempting to bind, and the caller goes on to flip the lease READY. * That is a readiness lie surviving a transient broker error, the daemon claims a responder it does * not have, which is the #1318 outage wearing the readiness flag instead of the exit path. */ rearmPlane3(): Promise; /** (Re)bind the Plane-3 fan-out writer + trusted reader. Idempotent — the durables resume from their * cursor. Called by {@link startPlane3} once AND by {@link connectAndBind} on every (re)connect, so * the delivery daemon's reconnect RE-ARMS the backstop + the ctl.delivery responder. Without this, a broker blip would silently kill * the loops while `durableJoinFor` kept reporting `durable:true` (the impl-review's BLOCKER-1). No-op * unless this endpoint hosts Plane-3 (`this.plane3` set). */ private armPlane3; /** (Re)register the `ctl.delivery` control responder on the CURRENT connection. A reconnect drains the * old connection (the old sub is dead and `clearConnectionScoped` leaves caller-owned subs alone), so * this MUST run on every arm — otherwise durable join/leave/list silently lose their responder after a * broker blip. The stale sub is dropped (unsubscribed + removed from `this.subs`) before re-creating. * `boundReply` is essential here: the daemon holds a wildcard reply-publish grant, so the serve path * must reject any reply target outside the authenticated sender's own subtree (confused-deputy fix). */ private armDeliveryControl; /** Serve one PRIVILEGED delivery-admin request (the D5 rail-split). The cred layer is the caller * boundary — only the supervisor profile can publish here — and `serveControl`'s sender check + * bounded reply still apply on top. `reloadCreds` is the class-2 renewal ADOPTION step: re-read * the renewal-owner-re-signed creds file, pin, swap the live connection, reconnect the membership * feed's rw connection, and reply with proof (identities + the adopted JWT windows) — or a * structured failure (e.g. the file was never re-signed), never a silent partial. */ private handleDeliveryAdmin; /** Fan-out loop: bind the privileged `fanout` durable on CHAT and route each message (routing only — * the trusted reader is the auth gate). */ private runFanout; /** Route ONE chat message to eligible owners' mixed inboxes. `durable` channel → its `durable-active` * members within interval; `live` channel → `@mention` targets authorized to read it (ACL only). * Members KV is scanned FRESH per message (no cache — red-team BLOCKER-1 catch-up correctness). */ private fanOutMessage; /** Trusted-reader loop: bind the single privileged `reader` durable over `dinbox.>` and re-authorize * + transfer each entry. */ private runReader; /** Re-authorize ONE mixed-inbox entry and transfer it to the owner's DELIVER store. Deny (drop) on a * revoked/narrowed ACL or out-of-interval seq; on transfer success, ack the mixed entry (durability * has moved to DLV — an §8 equivalent per-member at-least-once mechanism). The agent acks DLV. */ private readerHandle; /** Agent-side: bind + pump our pre-created Plane-3 DELIVER durable (`dlv_`). Every message here is * delivery-daemon-written (DLV is delivery-write-only, broker-enforced) and is a CHANNEL message by contract * (the backstop never carries DMs), so `kind=channel` is path-derived (SPEC §4) and the body is * trusted (no spoof-guard). `durable:true` — real JetStream ack, coalesced with the core-sub live * copy by `MeshAgent.ingest`. No-op when the durable isn't present (open mode / not provisioned). */ private pumpDlv; /** Agent-side: request a Plane-3 durable backstop for a channel via the server-side delivery daemon (ctl.delivery). Throws * when no privileged writer is present (open / no delivery daemon). 30s timeout — activation catch-up may * run before the reply (the window is small, but a busy channel can take more than the 5s default). */ durableJoinChannel(channel: string): Promise<{ durable: boolean; reason?: string; generation?: number; }>; /** Agent-side: release a Plane-3 durable backstop (tombstone membership at the leave cursor). Passes * the join generation so a stale leave can't tombstone a newer rejoin (the delivery daemon validates * it) AND this incarnation's lifecycleUid — membership rows are lifecycle-keyed (SPEC 13.1), and a * leave must resolve its OWN row even after the ACL row was narrowed or purged. */ durableLeaveChannel(channel: string, generation?: number): Promise; /** Fail-closed async cleanup for a channel forced out by a LATE sub.allow refusal (the broker revoked * the live read). The sync sub callback can't await, so this RETRIES the Plane-3 tombstone with capped * backoff UNTIL IT SUCCEEDS (or the endpoint stops) — the §7 boundary always closes once the manager * is reachable, never a silent give-up. While pending, the channel is tracked in * {@link pendingDurableLeave} and surfaced via {@link pendingDurableLeaves} (the connector shows it in * `cotal_channels` as `durable-unclosed`, never ordinary absence). The generation is kept the whole * time. Authoritative closure of a revoked membership is also handled by revocation (rotate creds + tear down). */ private closeRefusedMembership; /** Channels with a Plane-3 durable membership whose §7 tombstone is still pending after a refused live * sub (see {@link closeRefusedMembership}) — surfaced by the connector as a `durable-unclosed` state so * it is never presented as ordinary "not subscribed". */ pendingDurableLeaves(): string[]; /** A control request that found NO responder — open / manager-less (no privileged control plane), * distinct from a responder that errored. nats.js surfaces it as NoRespondersError, or a RequestError * whose `isNoResponders()` is true. */ private isNoResponders; /** Agent-side: this session's CURRENT durable memberships (channel + join generation) from the * manager — the agent holds no read on the privileged members KV. `undefined` ⇒ NO control responder * (open / no delivery daemon, so there is no Plane-3 and no memberships). THROWS on a responder-present RPC * failure, so a caller can FAIL-CLOSED rather than mistaking a transient error for "no membership". */ private fetchMemberships; /** Agent-side, first connect (auth): SELF-JOIN this session's durable boot channels via the * server-side delivery daemon — replacing the old manager-written boot membership. Each concrete * `durable`-class boot channel gets a `durableJoin` whose returned generation seeds the leave mirror * + durable-state surface; an already-active membership (a relaunch) is idempotent (no re-catch-up). * If the daemon is down/absent at first connect (or reports a transient `durable:false`), the channel * is handed to {@link reconcileBootJoin} for capped-backoff retry — so the backstop is RESTORED once * the daemon recovers, not left silently live-only. Until a membership exists the channel renders * degraded in `cotal_channels` ({@link hasDurableMembership}). */ private armBootDurableMemberships; /** Retry a boot durable self-join with capped backoff until a membership EXISTS (success → seed * `plane3Channels`) or the channel is left / the endpoint stops. Mirrors {@link closeRefusedMembership}: * a one-shot first-connect attempt that swallowed a daemon outage would leave the boot channel live-only * forever after the daemon recovers (and the lease-based health could then read "active" with no owner * membership). This loop is the reconcile that closes that gap. Idempotent — a channel already pending * is not double-driven; survives reconnect (it re-issues `durableJoinChannel` on the current connection). */ private reconcileBootJoin; /** True if this session holds an established Plane-3 durable membership for `channel` (in `plane3Channels`). * Drives the membership-aware delivery-health surface: a joined durable channel that is NOT yet a member * (boot self-join pending / daemon down) must render degraded, never "active" off a live lease alone. */ hasDurableMembership(channel: string): boolean; /** Lazily obtain a JetStream manager — so a non-consuming endpoint (e.g. the supervisor, * consume:false) can still pre-create others' durables. */ private manager; /** Bind this endpoint's durable consumers: DM inbox, chat, and (if a role) the task queue. */ private startConsumers; /** Drive one consumer: decode, drop our own echo, and hand each message to listeners with ack control. */ private pump; /** Open a native core subscription to a channel's live feed (the manager-free live read path, * broker-enforced by `sub.allow`). At-most-once — no replay, no ack; it is the live delivery for * every channel (boot + runtime). For a `durable` channel it is also the low-latency wake-hint * alongside the Plane-3 durable copy, coalesced by the receiver's id-dedup. Drops our own echo + * spoofed senders. */ private subscribeChat; /** Close a channel's core subscription (manager-free leave). */ private unsubscribeChat; /** Confirm a just-opened core subscription was accepted by the broker. A `sub.allow` violation is * async in NATS, so flush (round-trips the SUB) then settle briefly to let the refusal land — a * denied subscribe must not read as a successful join (SPEC conformance #13). */ private confirmChatSub; /** The highest join watermark among the joined subscriptions that cover `concreteChannel` * (a wildcard sub like `team.>` covers `team.backend`), or undefined if none — the tail * drops a chat message with `seq <= ` this. */ private dropWatermark; /** The durable's info (rebind) or null (fresh — 404). Gates create/backfill to the join event * and exposes the current `filter_subjects` for restart reconciliation. */ private consumerInfo; /** Delete one named consumer, swallowing ONLY structured consumer/stream absence. */ private deleteConsumerIfPresent; /** Current frontier (last sequence) of the chat stream — a channel's join watermark, and the * focus-watermark a connector captures on entering `focus` (recall reads ambient after it). */ chatFrontier(): Promise; /** Phase 1 of a join — arm each channel's tail-drop watermark at the current frontier. MUST run * BEFORE opening the core subscription so the live tail can never carry a just-joined message * un-watermarked — which would double-emit it (live + backfill). * Returns the per-channel frontiers for {@link backfillArmed}. */ private armJoin; /** Phase 2 of a join — backfill each armed channel's history up to its frontier (replay-gated), * AFTER the filter flip. Returns the total backfilled. */ private backfillArmed; /** Replay policy + backfill window read straight from the registry bucket (vs the watch cache) * — the authoritative read for a join decision (a join is infrequent, and at startup the async * cache may not have caught up). Falls to the built-in default only with no registry open. */ private joinPolicyFresh; /** * Read retained chat history on ONE channel subject through a name-scoped, single-filter * EPHEMERAL pull consumer — the broker-contained replacement for the removed Direct Get. The * create rides `$JS.API.CONSUMER.CREATE...`, whose trailing filter * token nats-server pins to the request body (JSConsumerCreateFilterSubjectMismatchErr, code * 10131) — so an agent can only ever replay a channel its `allowSubscribe` grants. Single filter * only (plural isn't ACL-constrainable); `AckPolicy.None` + `mem_storage` so it leaves no durable * state, and it is deleted right after. Returns raw messages in stream order from `start`, * stopping once past `untilSeq` (exclusive of it) or after `limit`. The per-instance name means * calls must be serial — every reader here awaits to completion, so they are. */ private collectHistory; private collectHistoryInner; /** Read a channel's retained history up to `upToSeq` (the join frontier) and emit each message * as a `historical` "message" event. `sinceMs` bounds how far back via a native consumer * `start_time` (now − window); unset ⇒ the full retained window. New messages (`seq > upToSeq`) * are skipped — the live tail owns them. Reads through the contained {@link collectHistory}. */ private backfillChannel; /** * Replay-gated pull of a channel's retained ambient from `sinceSeq` (exclusive) forward — the * focus-recall read behind `cotal_inbox`. Returns the messages (NOT emitted — this is a pull, * not a push into context) plus `dropped: true` when the window is not complete: either the * channel's earliest *retained* message is already newer than the watermark (some ambient aged * out of the per-subject window), or replay is off for the channel below. Either way the caller * must say so rather than silently reporting an empty, complete window. * * Honors the **same** per-channel replay gate as join-backfill ({@link joinPolicyFresh}): a * `replay=off` channel returns no messages, so `focus` can't become a history bypass for a * channel that denies replay to everyone else (the read ACL bounds *which* channels recall can * touch; this app gate bounds *whether* a permitted channel replays). Ingest still ack-drops * focus-mode ambient/mentions on this channel on the promise that they stay recallable (#977) — * the gate means that promise cannot be kept, so it reports `dropped: true` rather than pretend * the window was empty and complete. */ recallChannel(channel: string, sinceSeq: number): Promise<{ messages: CotalMessage[]; dropped: boolean; }>; /** Did focus recall on `subject` miss ambient that aged out past the watermark? Ambient is only * ever discarded once a sender-subject reaches {@link MAX_MSGS_PER_SUBJECT} (`DiscardPolicy.Old`); * below the cap nothing was evicted, so the window is complete — return false without crying * wolf. At the cap, the surviving oldest seq decides: if it already postdates the watermark, the * eviction reached into the "since you focused" window. (Avoids the false positive of comparing a * per-subject oldest against the stream-global frontier, which fires on any other channel's * traffic.) */ private channelDropped; /** Sequence of the earliest message still retained on a channel subject (any sender), or * undefined if nothing is retained. One message through the contained {@link collectHistory} — * used for the recall drop marker. */ private channelOldestSeq; private publishPresence; /** #1356: drop the presence-refusal record when the connection that OBSERVED those refusals goes * away. "This bucket is refusing writes" is a claim about a specific broker connection; once that * connection is torn down or rebuilt the claim has no remaining basis, and a later failure on a * fresh connection must establish it again from its own evidence. * * Cleared at the SOURCE rather than guarded at each reader, because a guard protects one consumer * and clearing protects every consumer, including ones not yet written. Measured: without this, a * failed bind against an unreachable server still reported the presence-refusal sentence while the * endpoint's own `connectionIssue` already said "connection refused". */ private clearPresenceWriteFailure; /** #1356: the presence bucket has been refusing writes since this time, or `undefined` when the * last publish succeeded. Cleared by the first successful write, so a survived blip reads as * healthy and only a SUSTAINED failure carries a duration. * * Presence writes are the CANARY, not the scope: the broker can disable JetStream account-wide * while the NATS connection stays up, so a caller must not read this as "only presence is * affected". It reports what was observed, not how far the fault extends. */ presenceWriteFailure(): { since: number; forMs: number; error?: string; bucket: string; } | undefined; /** Bind a presence watch on the current connection. Resolves true when the watch was * installed, false when the endpoint stopped or rebuilt while the bind was in flight: that * bind's iterator is released here and nothing is installed, because the epoch that asked * for it is gone and the epoch that replaced it binds its own watch through * {@link connectAndBind}. Without this fence a bind that completes after {@link stop} would * resurrect a watch on a stopped endpoint, and one that completes after a rebuild would * overwrite the fresh epoch's watch with a dead-connection iterator. */ private startPresenceWatch; /** * Replace a presence watch that has gone silent past TTL while the connection is up. The new * ordered consumer starts from the bucket's current last-per-subject state, so a peer that is * heartbeating is re-observed within one replay and a peer that is gone is aged out by the * next sweep exactly as if the watch had never stalled. Rate-limited to one attempt per TTL * per observer, never overlapping, never on a stopped or rebuilding endpoint (those own their * watch through {@link connectAndBind}). A stop or rebuild that lands while the bind is in * flight retires it: {@link startPresenceWatch} releases the late iterator and reports * nothing, since the epoch that was silent no longer exists. A failed bind is reported and * the view stays stale. */ private rebindStalePresenceWatch; /** Watch the channel registry: replay existing keys, then stream updates, into the local * cache. Best-effort — a registry the endpoint can't read leaves the cache empty (effective * policy then falls back to the default), never a fault. */ private startChannelWatch; private handleChannelEntry; /** The watch was just bound onto a bucket with no keys. * * A NON-REGISTERING observer (a `cotal status` probe, a lease checker) has real knowledge: * nobody is present. Every peer still in its roster is known gone (its key is not there to * replay), so it is marked offline now rather than aged out against a delivery that cannot * come; the silence gate is disarmed and the view reads current until the first write lands. * Without that the empty-bucket view relapsed to stale one window later and rebound again on * every window, one consumer create and one warning per TTL for as long as the mesh was empty. * * A REGISTERING observer (the manager) is itself one of the keys that should be there. An * empty bucket under it means the bucket was wiped since its last heartbeat (the stream * recreation), and the same wipe took every peer's record: their absence says the bucket is * new, not that they left. a reviewer reproduced the previous behaviour at default timing: * the rebind landed ~0.9s after the recreation, the observer marked every peer AND ITSELF * offline, and held the view current for up to one heartbeat, a false verdict `cotal ps` * would print as `mesh offline`. So a registering observer re-publishes its own record NOW, * which the new watch delivers, and lets the ordinary per-peer age-out run from that delivery: * a peer that is still heartbeating rewrites its key within its own heartbeat interval and is * re-observed live; one that is gone ages out exactly as after a plain rebind. The roster is * not touched here and the view is not held; the delivery is what makes it current. * * What neither branch covers: a consumer that dies again while the bucket is still empty is * not detectable by silence, so the first write after that is missed until the observer * restarts. */ private onPresenceBucketEmpty; /** See {@link onPresenceBucketEmpty}: the non-registering branch. */ private markPresenceBucketEmpty; private handleKvEntry; private applyPresence; /** Materialize an OFFLINE presence record: drop the advisory attention fields. An offline peer must * not show a stale `[focus]` or "locally muted #x" hint — SPEC: attention removed on offline sweep, * channel modes reset on restart. card/activity/ts are kept. */ private toOffline; /** Mark a known peer offline (on KV delete/purge), keeping it in the roster. */ private markOffline; private emitPresenceViewIfChanged; private sweep; } /** Auth subset of connect() options, shared by the endpoint and isReachable. `bearer` may be a * sync GETTER — nats.js re-evaluates token authenticators per (re)connect attempt, which is how a * refreshing endpoint presents its freshest bearer without rebuilding the connection options. */ interface AuthOpts { token?: string; user?: string; pass?: string; /** May be a sync GETTER (like `bearer`) — the authenticator then re-reads it per (re)connect * attempt, which is how a standing-renewal endpoint presents its freshest cred. */ creds?: string | (() => string); bearer?: string | (() => string); sentinelCreds?: string; tls?: boolean; } /** True when a failure is a NATS *permission denial* — the subject is forbidden to this * endpoint's creds — rather than a missing responder or a timeout. The two need opposite * fixes (grant the capability vs. start/await the service), so callers (e.g. a control * request that can't reach the manager) must tell them apart instead of defaulting to * "service down". Unwraps a wrapped `cause` and falls back to the server's error text, since * a denied publish can surface either as the typed error or inside a request rejection. */ export declare function isPermissionDenied(e: unknown): boolean; /** True ONLY for a denial on a **publish** — the single case that proves the message was never * ACCEPTED or stored. (Not "never reached the server": the server necessarily received enough of * it to reject it. The distinction matters precisely here, because this helper exists to separate * provably-not-stored from possibly-stored, and the looser phrasing overstates the very thing * being measured.) {@link isPermissionDenied} deliberately does not look at the operation: it exists to * separate "denied" from "service down", and that answer is the same either way. The operation * matters enormously to a caller that reports *delivery*, because a JetStream publish is * request/PubAck and the subscription half is the reply inbox — a denial THERE rejects * `js.publish()` while the stream may already hold the message. Verified against a live broker: a * user allowed to publish but denied its `_INBOX` subscription got * `Permissions Violation for Subscription to "_INBOX.….*"` back from `js.publish()`, and an * unrestricted observer then read `messages: 1` off the stream. * * Note what is deliberately NOT accepted: the untyped text fallback above. A permission-shaped * message string carries no operation, so it cannot prove non-delivery, and guessing "publish" * from wording would reintroduce exactly the false certainty this exists to prevent. Anything not * provably a publish denial is unknown, and a caller reporting delivery must fail toward * "I could not confirm" rather than toward "it did not happen" — the costly mistake is telling * someone to re-send a message that was in fact stored. */ export declare function isPublishPermissionDenied(e: unknown): boolean; /** Whether a server list dials over websocket: the FIRST entry's scheme decides (one list, one * transport — a mixed tcp+ws list would race two transports over one identity). */ export declare function wsServers(servers: string): boolean; /** Default probe budget by TRANSPORT. 1s was tuned for the loopback/LAN TCP brokers every local * probe dials; a ws(s) broker is by definition published through an HTTPS edge (CDN tunnel, * reverse proxy), where TLS + upgrade + INFO + the auth round-trip routinely exceeds 1s cold — * measured ~60% spurious "not reachable" against a Cloudflare-fronted broker. Callers passing an * explicit `timeoutMs` are untouched. * * EXPORTED because a caller that judges a probe by WHEN it answered has to compare against the * deadline this function actually handed the probe. The delivery watchdog does exactly that, and * hardcoding 1000 there silently misread every ws broker: honest refusals arrive at 2-5s, past a * budget that was never theirs, and would be classified as this process's starvation rather than * the server's refusal. The budget and the judgment must come from one place. */ export declare function defaultProbeTimeoutMs(servers: string): number; /** Pick the dial function by SCHEME: `ws://`/`wss://` servers go through nats-core's * `wsconnect` (the websocket transport - e.g. a broker published through an HTTPS edge at * `wss://host/path`), everything else through the TCP transport. The websocket dial OWNS its * transport options: the URL scheme already decides TLS there, and the w3c transport refuses a * `tls` block outright ("'tls' is not configurable"), so it is stripped here — at the one point * that knows which transport is dialing — rather than at every caller composing auth options. */ export declare function dialerFor(servers: string): typeof connect; /** Whether a NATS server is *running* at `servers`. With NO creds this is a SILENT plaintext * liveness check ({@link tcpInfoProbe}): it reads the server's pre-auth `INFO` greeting and closes * WITHOUT authenticating, so a live broker (open OR auth — INFO precedes auth) returns true while * emitting no broker auth-error log. It may return false for a TLS-first listener — the credless * probe is plaintext-only. With creds/token/tls supplied it is instead the AUTHORITATIVE identity * check: a real authenticated connect, true on success AND on an auth rejection (a server that * refuses these creds is still up — so the caller surfaces the real auth failure, and `up` won't * start a duplicate on the bound port). Only a genuine connection failure (refused/timeout) is false. */ export declare function isReachable(servers?: string, opts?: AuthOpts & { timeoutMs?: number; }): Promise; /** What a connect attempt told us about the server — the distinction {@link isReachable} flattens. * `auth-required` means a server answered but rejected these creds (so it IS up); `stale-auth` * means the PRESENTED CREDENTIAL ITSELF is dead — expired by its bounded lifetime, either because * the broker said "authentication expired" or because the cred is LOCALLY PROVABLY expired (its own * JWT `exp` is past). The local check is decided without a round-trip, so a slow or failed connect * never downgrades a dead cred to `unreachable`; the repair is `doctor auth` either way, never a * registry prune (the D5 credential-death event). `unreachable` means nothing usable answered and * the cred is not provably dead (refused / timeout / a stale registry entry). */ export type ProbeResult = { ok: true; } | { ok: false; reason: "auth-required"; } | { ok: false; reason: "stale-auth"; } | { ok: false; reason: "unreachable"; }; /** Like {@link isReachable}, but distinguishes "up but won't take these creds" from "nothing there". * `spawn` needs the difference: auth-required → name the trust dir + next step; unreachable → the * mesh is down (prune the stale entry, tell the user to `cotal up`). Pass `creds` to confirm a * specific identity is accepted (`ok`); omit them to probe mere liveness (an auth broker answers * `auth-required`, which still proves it's up). */ export declare function probeConnect(server?: string, opts?: AuthOpts & { timeoutMs?: number; }): Promise; export {}; //# sourceMappingURL=endpoint.d.ts.map