/** * config-replication.ts, the master's settings, on every machine that might * have to serve them. * * Leader election decides which machine reads an inbox. That is only half an * answer: a machine that wins a surface and does not hold the surface's * configuration, or its credential, cannot serve it, and the handover is * theatre. This closes that. * * ── shape ────────────────────────────────────────────────────────────────── * * The MASTER is the source of truth. It issues revisions, and it is the only * node that writes one. * * A machine that joins pulls a SNAPSHOT. From then on it receives DELTAS. * * An edit made on a machine that is not the master is FORWARDED to the master * as a proposal, and comes back as a delta like any other change. A standby * never issues a revision of its own, so there is exactly one writer and no * merge to argue about in the normal case. * * Everything rides the group transport and the group keyring, the same signed, * group-scoped channel every other cluster datagram uses. There is no second * channel and no second trust boundary. * * ── secrets ──────────────────────────────────────────────────────────────── * * A credential never travels in the clear and never travels as another node's * ciphertext. It is sealed to each recipient's agreement key for the journey, * and the receiver hands the plaintext to its OWN secret store, which encrypts * it under its OWN keyfile at rest. Nothing here logs a value, and nothing here * puts one in `/status`, a replicated secret is reported as a path and a * revision, never as a value. */ import { type NodeKeyMaterial } from './group-crypto.js'; import { type ConfigReplicaDocument } from './config-replica.js'; import type { GroupStateDocument } from './group-state.js'; import type { ClusterEnvelope } from './protocol-envelope.js'; import type { ClusterLogger } from './types.js'; /** Message types this layer speaks. All group-key signed, like everything else. */ export declare const CONFIG_MESSAGE_TYPES: { readonly snapshot: "CONFIG_SNAPSHOT"; readonly delta: "CONFIG_DELTA"; readonly propose: "CONFIG_PROPOSE"; readonly request: "CONFIG_REQUEST"; }; /** The narrow slice of config this layer touches. */ export interface ReplicatedConfigStore { get(path: string): unknown; set(path: string, value: unknown): void; /** * Return a setting to its default. Optional: a store without it simply keeps * the local value after a group-wide delete, and the tombstone still stops * that value from being replicated back out. */ reset?(path: string): void; } /** The narrow slice of the secret store this layer touches. */ export interface ReplicatedSecretStore { get(key: string): Promise; set(key: string, value: string): Promise; delete(key: string): Promise; } /** What the replication service borrows from the group runtime. */ export interface ConfigReplicationHost { readonly nodeId: string; readonly logger: ClusterLogger; now(): number; /** True when this machine is the one that issues revisions. */ isMaster(): boolean; groupState(): GroupStateDocument | null; nodeKeys(): NodeKeyMaterial | null; config(): ReplicatedConfigStore | null; secrets(): ReplicatedSecretStore | null; /** Send an already-built group message. */ send(type: string, body: Record): Promise; /** Persist the replica document alongside the roster. */ persist(document: ConfigReplicaDocument): void; } /** What `cluster status` reports about replication. Never a value. */ export interface ConfigReplicationStatus { readonly revision: number; readonly entries: number; readonly secrets: number; readonly tombstones: number; readonly lastAppliedFrom: string | null; readonly lastAppliedAt: number | null; readonly pendingProposals: number; } export declare class ConfigReplicationService { private readonly host; private document; private lastAppliedFrom; private lastAppliedAt; private pendingProposals; constructor(host: ConfigReplicationHost, groupId: string); /** Adopt a document read back from disk, or from a group this node just joined. */ adopt(document: ConfigReplicaDocument): void; get replica(): ConfigReplicaDocument; status(): ConfigReplicationStatus; /** * Fold local changes into the replica, and broadcast them. * * Runs on the master only. It is a reconcile rather than a change hook * because the config manager has no change event: comparing the replica * against what is actually on disk catches an edit made through any path at * all, including one made while this process was not running. */ reconcileLocalConfig(): Promise; /** * Record an operator edit made on this machine, wherever it lands. * * On the master this writes the path EXPLICITLY rather than going through the * reconcile, because the reconcile deliberately skips anything the group has * deleted. Setting a deleted path again is the operator undoing the deletion, * and this is the only path that can. */ announceLocalChange(path: string): Promise; /** Replicate a secret the operator set on this machine. */ announceLocalSecret(configPath: string): Promise; /** Delete a setting across the group. */ announceLocalDelete(path: string, secret?: boolean): Promise; private broadcastDelta; /** * Record a secret in the replica and send it, sealed per member. * * The document holds only the fact that the path has a secret at a revision. * The value itself never lands in the document, so it is never written to the * replica file and never appears in a snapshot that is not individually * sealed. */ private publishSecret; /** Seal `value` to every current member and send it. */ private sendSealed; /** Ask the group for everything. Sent by a machine that has just joined. */ requestSnapshot(): Promise; /** Route a config-replication datagram that already verified under the group key. */ handle(envelope: ClusterEnvelope): Promise; private onRequest; private onSnapshot; private onDelta; /** * A proposal from a machine that is not the master. * * Only the master acts on one, and it re-derives the value from its own * policy check before issuing a revision, a peer does not get to name a path * this machine would not have replicated itself. */ private onPropose; private openSealed; /** * Take a credential handed over by the group. * * The plaintext goes straight into this machine's OWN secret store, which * encrypts it under this machine's OWN keyfile. Another node's ciphertext is * never written verbatim, it could not be read back here anyway, and storing * it would quietly make the group's secrets undecryptable after a keyfile * rotation on one machine. */ private applySealedSecret; /** Apply a merged document to this machine's config, then persist it. */ private applyDocument; /** * Every peer-sourced change is logged with the node it came from. * * A value is never logged, whether or not it is a secret: a replicated * surface token and a replicated surface id are equally nobody's business in * a log file, and the path plus the origin is what an operator actually needs * to answer "why did this machine change". */ private logApplied; /** Bound the document. Called from the group runtime's housekeeping pass. */ sweep(now: number): void; /** * True while this machine still has nothing and is not the one issuing * revisions, the condition under which it should keep asking. * * Asking again is how a snapshot that was missed because the master was busy, * or was sent while this machine was still starting, is recovered. It costs * one small datagram per housekeeping pass and stops the moment anything * arrives. */ get needsSnapshot(): boolean; /** * Anti-entropy: another member says it is at `revision`. * * A machine that was partitioned when a change went out never receives that * delta, and nothing else would ever tell it. Every roster gossip carries the * sender's revision, so a machine that is behind notices on the next one and * asks for a snapshot, which carries the DELETIONS as well as the values, * and is therefore what stops a healed partition from running settings the * operator removed while it was away. */ notePeerRevision(revision: number): Promise; } //# sourceMappingURL=config-replication.d.ts.map