Merge pull request 'A provisioner is told what the mesh derived for each consumer (hq ADR 0201, issue 124)' (#10) from feat/a-provider-declares-what-it-derives into main
mesh/delivery delivered

This commit is contained in:
2026-10-04 01:47:42 +00:00
2 changed files with 87 additions and 10 deletions
+38 -10
View File
@@ -23,6 +23,16 @@ export interface Provision {
readonly password: string;
/** What the consumer contributed, per the interface's spec keys (e.g. `{ name: "umami" }`). */
readonly values: Readonly<Record<string, unknown>>;
/** What this provider's own definition said it derives for this consumer, already derived by
* the mesh (novox/hq ADR 0201).
*
* **The rule is declared, not computed here.** A provider that names the resource it gives each
* consumer — a bucket, a database, a vhost — writes that name in its `serves` block with
* `${consumer:as}` or `${consumer:as:dns}` in it. The mesh fills it for this consumer and hands
* the same filled value to the consumer's own configuration, so the two ends cannot disagree.
* An adapter reads it from here; an adapter that recomputed it would be the second rule this
* removed. Empty for a provider that derives nothing. */
readonly derived: Readonly<Record<string, unknown>>;
/** Where the consumer is, for a provider that must reach back to it. Usually unused. */
readonly at?: string;
/** The consumer node, for logging and lifecycle events. */
@@ -34,7 +44,12 @@ export interface Provision {
* the credential is the mesh's, already delivered to the consumer. */
export interface Adapter {
create(p: Provision): Promise<void>;
remove(p: { readonly as: string }): Promise<void>;
/** Withdraw what `create` made for this consumer. `derived` is what the mesh last derived for
* it, remembered by the harness: the mesh has already dropped the consumer from the
* contributions file by the time this is called, so it cannot be read again, and a provider
* that recomputed it would be keeping the second copy of a rule this removed
* (novox/hq ADR 0201). */
remove(p: { readonly as: string; readonly derived: Readonly<Record<string, unknown>> }): Promise<void>;
/** Optional: whether the backend still holds this consumer's credential exactly as `p` says.
* Asked of every consumer already applied, every `verifyEveryMs`. `false` makes the harness apply
* it again on the same pass, so a backend that lost what it was given (a server restarted
@@ -69,6 +84,7 @@ interface Contribution {
readonly node?: string;
readonly at?: string;
readonly values?: Readonly<Record<string, unknown>>;
readonly derived?: Readonly<Record<string, unknown>>;
}
/**
@@ -88,7 +104,9 @@ export function runProvisioner(resource: string, adapter: Adapter, opts: Provisi
// hour rather than one a minute, and it is said loudly rather than quietly repeated.
const lost = new Map<string, { times: number; nextAt: number }>();
const applied = new Map<string, string>(); // login (`as`) -> hash of what was last applied
// login (`as`) -> what was last applied: the hash that says whether it changed, and the
// derivation to hand back on withdrawal, which by then is no longer in the file.
const applied = new Map<string, { hash: string; derived: Readonly<Record<string, unknown>> }>();
let stopped = false;
async function reconcile(): Promise<void> {
@@ -112,12 +130,14 @@ export function runProvisioner(resource: string, adapter: Adapter, opts: Provisi
console.error(`[provisioner:${resource}] ${g.as}: secret not readable yet (${g.secret}): ${err}`);
continue;
}
const h = hash(g.as, password, g.values ?? {});
const p: Provision = { as: g.as, password, values: g.values ?? {}, at: g.at, consumer: g.node };
const h = hash(g.as, password, g.values ?? {}, g.derived ?? {});
const p: Provision = {
as: g.as, password, values: g.values ?? {}, derived: g.derived ?? {}, at: g.at, consumer: g.node,
};
// Set when the backend reported this consumer lost: how many successful re-applies in a row
// it has now needed, counting this one.
let reapplying: number | undefined;
if (applied.get(g.as) === h) {
if (applied.get(g.as)?.hash === h) {
if (!verifying) continue;
const brake = lost.get(g.as);
if (brake && Date.now() < brake.nextAt) continue;
@@ -148,7 +168,7 @@ export function runProvisioner(resource: string, adapter: Adapter, opts: Provisi
}
try {
await adapter.create(p);
applied.set(g.as, h);
applied.set(g.as, { hash: h, derived: p.derived });
if (reapplying === undefined) {
// Applied for a new login, password or values: whatever was counted before does not carry over.
lost.delete(g.as);
@@ -171,10 +191,10 @@ export function runProvisioner(resource: string, adapter: Adapter, opts: Provisi
// Withdraw every login the provider made that the mesh no longer asks for. The mesh drops a
// consumer from the file when it goes away; that is how a login is withdrawn rather than kept
// working for ever. Only logins this harness created are touched.
for (const as of [...applied.keys()]) {
for (const [as, was] of [...applied.entries()]) {
if (wantByAs.has(as)) continue;
try {
await adapter.remove({ as });
await adapter.remove({ as, derived: was.derived });
applied.delete(as);
lost.delete(as);
} catch (err) {
@@ -247,8 +267,16 @@ function scrub(err: unknown, password: string): string {
return text;
}
function hash(as: string, password: string, values: Readonly<Record<string, unknown>>): string {
return JSON.stringify([as, password, values]);
function hash(
as: string,
password: string,
values: Readonly<Record<string, unknown>>,
derived: Readonly<Record<string, unknown>>,
): string {
// The derivation is in the hash because it can change without the login or the password
// changing: a provider that renames what it derives has given this consumer a different
// resource, and an adapter trusted from memory would never be told (novox/hq ADR 0201).
return JSON.stringify([as, password, values, derived]);
}
function envOrThrow(name: string): string {