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
49 lines
1.9 KiB
JavaScript
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)}`);
|
|
}
|
|
}
|