import type { ClusterGroupRuntime, DiscoveredGroup, SurfaceHolding } from './group-runtime.js'; import { type GroupStateDocument } from './group-state.js'; import { type ClusterSecretStore } from './group-store.js'; import { type ClusterGroupSettings } from './group-settings.js'; import type { AdmissionFailure } from './group-admissions.js'; /** How long a join or a rejoin waits for the group to answer. */ export declare const ADMISSION_TIMEOUT_MS = 15000; /** Every operation returns one of these. `ok: false` always names the fix. */ export type GroupOperationResult = { readonly ok: true; readonly data: T; } | { readonly ok: false; readonly error: string; readonly fix: string; /** * True when this machine is out of the group and waiting will not change * it, as opposed to the ordinary "nobody answered yet", which resolves on * its own the moment another machine comes up. * * An automatic caller must not swallow this one. It is the difference * between a machine that will rejoin by itself and a machine that needs * the operator, and both look identical from the outside otherwise: a * healthy daemon that is simply never given any work. */ readonly terminal?: boolean; /** * The admission layer's own classification, when this failure came from * one. Lets an automatic caller tell "nobody answered yet" (ordinary, * self-resolving) from "replies arrived that I could not verify" (worth * telling the operator, but not proof of anything). */ readonly failure?: AdmissionFailure; }; /** One machine, as `cluster nodes` reports it. */ export interface NodeReport { readonly nodeId: string; readonly displayName: string; readonly admittedAt: number; readonly lastSeenAt: number; readonly isThisMachine: boolean; } /** One removal, as `cluster nodes` reports it. */ export interface RemovedNodeReport { readonly nodeId: string; readonly removedAt: number; readonly reason: string; } /** What `cluster status` reports. Contains no key material of any kind. */ export interface GroupStatusReport { readonly membership: 'no-group' | 'member' | 'unreadable-key-material'; readonly groupId: string | null; readonly groupName: string | null; readonly nodeId: string; readonly nodeName: string; readonly version: string; readonly memberCount: number; /** Null when the per-surface layer has not supplied it, never a fabricated empty list. */ readonly surfaces: readonly SurfaceHolding[] | null; readonly keyGeneration: number | null; readonly keyGenerationsHeld: number; readonly keyGenerationCap: number; readonly acceptedGenerations: readonly number[]; readonly removedNodeCount: number; readonly rotationHours: number; /** Replicated settings: counts and provenance only, never a value. */ readonly replication: { readonly revision: number; readonly entries: number; readonly secrets: number; readonly tombstones: number; readonly lastAppliedFrom: string | null; readonly lastAppliedAt: number | null; readonly pendingProposals: number; } | null; readonly wire: { readonly sent: number; readonly received: number; readonly droppedOtherGroup: number; readonly droppedBadSignature: number; readonly droppedMalformed: number; readonly droppedOldGeneration: number; readonly droppedNoGroup: number; } | null; /** One plain sentence naming what to do, when there is something to do. */ readonly advice: string | null; } export interface GroupOperationsContext { readonly runtime: ClusterGroupRuntime; readonly secrets: ClusterSecretStore; readonly settings: ClusterGroupSettings; readonly nodeId: string; readonly nodeDisplayName: string; readonly version: string; readonly now: () => number; } /** * Read-only view of the group. * * Deliberately says `no-group` rather than pretending to be a degraded member: * `cluster.enabled` with nothing stored is a real, nameable state, and the * advice line names the two commands that leave it. */ export declare function groupStatus(context: GroupOperationsContext): GroupOperationResult; export interface CreateGroupInput { readonly displayName?: string | undefined; /** * An operator-chosen join key instead of a generated one. * * Run through scrypt with the group's salt, because a phrase a human picked * has far less entropy than the generated key and the derivation is the only * thing standing between it and an offline guess. */ readonly passphrase?: string | undefined; } export interface CreateGroupResult { readonly groupId: string; readonly groupName: string; readonly joinKey: string; readonly generatedKey: boolean; } /** Create a group with this machine as its first member. */ export declare function createGroup(context: GroupOperationsContext, input: CreateGroupInput): Promise>; export interface JoinGroupInput { readonly groupId: string; readonly joinKey: string; readonly timeoutMs?: number | undefined; } export interface JoinGroupResult { readonly groupId: string; readonly groupName: string; readonly memberCount: number; } /** * Join an existing group. * * The group id names WHICH group, and the join key proves the right to be in * it. Both are needed: the id alone is public (it is in every beacon) and the * key alone would not say which group it was for. */ export declare function joinGroup(context: GroupOperationsContext, input: JoinGroupInput): Promise>; export interface JoinKeyResult { readonly groupId: string; readonly groupName: string; readonly joinKey: string; } /** * Show the join key. * * On demand and repeatable, from any member, not a one-shot reveal at create * time. A key you can only see once is a key that gets written on a sticky * note, and the operator adding a fourth machine six months later has every * right to just ask for it again. */ export declare function joinKeyForGroup(context: GroupOperationsContext): GroupOperationResult; export interface NodesResult { readonly groupId: string; readonly groupName: string; readonly members: readonly NodeReport[]; readonly removed: readonly RemovedNodeReport[]; } /** The roster. */ export declare function groupNodes(context: GroupOperationsContext): GroupOperationResult; export interface ForgetNodeResult { readonly nodeId: string; readonly displayName: string; readonly memberCount: number; readonly keyGeneration: number; } /** * Remove a machine from the group. * * Two things happen, and both are required. A TOMBSTONE is written, at a * generation above any add, so a peer that was partitioned during the removal * cannot bring the machine back when it reconnects. And the group key is * rotated IMMEDIATELY with no acceptance window, so the key still sitting on * that machine's disk stops being accepted by everyone who adopts the rotation. * * Members that have not yet adopted keep accepting the old key for the moments * it takes the announcement to reach them. That gap is inherent to a network * and is not papered over here: it closes as the rotation propagates, and the * tombstone, which is what governs re-entry, is effective immediately. */ export declare function forgetNode(context: GroupOperationsContext, nodeId: string): Promise>; export interface RotateKeyResult { readonly groupId: string; readonly keyGeneration: number; readonly memberCount: number; readonly immediate: boolean; readonly acceptedGenerations: readonly number[]; } /** * Replace the group key now, because the operator said so. * * Two shapes, and the difference is what happens to the key being retired: * * default, the outgoing generation stays accepted for the usual few minutes, * so machines that have not yet picked up the new key keep being heard and * nothing is interrupted. This is the right answer for routine hygiene. * * --now, the outgoing generation stops being accepted immediately, and the * group's SIGNING key is replaced as well. This is the answer when a key is * believed to have leaked: whatever was taken stops working the moment each * machine adopts the replacement, and the cost is that a machine which is * asleep right now has to ask to come back when it wakes. */ export declare function rotateGroupKey(context: GroupOperationsContext, input?: { readonly immediate?: boolean | undefined; }): Promise>; export interface LeaveGroupResult { readonly groupId: string; readonly groupName: string; } /** * Leave the group, from this machine. * * This forgets the group HERE. It does not remove this machine from the other * members' rosters, they will simply stop hearing from it, and the operator * can tidy up with `cluster forget` on any of them. Making one machine's * decision to leave silently rewrite everyone else's membership would be a * strictly worse default. */ export declare function leaveGroup(context: GroupOperationsContext): Promise>; export interface RenameGroupResult { readonly groupId: string; readonly groupName: string; } /** * Rename the group. * * The name is replicated group state, so renaming on any machine renames it * everywhere. It is also what the discovery beacon advertises, which means it * is visible to anything on the network, the setting's description says so * plainly, and the default is neutral for that reason. */ export declare function renameGroupTo(context: GroupOperationsContext, displayName: string): Promise>; /** Groups seen advertising themselves on this network that this machine is not in. */ export declare function groupsOnTheNetwork(context: GroupOperationsContext): GroupOperationResult; /** * Ask the group to take this machine back. * * Called on start by a machine that already has key material, and available by * hand when a return needs a nudge. Succeeds only if the node id is still on * the roster, see `decideAdmission`. */ export declare function rejoinGroup(context: GroupOperationsContext): Promise>; /** True when `state` still lists this machine. Used by the start-up rejoin decision. */ export declare function stillOnRoster(state: GroupStateDocument | null, nodeId: string): boolean; /** The verb surface the daemon serves on `/api/cluster/*`. One line per operation. */ export interface ClusterGroupVerbSurface { status(): GroupOperationResult; create(input: { name?: string; passphrase?: string; }): Promise>; join(input: { groupId: string; joinKey: string; }): Promise>; key(): GroupOperationResult; nodes(): GroupOperationResult; groups(): GroupOperationResult; forget(nodeId: string): Promise>; rotate(input: { immediate?: boolean; }): Promise>; leave(): Promise>; rename(name: string): Promise>; } /** * Bind the operations to one context. * * The daemon route module, the TUI command and any web UI all take this object * and nothing else, which is what keeps them renderings rather than * reimplementations. */ export declare function createClusterGroupVerbs(context: GroupOperationsContext): ClusterGroupVerbSurface; //# sourceMappingURL=group-operations.d.ts.map