Add mesh-vault; redis, postgres and lavinmq take their passwords from files #30

Merged
jschoubben merged 1 commits from feat/secrets-vault into main 2026-09-21 08:03:13 +00:00
12 changed files with 635 additions and 6 deletions
+4 -2
View File
@@ -36,6 +36,7 @@
"amqp": "/var/lib/lavinmq-module/grants"
},
"own-secrets": {
"admin": "/var/lib/lavinmq-module/admin.secret",
"broker": "/var/lib/mesh/lavinmq/broker"
},
"listens": [
@@ -99,14 +100,15 @@
"network": "host",
"volumes": [
"/var/lib/mesh/lavinmq/broker:/run/secrets/broker:ro",
"/var/lib/lavinmq-module/grants:/var/lib/lavinmq-module/grants:ro"
"/var/lib/lavinmq-module/grants:/var/lib/lavinmq-module/grants:ro",
"/var/lib/lavinmq-module/admin.secret:/run/secrets/admin:ro"
],
"env": {
"MESH_BROKER_FILE": "/run/secrets/broker",
"MESH_RECEIVES": "/var/lib/lavinmq-module/grants/mesh.json",
"MESH_PROVISION_LAVINMQ": "http://127.0.0.1:15672",
"MESH_PROVISION_ADMIN_USER": "guest",
"MESH_LAVINMQ_ADMIN_PASSWORD": "guest"
"MESH_PROVISION_PASSWORD_FILE": "/run/secrets/admin"
}
}
],
+22
View File
@@ -0,0 +1,22 @@
# mesh-vault's runtime: the tool runtime, carrying this module's compiled provisioner, tools and event
# consumer. The same shape as postgres's, minus the client the database needs: mesh-vault reaches no
# server, because what it provides is a value the mesh already delivered to its node.
#
# **Built from this module's own directory and nothing else.** The sdk is in the base image, so
# nothing is copied out of a neighbouring checkout (novox/hq ADR 0069). Two bases, named rather than
# pinned — the image this is COMPILED in and the image it RUNS in — answered by the mesh from
# `build.on` in module.json (novox/hq issue 044).
ARG BUILD_BASE
ARG RUNTIME_BASE
FROM ${BUILD_BASE} AS build
WORKDIR /app/modules/vault
COPY . .
RUN node /app/node_modules/typescript/bin/tsc client.ts index.ts provisioner/index.ts tools/index.ts \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
FROM ${RUNTIME_BASE}
COPY --from=build /app/modules/vault/dist /app/modules/vault/dist
# The entrypoints a tool host loads from this module: its event consumer, its tools and its
# provisioner — one image, one process, one broker account (novox/hq ADR 0052).
ENV MESH_TOOL_MODULES=/app/modules/vault/dist/index.js,/app/modules/vault/dist/tools/index.js,/app/modules/vault/dist/provisioner/index.js
+172
View File
@@ -0,0 +1,172 @@
// 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 — `<node>-<module>`, so it names the holder. */
readonly as: string;
/** The consumer's node. */
readonly consumer: string;
/** sha256 of the value the mesh last delivered, `sha256:<hex>`. 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:<hex>`. 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}` };
}
}
+31
View File
@@ -0,0 +1,31 @@
// 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):
// module.mesh-vault.secret.provisioned — a consumer was granted a secret
// module.mesh-vault.secret.rotated — that consumer's value changed (`rotate secret`)
// module.mesh-vault.secret.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<SecretEvent>("module.mesh-vault.secret.provisioned", async (e) => {
console.log(`[mesh-vault] secret provisioned for ${e.body.as} on ${e.body.consumer} (${e.body.fingerprint})`);
});
await on<SecretEvent>("module.mesh-vault.secret.rotated", async (e) => {
console.log(`[mesh-vault] secret rotated for ${e.body.as} — rotation ${e.body.rotations} (${e.body.fingerprint})`);
});
await on<SecretEvent>("module.mesh-vault.secret.deprovisioned", async (e) => {
console.log(`[mesh-vault] secret withdrawn from ${e.body.as}`);
});
console.log("[mesh-vault] auditing secret lifecycle events");
+105
View File
@@ -0,0 +1,105 @@
{
"module": "mesh-vault",
"version": "1",
"provides": [
{
"name": "secret",
"scope": "mesh"
}
],
"capabilities": [
"container-runtime"
],
"emits": [
"module.mesh-vault.secret.provisioned",
"module.mesh-vault.secret.rotated",
"module.mesh-vault.secret.deprovisioned"
],
"consumes": [
"module.mesh-vault.secret.provisioned",
"module.mesh-vault.secret.rotated",
"module.mesh-vault.secret.deprovisioned"
],
"receives": {
"secret": "/var/lib/mesh-vault/grants/mesh.json"
},
"grants": {
"secret": "/var/lib/mesh-vault/grants"
},
"keeps": "/var/lib/mesh-vault/root",
"own-secrets": {
"broker": "/var/lib/mesh/mesh-vault/broker"
},
"resources": [
{
"id": "mesh-state",
"type": "directory",
"path": "/var/lib/mesh/mesh-vault",
"mode": "0700"
},
{
"id": "state",
"type": "directory",
"path": "/var/lib/mesh-vault",
"mode": "0700"
},
{
"id": "grants",
"type": "directory",
"path": "/var/lib/mesh-vault/grants",
"mode": "0700"
},
{
"id": "ledger",
"type": "directory",
"path": "/var/lib/mesh-vault/ledger",
"mode": "0700"
},
{
"id": "root",
"type": "directory",
"path": "/var/lib/mesh-vault/root",
"mode": "0700"
},
{
"id": "runtime",
"type": "container",
"name": "mesh-vault",
"network": "host",
"volumes": [
"/var/lib/mesh/mesh-vault/broker:/run/secrets/broker:ro",
"/var/lib/mesh-vault/grants:/var/lib/mesh-vault/grants:ro",
"/var/lib/mesh-vault/ledger:/var/lib/mesh-vault/ledger",
"/var/lib/mesh-vault/root:/var/lib/mesh-vault/root:ro"
],
"env": {
"MESH_BROKER_FILE": "/run/secrets/broker",
"MESH_RECEIVES": "/var/lib/mesh-vault/grants/mesh.json",
"MESH_VAULT_LEDGER": "/var/lib/mesh-vault/ledger",
"MESH_VAULT_ROOT": "/var/lib/mesh-vault/root"
},
"artifact": "runtime"
}
],
"build": {
"on": [
{
"arg": "BUILD_BASE",
"module": "mesh-tools",
"artifact": "build"
},
{
"arg": "RUNTIME_BASE",
"module": "mesh-tools",
"artifact": "runtime"
}
],
"artifacts": [
{
"name": "runtime",
"kind": "image",
"from": "Dockerfile"
}
]
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"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"
}
}
+48
View File
@@ -0,0 +1,48 @@
// 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<string, string | number>): Promise<void> {
try {
await emit(type, body);
} catch (err) {
console.error(`[provisioner:secret] emit ${type} failed: ${err}`);
}
}
runProvisioner("secret", {
async create(p: Provision): Promise<void> {
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<void> {
if (!ledger.withdraw(p.as)) return;
console.log(`[mesh-vault] withdrawn: ${p.as}`);
await announce("module.mesh-vault.secret.deprovisioned", { as: p.as });
},
});
+80
View File
@@ -0,0 +1,80 @@
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/);
});
+131
View File
@@ -0,0 +1,131 @@
// 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 " +
"`mesh-controller secret recover --from-export`. 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:<hex>`). " +
"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:<hex> 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<string, unknown> {
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 [];
}
});
+12
View File
@@ -0,0 +1,12 @@
{
"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"]
}
+3 -2
View File
@@ -72,14 +72,15 @@
"name": "mesh-store",
"image": "postgres@sha256:7456ef82e5f5bc43d997f4781bbd7c0d6389bff397564649a356e206ba473aee",
"env": {
"POSTGRES_PASSWORD": "bootstrap",
"POSTGRES_PASSWORD_FILE": "/run/secrets/superuser",
"PGDATA": "/var/lib/postgresql/data/pgdata"
},
"ports": [
"5432:5432"
],
"volumes": [
"mesh-store-data:/var/lib/postgresql/data"
"mesh-store-data:/var/lib/postgresql/data",
"/var/lib/postgres/superuser.secret:/run/secrets/superuser:ro"
]
},
{
+10 -2
View File
@@ -7,6 +7,9 @@
"scope": "mesh"
}
],
"requires": [
"secret"
],
"capabilities": [
"container-runtime"
],
@@ -29,8 +32,10 @@
"grants": {
"redis-cache": "/var/lib/redis-module/grants"
},
"secrets": {
"secret": "/var/lib/redis-module/default.secret"
},
"own-secrets": {
"default": "/var/lib/redis-module/default.secret",
"broker": "/var/lib/mesh/redis/broker"
},
"listens": [
@@ -72,7 +77,7 @@
"type": "file",
"path": "/var/lib/redis-module/redis.conf",
"mode": "0600",
"content": "requirepass ${secret:default}\nappendonly yes\ndir /data\n",
"content": "requirepass ${secret:secret}\nappendonly yes\ndir /data\n",
"owner": "999:999"
},
{
@@ -95,6 +100,9 @@
],
"args": [
"/etc/redis/redis.conf"
],
"restart-on": [
"server-conf"
]
},
{