Files
mesh-tools/dlxprobe.mjs
jschoubben 04a689e008 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
2026-09-04 01:51:17 +02:00

49 lines
1.9 KiB
JavaScript

import amqp from "amqplib";
const PORT = process.argv[2];
const MPORT = process.argv[3];
const B = `http://127.0.0.1:${MPORT}`;
const AUTH = "Basic " + Buffer.from("guest:guest").toString("base64");
async function api(method, path, body) {
const r = await fetch(B + path, {
method,
headers: { "content-type": "application/json", authorization: AUTH },
body: body ? JSON.stringify(body) : undefined,
});
if (r.status >= 300 && r.status !== 404) throw new Error(`${method} ${path} -> ${r.status}`);
}
await api("PUT", "/api/exchanges/%2f/mesh.events.dead", { type: "topic", durable: true });
await api("PUT", "/api/users/al", { password: "s", tags: "" });
const Q = "anchor.al.events";
const D = "mesh.events.dead";
const q = Q.replace(/\./g, "\\.");
const d = D.replace(/\./g, "\\.");
// configure, write, read patterns per grant on the dead exchange
const combos = {
"none": { configure: `^${q}$`, write: `^${q}$`, read: `^${q}$` },
"read-dead": { configure: `^${q}$`, write: `^${q}$`, read: `^(${q}|${d})$` },
"write-dead": { configure: `^${q}$`, write: `^(${q}|${d})$`, read: `^${q}$` },
"configure-dead": { configure: `^(${q}|${d})$`, write: `^${q}$`, read: `^${q}$` },
"read+write-dead": { configure: `^${q}$`, write: `^(${q}|${d})$`, read: `^(${q}|${d})$` },
};
let i = 0;
for (const [label, perms] of Object.entries(combos)) {
await api("PUT", "/api/permissions/%2f/al", perms);
const queue = `${Q}.${i++}`; // fresh each time
try {
const c = await amqp.connect(`amqp://al:s@127.0.0.1:${PORT}/`);
const ch = await c.createChannel();
ch.on("error", () => {});
await ch.assertQueue(queue, { durable: true, deadLetterExchange: D });
console.log(`${label}: declare-with-DLX OK`);
await c.close();
} catch (e) {
console.log(`${label}: FAIL - ${String(e.message).slice(0, 70)}`);
}
}