id-kit / test / bridge-adversarial.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
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
// Adversarial two-origin bridge cases NOT covered by the kit's own harness.
// Same DOM-stub approach as id-kit/test/bridge.test.mjs, driven harder:
// raced CAS during boot, second-client takeover, forged host messages,
// stale/foreign pushes, clear CAS, ephemeral flag, origin table vs TS regex.
import test from "node:test";
import assert from "node:assert/strict";

const KIT = new URL("..", import.meta.url).href.replace(/\/+$/, "");
const APP_ORIGIN = "https://chat.ardegazu.ro";
const BRIDGE_URL = "https://ardegazu.ro/id/";
const BRIDGE_ORIGIN = "https://ardegazu.ro";
const STORE_KEY = "ardegazu-id:v1";
const NS = "chat-ardegazu-ro-v2";

class FakeStorage {
  #m = new Map();
  getItem(k) { return this.#m.has(k) ? this.#m.get(k) : null; }
  setItem(k, v) { this.#m.set(k, String(v)); }
  removeItem(k) { this.#m.delete(k); }
}

function makeListenerHub() {
  const listeners = new Map();
  return {
    addEventListener(type, fn) {
      if (!listeners.has(type)) listeners.set(type, new Set());
      listeners.get(type).add(fn);
    },
    removeEventListener(type, fn) { listeners.get(type)?.delete(fn); },
    dispatch(type, ev) { for (const fn of [...(listeners.get(type) ?? [])]) fn(ev); },
  };
}

let world = null;

function makeWorld({ hostAnswers = true } = {}) {
  const w = {
    context: "client",
    clientLS: new FakeStorage(),
    hostLS: new FakeStorage(),
    hostBroken: false,
    hostReplies: [], // every message the host posts back to the client
  };
  const clientHub = makeListenerHub();
  const hostHub = makeListenerHub();
  const inHost = (fn) => {
    const prev = w.context;
    w.context = "host";
    try { return fn(); } finally { w.context = prev; }
  };
  const hostWin = {
    ...hostHub,
    parent: null,
    postMessage(msg, _origin) {
      w.beforeHostSees?.(msg);
      queueMicrotask(() =>
        inHost(() => hostHub.dispatch("message", { origin: APP_ORIGIN, source: clientPort, data: msg })));
    },
    dispatchStorage(ev) { queueMicrotask(() => inHost(() => hostHub.dispatch("storage", ev))); },
    forge(ev) { inHost(() => hostHub.dispatch("message", ev)); },
  };
  const clientPort = {
    postMessage(msg, origin) {
      if (msg?.t === "id-ready") assert.equal(origin, "*");
      else {
        assert.equal(origin, APP_ORIGIN, "host must reply to the proven app origin");
        w.hostReplies.push(msg);
      }
      queueMicrotask(() => clientHub.dispatch("message", { origin: BRIDGE_ORIGIN, source: hostWin, data: msg }));
    },
  };
  hostWin.parent = clientPort;
  const clientWin = { ...clientHub, parent: null };
  const doc = {
    body: {
      appendChild(_f) {
        if (!hostAnswers) return;
        queueMicrotask(async () => {
          const { initBridgeHost } = await import(`${KIT}/dist/index.js`);
          inHost(() => initBridgeHost());
        });
      },
    },
    documentElement: { appendChild() {} },
    createElement() {
      return { hidden: false, src: "", contentWindow: hostWin, setAttribute() {}, addEventListener() {}, remove() {} };
    },
  };
  w.clientWin = clientWin;
  w.hostWin = hostWin;
  w.document = doc;
  return w;
}

for (const [name, get] of [
  ["window", () => (world?.context === "host" ? world?.hostWin : world?.clientWin)],
  ["document", () => world?.document],
  ["localStorage", () => {
    if (!world) return undefined;
    if (world.context === "host") {
      if (world.hostBroken) throw new Error("storage unavailable");
      return world.hostLS;
    }
    return world.clientLS;
  }],
]) Object.defineProperty(globalThis, name, { get, configurable: true });

const {
  IdBridge, SEED_RE, sanitizeRecord, isAllowedAppOrigin, APP_ORIGIN_RE, DEV_ORIGINS, Identity,
  MAX_RECORD_BYTES, MAX_APP_STATE_BYTES, MAX_APPS_BYTES,
} = await import(`${KIT}/dist/index.js`);

const newBridge = (opts = {}) => new IdBridge({ ns: NS, bridgeUrl: BRIDGE_URL, timeoutMs: 250, ...opts });
const hostRecord = () => {
  const raw = world.hostLS.getItem(STORE_KEY);
  return raw ? JSON.parse(raw) : null;
};
const tick = (ms = 20) => new Promise((r) => setTimeout(r, ms));
const SEED_A = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8";
const SEED_B = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
const recOf = (seed, extra = {}) => ({
  v: 1, seed, name: "", hue: null, glyph: null, lang: null, soc: null,
  createdAt: 1756150000000, updatedAt: 1756160000000, ...extra,
});

// ---- origin allowlist: dist behavior must equal the TS regex + dev list ----
test("origin allowlist: adversarial table matches the TS automaton exactly", () => {
  const TS_RE = /^https:\/\/([a-z0-9-]+\.)?ardegazu\.ro$/; // literal from src/protocol.ts @ 2949cdc
  const TS_DEV = ["http://localhost:4173", "http://localhost:5173", "http://127.0.0.1:4173", "http://127.0.0.1:5173"];
  const table = [
    "https://ardegazu.ro", "https://chat.ardegazu.ro", "https://game1.ardegazu.ro", "https://a-0b.ardegazu.ro",
    "https://a.b.ardegazu.ro", "https://evilardegazu.ro", "https://ardegazu.ro.evil.com", "https://xardegazu.ro",
    "https://ardegazu.ro:8443", "https://ardegazu.ro:443", "https://ardegazu.ro/", "https://ardegazu.ro.",
    "http://ardegazu.ro", "https://ARDEGAZU.RO", "https://Chat.ardegazu.ro", "https://ardegazu.rp",
    "null", "", "file://", "about:blank", "https://ardegazu‍.ro", "https://ardegazu.ro\n",
    "http://localhost:4173", "http://localhost:5173", "http://127.0.0.1:4173", "http://127.0.0.1:5173",
    "https://localhost:5173", "http://localhost:51733", "http://localhost:517", "http://localhost",
    "http://[::1]:5173", "http://127.0.0.2:5173", "chrome-extension://abc",
  ];
  for (const o of table) {
    const expected = TS_RE.test(o) || TS_DEV.includes(o);
    assert.equal(isAllowedAppOrigin(o), expected, `isAllowedAppOrigin(${JSON.stringify(o)})`);
    assert.equal(APP_ORIGIN_RE.test(o), TS_RE.test(o), `APP_ORIGIN_RE(${JSON.stringify(o)})`);
  }
  assert.deepEqual([...DEV_ORIGINS], TS_DEV);
  // multiline trick: anchors must not be fooled by embedded newlines
  assert.equal(isAllowedAppOrigin("https://evil.com\nhttps://ardegazu.ro"), false);
});

// ---- raced CAS during boot: competitor claims the bridge between get and put
test("boot race: bridge claimed between get and put -> conflict surfaced, mirror kept", async () => {
  world = makeWorld();
  world.clientLS.setItem(`${NS}:id`, SEED_A);
  world.clientLS.setItem(`${NS}:profile`, JSON.stringify({ hue: 5, glyph: null, lang: null, ts: 5 }));
  world.beforeHostSees = (msg) => {
    if (msg?.t === "put") world.hostLS.setItem(STORE_KEY, JSON.stringify(recOf(SEED_B, { name: "Racer" })));
  };
  const b = newBridge();
  const r = await b.boot();
  assert.equal(r.source, "mirror");
  assert.equal(r.seed, SEED_A, "mirror seed kept despite the race");
  assert.ok(r.conflict, "raced claim surfaces as conflict");
  assert.equal(r.conflict.seed, SEED_B);
  assert.equal(hostRecord().seed, SEED_B, "racer's record not clobbered");
  b.destroy();
});

test("boot race with the SAME seed: no conflict surfaced (rec.seed === ours)", async () => {
  world = makeWorld();
  world.clientLS.setItem(`${NS}:id`, SEED_A);
  world.beforeHostSees = (msg) => {
    if (msg?.t === "put") world.hostLS.setItem(STORE_KEY, JSON.stringify(recOf(SEED_A, { name: "Twin" })));
  };
  const b = newBridge();
  const r = await b.boot();
  assert.equal(r.source, "mirror");
  assert.equal(r.seed, SEED_A);
  assert.equal(r.conflict, null, "same-seed race is not a conflict");
  b.destroy();
});

// ---- game1's auto-adopt flow, straight from boot.ts --------------------------
test("game1 auto-adopt: conflict -> adoptBridgeSeed(boot.conflict) -> clean re-boot", async () => {
  world = makeWorld();
  world.clientLS.setItem(`${NS}:id`, SEED_A);
  world.hostLS.setItem(STORE_KEY, JSON.stringify(recOf(SEED_B, { name: "Suite", hue: 12, lang: "ro" })));
  const bridge = newBridge();
  let boot = await bridge.boot();
  assert.ok(boot.conflict);
  bridge.adoptBridgeSeed(boot.conflict);
  boot = await bridge.boot();
  assert.equal(boot.conflict, null, "clean on the suite seed after adopt");
  assert.equal(boot.seed, SEED_B);
  assert.equal(boot.profile.name, "Suite");
  assert.equal(boot.profile.lang, "ro");
  assert.equal(world.clientLS.getItem(`${NS}:id-prev`), SEED_A, "old seed backed up");
  // netIdentity path from game1/client/src/id/boot.ts
  const identity = boot.identity;
  assert.ok(identity);
  const sig = await identity.assert("game1.ardegazu.ro/v1", "room-1", "peer-1");
  const { verifyAssertion } = await import(`${KIT}/dist/index.js`);
  assert.ok(await verifyAssertion("game1.ardegazu.ro/v1", "room-1", "peer-1", identity.publicKeyB64, sig));
  assert.equal(identity.fingerprint.hex.length, 64, "fingerprint getter survives :advanced");
  bridge.destroy();
});

// ---- pushes: foreign seed and stale ts must not touch the mirror -------------
test("push with a DIFFERENT seed: onChange+lastRecord update, mirror untouched", async () => {
  world = makeWorld();
  const b = newBridge();
  const r = await b.boot();
  const pushed = [];
  b.onChange((rec) => pushed.push(rec));
  world.hostLS.setItem(STORE_KEY, JSON.stringify(recOf(SEED_B, { name: "Other", updatedAt: Date.now() + 9e9 })));
  world.hostWin.dispatchStorage({ key: STORE_KEY });
  await tick();
  assert.equal(pushed.length, 1);
  assert.equal(b.lastRecord().seed, SEED_B);
  assert.equal(world.clientLS.getItem(`${NS}:id`), r.seed, "mirror seed NOT switched by a push");
  assert.equal(world.clientLS.getItem(`${NS}:name`), null, "mirror profile NOT absorbed from foreign seed");
  b.destroy();
});

test("push with same seed but STALE ts: mirror not overwritten", async () => {
  world = makeWorld();
  const b = newBridge();
  const r = await b.boot();
  await b.putProfile({ name: "Mine" });
  world.hostLS.setItem(STORE_KEY, JSON.stringify(recOf(r.seed, { name: "Stale", updatedAt: 1 })));
  world.hostWin.dispatchStorage({ key: STORE_KEY });
  await tick();
  assert.equal(world.clientLS.getItem(`${NS}:name`), "Mine", "stale push absorbed nothing");
  assert.equal(b.lastRecord().name, "Stale", "lastRecord still tracks the push");
  b.destroy();
});

test("storage event key filtering: null key pushes, foreign key does not", async () => {
  world = makeWorld();
  const b = newBridge();
  await b.boot();
  const pushed = [];
  b.onChange((rec) => pushed.push(rec));
  world.hostWin.dispatchStorage({ key: "unrelated-key" });
  await tick();
  assert.equal(pushed.length, 0, "foreign key ignored");
  world.hostWin.dispatchStorage({ key: null }); // storage.clear()
  await tick();
  assert.equal(pushed.length, 1, "null key (clear) pushes state");
  b.destroy();
});

// ---- second client takeover: pushes go to the latest proven client only ------
test("host proven-client semantics: a newer valid client takes the push channel", async () => {
  world = makeWorld();
  const b = newBridge();
  await b.boot();
  const pushed = [];
  b.onChange((rec) => pushed.push(rec));
  const gotB = [];
  const clientB = { postMessage(msg, origin) { gotB.push({ msg, origin }); } };
  world.hostWin.forge({ origin: "https://board.ardegazu.ro", source: clientB, data: { t: "get", v: 1, reqId: 1 } });
  await tick();
  assert.equal(gotB.length, 1, "clientB got its get answer");
  assert.equal(gotB[0].origin, "https://board.ardegazu.ro", "reply targets the NEW proven origin");
  world.hostWin.dispatchStorage({ key: STORE_KEY });
  await tick();
  assert.equal(gotB.length, 2, "push went to the latest proven client");
  assert.equal(pushed.length, 0, "old client no longer receives pushes");
  b.destroy();
});

// ---- forged host messages: guards, CAS, clear, oversize, patch shapes --------
function forgeCollect(data, origin = APP_ORIGIN) {
  const got = [];
  world.hostWin.forge({ origin, source: { postMessage(m) { got.push(m); } }, data });
  return got;
}

test("host guards: malformed requests are silently dropped", async () => {
  world = makeWorld();
  const b = newBridge();
  await b.boot();
  for (const data of [
    null, "get", 42,
    { t: "get", v: 2, reqId: 1 },              // wrong version
    { t: "get", v: 1 },                        // missing reqId
    { t: "get", v: 1, reqId: "1" },            // reqId wrong type
    { t: "steal", v: 1, reqId: 1 },            // unknown type
    { v: 1, reqId: 1 },                        // missing t
  ]) {
    const got = forgeCollect(data);
    await tick(5);
    assert.equal(got.length, 0, `dropped: ${JSON.stringify(data)}`);
  }
  b.destroy();
});

test("host CAS on clear: wrong expect conflicts, right expect clears", async () => {
  world = makeWorld();
  const b = newBridge();
  const r = await b.boot();
  let got = forgeCollect({ t: "clear", v: 1, reqId: 7, expect: SEED_B });
  await tick(5);
  assert.equal(got[0].t, "conflict");
  assert.equal(got[0].reqId, 7, "conflict echoes reqId");
  assert.equal(got[0].rec.seed, r.seed, "conflict carries the current record");
  assert.ok(hostRecord(), "nothing cleared");
  got = forgeCollect({ t: "clear", v: 1, reqId: 8, expect: r.seed });
  await tick(5);
  assert.equal(got[0].t, "state");
  assert.equal(got[0].rec, null);
  assert.equal(hostRecord(), null, "cleared");
  b.destroy();
});

test("host put: birthday preserved on same seed, fresh on new seed; soc undefined -> null", async () => {
  world = makeWorld();
  const b = newBridge();
  const r = await b.boot();
  const birth = hostRecord().createdAt;
  await tick(5);
  // same seed, absurd createdAt in the request: birthday kept
  let got = forgeCollect({ t: "put", v: 1, reqId: 9, expect: r.seed,
    rec: { seed: r.seed, name: "X", createdAt: 4242, updatedAt: 4242 } });
  await tick(5);
  assert.equal(got[0].t, "state");
  assert.equal(hostRecord().createdAt, birth, "unchanged seed keeps its birthday");
  assert.ok(hostRecord().updatedAt >= birth, "updatedAt re-stamped by the host");
  assert.equal(hostRecord().soc, null, "absent soc normalized to null");
  assert.deepEqual(Object.keys(hostRecord()),
    ["v", "seed", "name", "hue", "glyph", "lang", "soc", "apps", "createdAt", "updatedAt"],
    "exact stored key order, extras dropped");
  // new seed: birthday starts from the sanitized record
  got = forgeCollect({ t: "put", v: 1, reqId: 10, expect: r.seed,
    rec: { seed: SEED_B, createdAt: 12345, updatedAt: 1 } });
  await tick(5);
  assert.equal(got[0].t, "state");
  assert.equal(hostRecord().createdAt, 12345, "new seed starts fresh");
  b.destroy();
});

test("host put: unsanitizable record or oversize soc -> conflict, nothing written", async () => {
  world = makeWorld();
  const b = newBridge();
  const r = await b.boot();
  const before = JSON.stringify(hostRecord());
  let got = forgeCollect({ t: "put", v: 1, reqId: 11, expect: r.seed, rec: { seed: "short" } });
  await tick(5);
  assert.equal(got[0].t, "conflict");
  got = forgeCollect({ t: "put", v: 1, reqId: 12, expect: r.seed,
    rec: { seed: r.seed, soc: "x".repeat(130 * 1024) } });
  await tick(5);
  assert.equal(got[0].t, "conflict", "oversize put refused");
  got = forgeCollect({ t: "soc", v: 1, reqId: 13, expect: r.seed, soc: "x".repeat(130 * 1024) });
  await tick(5);
  assert.equal(got[0].t, "conflict", "oversize soc refused");
  assert.equal(JSON.stringify(hostRecord()), before, "record untouched throughout");
  b.destroy();
});

test("host profile: non-object patch treated as {}, only updatedAt moves", async () => {
  world = makeWorld();
  const b = newBridge();
  const r = await b.boot();
  await b.putProfile({ name: "Keep" });
  const before = hostRecord();
  const got = forgeCollect({ t: "profile", v: 1, reqId: 14, expect: r.seed, patch: 42 });
  await tick(5);
  assert.equal(got[0].t, "state");
  const after = hostRecord();
  assert.equal(after.name, "Keep");
  assert.equal(after.hue, before.hue);
  assert.ok(after.updatedAt >= before.updatedAt);
  b.destroy();
});

test("host profile guard: patch on an EMPTY bridge conflicts (cur null)", async () => {
  world = makeWorld();
  const b = newBridge();
  await b.boot();
  forgeCollect({ t: "clear", v: 1, reqId: 15, expect: hostRecord().seed });
  await tick(5);
  const got = forgeCollect({ t: "profile", v: 1, reqId: 16, expect: null, patch: { name: "x" } });
  await tick(5);
  assert.equal(got[0].t, "conflict", "profile CAS needs an existing record");
  b.destroy();
});

// ---- app sections: write-isolated by origin, readable by everyone -----------
test("app sections: each origin writes only the key its own origin names", async () => {
  world = makeWorld();
  const b = newBridge();
  const r = await b.boot();
  let got = forgeCollect({ t: "app", v: 1, reqId: 20, expect: r.seed, state: { from: "chat" } });
  await tick(5);
  assert.equal(got[0].t, "state");
  // a SECOND app writes: its own key, chat's entry untouched — and nothing in
  // the payload lets it aim elsewhere (no key field exists on the wire)
  got = forgeCollect({ t: "app", v: 1, reqId: 21, expect: r.seed, state: { from: "board" }, key: "chat" },
    "https://board.ardegazu.ro");
  await tick(5);
  assert.equal(got[0].t, "state");
  let apps = hostRecord().apps;
  assert.deepEqual(Object.keys(apps), ["board", "chat"], "one entry per origin, key-sorted");
  assert.deepEqual(apps.chat.state, { from: "chat" }, "board cannot touch chat's section");
  assert.deepEqual(apps.board.state, { from: "board" });
  assert.ok(apps.board.ts > 0 && apps.chat.ts > 0, "host-stamped ts");
  // the apex and the dev origins hold reserved keys of their own
  forgeCollect({ t: "app", v: 1, reqId: 22, expect: r.seed, state: 1 }, "https://ardegazu.ro");
  await tick(5);
  forgeCollect({ t: "app", v: 1, reqId: 23, expect: r.seed, state: 2 }, "http://localhost:5173");
  await tick(5);
  assert.deepEqual(Object.keys(hostRecord().apps), ["board", "chat", "dev", "home"]);
  // deleting: null removes only the caller's own entry
  got = forgeCollect({ t: "app", v: 1, reqId: 24, expect: r.seed, state: null },
    "https://board.ardegazu.ro");
  await tick(5);
  assert.equal(got[0].t, "state");
  assert.deepEqual(Object.keys(hostRecord().apps), ["chat", "dev", "home"]);
  b.destroy();
});

test("app sections: CAS, oversize, unserializable and record-cap all conflict", async () => {
  world = makeWorld();
  const b = newBridge();
  const r = await b.boot();
  forgeCollect({ t: "app", v: 1, reqId: 30, expect: r.seed, state: { keep: true } });
  await tick(5);
  const before = JSON.stringify(hostRecord());
  // wrong expect
  let got = forgeCollect({ t: "app", v: 1, reqId: 31, expect: SEED_B, state: { evil: 1 } });
  await tick(5);
  assert.equal(got[0].t, "conflict");
  assert.equal(got[0].reqId, 31);
  // over the per-entry cap
  got = forgeCollect({ t: "app", v: 1, reqId: 32, expect: r.seed, state: "x".repeat(65 * 1024) });
  await tick(5);
  assert.equal(got[0].t, "conflict", "oversize section refused");
  // not JSON-serializable (postMessage would refuse it; the host must too)
  const cyclic = {}; cyclic.self = cyclic;
  got = forgeCollect({ t: "app", v: 1, reqId: 33, expect: r.seed, state: cyclic });
  await tick(5);
  assert.equal(got[0].t, "conflict", "unserializable section refused");
  // an allowed origin whose label is too long for a key gets no section at all
  const longLabel = `https://${"a".repeat(33)}.ardegazu.ro`;
  assert.equal(isAllowedAppOrigin(longLabel), true, "the gate lets it in…");
  got = forgeCollect({ t: "app", v: 1, reqId: 34, expect: r.seed, state: 1 }, longLabel);
  await tick(5);
  assert.equal(got[0].t, "conflict", "…but no key derives, so no section");
  assert.equal(JSON.stringify(hostRecord()), before, "record untouched throughout");

  // whole-record cap: a fat soc leaves no room even for a legal-size section
  forgeCollect({ t: "soc", v: 1, reqId: 35, expect: r.seed, soc: "s".repeat(126 * 1024) });
  await tick(5);
  const legal = "x".repeat(MAX_APP_STATE_BYTES - 2);
  got = forgeCollect({ t: "app", v: 1, reqId: 36, expect: r.seed, state: legal });
  await tick(5);
  assert.equal(got[0].t, "conflict", "whole-record cap still rules");
  assert.equal(got[0].reason, "full", "…and says so");
  assert.deepEqual(hostRecord().apps.chat.state, { keep: true }, "old section intact");
  b.destroy();
});

// ---- finding 1: `apps` never comes off the wire -----------------------------
// A `put` is a PROFILE write that any allowed origin may send. If it honored
// rec.apps, one ordinary app could install a whole forged sections map — with
// an attacker-chosen `ts` that wins every freshest-wins merge downstream — and
// round-trip the seed back over two puts so the identity looked untouched.
test("forged put: an allowed origin cannot install an apps map (different seed)", async () => {
  world = makeWorld();
  const b = newBridge();
  const r = await b.boot();
  // honest sections, written by their real owners
  forgeCollect({ t: "app", v: 1, reqId: 60, expect: r.seed, state: { iban: "RO49-REAL" } },
    "https://banca.ardegazu.ro");
  await tick(5);
  forgeCollect({ t: "app", v: 1, reqId: 61, expect: r.seed, state: { role: "hub" } },
    "https://ardegazu.ro");
  await tick(5);
  const honest = JSON.stringify(hostRecord().apps);

  const YEAR_2243 = 8640000000000;
  const forged = {
    banca: { state: { iban: "RO49-ATTACKER" }, ts: YEAR_2243 },
    home: { state: { role: "pwned" }, ts: YEAR_2243 },
    board: { state: { anything: true }, ts: YEAR_2243 },
  };
  // step 1: flip the seed, smuggling the forged map in
  let got = forgeCollect({ t: "put", v: 1, reqId: 62, expect: r.seed,
    rec: recOf(SEED_B, { apps: forged }) });
  await tick(5);
  assert.equal(got[0].t, "state");
  assert.equal(hostRecord().apps, null, "a new seed starts sectionless — no forged map");
  // step 2: flip it back, smuggling the same map again
  got = forgeCollect({ t: "put", v: 1, reqId: 63, expect: SEED_B,
    rec: recOf(r.seed, { apps: forged }) });
  await tick(5);
  assert.equal(got[0].t, "state");
  assert.equal(hostRecord().seed, r.seed, "the seed round-tripped, as the attack intends…");
  assert.equal(hostRecord().apps, null, "…but no section came back with it");
  assert.notEqual(honest, "null", "sanity: the honest sections really existed");
  b.destroy();
});

test("forged put: same-seed and empty-bridge variants cannot install an apps map", async () => {
  world = makeWorld();
  const b = newBridge();
  const r = await b.boot();
  forgeCollect({ t: "app", v: 1, reqId: 64, expect: r.seed, state: { real: true } },
    "https://banca.ardegazu.ro");
  await tick(5);
  const before = JSON.stringify(hostRecord().apps);
  const forged = { banca: { state: { real: false }, ts: 8640000000000 } };

  // same seed: sections carry over from STORAGE, the payload is ignored
  let got = forgeCollect({ t: "put", v: 1, reqId: 65, expect: r.seed,
    rec: recOf(r.seed, { apps: forged }) });
  await tick(5);
  assert.equal(got[0].t, "state");
  assert.equal(JSON.stringify(hostRecord().apps), before, "same-seed put left sections alone");

  // clear + put: an empty bridge, expect null — still no way in
  forgeCollect({ t: "clear", v: 1, reqId: 66, expect: r.seed });
  await tick(5);
  assert.equal(hostRecord(), null);
  got = forgeCollect({ t: "put", v: 1, reqId: 67, expect: null,
    rec: recOf(SEED_A, { apps: forged }) });
  await tick(5);
  assert.equal(got[0].t, "state");
  assert.equal(hostRecord().seed, SEED_A);
  assert.equal(hostRecord().apps, null, "an empty bridge takes no sections from a put");
  assert.equal(got[0].rec.apps, null, "and the reply says so too");
  b.destroy();
});

// ---- finding 2: the cap belongs on what gets STORED --------------------------
test("put cap: the check is on the record that gets stored, not on the payload", async () => {
  world = makeWorld();
  const b = newBridge();
  const r = await b.boot();
  // a section pushes the stored record past the cap even though the incoming
  // rec (which arrives with apps:null) is comfortably under it
  forgeCollect({ t: "app", v: 1, reqId: 70, expect: r.seed,
    state: "x".repeat(MAX_APP_STATE_BYTES - 2) });
  await tick(5);
  const before = JSON.stringify(hostRecord());
  assert.ok(before.length < MAX_RECORD_BYTES);
  const got = forgeCollect({ t: "put", v: 1, reqId: 71, expect: r.seed,
    rec: recOf(r.seed, { soc: "s".repeat(120 * 1024) }) });
  await tick(5);
  assert.equal(got[0].t, "conflict", "the grafted-on sections count toward the cap");
  assert.equal(JSON.stringify(hostRecord()), before, "nothing stored");
  assert.ok(JSON.stringify(hostRecord()).length <= MAX_RECORD_BYTES);
  b.destroy();
});

// ---- finding 3: sections cannot spend the core's reserve ---------------------
test("section budget: two apps cannot brick soc, and the map has a hard ceiling", async () => {
  world = makeWorld();
  const b = newBridge();
  const r = await b.boot();
  // the old per-entry cap was half the record: two apps saturated it forever
  for (const o of ["https://game1.ardegazu.ro", "https://game2.ardegazu.ro"]) {
    const got = forgeCollect({ t: "app", v: 1, reqId: 80, expect: r.seed, state: "x".repeat(65534) }, o);
    await tick(5);
    assert.equal(got[0].t, "conflict", `${o}: 64 KiB section refused`);
    assert.equal(got[0].reason, "size");
  }
  // fill the sections budget with legal-size entries instead
  const big = "x".repeat(MAX_APP_STATE_BYTES - 2);
  let filled = 0;
  for (const label of ["game1", "game2", "game3", "game4", "game5", "game6", "chat", "board"]) {
    const got = forgeCollect({ t: "app", v: 1, reqId: 81, expect: r.seed, state: big },
      `https://${label}.ardegazu.ro`);
    await tick(5);
    if (got[0].t === "state") { filled += 1; continue; }
    assert.equal(got[0].reason, "full", `${label} hit the sections ceiling`);
    break;
  }
  assert.ok(filled >= 2, "the budget holds more than the two apps that used to fill it");
  const apps = JSON.stringify(hostRecord().apps);
  assert.ok(apps.length <= MAX_APPS_BYTES, "the apps map never exceeds its own budget");
  // …and the reserve is still there for the core fields
  let got = forgeCollect({ t: "soc", v: 1, reqId: 82, expect: r.seed, soc: { friends: ["fp1"] } });
  await tick(5);
  assert.equal(got[0].t, "state", "social-kit can always write its blob");
  got = forgeCollect({ t: "profile", v: 1, reqId: 83, expect: r.seed, patch: { name: "Ana" } });
  await tick(5);
  assert.equal(got[0].t, "state", "so can the profile");
  assert.equal(hostRecord().name, "Ana");
  b.destroy();
});

// ---- finding 4: a refusal says WHY -------------------------------------------
test("app refusals: cas / key / size / full are distinguishable", async () => {
  world = makeWorld();
  const b = newBridge();
  const r = await b.boot();
  const reasonOf = async (data, origin) => {
    const got = forgeCollect(data, origin);
    await tick(5);
    assert.equal(got[0].t, "conflict");
    return got[0].reason;
  };
  assert.equal(await reasonOf({ t: "app", v: 1, reqId: 90, expect: SEED_B, state: 1 }), "cas");
  assert.equal(
    await reasonOf({ t: "app", v: 1, reqId: 91, expect: r.seed, state: 1 },
      `https://${"a".repeat(33)}.ardegazu.ro`),
    "key", "an allowed origin with no derivable key");
  assert.equal(
    await reasonOf({ t: "app", v: 1, reqId: 92, expect: r.seed, state: 1 },
      "https://home.ardegazu.ro"),
    "key", "a reserved label derives no key either");
  assert.equal(
    await reasonOf({ t: "app", v: 1, reqId: 93, expect: r.seed, state: "x".repeat(20 * 1024) }),
    "size");
  const cyclic = {}; cyclic.self = cyclic;
  assert.equal(await reasonOf({ t: "app", v: 1, reqId: 94, expect: r.seed, state: cyclic }), "size",
    "unserializable counts as a size refusal");
  forgeCollect({ t: "soc", v: 1, reqId: 95, expect: r.seed, soc: "s".repeat(126 * 1024) });
  await tick(5);
  assert.equal(
    await reasonOf({ t: "app", v: 1, reqId: 96, expect: r.seed, state: "x".repeat(8 * 1024) }),
    "full");
  // the other arms stay exactly as they were: no reason field at all
  const got = forgeCollect({ t: "clear", v: 1, reqId: 97, expect: SEED_B });
  await tick(5);
  assert.equal(got[0].t, "conflict");
  assert.equal("reason" in got[0], false, "reason is additive, app-arm only");
  b.destroy();
});

test("putAppStateDetailed: the boolean API is unchanged, the reason is new", async () => {
  world = makeWorld();
  const b = newBridge();
  await b.boot();
  assert.deepEqual({ ...(await b.putAppStateDetailed({ ok: 1 })) }, { ok: true, reason: null });
  assert.equal(await b.putAppState({ ok: 2 }), true, "putAppState still resolves a boolean");
  const tooBig = await b.putAppStateDetailed("x".repeat(20 * 1024));
  assert.equal(tooBig.ok, false);
  assert.equal(tooBig.reason, "size", "a caller can stop retrying a size refusal");
  assert.equal(await b.putAppState("x".repeat(20 * 1024)), false, "…and still see a plain false");
  assert.deepEqual(b.lastRecord().apps.chat.state, { ok: 2 }, "the refused write changed nothing");
  b.destroy();

  // no seed / no bridge: decided locally, never a host round trip
  world = makeWorld({ hostAnswers: false });
  const b2 = newBridge({ timeoutMs: 40 });
  assert.equal((await b2.putAppStateDetailed(1)).reason, "noseed", "no identity yet");
  await b2.boot();
  assert.equal((await b2.putAppStateDetailed(1)).reason, "offline", "bridge unreachable");
  assert.equal(await b2.putAppState(1), false);
  b2.destroy();
});

// ---- reserved keys: home.* and dev.* must never claim someone else's section --
test("reserved labels: home.ardegazu.ro and dev.ardegazu.ro get no section at all", async () => {
  world = makeWorld();
  const b = newBridge();
  const r = await b.boot();
  // the apex and the dev origins own "home" and "dev" by construction
  forgeCollect({ t: "app", v: 1, reqId: 100, expect: r.seed, state: { real: "hub" } },
    "https://ardegazu.ro");
  await tick(5);
  forgeCollect({ t: "app", v: 1, reqId: 101, expect: r.seed, state: { real: "dev" } },
    "http://localhost:5173");
  await tick(5);
  for (const o of ["https://home.ardegazu.ro", "https://dev.ardegazu.ro"]) {
    const got = forgeCollect({ t: "app", v: 1, reqId: 102, expect: r.seed, state: { stolen: true } }, o);
    await tick(5);
    assert.equal(got[0].t, "conflict", `${o} cannot write`);
    assert.equal(got[0].reason, "key");
  }
  assert.deepEqual(hostRecord().apps.home.state, { real: "hub" }, "the apex keeps its section");
  assert.deepEqual(hostRecord().apps.dev.state, { real: "dev" }, "dev keeps its section");
  b.destroy();
});

test("app sections: a legacy nine-field put keeps them; a new seed starts empty", async () => {
  world = makeWorld();
  const b = newBridge();
  const r = await b.boot();
  forgeCollect({ t: "app", v: 1, reqId: 40, expect: r.seed, state: { dir: "x" } });
  await tick(5);
  // exactly what a pre-`apps` deployed client sends: nine fields, no apps
  const got = forgeCollect({ t: "put", v: 1, reqId: 41, expect: r.seed, rec: recOf(r.seed, { name: "Old" }) });
  await tick(5);
  assert.equal(got[0].t, "state");
  assert.equal(hostRecord().name, "Old");
  assert.deepEqual(hostRecord().apps.chat.state, { dir: "x" }, "old clients cannot wipe sections");
  assert.deepEqual(got[0].rec.apps.chat.state, { dir: "x" }, "the state reply carries them too");
  // a DIFFERENT seed is a different identity: its record starts sectionless
  forgeCollect({ t: "put", v: 1, reqId: 42, expect: r.seed, rec: recOf(SEED_B) });
  await tick(5);
  assert.equal(hostRecord().seed, SEED_B);
  assert.equal(hostRecord().apps, null, "sections do not follow a seed change");
  b.destroy();
});

test("app sections: every reader sees them, in replies and in pushes", async () => {
  world = makeWorld();
  const b = newBridge();
  const r = await b.boot();
  const pushed = [];
  b.onChange((rec) => pushed.push(rec));
  // another app writes; the hub-level storage event fans it out
  forgeCollect({ t: "app", v: 1, reqId: 50, expect: r.seed, state: { iban: "RO49" } },
    "https://banca.ardegazu.ro");
  await tick(5);
  // banca's request made IT the proven client; a real request from us takes
  // the push channel back (the host memoizes exactly one embedder)
  await b.putProfile({});
  assert.deepEqual(b.lastRecord().apps.banca.state, { iban: "RO49" }, "own replies carry every section");
  world.hostWin.dispatchStorage({ key: STORE_KEY });
  await tick();
  assert.equal(pushed.length, 1);
  assert.deepEqual(pushed[0].apps.banca.state, { iban: "RO49" }, "readers see other apps' sections");
  assert.deepEqual(b.lastRecord().apps.banca.state, { iban: "RO49" });
  b.destroy();
});

// ---- old v1 records without lang --------------------------------------------
test("legacy record without lang: adopt yields lang null, mirror JSON exact", async () => {
  world = makeWorld();
  world.hostLS.setItem(STORE_KEY, JSON.stringify({
    v: 1, seed: SEED_A, name: "Old", hue: 7, glyph: null, soc: { f: 1 },
    createdAt: 1700000000000, updatedAt: 1700000000001,
  }));
  const b = newBridge();
  const r = await b.boot();
  assert.equal(r.source, "bridge");
  assert.equal(r.profile.lang, null);
  assert.deepEqual(Object.keys(r.profile), ["name", "hue", "glyph", "lang"], "BootResult.profile shape");
  assert.equal(world.clientLS.getItem(`${NS}:profile`),
    JSON.stringify({ hue: 7, glyph: null, lang: null, ts: 1700000000001 }),
    "mirror profile JSON byte-exact, TS key order hue,glyph,lang,ts");
  b.destroy();
});

// ---- unreachable-bridge stickiness after a failed boot -----------------------
test("after an offline boot, putProfile still mirrors locally and resolves; putSoc false", async () => {
  world = makeWorld({ hostAnswers: false });
  const b = newBridge({ timeoutMs: 40 });
  const r = await b.boot();
  assert.equal(r.bridged, false);
  await b.putProfile({ name: "OfflineName", hue: 300 });
  assert.equal(world.clientLS.getItem(`${NS}:name`), "OfflineName", "mirror written despite dead bridge");
  assert.equal(JSON.parse(world.clientLS.getItem(`${NS}:profile`)).hue, 300);
  assert.equal(await b.putSoc({ x: 1 }), false, "putSoc reports failure");
  assert.equal(await b.publishSeed(SEED_B), false, "publishSeed reports failure");
  b.destroy();
});

// ---- ephemeral flag on replies ----------------------------------------------
test("private-mode host: state replies carry ephemeral:true", async () => {
  world = makeWorld();
  world.hostBroken = true;
  const b = newBridge();
  await b.boot();
  const states = world.hostReplies.filter((m) => m.t === "state");
  assert.ok(states.length >= 1);
  for (const s of states) assert.equal(s.ephemeral, true, "ephemeral flagged");
  // non-ephemeral world never sets the flag
  world = makeWorld();
  const b2 = newBridge();
  await b2.boot();
  for (const s of world.hostReplies.filter((m) => m.t === "state"))
    assert.equal("ephemeral" in s, false, "flag absent when storage works");
  b.destroy(); b2.destroy();
});

// ---- lastRecord quirk parity, the other half ---------------------------------
test("boot get on an occupied bridge DOES set lastRecord (only the put reply is skipped)", async () => {
  world = makeWorld();
  world.hostLS.setItem(STORE_KEY, JSON.stringify(recOf(SEED_A, { name: "Held" })));
  const b = newBridge();
  await b.boot();
  assert.equal(b.lastRecord().name, "Held", "get reply recorded");
  b.destroy();
});

// ---- publishSeed force with a never-bridged lastRec --------------------------
test("publishSeed force before any bridge contact: expect null -> conflicts on occupied bridge", async () => {
  world = makeWorld();
  world.hostLS.setItem(STORE_KEY, JSON.stringify(recOf(SEED_A)));
  const b = newBridge();
  // no boot: lastRec null; force -> expect null -> CAS fails on occupied bridge
  assert.equal(await b.publishSeed(SEED_B, { force: true }), false);
  assert.equal(hostRecord().seed, SEED_A, "unchanged");
  assert.equal(await b.publishSeed("not-a-seed", { force: true }), false, "seed regex gate");
  b.destroy();
});

// ---- mirror name clamping round trip -----------------------------------------
test("putProfile clamps on both sides: 99-char name -> 32 chars mirrored and stored", async () => {
  world = makeWorld();
  const b = newBridge();
  await b.boot();
  await b.putProfile({ name: "y".repeat(99), hue: 359.9, glyph: "🦊🐢", lang: "ROMANIAN" });
  assert.equal(world.clientLS.getItem(`${NS}:name`), "y".repeat(32));
  const rec = hostRecord();
  assert.equal(rec.name, "y".repeat(32));
  assert.equal(rec.hue, 359);
  assert.equal(rec.glyph, "🦊");
  assert.equal(rec.lang, null, "invalid lang collapses to null");
  b.destroy();
});

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