MAS-WP-0009 T01: independent JS implementation (impl/js), differential test, SCP-9

impl/js (Node, zero deps, written from the specs): parser, v0.2
statute-aware rules, signing, outside master. Passes addresses.json,
statute-aware.json and auth.json unchanged; its master passes the 11
slave-setup mode flows against the Python email side (ExternalMaster).
Differential testing vs mas_core found two unstated stand-in rules, now
SCP-9 in the v0.2 draft with fixtures si-011..013, dt-109; ABNF comments
no longer say v0.1. Runner: external masters, step errors fail the flow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5787@bnt-lap001
Assistant-Session: 5db26b82-138b-40cc-9856-4d5eb0f752fd
This commit is contained in:
tegwick 2026-10-19 13:22:22 +02:00
parent ecb1ba47f6
commit a841ceec73
23 changed files with 938 additions and 49 deletions

116
impl/js/src/master.mjs Normal file
View file

@ -0,0 +1,116 @@
// An outside master (slave role, CONTRACT.md section 3.2), written from the
// mode contract. It receives email's signed events, and sends signed
// commands when told to through a loopback-only control API, so a flow
// runner in any language can drive it.
//
// node src/master.mjs --peer-id cms-1 --key-hex HEX [--email-url URL] [--port 0]
// -> prints {"events_url": ..., "control_url": ...} on the first stdout line
//
// Contract: POST /events (signed by email)
// Control: POST /control/config {email_url}
// POST /control/command {type, case?, ...fields} -> {status, body}
// GET /control/received -> [messages]
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
import { sign, verify, AuthError } from "./signing.mjs";
const CONTRACT_VERSION = "0.1";
export class OutsideMaster {
constructor({ peerId = "cms-1", emailPeer = "email", key, emailUrl = null }) {
this.peerId = peerId;
this.emailPeer = emailPeer;
this.keys = { [emailPeer]: key };
this.emailUrl = emailUrl;
this.received = [];
this.seen = new Map(); // idempotency: message id -> response
}
/** Contract endpoint: one signed message from email. Returns [status, body]. */
receive(headers, raw) {
let peer;
try {
peer = verify(headers, raw, this.keys);
} catch (e) {
if (e instanceof AuthError) return [e.status, { ok: false, error: e.code }];
throw e;
}
if (peer !== this.emailPeer) return [403, { ok: false, error: "wrong-peer" }];
let msg;
try {
msg = JSON.parse(raw.toString("utf8"));
} catch {
return [400, { ok: false, error: "invalid-json" }];
}
if (msg.mas_modes_version !== CONTRACT_VERSION || typeof msg.type !== "string" || !msg.id) {
return [400, { ok: false, error: "schema" }];
}
if (this.seen.has(msg.id)) return this.seen.get(msg.id);
this.received.push(msg);
const out = [200, { ok: true }];
this.seen.set(msg.id, out);
return out;
}
/** Send one signed command to email. Returns [status, body]. */
async command(type, fields = {}) {
const msg = { mas_modes_version: CONTRACT_VERSION, type, id: randomUUID().replaceAll("-", ""), ...fields };
for (const k of Object.keys(msg)) if (msg[k] === null || msg[k] === undefined) delete msg[k];
const body = Buffer.from(JSON.stringify(msg));
const res = await fetch(this.emailUrl, {
method: "POST", body,
headers: { "Content-Type": "application/json", ...sign(body, this.peerId, this.keys[this.emailPeer]) },
});
const text = await res.text();
let parsed = null;
try { parsed = text ? JSON.parse(text) : null; } catch { parsed = text; }
return [res.status, parsed];
}
}
const isLoopback = (addr) => addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
export function serve(master, port = 0) {
const server = createServer(async (req, res) => {
const chunks = [];
for await (const c of req) chunks.push(c);
const raw = Buffer.concat(chunks);
const send = (status, obj) => {
const out = JSON.stringify(obj);
res.writeHead(status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(out) });
res.end(out);
};
try {
if (req.method === "POST" && req.url === "/events") return send(...master.receive(req.headers, raw));
if (!req.url.startsWith("/control/")) return send(404, { ok: false, error: "not-found" });
if (!isLoopback(req.socket.remoteAddress)) return send(403, { ok: false, error: "control is loopback-only" });
if (req.method === "GET" && req.url === "/control/received") return send(200, master.received);
const body = raw.length ? JSON.parse(raw.toString("utf8")) : {};
if (req.method === "POST" && req.url === "/control/config") {
master.emailUrl = body.email_url;
return send(200, { ok: true });
}
if (req.method === "POST" && req.url === "/control/command") {
const { type, ...fields } = body;
const [status, resp] = await master.command(type, fields);
return send(200, { status, body: resp });
}
send(404, { ok: false, error: "not-found" });
} catch (e) {
send(500, { ok: false, error: String(e) });
}
});
return new Promise((resolve) => server.listen(port, "127.0.0.1", () => resolve(server)));
}
if (import.meta.url === `file://${process.argv[1]}`) {
const arg = (name, dflt) => {
const i = process.argv.indexOf(`--${name}`);
return i >= 0 ? process.argv[i + 1] : dflt;
};
const master = new OutsideMaster({ peerId: arg("peer-id", "cms-1"), key: Buffer.from(arg("key-hex"), "hex"),
emailUrl: arg("email-url", null) });
const server = await serve(master, Number(arg("port", "0")));
const base = `http://127.0.0.1:${server.address().port}`;
process.stdout.write(JSON.stringify({ events_url: `${base}/events`, control_url: `${base}/control` }) + "\n");
}