// Cross-implementation gate for the per-viewer names blob (#577). The native // client (ios/LChatCore Nicknames.swift + Crypto.swift seal/open) reimplements // the web's sealForPub/openSealed for the {v:1, names:{uid:{name,seen}}} blob; // if either side drifts (wire shape, JSON shape), nicknames stop opening on // one platform. This pins the format two ways: // // 1. a fresh seal → open round-trips in JS (crypto.js self-consistency), and // 2. the committed fixture (LChatCore's Fixtures/nicknames-vector.json) // still opens to its expected map — the SAME fixture NicknamesVectorTests // consumes (it also seals natively and re-opens, covering both directions). // // Regenerate with: node test/nicknames.test.mjs --emit import { readFileSync, writeFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; import { generateUserKeypair, sealForPub, openSealed } from "../web/static/crypto.js"; const here = dirname(fileURLToPath(import.meta.url)); const fixturePath = join(here, "..", "ios", "LChatCore", "Tests", "LChatCoreTests", "Fixtures", "nicknames-vector.json"); // A stable map exercising the shapes that matter: a plain rename, a // multi-word name, and unicode. const NAMES = { "uid-dermot": { name: "Dermo", seen: "Dermot" }, "uid-anna": { name: "Mum", seen: "Anna Lee" }, "uid-jose": { name: "José 🎉", seen: "Jose" }, }; function assert(cond, msg) { if (!cond) { console.error("FAIL:", msg); process.exit(1); } } async function emit() { const kp = await generateUserKeypair(); const sealedB64 = await sealForPub(kp.publicKey, { v: 1, names: NAMES }); const opened = await openSealed(sealedB64, kp.privateKey); assert(JSON.stringify(opened.names) === JSON.stringify(NAMES), "round-trip"); const fixture = { _comment: "Per-viewer names blob test vector (#577). Generated by test/nicknames.test.mjs " + "from web/static/crypto.js sealForPub. LChatCore's NicknamesVectorTests opens " + "sealedB64 with privPkcs8B64 and must get expectedNames — and seals its own " + "blob to pubRawB64, which must open on both sides. base64url throughout. " + "Regenerate with: node test/nicknames.test.mjs --emit", privPkcs8B64: kp.privateKey, pubRawB64: kp.publicKey, sealedB64, expectedNames: NAMES, }; writeFileSync(fixturePath, JSON.stringify(fixture, null, 2) + "\n"); console.log("wrote", fixturePath); } async function test() { // (1) crypto.js self-consistency. const kp = await generateUserKeypair(); const sealed = await sealForPub(kp.publicKey, { v: 1, names: NAMES }); const opened = await openSealed(sealed, kp.privateKey); assert(JSON.stringify(opened.names) === JSON.stringify(NAMES), "fresh seal round-trips"); // (2) the committed fixture still opens — the same bytes Swift consumes. const fx = JSON.parse(readFileSync(fixturePath, "utf8")); const fromFixture = await openSealed(fx.sealedB64, fx.privPkcs8B64); assert( JSON.stringify(fromFixture.names) === JSON.stringify(fx.expectedNames), "committed fixture opens to expectedNames", ); console.log("ALL PASSED"); } if (process.argv.includes("--emit")) await emit(); else await test();