// Cross-implementation room-crypto vectors: the CLJS port must derive the same
// room ids and OPEN envelopes sealed by the deployed TS implementation (fixtures
// generated while the TS was canon), plus seal/open round trips of its own
// (randomized-IV sealing can't be byte-fixed).
//
// The net/ layer is vendored byte-identically from the game1 CLJS canon, so this
// suite is really a guard that the vendoring and game2's APP_SALT still derive
// the room ids the deployed build speaks.
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import * as lib from "../test-dist/testlib.js";
const v = JSON.parse(readFileSync(new URL("./vectors/room.json", import.meta.url), "utf8"));
test("room id derivation matches TS (HKDF roomid, game2's salt)", async () => {
assert.equal(v.appSalt, "game2.ardegazu.ro/v2");
const rc1 = await lib.createRoomCrypto(v.secret1, v.appSalt);
assert.equal(rc1.roomId, v.roomId1);
const rc2 = await lib.createRoomCrypto(v.secret2, v.appSalt);
assert.equal(rc2.roomId, v.roomId2);
});
test("the room-scoped protocol id is the DEPLOYED /game2/2/ prefix", () => {
// valley-blocks is the one game whose prefix is its repo name, not its
// codename — the wire must keep speaking exactly this.
assert.equal(v.protocolPrefix, "/game2/2/");
assert.equal(lib.protocolOf(v.roomId1), v.protocol1);
assert.ok(lib.protocolOf(v.roomId1).startsWith(v.protocolPrefix));
});
test("opens TS-sealed hellos (plain and with an identity block)", async () => {
const rc = await lib.createRoomCrypto(v.secret1, v.appSalt);
const plain = await rc.openHello(v.peerA, v.peerB, v.envAtoB);
assert.ok(plain, "A->B hello must open");
assert.equal(plain.v, 2);
const withId = await rc.openHello(v.peerB, v.peerA, v.envBtoA);
assert.ok(withId, "B->A hello must open");
assert.equal(withId.v, 2);
assert.deepEqual(withId.id, v.envBtoAId);
});
test("rejects swapped direction, wrong peer, wrong room", async () => {
const rc = await lib.createRoomCrypto(v.secret1, v.appSalt);
assert.equal(await rc.openHello(v.peerB, v.peerA, v.envAtoB), null, "AAD direction");
assert.equal(await rc.openHello(v.peerA, v.peerA, v.envAtoB), null, "wrong recipient");
const other = await lib.createRoomCrypto(v.secret2, v.appSalt);
assert.equal(await other.openHello(v.peerA, v.peerB, v.envAtoB), null, "wrong room");
});
test("seal/open round trip with extra fields riding next to v", async () => {
const rc = await lib.createRoomCrypto(v.secret1, v.appSalt);
const env = await rc.sealHello(v.peerA, v.peerB, { id: { pub: "p", sig: "s" } });
assert.equal(env.v, 2);
assert.equal(typeof env.iv, "string");
assert.equal(typeof env.ct, "string");
const pt = await rc.openHello(v.peerA, v.peerB, env);
assert.ok(pt);
assert.equal(pt.v, 2);
assert.deepEqual(pt.id, { pub: "p", sig: "s" });
// and sequential seals never reuse an IV
const env2 = await rc.sealHello(v.peerA, v.peerB, undefined);
assert.notEqual(env2.iv, env.iv);
assert.ok(await rc.openHello(v.peerA, v.peerB, env2));
});
test("malformed envelopes and short secrets are rejected", async () => {
const rc = await lib.createRoomCrypto(v.secret1, v.appSalt);
for (const bad of [null, {}, { v: 1, iv: "", ct: "" }, { v: 2, iv: 42, ct: "" }]) {
assert.equal(await rc.openHello(v.peerA, v.peerB, bad), null);
}
await assert.rejects(() => lib.createRoomCrypto(v.tooShortSecret, v.appSalt));
});
test("base64 primitives match the TS byte for byte", () => {
for (const { bytes, b64, b64url } of v.b64) {
const u8 = new Uint8Array(bytes);
assert.equal(lib.toB64(u8), b64, JSON.stringify(bytes));
assert.equal(lib.toB64url(u8), b64url, JSON.stringify(bytes));
assert.deepEqual([...lib.fromB64url(b64url)], bytes, JSON.stringify(bytes));
}
});
test("newRoomSecret mints 32-byte b64url secrets", () => {
const s = lib.newRoomSecret();
assert.match(s, /^[A-Za-z0-9_-]{43}$/);
assert.equal(lib.fromB64url(s).length, 32);
assert.equal(lib.toB64url(lib.fromB64url(s)), s);
assert.deepEqual(
{ length: s.length, bytes: lib.fromB64url(s).length, roundTrips: lib.toB64url(lib.fromB64url(s)) === s },
v.newRoomSecretShape,
);
});