// `mapShardEvent` and the shard-event push fan-out, moved out of core's // pushDispatch.test.js in Phase 3 (MODULE_SYSTEM.md §2.7.1). // // Core keeps the push INFRASTRUCTURE and its tests — the SSRF guard on an ntfy // endpoint, and `publish()` sending a content-free tickle. What is here is the // CATALOG and the mapping into it: which shard event becomes which stream, which // kinds are owner-keyed, and which must never produce a public target. That last // one is a security boundary and it is module-owned by design (MODULE_API.md // §2.4) — the kinds, the streams and the public-safety filter are one file that // moves together. const { test } = require('node:test') const assert = require('node:assert/strict') const { fakeCtx } = require('./_fakes') require('../core').init(fakeCtx()) const { mapShardEvent, createTracker } = require('../config/shardStreams') const shardPush = require('../utils/shardPush') // Moved with the fromShardEvent tests. They set NTFY_* because the push // dispatcher they hand results to reads them — core's variables, read by core's // code, which is why this stays a plain env helper rather than becoming // something on ctx: the module never reads these itself. // ── mapShardEvent ─────────────────────────────────────────────────────────── test('server.hello / shutdown / crashed map to the public server.status stream', () => { const t = createTracker() assert.deepEqual(mapShardEvent({ kind: 'server.hello', bootId: 'b1' }, t), [ { streamId: 'server.status', ref: 'up:b1' }, ]) assert.deepEqual(mapShardEvent({ kind: 'server.shutdown' }, t), [{ streamId: 'server.status', ref: 'down' }]) assert.deepEqual(mapShardEvent({ kind: 'server.crashed' }, t), [{ streamId: 'server.status', ref: 'down' }]) }) test('house.decay INTO idoc yields the public idoc.warning AND the owner-keyed house.idoc', () => { const t = createTracker() const out = mapShardEvent({ kind: 'house.decay', to: 'IDOC', serial: '0x40', ownerAcct: 'bob' }, t) assert.deepEqual(out, [ { streamId: 'idoc.warning', ref: '0x40' }, { streamId: 'house.idoc', ref: '0x40', ownerAccount: 'bob' }, ]) // A non-IDOC decay stage produces nothing. assert.deepEqual(mapShardEvent({ kind: 'house.decay', to: 'Fairly', serial: '0x41' }, t), []) }) test('champ.update fires champ.start only on the inactive→active transition', () => { const t = createTracker() // First sight active → start. assert.deepEqual(mapShardEvent({ kind: 'champ.update', serial: 'c1', active: true }, t), [ { streamId: 'champ.start', ref: 'c1' }, ]) // Still active → no re-fire. assert.deepEqual(mapShardEvent({ kind: 'champ.update', serial: 'c1', active: true }, t), []) // Goes inactive, then active again → fires again. assert.deepEqual(mapShardEvent({ kind: 'champ.update', serial: 'c1', active: false }, t), []) assert.deepEqual(mapShardEvent({ kind: 'champ.update', serial: 'c1', active: true }, t), [ { streamId: 'champ.start', ref: 'c1' }, ]) }) test('city.update fires governor.election only on a real governor change, never on first sight', () => { const t = createTracker() // First sight of the city → no election (could be a reconnect snapshot). assert.deepEqual(mapShardEvent({ kind: 'city.update', city: 'Britain', governor: { serial: '0x1' } }, t), []) // Same governor → nothing. assert.deepEqual(mapShardEvent({ kind: 'city.update', city: 'Britain', governor: { serial: '0x1' } }, t), []) // New governor → election. assert.deepEqual(mapShardEvent({ kind: 'city.update', city: 'Britain', governor: { serial: '0x2' } }, t), [ { streamId: 'governor.election', ref: 'Britain' }, ]) }) test('personal streams are owner-keyed and sensitive kinds never yield a public target', () => { const t = createTracker() const sale = mapShardEvent({ kind: 'vendor.sale', ownerAcct: 'bob', t: 7 }, t) assert.deepEqual(sale, [{ streamId: 'vendor.sale', ref: '7', ownerAccount: 'bob' }]) const login = mapShardEvent({ kind: 'account.login.attempt', acct: 'bob', ip: '1.2.3.4', t: 9 }, t) assert.deepEqual(login, [{ streamId: 'account.login', ref: '9', ownerAccount: 'bob' }]) // Every personal target carries an ownerAccount (never a bare public push). for (const target of [...sale, ...login]) assert.ok(target.ownerAccount, 'personal target must be owner-keyed') // A truly sensitive, unmapped kind produces nothing at all. assert.deepEqual(mapShardEvent({ kind: 'cheat.fastwalk', acct: 'bob' }, t), []) assert.deepEqual(mapShardEvent({ kind: 'admin.audit', actor: 'staff' }, t), []) }) // ── fromShardEvent (owner resolution) ─────────────────────────────────────── // // **These two tests changed shape in the move, and the change is the boundary.** // In core they asserted through `publish()` into `pushDevices` and a captured // fetch — which endpoints were hit, how many requests went out. None of that is // this module's any more: `publish` is `ctx.push.publish`, core's, and the device // registry and the relay are behind it. Reaching for them from here would mean // reaching past `ctx`, which is exactly what §5.1 forbids. // // What remains is what the module actually owns, and it is the part worth // guarding: a game account resolves to a website user through `shardLinks`, a // personal target that resolves to nobody is dropped rather than published, and // a public target publishes with no owner. Core's own pushDispatch tests still // cover the fan-out on the other side of the seam. /** Record what the module asked core to publish. */ function capturePublish() { const calls = [] return { calls, publish: async (streamId, opts) => { calls.push({ streamId, ...opts }) } } } test('fromShardEvent resolves a personal event to the owning user, or drops it if unlinked', async () => { const { calls, publish } = capturePublish() const shardLinks = { getByAccount: async (acct) => (acct === 'mine' ? { userId: 42 } : null) } const deps = { shardLinks, publish, tracker: createTracker() } await shardPush.fromShardEvent({ kind: 'vendor.sale', ownerAcct: 'mine', t: 1 }, deps) assert.equal(calls.length, 1) assert.equal(calls[0].streamId, 'vendor.sale') assert.equal(calls[0].ownerUserId, 42, 'the game account must resolve to the website user') // Unlinked account → nobody to notify → nothing published. Not an error: a // player who never linked their account is the ordinary case, not a fault. calls.length = 0 await shardPush.fromShardEvent({ kind: 'vendor.sale', ownerAcct: 'stranger', t: 2 }, deps) assert.equal(calls.length, 0) }) test('fromShardEvent publishes a public shard event with no owner', async () => { const { calls, publish } = capturePublish() await shardPush.fromShardEvent( { kind: 'server.hello', bootId: 'b1' }, { publish, tracker: createTracker(), shardLinks: { getByAccount: async () => null } }, ) assert.equal(calls.length, 1) assert.equal(calls[0].streamId, 'server.status') assert.equal(calls[0].ownerUserId, undefined, 'a public target must not be owner-keyed') }) test('a sensitive kind never reaches publish at all', async () => { // The public-safety filter is module-internal by design (MODULE_API.md §2.4): // the kinds, the streams and the filter are one file that moves together, so // core never holds a rule about data only this module defines. Which makes // this the right side of the boundary for the test too. const { calls, publish } = capturePublish() const deps = { publish, tracker: createTracker(), shardLinks: { getByAccount: async () => null } } for (const kind of ['cheat.fastwalk', 'admin.audit']) { await shardPush.fromShardEvent({ kind, acct: 'bob', actor: 'staff' }, deps) } assert.equal(calls.length, 0) })