// Phase 1 mixed-topology peering gate. A STANDALONE dev instance (SQLite MAIN on // :3000) pairs with a specific TENANT (t1) on the multi-tenant Postgres "prod" // (:3002), promotes its metadata ops to t1, and we verify: // 1. the ops land in t1's journal and NOT in sibling tenant t2 (mixed-topology // isolation -- a standalone dev deploying to one tenant on a shared server); // 2. the tenant-bound HMAC: a peer request signed for t1 is REJECTED (401) when // replayed against t2 on the same server, while the SAME request signed for // t2 passes auth -- proving the rejection is host-specific, not the secret. // // dev and t2 both hold a dev peer row with the SAME shared secret, so the only // thing standing between a t1-signed request and t2 accepting it is the host // binding in the canonical string (lib/crypto.js buildCanonical -> targetHost). // // Run: node test/mixedTopologyGate.js (MAIN :3000 + PG :3002 up, dev-deploy // installed on MAIN and per-tenant on t1/t2). Self-skips if either is down. const http = require("http"); const net = require("net"); const { buildCanonical, sign, normalizeHost } = require("../lib/crypto"); const DEV = { port: 3000, host: "localhost:3000", base: "http://localhost:3000" }; const PROD_PORT = 3002; const ADMIN_PW = "AdminP@ss1"; const INGEST = "/dev-deploy/api/ingest"; const thost = (t) => t + ".localhost.localdomain:" + PROD_PORT; const jars = { dev: {}, t1: {}, t2: {} }; let pass = 0; let fail = 0; const ok = (cond, msg) => { if (cond) { pass++; console.log(" PASS " + msg); } else { fail++; console.log(" FAIL " + msg); } }; const portOpen = (port) => { return new Promise((resolve) => { const s = net.connect(port, "127.0.0.1"); const done = (up) => { s.destroy(); resolve(up); }; s.setTimeout(1000); s.on("connect", () => done(true)); s.on("timeout", () => done(false)); s.on("error", () => done(false)); }); }; const HOSTOF = (inst) => (inst === "dev" ? DEV.host : thost(inst)); const PORTOF = (inst) => (inst === "dev" ? DEV.port : PROD_PORT); const ADMIN = (inst) => (inst === "dev" ? "admin@local" : "admin@" + inst + ".local"); const storeCookies = (inst, headers) => { const sc = headers["set-cookie"]; if (!sc) { return; } for (const line of sc) { const pair = line.split(";")[0]; const eq = pair.indexOf("="); if (eq > 0) { jars[inst][pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim(); } } }; // opts.host overrides the Host header; opts.headers sets raw headers (for the // signed replay); opts.rawBody sends an exact body string; opts.body is a form. const request = (inst, method, path, opts) => { const options = opts || {}; return new Promise((resolve, reject) => { const headers = Object.assign({ Host: options.host || HOSTOF(inst) }, options.headers || {}); const jar = jars[inst]; if (jar && Object.keys(jar).length > 0) { headers["Cookie"] = Object.keys(jar).map((k) => k + "=" + jar[k]).join("; "); } let data = null; if (options.rawBody !== undefined) { data = options.rawBody; headers["Content-Length"] = Buffer.byteLength(data); } else if (options.body) { data = new URLSearchParams(options.body).toString(); headers["Content-Type"] = "application/x-www-form-urlencoded"; headers["Content-Length"] = Buffer.byteLength(data); } const r = http.request({ host: "127.0.0.1", port: PORTOF(inst), method: method, path: path, headers: headers }, (resp) => { storeCookies(inst, resp.headers); let body = ""; resp.on("data", (c) => { body += c; }); resp.on("end", () => resolve({ status: resp.statusCode, headers: resp.headers, body: body })); }); r.on("error", reject); if (data !== null && data !== undefined) { r.write(data); } r.end(); }); }; const csrfOf = (h) => { const m = h.match(/name="_csrf" value="([^"]+)"/); return m ? m[1] : ""; }; const envIdOf = (h) => { const m = h.match(/env_id<\/strong> is ([0-9a-f-]{36})<\/code>/); return m ? m[1] : ""; }; const opCountOf = (h) => { const m = h.match(/Ops recorded<\/th>(\d+)<\/td>/); return m ? parseInt(m[1], 10) : null; }; const secretOf = (h) => { const m = h.match(/

([0-9a-f]+)<\/p>/); return m ? m[1] : ""; }; // Find the peers-table row carrying envId and pull its peer_id out of the row's // promote/delete form input. const peerIdFor = (html, envId) => { for (const row of html.split("")) { if (row.indexOf("" + envId + "") >= 0) { const m = row.match(/name="peer_id" value="(\d+)"/); if (m) { return m[1]; } } } return null; }; const authed = async (inst) => /true/.test((await request(inst, "GET", "/auth/authenticated")).body); const bootstrap = async (inst) => { const lp = await request(inst, "GET", "/auth/login"); await request(inst, "POST", "/auth/login", { body: { email: ADMIN(inst), password: ADMIN_PW, _csrf: csrfOf(lp.body) } }); if (await authed(inst)) { return true; } const cp = await request(inst, "GET", "/auth/create_first_user"); const cc = csrfOf(cp.body); if (cc) { await request(inst, "POST", "/auth/create_first_user", { body: { email: ADMIN(inst), password: ADMIN_PW, default_language: "en", _csrf: cc } }); } return await authed(inst); }; const ddCsrf = async (inst) => csrfOf((await request(inst, "GET", "/admin/dev-deploy/peers")).body); // Remove any existing peer row for envId (idempotent reruns). const clearPeer = async (inst, envId) => { const pid = peerIdFor((await request(inst, "GET", "/admin/dev-deploy/peers")).body, envId); if (pid) { await request(inst, "POST", "/admin/dev-deploy/peers/delete", { body: { peer_id: pid, _csrf: await ddCsrf(inst) } }); } }; const addPeer = async (inst, envId, label, baseUrl, secret) => { const body = { env_id: envId, label: label, base_url: baseUrl, _csrf: await ddCsrf(inst) }; if (secret) { body.existing_secret = secret; } return await request(inst, "POST", "/admin/dev-deploy/peers/add", { body: body }); }; const run = async () => { ok(await bootstrap("dev"), "dev (standalone MAIN :3000) admin session"); ok(await bootstrap("t1"), "t1 admin session"); ok(await bootstrap("t2"), "t2 admin session"); const devEnv = envIdOf((await request("dev", "GET", "/admin/dev-deploy/peers")).body); const t1Env = envIdOf((await request("t1", "GET", "/admin/dev-deploy/peers")).body); const t2Env = envIdOf((await request("t2", "GET", "/admin/dev-deploy/peers")).body); ok(/^[0-9a-f-]{36}$/.test(devEnv), "dev env_id (" + devEnv + ")"); ok(t1Env && t2Env && t1Env !== t2Env && t1Env !== devEnv, "dev/t1/t2 have distinct env_ids"); // Idempotent: drop any peer rows left by a prior run. await clearPeer("dev", t1Env); await clearPeer("t1", devEnv); await clearPeer("t2", devEnv); // Pair dev -> t1 (dev generates the shared secret); t1 stores dev with it. const addRes = await addPeer("dev", t1Env, "prod-t1", "http://" + thost("t1")); const secret = secretOf(addRes.body); ok(/^[0-9a-f]{64}$/.test(secret), "dev paired with t1 (shared secret issued)"); await addPeer("t1", devEnv, "dev", DEV.base, secret); // t2 also holds the dev peer with the SAME secret -> the cross-tenant replay // can ONLY be stopped by the host binding, not by a differing secret. await addPeer("t2", devEnv, "dev", DEV.base, secret); const t1Ops0 = opCountOf((await request("t1", "GET", "/admin/dev-deploy/")).body); const t2Ops0 = opCountOf((await request("t2", "GET", "/admin/dev-deploy/")).body); // dev promotes its journal to t1. const t1PeerId = peerIdFor((await request("dev", "GET", "/admin/dev-deploy/peers")).body, t1Env); ok(!!t1PeerId, "dev has a peer_id for t1 (" + t1PeerId + ")"); const prom = await request("dev", "POST", "/admin/dev-deploy/promote", { body: { peer_id: t1PeerId, _csrf: await ddCsrf("dev") } }); ok(prom.status >= 200 && prom.status < 400, "dev promote -> t1 returned HTTP " + prom.status); const t1Ops1 = opCountOf((await request("t1", "GET", "/admin/dev-deploy/")).body); const t2Ops1 = opCountOf((await request("t2", "GET", "/admin/dev-deploy/")).body); ok(t1Ops1 !== null && t1Ops1 > t1Ops0, "t1 received dev's ops (" + t1Ops0 + " -> " + t1Ops1 + ")"); ok(t2Ops1 === t2Ops0, "t2 journal UNCHANGED by dev->t1 promote (" + t2Ops0 + " -> " + t2Ops1 + ")"); // --- Tenant-bound HMAC --- // Build a peer-signed ingest exactly as transport.js would, then aim it at the // wrong tenant. Signature material is identical except the target host. const secretBuf = Buffer.from(secret, "hex"); const mkHeaders = (targetHost, body) => { const ts = String(Date.now()); const nonce = "00112233445566778899aabbccddeeff"; const canonical = buildCanonical({ timestamp: ts, nonce: nonce, method: "POST", path: INGEST, targetHost: normalizeHost(targetHost), body: body }); return { "X-DD-Env-Id": devEnv, "X-DD-Timestamp": ts, "X-DD-Nonce": nonce, "X-DD-Signature": sign(secretBuf, canonical), "Content-Type": "application/vnd.dev-deploy+json" }; }; const body = "{}"; // Signed for t1, replayed at t2 -> must be rejected by the host binding. const replay = await request("t2", "POST", INGEST, { host: thost("t2"), headers: mkHeaders(thost("t1"), body), rawBody: body }); ok(replay.status === 401, "ingest signed for t1 REJECTED at t2 (tenant-bound HMAC; HTTP " + replay.status + ")"); // Same request, but signed for t2 -> auth must pass (proves the rejection was // host-specific, not a secret/timestamp problem). Non-401 = auth accepted. const control = await request("t2", "POST", INGEST, { host: thost("t2"), headers: mkHeaders(thost("t2"), body), rawBody: body }); ok(control.status !== 401, "same ingest signed for t2 passes auth at t2 (HTTP " + control.status + ", host-specific rejection confirmed)"); // Cleanup (best-effort): drop the peer rows so reruns start clean. await clearPeer("dev", t1Env); await clearPeer("t1", devEnv); await clearPeer("t2", devEnv); console.log("\nRESULT: " + pass + " passed, " + fail + " failed"); process.exit(fail ? 1 : 0); }; const main = async () => { if (!(await portOpen(DEV.port))) { console.log("SKIP: standalone dev not reachable on 127.0.0.1:" + DEV.port); process.exit(0); } if (!(await portOpen(PROD_PORT))) { console.log("SKIP: Postgres multi-tenant prod not reachable on 127.0.0.1:" + PROD_PORT); process.exit(0); } await run(); }; main().catch((e) => { console.error("MIXED-TOPOLOGY GATE ERROR:", e); process.exit(2); });