runtime: connect with a sealed credential, scoped, over pinned amqps (ADR 0048)
A module reads its broker credential from MESH_BROKER_FILE — the sealed
{url,fingerprint} the mesh delivered — and connects over amqps pinned to
exactly that certificate. The pin is two-phase (fetch cert, verify, then
trust only it), because Node's checkServerIdentity does not run under
rejectUnauthorized:false, so a naive connect-then-check would already have
sent the password to whoever answered.
A scoped module (assumeExchanges) never declares the exchanges (its account
may not) nor its own queue with a dead-letter (the broker refuses that to a
non-administrator) — the mesh pre-declared the queue, so it passively checks
it, binds and consumes. The RPC reply queue is lazy, and a module that
registered no tools serves none: a pure-events consumer touches only what its
account allows.
Verified end-to-end against a real broker as the scoped account: the audit
logger consumes # and records events, over an account that is not the
broker's own.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
+42
-16
@@ -6,23 +6,59 @@
|
||||
// mesh-tools emit TYPE [JSON] emit one event onto the mesh and exit — an operable primitive,
|
||||
// and what an events test uses to put a message on the wire.
|
||||
//
|
||||
// MESH_BROKER_URL amqp://… the mesh broker (both modes)
|
||||
// MESH_TOOL_MODULES /path/a,/path/b,… compiled module entrypoints (serve mode)
|
||||
// MESH_MODULE / MESH_NODE the identity stamped onto emitted events (ADR 0047)
|
||||
// The broker, in order of preference:
|
||||
// MESH_BROKER_FILE a sealed {url, fingerprint} the mesh delivered (novox/hq ADR 0048) — an
|
||||
// amqps account scoped to this module. Preferred: a module holds its own.
|
||||
// MESH_BROKER_URL a plain URL, for the bootstrap/admin case before a module has an account.
|
||||
// MESH_TOOL_MODULES /path/a,/path/b,… compiled module entrypoints (serve mode)
|
||||
// MESH_MODULE / MESH_NODE the identity stamped onto emitted events (ADR 0047)
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { connectAmqp } from "./broker-amqp.js";
|
||||
import type { Credential } from "./broker-amqp.js";
|
||||
import { runTools } from "./runtime.js";
|
||||
import { useBroker } from "@novox/mesh-sdk/messaging";
|
||||
import type { Broker } from "@novox/mesh-sdk/messaging";
|
||||
import { emit } from "@novox/mesh-sdk/events";
|
||||
|
||||
/**
|
||||
* Connect the way this process is meant to: with its sealed credential if the mesh gave it one, and
|
||||
* over the plain bootstrap URL otherwise. A scoped module assumes the substrate's exchanges exist —
|
||||
* its account may not declare them (ADR 0048).
|
||||
*/
|
||||
async function connectBroker(): Promise<Broker> {
|
||||
const file = process.env.MESH_BROKER_FILE;
|
||||
if (file) {
|
||||
let credential: Credential;
|
||||
try {
|
||||
credential = JSON.parse(readFileSync(file, "utf8")) as Credential;
|
||||
} catch (err) {
|
||||
console.error(`mesh-tools: cannot read the broker credential at ${file}: ${err}`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!credential.url) {
|
||||
console.error(`mesh-tools: ${file} carries no url — it is not a broker credential`);
|
||||
process.exit(1);
|
||||
}
|
||||
return connectAmqp(credential, { assumeExchanges: true });
|
||||
}
|
||||
const url = process.env.MESH_BROKER_URL;
|
||||
if (!url) {
|
||||
console.error(
|
||||
"mesh-tools: set MESH_BROKER_FILE (a sealed credential) or MESH_BROKER_URL — there is no broker to reach",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
return connectAmqp(url);
|
||||
}
|
||||
|
||||
async function serve(): Promise<void> {
|
||||
const url = requireEnv("MESH_BROKER_URL");
|
||||
const moduleEntrypoints = (process.env.MESH_TOOL_MODULES ?? "")
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const broker = await connectAmqp(url);
|
||||
const broker = await connectBroker();
|
||||
const stop = await runTools({ broker, moduleEntrypoints });
|
||||
|
||||
const shutdown = async (): Promise<void> => {
|
||||
@@ -35,7 +71,6 @@ async function serve(): Promise<void> {
|
||||
}
|
||||
|
||||
async function emitOnce(type: string, bodyJson: string): Promise<void> {
|
||||
const url = requireEnv("MESH_BROKER_URL");
|
||||
let body: unknown = {};
|
||||
if (bodyJson) {
|
||||
try {
|
||||
@@ -45,7 +80,7 @@ async function emitOnce(type: string, bodyJson: string): Promise<void> {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
const broker = await connectAmqp(url);
|
||||
const broker = await connectBroker();
|
||||
useBroker(() => broker);
|
||||
// emit awaits the broker's publish confirm (ADR 0047), so the event is accepted before we close.
|
||||
await emit(type, body);
|
||||
@@ -66,13 +101,4 @@ async function main(): Promise<void> {
|
||||
await serve();
|
||||
}
|
||||
|
||||
function requireEnv(name: string): string {
|
||||
const v = process.env[name];
|
||||
if (!v) {
|
||||
console.error(`mesh-tools: ${name} is not set — the runtime cannot serve without it`);
|
||||
process.exit(1);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
void main();
|
||||
|
||||
Reference in New Issue
Block a user