social-kit / test / presence-brains.test.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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
/**
 * The optional `brains` beacon field (2.1.0), black-box against the committed
 * dist over a fake pubsub + the fake DOM.
 *
 * Wire contract under test:
 *  - beacon payload gains ONE additive key, `br` (game id -> version string),
 *    only when the agent's optional `brains` thunk yields a non-empty map;
 *    every existing caller's payload is byte-identical to 2.0.0;
 *  - the SAME clamp runs on send and on receive (untrusted network input);
 *  - a received value surfaces as `brains` on the presence entry, and a legacy
 *    beacon (no `br`) leaves the key absent entirely.
 */
import test from "node:test";
import assert from "node:assert/strict";
import { installFakeDom } from "./helpers/fake-dom.mjs";

const env = installFakeDom();

// The agent registers its beacon-on-visible handler with document; capturing it
// is how we fire extra beacons on demand (the real cadence is SOC_BEACON_MS).
const docListeners = new Map();
env.document.addEventListener = (type, cb) => {
  if (!docListeners.has(type)) docListeners.set(type, []);
  docListeners.get(type).push(cb);
};

const { SocialAgent, FriendStore, pairChannel, buildEnvelope, openEnvelope, envCtx } =
  await import("../dist/index.js");
const { Identity } = await import("ardegazu-id-kit");
const { deriveSuiteXKeyPair, signXCert, SUITE_SALT } = await import("ardegazu-id-kit/xkey");

const dec = new TextDecoder();
const enc = new TextEncoder();
const tick = (ms = 60) => new Promise((r) => setTimeout(r, ms));

const seedA = Identity.newSeed();
const seedB = Identity.newSeed();
const idA = await Identity.fromSeed(seedA);
const idB = await Identity.fromSeed(seedB);
const xA = await deriveSuiteXKeyPair(seedA);
const xB = await deriveSuiteXKeyPair(seedB);
const certA = await signXCert(idA, SUITE_SALT, xA.pubB64);
const certB = await signXCert(idB, SUITE_SALT, xB.pubB64);

// B's view of the shared pair channel: seals/opens the same wires A does.
const chB = await pairChannel(xB, idB.publicKeyB64, idA.publicKeyB64, xA.pubB64);

let nsSeq = 0;

/** A started agent for identity A with B as an accepted friend. */
async function mkAgent(brains) {
  const ns = `t-brains-${nsSeq++}`;
  const store = new FriendStore(ns);
  store.upsert({
    pub: idB.publicKeyB64,
    x: xB.pubB64,
    xs: certB,
    pet: "",
    name: "bot-b",
    state: "friend",
    addedTs: Date.now(),
    petTs: 0,
    lastSeenTs: 0,
    lastApp: "",
  });
  const published = [];
  const listeners = [];
  const pubsub = {
    subscribe: () => {},
    unsubscribe: () => {},
    addEventListener: (_ev, fn) => listeners.push(fn),
    publish: async (topic, data) => void published.push([topic, dec.decode(data)]),
  };
  const presence = [];
  const opts = {
    ns,
    app: "game2.ardegazu.ro",
    identity: idA,
    seed: seedA,
    myName: () => "ana",
    store,
    pubsub,
    events: { presence: (pub, info) => presence.push([pub, info]) },
  };
  if (brains !== undefined) opts.brains = brains;
  const agent = new SocialAgent(opts);
  await agent.start();
  await tick();

  /** Beacon payloads A published on the PAIR channel, oldest first. */
  const beacons = async () => {
    const out = [];
    for (const [topic, wire] of published) {
      if (topic !== chB.topic) continue;
      const opened = await chB.open(wire, [idA.publicKeyB64]);
      if (opened && opened.payload && opened.payload.k === "b") out.push(opened.payload);
    }
    return out;
  };
  /** Fire the visibility handler => one more beacon round. */
  const refire = async () => {
    for (const cb of docListeners.get("visibilitychange") ?? []) cb();
    await tick();
  };
  /** Deliver a beacon sealed by B into A's wire handler. */
  const deliverFromB = async (payload) => {
    const wire = await chB.seal(idB.publicKeyB64, payload);
    for (const fn of listeners) fn({ detail: { topic: chB.topic, data: enc.encode(wire) } });
    await tick();
  };
  return { agent, store, published, beacons, refire, deliverFromB, presence };
}

// ---- send side --------------------------------------------------------------

test("beacon omits `br` entirely when the brains option is absent (2.0.0 bytes)", async () => {
  const a = await mkAgent(undefined);
  const bs = await a.beacons();
  assert.ok(bs.length >= 1, "A beaconed to its friend");
  assert.deepEqual(Object.keys(bs[0]), ["k", "name", "app", "ts"], "payload shape unchanged");
  assert.equal("br" in bs[0], false);
  a.agent.stop();
});

test("beacon carries `br` when the thunk yields a non-empty map, and re-reads it every round", async () => {
  let version = "v7";
  const a = await mkAgent(() => ({ "valley-blocks": version, g2: "a.b-c_d@1" }));
  let bs = await a.beacons();
  assert.deepEqual(bs[0].br, { "valley-blocks": "v7", g2: "a.b-c_d@1" });
  assert.deepEqual(Object.keys(bs[0]), ["k", "name", "app", "br", "ts"]);

  // the beacon is the whole update mechanism: change the value, next round carries it
  version = "v8";
  await a.refire();
  bs = await a.beacons();
  assert.ok(bs.length >= 2, "a second beacon went out");
  assert.equal(bs[bs.length - 1].br["valley-blocks"], "v8", "fresh read at beacon time");
  a.agent.stop();
});

test("send-side clamp: 8-entry cap, key charset, non-string and oversize values", async () => {
  const a = await mkAgent(() => ({
    // 10 valid entries — only the first 8 survive
    g1: "1", g2: "2", g3: "3", g4: "4", g5: "5", g6: "6", g7: "7", g8: "8", g9: "9", g10: "10",
  }));
  let bs = await a.beacons();
  assert.equal(Object.keys(bs[0].br).length, 8, "capped at 8 entries");
  assert.deepEqual(Object.keys(bs[0].br), ["g1", "g2", "g3", "g4", "g5", "g6", "g7", "g8"]);
  a.agent.stop();

  const b = await mkAgent(() => ({
    ok: "fine",
    Bad: "uppercase key",            // key charset
    "-lead": "leading dash",         // key charset
    "a_b": "underscore key",         // key charset
    "": "empty key",                 // key charset
    "way-too-long-game-id-abcdef": "25+ chars", // key length
    n: 42,                           // non-string value (never stringified)
    z: null,
    t: true,
    o: { v: 1 },
    arr: ["v1"],
    big: "x".repeat(33),             // oversize value
    spacey: "v 1",                   // value charset
    slashy: "a/b",                   // value charset
  }));
  bs = await b.beacons();
  assert.deepEqual(bs[0].br, { ok: "fine" }, "only the clean entry survives");
  b.agent.stop();
});

test("send-side clamp: non-object input, empty result and a throwing thunk all omit `br`", async () => {
  for (const bad of [() => null, () => 42, () => "v1", () => ["v1"], () => ({}), () => ({ BAD: "x" }),
                     () => { throw new Error("bot bug"); }]) {
    const a = await mkAgent(bad);
    const bs = await a.beacons();
    assert.ok(bs.length >= 1, "the beacon still went out");
    assert.equal("br" in bs[0], false, `omitted for ${bad}`);
    assert.deepEqual(Object.keys(bs[0]), ["k", "name", "app", "ts"]);
    a.agent.stop();
  }
});

// ---- receive side -----------------------------------------------------------

test("received `br` surfaces as `brains` on the presence entry", async () => {
  const a = await mkAgent(undefined);
  await a.deliverFromB({ k: "b", name: "bot-b", app: "game2.ardegazu.ro", br: { "valley-blocks": "v7@3" }, ts: Date.now() });
  const info = a.agent.presenceOf(idB.publicKeyB64);
  assert.ok(info, "presence recorded");
  assert.deepEqual(info.brains, { "valley-blocks": "v7@3" });
  const [pub, fired] = a.presence[a.presence.length - 1];
  assert.equal(pub, idB.publicKeyB64);
  assert.equal(fired, info, "the onChange snapshot is the same entry");
  a.agent.stop();
});

test("legacy peer: a beacon without `br` yields NO `brains` key on the entry", async () => {
  const a = await mkAgent(undefined);
  await a.deliverFromB({ k: "b", name: "bot-b", app: "game2.ardegazu.ro", ts: Date.now() });
  const info = a.agent.presenceOf(idB.publicKeyB64);
  assert.ok(info, "presence recorded");
  assert.equal("brains" in info, false, "no brains key at all");
  assert.deepEqual(Object.keys(info), ["name", "app", "join", "ts"], "2.0.0 entry shape");

  // and a peer that stops advertising drops the key again on the next beacon
  await a.deliverFromB({ k: "b", name: "bot-b", app: "game2.ardegazu.ro", br: { g1: "v1" }, ts: Date.now() });
  assert.deepEqual(a.agent.presenceOf(idB.publicKeyB64).brains, { g1: "v1" });
  await a.deliverFromB({ k: "b", name: "bot-b", app: "game2.ardegazu.ro", ts: Date.now() });
  assert.equal("brains" in a.agent.presenceOf(idB.publicKeyB64), false, "key gone again");
  a.agent.stop();
});

test("receive-side clamp is the same clamp: cap, key charset, value rules, junk input", async () => {
  const a = await mkAgent(undefined);
  const brainsAfter = async (br) => {
    await a.deliverFromB({ k: "b", name: "b", app: "game2.ardegazu.ro", br, ts: Date.now() });
    return a.agent.presenceOf(idB.publicKeyB64).brains;
  };

  assert.equal(Object.keys(await brainsAfter({
    g1: "1", g2: "2", g3: "3", g4: "4", g5: "5", g6: "6", g7: "7", g8: "8", g9: "9",
  })).length, 8, "8-entry cap on receive");

  assert.deepEqual(await brainsAfter({
    ok: "fine", Bad: "x", "-lead": "x", a_b: "x", "": "x",
    "way-too-long-game-id-abcdef": "x",
    n: 1, z: null, t: false, o: {}, arr: ["x"],
    big: "y".repeat(33), spacey: "v 1",
  }), { ok: "fine" }, "same drop rules on receive");

  for (const junk of [null, 42, "v1", ["g1"], {}, { BAD: "x" }, { g1: 1 }, true]) {
    await a.deliverFromB({ k: "b", name: "b", app: "game2.ardegazu.ro", br: junk, ts: Date.now() });
    const info = a.agent.presenceOf(idB.publicKeyB64);
    assert.equal("brains" in info, false, `dropped whole field for ${JSON.stringify(junk)}`);
  }
  a.agent.stop();
});

// ---- wire safety ------------------------------------------------------------

test("signed envelope carrying an additive `br` key still verifies end to end", async () => {
  const selfB = { identity: idB, x: xB, xCert: certB, name: "bot-b" };
  const ctx = envCtx(idA.publicKeyB64);
  const body = { soc: { v: 1, ts: Date.now(), friends: [] }, br: { "valley-blocks": "v7@3" } };
  const built = await buildEnvelope(selfB, ctx, idA.publicKeyB64, xA.pubB64, "fsync", body, undefined);
  const opened = await openEnvelope(xA, ctx, built.bytes);
  assert.ok(opened, "the additive key does not disturb sign-then-wrap verification");
  assert.deepEqual(opened.body.br, { "valley-blocks": "v7@3" });
  assert.deepEqual(opened.body.soc, body.soc);
  // the outer wrap is untouched by the additive body key
  const outer = JSON.parse(dec.decode(built.bytes));
  assert.equal(outer.v, 1);
  assert.equal(typeof outer.w, "object");
});

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