// ROOM-CONTRACT v2 (hyper-social/docs/ROOM-CONTRACT.md) — every member is its own node with its own writer
// core, admitted by a real invitation, so every check here is authority-by-carrier, never a signed payload.
// §6's list, one test each, plus the v1 behaviours that still hold. The replay gate is room-replay.test.js.
import test from 'brittle'
import Corestore from 'corestore'
import b4a from 'b4a'
import crypto from 'hypercore-crypto'
import fs from '#node:fs'
import os from '#node:os'
import { join } from '#node:path'
import { Room, ADMIN, MODERATOR, PEER, READ_ONLY, OP_VERSION, canonicalOpBytes } from '../lib/room.js'
import { signAttested } from '../lib/attestation.js'
import { identity } from './helpers.js'
const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'favela-room-'))
async function who () {
const id = await identity()
return { root: id.rootHex, device: await id.device(), id }
}
const until = async (fn, ms = 8000) => {
const end = Date.now() + ms
while (Date.now() < end) { const v = await fn(); if (v) return v; await new Promise((resolve) => setTimeout(resolve, 30)) }
return null
}
// A room and its founder; `add` brings in another member on its own node through a real invitation.
async function world (t) {
const founder = await who()
const host = new Room(new Corestore(tmp()).namespace('room'), null)
await host.ready()
await host.found(founder.device)
await host.update()
const nodes = [host]
const streams = []
t.teardown(async () => { for (const s of streams) s.destroy(); for (const n of nodes) await n.close() })
const settle = async () => { for (let i = 0; i < 6; i++) { for (const n of nodes) await n.update(); await new Promise((resolve) => setTimeout(resolve, 20)) } }
// ⛔ A MESH, as on the room's topic: every node connects to every other. A star (everyone to the
// founder only) hides a joiner's optimistic block from nodes it is not connected to — it is not a
// writer yet — so a moderator could never see the request it is supposed to answer.
const node = async () => {
const r = new Room(new Corestore(tmp()).namespace('room'), host.key)
await r.ready()
for (const other of nodes) {
const s1 = other.base.replicate(true); const s2 = r.base.replicate(false)
s1.pipe(s2).pipe(s1)
streams.push(s1, s2)
}
nodes.push(r)
await until(async () => { await r.update(); return !!(await r.founder()) })
return r
}
// A member at `role`, joined by an invitation `by` mints (default: the founder's node).
const add = async (role = PEER, { by = host, person = null } = {}) => {
const p = person || await who()
const r = await node()
const { token } = await by.invite({ role })
await settle()
await r.join(token, p.device)
await until(async () => { await settle(); return (await host.roleOf(p.root)) !== null })
// ⛔ The joiner's weight is anchored by the next block of a weighted member AFTER the join is applied
// (ROOM-CONTRACT §4) — an append that lands before the host has seen the join anchors nothing.
await host.setSetting('title', 'room')
await settle()
return { ...p, room: r }
}
return { founder, host, node, add, settle }
}
test('★ founding: the creator is an ADMIN member, its writer core is the device, weight 2', async (t) => {
const { founder, host } = await world(t)
t.alike((await host.members()).map((m) => [m.root, m.role]), [[founder.root, ADMIN]])
t.alike((await host.devices(founder.root)).map((d) => d.writer), [host.writer], 'keyed by the carrying writer core')
const w = await host.base.system.get(host.base.local.key)
t.is(w.maxWeight, 2, 'admin device at weight 2')
})
test('★ an invitation admits a member at its role; a peer device is granted weight 1 (§6.2)', async (t) => {
const { host, add, settle } = await world(t)
const p = await add(PEER)
t.is(await host.roleOf(p.root), PEER)
await p.room.post('hello', null)
await settle()
t.ok((await host.messages()).some((m) => m.body === 'hello' && m.author === p.root), 'and they can write')
const w = await host.base.system.get(p.room.base.local.key)
t.is(w.maxWeight, 1, 'a peer device carries weight 1, not 2')
})
test('⛔ a stranger\'s core writes nothing', async (t) => {
const { host, node, settle } = await world(t)
const stranger = await node()
await stranger.base.append(b4a.from(JSON.stringify({ t: 'message', v: OP_VERSION, name: 'x', body: 'spam' })), { optimistic: true })
await settle()
t.is((await host.messages()).length, 0)
})
test('⛔ READ-ONLY cannot post, but can flag', async (t) => {
const { host, add, settle } = await world(t)
const r = await add(READ_ONLY)
await r.room.post('should not land', null)
await r.room.flag('message-x', 'spam')
await settle()
t.is((await host.messages()).length, 0, 'no post')
t.is((await host.flags('message-x')).length, 1, 'the flag is recorded')
})
test('⛔ roles: a moderator manages peers but cannot touch or grant admin (§6.3)', async (t) => {
const { founder, host, add, settle } = await world(t)
const mod = await add(MODERATOR)
const p = await add(PEER)
await mod.room.setRole(p.root, READ_ONLY)
await settle()
t.is(await host.roleOf(p.root), READ_ONLY, 'moderator demotes a peer')
await mod.room.setRole(p.root, ADMIN)
await mod.room.removeMember(founder.root)
await settle()
t.is(await host.roleOf(p.root), READ_ONLY, 'a moderator cannot grant admin')
t.is(await host.roleOf(founder.root), ADMIN, 'a moderator removing an admin is ignored')
})
test('⛔ the last admin can be neither demoted nor leave; another admin may leave (§6.3, §6.4)', async (t) => {
const { founder, host, add, settle } = await world(t)
await host.setRole(founder.root, PEER)
await host.removeMember(founder.root)
await settle()
t.is(await host.roleOf(founder.root), ADMIN, 'still the admin')
const a2 = await add(PEER)
await host.setRole(a2.root, ADMIN)
await settle()
t.is(await host.roleOf(a2.root), ADMIN, 'promoted')
await a2.room.removeMember(a2.root)
await settle()
t.is(await host.roleOf(a2.root), null, 'an admin leaving, with another admin present, is accepted')
})
test('⛔ removed for good; left can rejoin (§6.5)', async (t) => {
const { host, add, node, settle } = await world(t)
const gone = await add(PEER)
await host.removeMember(gone.root)
await settle()
t.is(await host.roleOf(gone.root), null, 'removed')
t.is((await host.devices(gone.root)).length, 0, 'and their devices with them')
// A valid invitation does not bring a removed member back.
const { token } = await host.invite({ role: PEER })
await settle()
const again = await node()
await again.join(token, gone.device)
await host.setSetting('title', 'x'); await settle()
t.is(await host.roleOf(gone.root), null, 'join aborts for a removed member')
// add-device aborts too.
const third = await node()
await third.addDevice(gone.device)
await settle()
t.is(await host.roleOf(gone.root), null, 'add-device aborts for a removed member')
const leaver = await add(PEER)
await leaver.room.removeMember(leaver.root)
await settle()
t.is(await host.roleOf(leaver.root), null, 'left')
await add(PEER, { person: leaver })
t.is(await host.roleOf(leaver.root), PEER, 'a member who LEFT can come back with an invitation')
})
test('⛔ muted: message, flag and invite ignored; unmuted: accepted (§6.6)', async (t) => {
const { host, add, settle } = await world(t)
const p = await add(PEER)
await host.setStatus(p.root, { muted: true })
await settle()
await p.room.post('muted', null)
await p.room.flag('t1', 'x')
const { id } = await p.room.invite({ role: PEER })
await settle()
t.is((await host.messages()).length, 0, 'no message')
t.is((await host.flags('t1')).length, 0, 'no flag')
t.absent((await host.invites()).some((i) => i.id === id), 'no invitation')
await host.setStatus(p.root, { muted: false })
await settle()
await p.room.post('unmuted', null)
await settle()
t.ok((await host.messages()).some((m) => m.body === 'unmuted'), 'unmuted: accepted')
})
test('⛔ banned: cannot post or invite; the ban can be lifted (decided 2026-10-13)', async (t) => {
const { host, add, settle } = await world(t)
const p = await add(PEER)
await host.setStatus(p.root, { banned: true })
await settle()
await p.room.post('banned', null)
await settle()
t.is((await host.messages()).length, 0)
t.is(await host.roleOf(p.root), PEER, 'still a member — a ban is not a removal')
await host.setStatus(p.root, { banned: false })
await settle()
await p.room.post('back', null)
await settle()
t.ok((await host.messages()).some((m) => m.body === 'back'), 'lifted')
})
test('⛔ uninvite revokes: a later join with it is ignored (§6.7)', async (t) => {
const { host, node, settle } = await world(t)
const p = await who()
const { id, token } = await host.invite({ role: PEER })
await host.uninvite(id)
await settle()
const r = await node()
await r.join(token, p.device)
await host.setSetting('title', 'x'); await settle()
t.is(await host.roleOf(p.root), null)
})
test('⛔ a join whose attestation names another writer than its carrier is ignored (§6.8)', async (t) => {
const { host, node, settle } = await world(t)
const p = await who()
const { token } = await host.invite({ role: PEER })
await settle()
const r = await node()
const sk = b4a.from(token, 'hex')
const elsewhere = b4a.alloc(32, 7) // a writer key that is NOT r's core
const op = { t: 'join', v: OP_VERSION, invite: b4a.toString(sk.subarray(32), 'hex'), bound: signAttested(elsewhere, p.device), at: Date.now() }
op.sig = b4a.toString(crypto.sign(canonicalOpBytes(op), sk), 'hex')
await r.base.append(b4a.from(JSON.stringify(op)), { optimistic: true })
await host.setSetting('title', 'x'); await settle()
t.is(await host.roleOf(p.root), null)
})
test('delete-message: a non-author peer is ignored; the author or a moderator tombstones it (§6.9)', async (t) => {
const { host, add, settle } = await world(t)
const a = await add(PEER)
const b = await add(PEER)
const mod = await add(MODERATOR)
const n1 = await a.room.post('one', null)
const n2 = await a.room.post('two', null)
await settle()
await b.room.deleteMessage(n1)
await settle()
t.ok((await host.messages()).find((m) => m.name === n1).body === 'one', 'another peer cannot delete it')
await a.room.deleteMessage(n1)
await mod.room.deleteMessage(n2)
await settle()
const byName = Object.fromEntries((await host.messages()).map((m) => [m.name, m]))
t.ok(byName[n1].deleted && byName[n1].by === a.root && byName[n1].body === undefined, 'author: tombstone, body dropped')
t.ok(byName[n2].deleted && byName[n2].by === mod.root, 'moderator: tombstone')
})
test('an admin grant on an invitation works once; the second join gets the plain role (§6.10)', async (t) => {
const { host, node, settle } = await world(t)
const { token } = await host.invite({ role: ADMIN })
await settle()
const p1 = await who(); const p2 = await who()
const r1 = await node(); const r2 = await node()
await r1.join(token, p1.device)
await host.setSetting('title', 'a'); await settle()
await r2.join(token, p2.device)
await host.setSetting('title', 'b'); await settle()
await until(async () => { await settle(); return (await host.roleOf(p2.root)) !== null })
t.is(await host.roleOf(p1.root), ADMIN)
t.is(await host.roleOf(p2.root), PEER)
})
test('an invitation requiring confirmation waits; a peer cannot accept, a moderator can (decided 2026-10-13)', async (t) => {
const { host, add, node, settle } = await world(t)
const peer = await add(PEER)
const mod = await add(MODERATOR)
const p = await who()
const { token } = await host.invite({ role: PEER, requiresConfirmation: true })
await settle()
const r = await node()
await r.join(token, p.device)
await host.setSetting('title', 'x'); await settle()
t.is(await host.roleOf(p.root), null, 'nothing granted yet')
const [req] = await host.requests()
t.ok(req && req.root === p.root, 'a pending request')
// A moderator answers what its OWN node has seen.
t.ok(await until(async () => { await settle(); return (await mod.room.requests()).length === 1 }), 'the moderator\'s node sees it')
await peer.room.acceptJoin(req.writer)
await settle()
t.is(await host.roleOf(p.root), null, 'a peer cannot accept')
await mod.room.acceptJoin(req.writer)
await settle()
t.is(await host.roleOf(p.root), PEER, 'a moderator can')
})
test('settings: a closed set, moderators only; isJoiningDisabled stops joins', async (t) => {
const { host, add, node, settle } = await world(t)
const p = await add(PEER)
await p.room.setSetting('title', 'peer-set')
await host.setSetting('not-a-setting', 'x')
await host.setSetting('title', 'admin-set')
await settle()
t.alike(await host.settings(), { title: 'admin-set' })
await host.setSetting('isJoiningDisabled', 'true')
const { token } = await host.invite({ role: PEER })
await settle()
const q = await who()
const r = await node()
await r.join(token, q.device)
await host.setSetting('title', 'y'); await settle()
t.is(await host.roleOf(q.root), null)
})
test('★ listings are a moderator act; a peer cannot list', async (t) => {
const { host, add, settle } = await world(t)
const p = await add(PEER)
const other = b4a.toString(crypto.randomBytes(32), 'hex')
await p.room.listRoom(other, 'nope')
await settle()
t.is((await host.listings()).length, 0)
await host.listRoom(other, 'a bar')
await settle()
t.alike((await host.listings()).map((l) => l.room), [other])
await host.delistRoom(other)
await settle()
t.is((await host.listings()).length, 0)
})
test('⛔ one member cannot overwrite — or reattribute — another member\'s message; their own, they can', async (t) => {
const { host, add, settle } = await world(t)
const a = await add(PEER)
const b = await add(PEER)
await a.room.post('mine', null, 'the-name')
await settle()
await b.room.post('hijack', null, 'the-name')
await settle()
let m = (await host.messages()).find((x) => x.name === 'the-name')
t.is(m.author, a.root); t.is(m.body, 'mine')
await a.room.post('mine, revised', null, 'the-name')
await settle()
m = (await host.messages()).find((x) => x.name === 'the-name')
t.is(m.body, 'mine, revised')
})
test('★ a DM: founded with the other person as an initial member; their node binds its core with add-device', async (t) => {
const founder = await who()
const other = await who()
const host = new Room(new Corestore(tmp()).namespace('room'), null)
await host.ready()
await host.found(founder.device, { initialMembers: [{ root: other.root, role: PEER }] })
await host.update()
const r = new Room(new Corestore(tmp()).namespace('room'), host.key)
await r.ready()
const s1 = host.base.replicate(true); const s2 = r.base.replicate(false)
s1.pipe(s2).pipe(s1)
t.teardown(async () => { s1.destroy(); s2.destroy(); await host.close(); await r.close() })
t.is(await host.roleOf(other.root), PEER, 'a member before any device')
await until(async () => { await r.update(); return !!(await r.founder()) })
await r.addDevice(other.device)
await host.setSetting('title', 'DM')
await until(async () => { await host.update(); await r.update(); return (await host.devices(other.root)).length === 1 })
await r.post('hi', null)
t.ok(await until(async () => { await host.update(); await r.update(); return (await host.messages()).some((m) => m.body === 'hi') }), 'and they can write')
})
test('remove-device: only the member owning the device', async (t) => {
const { host, add, settle } = await world(t)
const a = await add(PEER)
const b = await add(PEER)
await b.room.removeDevice(a.room.writer)
await settle()
t.is((await host.devices(a.root)).length, 1, 'another member cannot remove it')
await a.room.removeDevice(a.room.writer)
await settle()
t.is((await host.devices(a.root)).length, 0, 'the owner can')
})
test('⛔ a v1 op is ignored (OP_VERSION 2)', async (t) => {
const { host, settle } = await world(t)
await host.base.append(b4a.from(JSON.stringify({ t: 'message', v: 1, name: 'old', body: 'v1' })))
await settle()
t.is((await host.messages()).length, 0)
})