/** * group-runtime.ts, the live group: beacons, gossip, admissions, rotation. * * One instance per daemon. It owns the datagram socket, holds the keyring every * outbound datagram is signed with, and is the only thing in the system that * writes group key material. * * The operator-facing operations (create, join, key, nodes, forget, leave) live * next door in group-operations.ts and go through this object, so the CLI, the * TUI and the web UI are three renderings of one implementation, never three * implementations. * * ── enabling clustering without a group ──────────────────────────────────── * * `cluster.enabled: true` with no group stored is NOT a state this runtime * pretends to run in. It starts in `no-group`: it listens, so `cluster join` * can show the operator what is on the network, and it sends nothing, signs * nothing and gates nothing. `cluster status` says so in one line, and names * the two commands that fix it. Half-starting, joining the multicast group and * broadcasting unsigned, is the outcome this explicitly avoids. */ import { type AdmissionOutcome } from './group-admissions.js'; import { type ClusterGroupSettings } from './group-settings.js'; import { type GroupStateDocument } from './group-state.js'; import { type ClusterSecretStore, type GroupKeyMaterial, type GroupKeyRecord } from './group-store.js'; import { GroupWireRouter } from './group-transport.js'; import { type ConfigReplicationStatus, type ReplicatedConfigStore } from './config-replication.js'; import { type ConfigReplicaDocument } from './config-replica.js'; import { type ClusterKeyring } from './protocol-envelope.js'; import type { ClusterClock, ClusterLogger, ClusterSurfaceHolding, ClusterTransport } from './types.js'; /** A group seen on the network that this node is not a member of. */ export interface DiscoveredGroup { readonly groupId: string; readonly displayName: string; readonly nodeCount: number; readonly version: string; readonly lastSeenAt: number; } /** * One surface this node consumes, and why. * * The per-surface election establishes this, so the shape is defined alongside * the rest of the election's types and re-exported here under the name the * group layer has always used for it. */ export type SurfaceHolding = ClusterSurfaceHolding; export interface ClusterGroupRuntimeOptions { readonly settings: ClusterGroupSettings; readonly transport: ClusterTransport; readonly secrets: ClusterSecretStore; readonly stateDirectory: string; readonly nodeId: string; /** Label for THIS machine in the roster. Never defaulted to a hostname. */ readonly nodeDisplayName: string; readonly version: string; readonly clock: ClusterClock; readonly logger: ClusterLogger; /** * Which surfaces this node currently consumes, and why. * * Supplied by the per-surface election layer, which owns that fact. Absent * means `cluster status` reports that the information is not available rather * than reporting an empty list as though the node held nothing. */ readonly surfaceHoldings?: (() => readonly SurfaceHolding[]) | undefined; /** * Which machine issues config revisions. * * Supplied by the composition root as the leader election's own answer, so * there is one notion of "master" in the process rather than two that can * disagree. Absent means this machine never issues one, which is the safe * reading for an embedder with no election. */ readonly isMaster?: (() => boolean) | undefined; /** The config this machine reads and writes. Absent disables replication. */ readonly config?: ReplicatedConfigStore | undefined; } /** Membership as `cluster status` reports it. */ export type GroupMembershipState = 'no-group' | 'member' | 'unreadable-key-material'; export declare class ClusterGroupRuntime { private readonly options; private material; private state; private readonly router; private keyringInstance; private readonly admissions; private readonly discovered; private cancelHousekeeping; private cancelBeacon; private started; private lastRotationCheckAt; private unreadableMaterial; private housekeeping; private replication; constructor(options: ClusterGroupRuntimeOptions); start(): Promise; stop(): Promise; /** Ask the group to admit this machine with a join key. */ requestJoin(input: { readonly groupId: string; readonly joinKey: string; readonly joinSalt: string; readonly timeoutMs: number; }): Promise; /** Ask the group to take this machine back and re-key it. */ requestRejoin(timeoutMs: number): Promise; /** The transport to hand `ClusterCoordinator`. Election traffic rides the group key. */ electionTransport(): ClusterTransport; /** * The keyring every datagram is signed and checked against. * * With no membership this is a keyring for the empty group: it holds nothing, * accepts nothing and signs nothing, so a node with no group cannot emit a * datagram anybody would act on even if something tried to make it. */ keyring(): ClusterKeyring; get membership(): GroupMembershipState; get groupState(): GroupStateDocument | null; get keyMaterial(): GroupKeyMaterial | null; get wireCounters(): GroupWireRouter['counters']; groupsOnTheNetwork(): readonly DiscoveredGroup[]; surfaceHoldings(): readonly SurfaceHolding[] | null; /** Re-read key material and roster from their stores. */ reloadMaterial(): Promise; private hasStoredMaterial; private requireMaterial; /** Install new material and its roster, persisting both. */ adoptMembership(material: GroupKeyMaterial, state: GroupStateDocument): Promise; /** * Drop this machine's membership. * * The key material is deleted by the caller (it owns the secrets store); this * clears the in-memory half so the very next datagram is signed with nothing * and accepted by nobody, rather than continuing to sign with a group this * machine has just left. */ forgetMembership(): Promise; /** Persist a changed roster and gossip it. */ commitState(state: GroupStateDocument, gossip?: boolean): Promise; /** The replicated settings document, for `cluster status`. */ replicationStatus(): ConfigReplicationStatus | null; /** The replicated settings document, for tests and for a snapshot on demand. */ replicaDocument(): ConfigReplicaDocument | null; /** Delete a setting across the group. */ announceConfigDelete(path: string, secret?: boolean): Promise; /** Ask the group for the settings this machine should be running. */ requestConfigSnapshot(): Promise; /** Tell the group about a setting an operator just changed on this machine. */ announceConfigChange(path: string): Promise; /** Tell the group about a credential an operator just set on this machine. */ announceSecretChange(configPath: string): Promise; /** * The secret store the replication layer may touch. * * Narrowed to the group's own store so a replicated credential is written * through this machine's SecretsManager, encrypted under this machine's own * keyfile, rather than stored as somebody else's ciphertext. */ private replicatedSecrets; private persistReplica; /** Persist changed key material. */ commitMaterial(material: GroupKeyMaterial): Promise; private sendGroupMessage; /** * Advertise this group. * * Carries the group id, the group's name, how many machines are in it, and * this node's build, and NOTHING else. No surfaces, no configuration, no * hostname, no username, no key. It is readable by anything on the network, * which is exactly why the group name defaults to something neutral and its * setting says plainly that it is visible. * * A joining machine needs the scrypt salt too, and does NOT get it from here: * it derives it from the group id, which the beacon already carries. One * fewer thing on the wire for the same result. */ private sendBeacon; private sendRoster; private scheduleBeacon; private scheduleHousekeeping; /** * Sweep bounded state, expire a pending admission, gossip, and rotate when * this node is the one that should. * * Everything periodic is here rather than on its own timer so there is one * place to look when something is or is not happening on a schedule. */ runHousekeeping(): Promise; private housekeepingPass; /** * Which member mints the next rotation. * * The smallest node id among members heard from recently. A pure function of * replicated state, so every member reaches the same answer without any * negotiation, and if that machine is off the next one takes over by itself. * * If two of them mint at once, which a partition can cause, the tie is * broken by `preferredKeyRecord`, not by whoever shouted first. */ private isRotationMinter; private rotateIfDue; /** * Replace the group key. * * The announcement is signed with the OUTGOING generation, because that is * the only key the recipients currently accept, announcing under the new key * would be a message nobody could read. Only after it is on the wire does * this node move itself forward. * * `revocation` differs in exactly one way, and it is the important one: the * acceptance window is not opened, on this node or on any node that adopts * the announcement. The machine that was just removed still holds the old * key, and the whole point of rotating on removal is that holding it stops * being enough. */ rotate(cause: 'scheduled' | 'revocation', reason: string): Promise; private onGroupMessage; private onBeacon; private onRoster; /** * Adopt a rotation announced by another member. * * The envelope already verified under a group key this node holds, so the * sender is a member. What is still checked here: that a wrap addressed to * THIS node is present and opens. A rotation with no wrap for this node is a * rotation this node was not included in, which is what a removed machine * sees, and it is right that it cannot follow. */ private onRekey; /** * Note a group seen on the network. * * The salt is kept alongside because a machine cannot even attempt a join * until it can derive the verifier, and the salt is public by construction. * Nothing here is authenticated and nothing here causes this node to act, it * populates a list the operator chooses from and that is all. */ private recordDiscoveredGroup; /** Every key generation this node holds, for the status report. */ heldGenerations(): readonly number[]; /** The keys this node would hand a joiner, swept to the current bounds. */ grantableKeys(now: number): readonly GroupKeyRecord[]; } //# sourceMappingURL=group-runtime.d.ts.map