diff --git a/modules/mesh-vault/client.ts b/modules/mesh-vault/client.ts deleted file mode 100644 index c322db20..00000000 --- a/modules/mesh-vault/client.ts +++ /dev/null @@ -1,172 +0,0 @@ -// mesh-vault's ledger — vault's own code, living in the module (novox/hq ADR 0039). The provisioner and -// the tools both import it, and nothing outside vault does. -// -// **The vault holds no value.** A `secret` is an ordinary pair credential: the controller mints it, -// seals it to the consumer's node and to this one, and the host unseals this node's copy into the -// file the contribution names (ADR 0048). That file is already on this machine, readable by nothing -// but the vault's runtime, and it is the only copy the vault ever sees. Writing a second copy — -// plain, or sealed to a key the vault keeps — would put back exactly the single place that can open -// everything, which is what sealing to the machine was built to remove (ADR 0085's open question is -// how to recover WITHOUT that; the answer is not "keep one anyway"). -// -// So what the vault keeps is what makes a secret *owned* rather than merely delivered: who holds -// one, since when, its fingerprint, and every time it changed. Enough to say "this holder's value is -// the one the mesh last delivered" and "it has been rotated twice, last on Tuesday" — and never -// enough to say what it is. The fingerprint is the only thing a tool may take or return, which is -// the rule the source mesh's secret tools were built on: the secret is never an argument. - -import { createHash } from "node:crypto"; -import { mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; - -/** One holder of a secret this vault provides — everything the vault knows, and no value. */ -export interface Held { - /** The login the mesh derived for the consumer — `-`, so it names the holder. */ - readonly as: string; - /** The consumer's node. */ - readonly consumer: string; - /** sha256 of the value the mesh last delivered, `sha256:`. Compared, never inverted. */ - readonly fingerprint: string; - /** Length of the value, so a holder can tell a truncated file from a wrong one. */ - readonly length: number; - /** When this holder was first granted a secret. */ - readonly since: string; - /** When the value last changed — equal to `since` until the first rotation. */ - readonly changed: string; - /** How many times the value has changed since `since`. */ - readonly rotations: number; - /** Every earlier fingerprint, oldest first: the audit trail a rotation leaves. */ - readonly history: readonly { readonly fingerprint: string; readonly until: string }[]; -} - -/** What recording a delivery found: a new holder, a changed value, or nothing new. */ -export type Outcome = "granted" | "rotated" | "unchanged"; - -/** sha256 of a value, as `sha256:`. The one thing about a secret that may be spoken. */ -export function fingerprint(value: string): string { - return "sha256:" + createHash("sha256").update(value, "utf8").digest("hex"); -} - -export class Ledger { - private readonly dir: string; - - constructor(dir: string) { - this.dir = dir; - mkdirSync(dir, { recursive: true, mode: 0o700 }); - } - - /** Build from the module's resolved environment: $MESH_VAULT_LEDGER is where holders are kept. */ - static fromEnv(env: NodeJS.ProcessEnv = process.env): Ledger { - const dir = env.MESH_VAULT_LEDGER; - if (!dir) { - throw new Error("MESH_VAULT_LEDGER is not set — the vault has nowhere to keep its ledger"); - } - return new Ledger(dir); - } - - /** - * Record that the mesh delivered `value` for `as`. Idempotent: the same value again changes - * nothing, a different value is a rotation and is remembered as one. The value is fingerprinted - * here and goes no further. - */ - record(as: string, consumer: string, value: string, now = new Date()): { held: Held; outcome: Outcome } { - const fp = fingerprint(value); - const at = now.toISOString(); - const before = this.get(as); - if (!before) { - const held: Held = { - as, consumer, fingerprint: fp, length: value.length, - since: at, changed: at, rotations: 0, history: [], - }; - this.write(held); - return { held, outcome: "granted" }; - } - if (before.fingerprint === fp && before.length === value.length) { - return { held: before, outcome: "unchanged" }; - } - const held: Held = { - ...before, consumer, fingerprint: fp, length: value.length, changed: at, - rotations: before.rotations + 1, - history: [...before.history, { fingerprint: before.fingerprint, until: at }], - }; - this.write(held); - return { held, outcome: "rotated" }; - } - - /** Forget a holder the mesh withdrew. Returns whether there was one to forget. */ - withdraw(as: string): boolean { - try { - unlinkSync(this.pathOf(as)); - return true; - } catch { - return false; - } - } - - get(as: string): Held | undefined { - try { - return JSON.parse(readFileSync(this.pathOf(as), "utf8")) as Held; - } catch { - return undefined; - } - } - - /** Every holder, by login. */ - list(): Held[] { - let names: string[]; - try { - names = readdirSync(this.dir); - } catch { - return []; - } - return names - .filter((n) => n.endsWith(".json")) - .map((n) => this.get(n.slice(0, -".json".length))) - .filter((h): h is Held => h !== undefined) - .sort((a, b) => a.as.localeCompare(b.as)); - } - - private pathOf(as: string): string { - if (!/^[a-z0-9][a-z0-9_.-]*$/.test(as)) { - throw new Error(`a login is a name, not a path: ${JSON.stringify(as)}`); - } - return join(this.dir, `${as}.json`); - } - - /** Written whole and renamed into place, so a reader never sees half a record. */ - private write(held: Held): void { - const final = this.pathOf(held.as); - const tmp = `${final}.${process.pid}.tmp`; - writeFileSync(tmp, JSON.stringify(held, null, 2) + "\n", { mode: 0o600 }); - renameSync(tmp, final); - } -} - -/** One entry of the mesh's contributions file, as the vault reads it for its tools. */ -export interface Contribution { - readonly from?: string; - readonly node?: string; - readonly as: string; - readonly secret: string; -} - -/** The consumers the mesh currently asks this vault to serve — the `receives` file, read plainly. */ -export function contributions(receives: string): Contribution[] { - let doc: { given?: Contribution[] }; - try { - doc = JSON.parse(readFileSync(receives, "utf8")) as { given?: Contribution[] }; - } catch { - return []; - } - return (doc.given ?? []).filter((g) => g.as && g.secret); -} - -/** Fingerprint of the value the host currently holds for one contribution, or why it could not. */ -export function deliveredFingerprint(c: Contribution): { fingerprint: string; length: number } | { error: string } { - try { - const value = readFileSync(c.secret, "utf8").replace(/\n$/, ""); - return { fingerprint: fingerprint(value), length: value.length }; - } catch (err) { - return { error: `the delivered secret is not readable: ${err}` }; - } -} diff --git a/modules/mesh-vault/cmd/mesh-vault/askers.go b/modules/mesh-vault/cmd/mesh-vault/askers.go new file mode 100644 index 00000000..44c367c7 --- /dev/null +++ b/modules/mesh-vault/cmd/mesh-vault/askers.go @@ -0,0 +1,37 @@ +package main + +// Who may ask `get` for which secret: the seam the real rule fills (novox/hq ADR 0349 §11). +// +// The rule is: a node's provisioner client, running as a service under a bus account of its own, for a +// secret that node's declaration names; or a module, for a secret its manifest declares, only once its +// code runs under a bus account of its own (issue 569). Neither exists yet — the client and its account are +// design 57's P3, the vault's answer to a node V4 — so the vault that runs allows nobody (NoAsker), and the +// tests set a stand-in list (AllowList). V4 and P3 put the real rule behind this interface. + +// Askers decides whether caller (the bus user the runtime proved) may have the secret name, and if not, +// why not. +type Askers interface { + May(caller, name string) (bool, string) +} + +// NoAsker allows nobody: the vault's rule until who may ask is decided by a declaration and an account. +type NoAsker struct{} + +// May refuses every caller. +func (NoAsker) May(string, string) (bool, string) { + return false, "the vault hands a secret to no asker yet: a node's provisioner client under its own account, " + + "for what its declaration names, is the first it will answer (novox/hq ADR 0349 §11, issue 647)" +} + +// AllowList is a stand-in rule: each caller, and the names it may have. +type AllowList map[string][]string + +// May allows a caller only the names listed for it. +func (l AllowList) May(caller, name string) (bool, string) { + for _, n := range l[caller] { + if n == name { + return true, "" + } + } + return false, "not on the list of who may ask for it" +} diff --git a/modules/mesh-vault/go.mod b/modules/mesh-vault/go.mod new file mode 100644 index 00000000..454acc9d --- /dev/null +++ b/modules/mesh-vault/go.mod @@ -0,0 +1,10 @@ +module mesh-vault + +go 1.25.0 + +require ( + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013235212-c8b07242a009 + golang.org/x/crypto v0.50.0 +) + +require golang.org/x/sys v0.43.0 // indirect diff --git a/modules/mesh-vault/go.sum b/modules/mesh-vault/go.sum new file mode 100644 index 00000000..afe914a8 --- /dev/null +++ b/modules/mesh-vault/go.sum @@ -0,0 +1,6 @@ +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013235212-c8b07242a009 h1:I9rm8HhYRJ0+mzQQz+VAH0kgv2YJWkvctEc67Yd0PX8= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013235212-c8b07242a009/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/modules/mesh-vault/index.ts b/modules/mesh-vault/index.ts deleted file mode 100644 index 07aee59f..00000000 --- a/modules/mesh-vault/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -// mesh-vault's events entrypoint, loaded by the per-node tool host (the provisioner runs in the same -// process — ADR 0052). The lifecycle events are EMITTED from the provisioner, where custody -// actually changes (novox/hq ADR 0041/0042): -// mesh-vault.provisioned — a consumer was granted a secret -// mesh-vault.rotated — that consumer's value changed (`rotate secret`) -// mesh-vault.deprovisioned — the consumer went away and its secret was withdrawn -// Here the vault reacts to them, keeping a lightweight audit line of who holds what and when it -// moved — the audit an owner of secrets is best placed to log. Fingerprints, never values. - -import { on } from "@novox/mesh-sdk/events"; - -interface SecretEvent { - as: string; - consumer?: string; - fingerprint?: string; - rotations?: number; -} - -await on("provisioned", async (e) => { - console.log(`[mesh-vault] secret provisioned for ${e.body?.as} on ${e.body?.consumer} (${e.body?.fingerprint})`); -}); - -await on("rotated", async (e) => { - console.log(`[mesh-vault] secret rotated for ${e.body?.as} — rotation ${e.body?.rotations} (${e.body?.fingerprint})`); -}); - -await on("deprovisioned", async (e) => { - console.log(`[mesh-vault] secret withdrawn from ${e.body?.as}`); -}); - -console.log("[mesh-vault] auditing secret lifecycle events"); diff --git a/modules/mesh-vault/module.json b/modules/mesh-vault/module.json index 87b6e6d7..3f9eb97c 100644 --- a/modules/mesh-vault/module.json +++ b/modules/mesh-vault/module.json @@ -10,18 +10,11 @@ } } ], - "capabilities": [ - "container-runtime" - ], "emits": [ "provisioned", "rotated", - "deprovisioned" - ], - "consumes": [ - "mesh-vault.provisioned", - "mesh-vault.rotated", - "mesh-vault.deprovisioned" + "deprovisioned", + "secret-changed" ], "receives": { "secret": "${dir:grants}/mesh.json" @@ -49,6 +42,12 @@ "path": "${dir:root}", "class": "valuable", "why": "the vault's root material" + }, + { + "id": "store", + "path": "${dir:store}", + "class": "valuable", + "why": "every secret the vault holds, sealed to its own key and to the operator key" } ], "consumers": { @@ -80,6 +79,11 @@ "id": "root", "type": "directory", "mode": "0700" + }, + { + "id": "store", + "type": "directory", + "mode": "0700" } ], "build": { @@ -87,29 +91,35 @@ { "name": "code", "kind": "bundle", - "language": "typescript", - "entrypoints": [ - "index.js", - "tools/index.js", - "provisioner/index.js" - ], + "language": "go", + "system": "arch", + "from": "cmd/mesh-vault", + "binary": "mesh-vault", "loads": [ - "index.js", - "tools/index.js", - "provisioner/index.js" + "mesh-vault" ], "env": { "MESH_RECEIVES": "${dir:grants}/mesh.json", "MESH_VAULT_LEDGER": "${dir:ledger}", - "MESH_VAULT_ROOT": "${dir:root}" + "MESH_VAULT_ROOT": "${dir:root}", + "MESH_VAULT_STORE": "${dir:store}", + "MESH_VAULT_STATE": "${dir:state}" } } ] }, + "tools": [ + "secret_export", + "secret_holders", + "secret_verify" + ], "claims": [ { "name": "mesh-vault", - "scope": "mesh" + "scope": "mesh", + "serves": [ + "get" + ] } ] } diff --git a/modules/mesh-vault/package.json b/modules/mesh-vault/package.json deleted file mode 100644 index 585de85b..00000000 --- a/modules/mesh-vault/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@novox/module-mesh-vault", - "version": "0.1.0", - "description": "mesh-vault — provides the mesh `secret` interface: a module's own secret as an ordinary pair credential, held, audited and rotated like any other (novox/hq ADR 0085). Its ledger, provisioner, tools and events live here (ADR 0039).", - "type": "module", - "private": true, - "scripts": { - "test": "node --test --experimental-strip-types 'test/*.test.ts'" - }, - "dependencies": { - "@novox/mesh-sdk": "^0.1.0" - }, - "devDependencies": { - "@types/node": "^22.0.0", - "typescript": "^5.6.0" - } -} diff --git a/modules/mesh-vault/provisioner/index.ts b/modules/mesh-vault/provisioner/index.ts deleted file mode 100644 index 37808a1e..00000000 --- a/modules/mesh-vault/provisioner/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -// mesh-vault's provisioner — the adapter that makes vault a provider of the mesh `secret` interface. The -// reconcile loop, the contributions file, and reading the mesh's minted value are the sdk harness's; -// this writes only the per-service half (novox/hq ADR 0039/0040/0048) — and for a vault that half is -// taking custody, not creating anything. -// -// The `secret` interface (ADR 0085, design 24): a consumer requires a value for its own use — the -// password of a store it runs privately, an internal token — and reads it from the file the mesh -// writes on its machine. There is no server to create a login on. **The value is the pair -// credential itself**: the controller minted it, sealed it to both nodes, and delivered each its -// copy. What makes it *owned* is this: the vault records who holds it and its fingerprint, notices -// when `rotate secret` delivers a different one, and says so on the mesh. Rotation is not new -// machinery — it is the machinery that already moves a database password, pointed at a secret the -// vault provides (design 13). - -import { runProvisioner, type Provision } from "@novox/mesh-sdk/provisioner"; -import { emit } from "@novox/mesh-sdk/events"; -import { Ledger } from "../client.js"; - -const ledger = Ledger.fromEnv(); - -/** Emit a lifecycle event without letting a broker hiccup fail the custody itself. */ -async function announce(type: string, body: Record): Promise { - try { - await emit(type, body); - } catch (err) { - console.error(`[provisioner:secret] emit ${type} failed: ${err}`); - } -} - -runProvisioner("secret", { - async create(p: Provision): Promise { - const { held, outcome } = ledger.record(p.as, p.consumer ?? "", p.password); - if (outcome === "unchanged") return; // the harness re-runs create on restart; nothing happened - console.log(`[mesh-vault] ${outcome}: ${held.as} (${held.fingerprint.slice(0, 19)}…, rotations ${held.rotations})`); - await announce(`module.mesh-vault.secret.${outcome === "granted" ? "provisioned" : "rotated"}`, { - consumer: held.consumer, - as: held.as, - fingerprint: held.fingerprint, - rotations: held.rotations, - }); - }, - - async remove(p: { as: string }): Promise { - if (!ledger.withdraw(p.as)) return; - console.log(`[mesh-vault] withdrawn: ${p.as}`); - await announce("deprovisioned", { as: p.as }); - }, -}); diff --git a/modules/mesh-vault/test/ledger.test.ts b/modules/mesh-vault/test/ledger.test.ts deleted file mode 100644 index e2ca648f..00000000 --- a/modules/mesh-vault/test/ledger.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import { mkdtempSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -import { Ledger, fingerprint, contributions, deliveredFingerprint } from "../client.ts"; - -function fresh(): Ledger { - return new Ledger(mkdtempSync(join(tmpdir(), "vault-ledger-"))); -} - -test("a first delivery is a grant, the same value again is nothing, a new value is a rotation", () => { - const ledger = fresh(); - const t0 = new Date("2026-09-20T10:00:00Z"); - const t1 = new Date("2026-09-21T10:00:00Z"); - - const granted = ledger.record("anchor-redis", "anchor", "first-value", t0); - assert.equal(granted.outcome, "granted"); - assert.equal(granted.held.rotations, 0); - assert.equal(granted.held.since, t0.toISOString()); - assert.equal(granted.held.fingerprint, fingerprint("first-value")); - - assert.equal(ledger.record("anchor-redis", "anchor", "first-value", t1).outcome, "unchanged"); - assert.equal(ledger.get("anchor-redis")!.rotations, 0, "an unchanged delivery counted as a rotation"); - - const rotated = ledger.record("anchor-redis", "anchor", "second-value", t1); - assert.equal(rotated.outcome, "rotated"); - assert.equal(rotated.held.rotations, 1); - assert.equal(rotated.held.since, t0.toISOString(), "a rotation reset the grant date"); - assert.equal(rotated.held.changed, t1.toISOString()); - assert.equal(rotated.held.fingerprint, fingerprint("second-value")); - assert.deepEqual(rotated.held.history, [{ fingerprint: fingerprint("first-value"), until: t1.toISOString() }]); -}); - -test("the ledger holds fingerprints and never the value, in files nobody else can read", () => { - const dir = mkdtempSync(join(tmpdir(), "vault-ledger-")); - const ledger = new Ledger(dir); - ledger.record("anchor-redis", "anchor", "the-actual-password", new Date()); - ledger.record("anchor-redis", "anchor", "the-rotated-password", new Date()); - for (const name of readdirSync(dir)) { - const raw = readFileSync(join(dir, name), "utf8"); - assert.doesNotMatch(raw, /the-actual-password|the-rotated-password/, `${name} holds a value`); - assert.equal(statSync(join(dir, name)).mode & 0o777, 0o600, `${name} is readable by others`); - } -}); - -test("withdrawing forgets a holder, and listing is by login", () => { - const ledger = fresh(); - ledger.record("b-app", "b", "x", new Date()); - ledger.record("a-app", "a", "y", new Date()); - assert.deepEqual(ledger.list().map((h) => h.as), ["a-app", "b-app"]); - assert.equal(ledger.withdraw("a-app"), true); - assert.equal(ledger.withdraw("a-app"), false, "withdrawing twice said it found something"); - assert.deepEqual(ledger.list().map((h) => h.as), ["b-app"]); -}); - -test("a login is a name, not a path", () => { - const ledger = fresh(); - assert.throws(() => ledger.record("../etc/passwd", "n", "v"), /a login is a name/); -}); - -test("what the mesh delivers is read from the contributions file and fingerprinted, never returned", () => { - const dir = mkdtempSync(join(tmpdir(), "vault-grants-")); - const secret = join(dir, "anchor.redis.secret"); - writeFileSync(secret, "minted-value\n"); // the host may leave a trailing newline; the value has none - const receives = join(dir, "mesh.json"); - writeFileSync(receives, JSON.stringify({ - requirement: "secret", - given: [ - { from: "redis", node: "anchor", as: "anchor-redis", secret }, - { from: "offer-only", node: "anchor" }, // a contribution with no login grants nothing - ], - })); - const asked = contributions(receives); - assert.deepEqual(asked.map((c) => c.as), ["anchor-redis"]); - const seen = deliveredFingerprint(asked[0]); - assert.deepEqual(seen, { fingerprint: fingerprint("minted-value"), length: "minted-value".length }); - assert.match(JSON.stringify(deliveredFingerprint({ as: "x", secret: join(dir, "missing") })), /not readable/); -}); diff --git a/modules/mesh-vault/tools/index.ts b/modules/mesh-vault/tools/index.ts deleted file mode 100644 index a6437e60..00000000 --- a/modules/mesh-vault/tools/index.ts +++ /dev/null @@ -1,131 +0,0 @@ -// mesh-vault's tools — vault's own code (novox/hq ADR 0039), served through the sdk's tool harness. They -// return structured data about the secrets this vault provides, and **never a value**: a holder is -// identified by its login and a value by its fingerprint. That is the rule the source mesh's -// secret_locate / secret_verify were built on, after a secret printed into a transcript. - -import { readFileSync } from "node:fs"; -import { join } from "node:path"; -import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"; -import { Ledger, contributions, deliveredFingerprint, type Held } from "../client.js"; - -/** The mesh's export of every operator-sealed secret, as the mesh wrote it into the root dir. */ -interface KeptExport { - export: number; - "operator-key": string; - fingerprint: string; - kept: { node: string; module: string; name: string; origin: string; sealed: string; key: string; "made-at": string }[]; - unrecoverable?: { node: string; module: string; name: string }[]; -} - -export function getVaultTools(ledger: Ledger, receives: string | undefined, root: string | undefined): ToolDefinition[] { - return [ - { - name: "secret_export", - description: - "The mesh's root secrets as this vault keeps them: every secret a module holds for itself, " + - "sealed to the operator's key (novox/hq ADR 0085, amended). Ciphertext — nothing here can " + - "open a line of it; the operator, holding the private key off the mesh, recovers one with " + - "`nox secret recover --from-export` on the control-node. Also lists what is NOT recoverable: secrets " + - "made before the mesh had an operator key.", - input: { - sealed: { - type: "boolean", - description: "include the sealed blobs (default true); false lists holders and the key only", - }, - }, - run: async (args) => { - if (!root) return { available: false, error: "this vault keeps no root secrets (MESH_VAULT_ROOT is not set)" }; - let doc: KeptExport; - try { - doc = JSON.parse(readFileSync(join(root, "export.json"), "utf8")) as KeptExport; - } catch (err) { - return { available: false, error: `the mesh has not written an export here yet: ${err}` }; - } - const withBlobs = args.sealed !== false; - return { - available: true, - export: doc.export, - "operator-key": doc["operator-key"], - fingerprint: doc.fingerprint, - count: doc.kept.length, - kept: doc.kept.map((k) => (withBlobs ? k : { node: k.node, module: k.module, name: k.name, origin: k.origin, "made-at": k["made-at"] })), - unrecoverable: doc.unrecoverable ?? [], - }; - }, - }, - { - name: "secret_holders", - description: - "Who holds a secret from this vault: each consumer's login, node and module, when it was " + - "granted, how many times it has been rotated and when, and the fingerprint of the current " + - "value. Fingerprints only — the value is never returned.", - input: {}, - run: async () => { - const asked = receives ? contributions(receives) : []; - const holders = ledger.list().map((h) => ({ - ...h, - module: asked.find((c) => c.as === h.as)?.from ?? null, - asked: asked.some((c) => c.as === h.as), - })); - return { holders, count: holders.length }; - }, - }, - { - name: "secret_verify", - description: - "Check one holder's secret without seeing it: the fingerprint the vault recorded against " + - "the fingerprint of the value the mesh currently delivers here, and optionally against a " + - "fingerprint computed on the holder's own machine (sha256 of the file, as `sha256:`). " + - "Two ends agreeing proves they agree, not that either works — the login itself is the test.", - input: { - as: { type: "string", description: "the holder's login, e.g. anchor-redis" }, - fingerprint: { - type: "string", - description: "optional: sha256: of the value as the holder reads it, computed there — never the value", - }, - }, - run: async (args) => { - const as = String(args.as ?? ""); - const recorded = ledger.get(as); - if (!recorded) return { as, known: false, error: `this vault holds nothing for ${as}` }; - const asked = receives ? contributions(receives).find((c) => c.as === as) : undefined; - const delivered = asked ? deliveredFingerprint(asked) : { error: "the mesh does not currently ask this vault to serve that login" }; - const given = args.fingerprint ? String(args.fingerprint) : undefined; - return verdict(recorded, delivered, given); - }, - }, - ]; -} - -function verdict( - recorded: Held, - delivered: { fingerprint: string; length: number } | { error: string }, - given: string | undefined, -): Record { - const deliveredMatches = "fingerprint" in delivered ? delivered.fingerprint === recorded.fingerprint : null; - const givenMatches = given === undefined ? null : given === recorded.fingerprint; - return { - as: recorded.as, - known: true, - recorded: recorded.fingerprint, - rotations: recorded.rotations, - changed: recorded.changed, - delivered: "fingerprint" in delivered ? delivered.fingerprint : null, - deliveredError: "error" in delivered ? delivered.error : null, - deliveredMatchesRecorded: deliveredMatches, - given: given ?? null, - givenMatchesRecorded: givenMatches, - givenIsAnEarlierValue: given === undefined ? null : recorded.history.some((h) => h.fingerprint === given), - ok: deliveredMatches !== false && givenMatches !== false, - }; -} - -// The tools exist only when the ledger can be reached from the environment; without it, vault -// contributes none rather than failing the whole tool runtime. -registerModuleTools("mesh-vault", (env) => { - try { - return getVaultTools(Ledger.fromEnv(env), env.MESH_RECEIVES, env.MESH_VAULT_ROOT); - } catch { - return []; - } -}); diff --git a/modules/mesh-vault/tsconfig.json b/modules/mesh-vault/tsconfig.json deleted file mode 100644 index 51f4046a..00000000 --- a/modules/mesh-vault/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "noEmit": true - }, - "include": ["client.ts", "index.ts", "provisioner/index.ts", "tools/index.ts"] -}