social-kit / test / vectors / generate.mjs
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/**
 * One-shot golden-vector extractor. Run against the TS implementation while it
 * is still canon:
 *
 *   npx tsx test/vectors/generate.mjs
 *
 * Writes test/vectors/*.json: every deterministic derivation from compute.mjs
 * plus the SEALED fixtures (random IVs/ephemeral keys — captured once here,
 * then frozen; the CLJS port must be able to OPEN them, never re-produce them
 * byte-for-byte). After the CLJS port these files are the wire-compat
 * CONTRACT: do not regenerate them.
 */

import { writeFile } from "node:fs/promises";
import { computeAll, buildFixedEnvelope, TS_FIXED } from "./compute.mjs";
import { openEnvelope } from "../../dist/index.js";
import { utf8 } from "ardegazu-id-kit";
import { SUITE_SALT, wrapTo } from "ardegazu-id-kit/xkey";

const { ac, ch, sections } = await computeAll();

// ---- sealed envelope fixtures (random IV/eph key — frozen once) -------------

const e = await buildFixedEnvelope(ac);
const w = await wrapTo(SUITE_SALT, e.ctx, ac.idB.publicKeyB64, ac.xB.pubB64, utf8(e.inner_json));
const outer_json = JSON.stringify({ v: 1, w });

// an otherwise-valid envelope that expired at TS_FIXED+1000 — openEnvelope must reject
const expUnsigned = { ...e.unsigned, exp: TS_FIXED + 1000 };
const { envelopeSigPreimage } = await import("./compute.mjs");
const { toB64url } = await import("ardegazu-id-kit");
const expSig = toB64url(await ac.idA.signRaw(utf8(envelopeSigPreimage(e.ctx, expUnsigned))));
const expInnerJson = JSON.stringify({ ...expUnsigned, sig: expSig });
const expW = await wrapTo(SUITE_SALT, e.ctx, ac.idB.publicKeyB64, ac.xB.pubB64, utf8(expInnerJson));
const expired_outer_json = JSON.stringify({ v: 1, w: expW });

// sanity: the sealed fixture must open TODAY against the TS impl
const opened = await openEnvelope(ac.xB, e.ctx, utf8(outer_json));
if (!opened || opened.sig !== e.sig) throw new Error("generated envelope fixture does not open — refusing to write");
if ((await openEnvelope(ac.xB, e.ctx, utf8(expired_outer_json))) !== null)
  throw new Error("expired fixture unexpectedly opens");

// ---- sealed pair-channel wires (random IVs — frozen once) -------------------

const pairPayload = { k: "hello", n: 1, msg: "hei ✨" };
const pair_wire_from_A = await ch.pairAB.seal(ac.idA.publicKeyB64, pairPayload);
const selfPayload = ["state", 2, { x: true }];
const self_wire = await ch.self.seal(ac.idA.publicKeyB64, selfPayload);
const openedPair = await ch.pairBA.open(pair_wire_from_A, [ac.idA.publicKeyB64, ac.idB.publicKeyB64]);
if (!openedPair || openedPair.from !== ac.idA.publicKeyB64) throw new Error("pair wire fixture does not open");
const openedSelf = await ch.self.open(self_wire, [ac.idA.publicKeyB64]);
if (!openedSelf) throw new Error("self wire fixture does not open");

// ---- assemble + write -------------------------------------------------------

const files = {
  "canon.json": {
    _doc: "canon.ts canonical-serialization byte contract: canon() outputs (string + UTF-8 hex + SHA-256), inputs the port must reject, and hkdfId/sha256B64url derivations. Inputs are described in input_desc (input_json only where a JSON round trip is canon-faithful); the construction code lives in compute.mjs canonCases().",
    deterministic: sections.canon,
  },
  "envelope.json": {
    _doc: "Signed envelope contract (envelope.ts). deterministic: fixed seeds -> keys, certs, the fixed inner envelope, its canonical sig preimage and byte-exact Ed25519 signature. sealed: TS-produced wrapbox blobs (random IV + ephemeral X25519 — NOT reproducible; the port must UNSEAL them via openEnvelope). exp is 2100-01-01 so the fixture never expires; sealed_expired must open to null.",
    deterministic: sections.envelope,
    sealed: {
      recipient_seed: sections.envelope.seeds.recipient_B,
      ctx: e.ctx,
      outer_json,
      expect_inner: { ...e.inner, from: { ...e.inner.from, name: "Ala" } },
      expired_outer_json,
      expect_expired: null,
    },
  },
  "pair.json": {
    _doc: "Pairwise/self channel derivations (pair.ts): static-static DH of the suite X keys -> channel id, gossip topic, mailbox room id, wrap ctx, sender tags — byte-exact, symmetric for both members. sealed: AES-GCM wires produced by the TS impl (random IVs; the port must open() them).",
    deterministic: sections.pair,
    sealed: {
      pair_wire_from_A: { wire: pair_wire_from_A, payload: pairPayload, from_seed: "A" },
      self_wire: { wire: self_wire, payload: selfPayload, from_seed: "A" },
    },
  },
  "receipts.json": {
    _doc: "Co-signed leaderboard receipts (receipts.ts): fixed 3-player match, exact canonical sig preimage, byte-exact signatures, receipt hash, and a verify accept/reject table (verifyReceipt semantics). ts is fixed at 2025-01-01 (past; no maxAgeMs in the table, so verdicts are time-stable until 2100 for the future-ts case).",
    deterministic: sections.receipts,
  },
  "isoweek.json": {
    _doc: "isoWeek/lbRoomId/lbTopic (receipts.ts): ISO-8601 week bucketing at UTC year boundaries incl. week-52/53 years, and the weekly-drop room/topic derivations.",
    deterministic: sections.isoweek,
  },
  "mailbox.json": {
    _doc: "Mailbox-facing deterministic derivations: identity-inbox room ids (pair.ts inboxRoomId) and the compiled-in endpoint fallbacks (consts.ts). The HTTP client itself is network-behavior, not vectored.",
    deterministic: sections.mailbox,
  },
  "invites.json": {
    _doc: "Invite composition (invites.ts): the SUITE_APPS catalog and the mintRoomUrl shape (random 32-byte fragment secret; shape-checked, not byte-fixed).",
    deterministic: sections.invites,
  },
  "friends.json": {
    _doc: "Friend links (friends.ts build/parse round trips with real X-certs — fully deterministic), sanitizeFriend shape clamps, and selfsync.ts profile-sync sanitize/fold vectors (incl. the equal-ts canon tiebreak).",
    deterministic: sections.friends,
  },
  "apps-sync.json": {
    _doc: "selfsync.cljs's `apps` section — the suite identity record's per-app map {<appKey>:{state,ts}}: sanitizeAppsSync shape/size/JSON-safety clamps and the PER-KEY foldAppsSync (newest ts wins that key, equal ts breaks on the greater canon of the entry, a key only one side has is adopted). Sizes are UTF-16 code units of JSON.stringify, id-kit's unit and id-kit's numbers. Size/map/JSON-safety rows carry input_desc + a verdict instead of the input, which is 16 KB+, cyclic or a function. The *_keys arrays exist because deepEqual is key-order-blind and key order is the wire.",
    deterministic: sections["apps-sync"],
  },
  "text-keys.json": {
    _doc: "The SocialTextKey i18n contract (text.ts): full sorted key list, the built-in EN table (wire-relevant: legacy invite labels), fill() substitution and mkT() fallback semantics.",
    deterministic: sections["text-keys"],
  },
};

for (const [name, data] of Object.entries(files)) {
  const url = new URL(`./${name}`, import.meta.url);
  await writeFile(url, JSON.stringify(data, null, 2) + "\n");
  console.log(`wrote ${name}`);
}
console.log("done — fixtures extracted from the TS implementation");

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/social-kit.git