OverviewFilesCommitsIssuesDocsReleasesPeers

keet-daemon / test / statebase-writer-admission.test.mjs

87 lines · javascript ref:
// (A) admission: a peer proves control of its keet ROOM writer key (manifest + sig);
// the indexer derives the device key, verifies the sig, and resolves device→member→role.
import { StateBaseV3 } from '../lib/state-base-v3.js'
import createTestnet from 'hyperdht/testnet'
import Corestore from 'corestore'
import Autobase from 'autobase'
import crypto from 'hypercore-crypto'
import b4a from 'b4a'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'

const ROOT = path.join(os.tmpdir(), 'sbadm-' + Bare.pid)
try { fs.rmSync(ROOT, { recursive: true, force: true }) } catch {}
const hex = (b) => b4a.toString(b, 'hex')
let pass = 0, fail = 0
const ok = (c, m) => { if (c) { pass++; console.log('  ok  ' + m) } else { fail++; console.log('  FAIL ' + m) } }
const sleep = (ms) => new Promise(r => setTimeout(r, ms))

// a "keet room writer" = keypair + manifest + deviceKey (deviceKey = Hypercore.key(manifest))
async function keetWriter (dir) {
  const store = new Corestore(dir)
  const base = new Autobase(store, null, { apply: async () => {}, open: (s) => s.get('v'), valueEncoding: 'json' })
  await base.ready()
  return { keyPair: base.local.keyPair, manifest: base.local.manifest, deviceKey: base.local.key }
}

async function main () {
  const testnet = await createTestnet(3)
  const bootstrap = testnet.bootstrap
  const roomSeed = crypto.randomBytes(32)

  const wA = await keetWriter(path.join(ROOT, 'wA'))
  const wD = await keetWriter(path.join(ROOT, 'wD'))
  const wE = await keetWriter(path.join(ROOT, 'wE')) // NOT a member

  // member keys (keet per-room member identities), lex pick creator
  const mA = crypto.randomBytes(32), mD = crypto.randomBytes(32)
  // roster A projects: member -> role + that member's keet writer device keys
  const roster = [
    { key: hex(mA), role: 1, devices: [hex(wA.deviceKey)] },
    { key: hex(mD), role: 3, devices: [hex(wD.deviceKey)] }
  ]
  const amCreatorKey = (hex(mA) < hex(mD)) ? mA : mD // lex-lowest admin = creator; mA is the only admin
  const opt = (selfKey, w) => ({
    roomSeed, encryptionKey: roomSeed, network: true, swarmOpts: { bootstrap },
    selfKey, getMembers: async () => roster, roomWriterKeyPair: w.keyPair, roomWriterManifest: w.manifest
  })

  // A = admin/creator
  const A = new StateBaseV3(path.join(ROOT, 'A'), crypto.randomBytes(32), opt(mA, wA))
  await A.ready()
  ok(A.writable, '1. A (admin) opens writable = creator')
  await A.projectMembers(roster)

  // D = peer member, joins A's room
  const D = new StateBaseV3(path.join(ROOT, 'D'), A._roomKey, opt(mD, wD))
  await D.ready()
  ok(!D.writable, '2. D (peer) opens NOT writable')

  // D writes -> optimistic manifest proof -> A admits via device->member->role
  await D.set('world:players:D', { pos: [1, 2, 3] })
  const deadline = Date.now() + 30000
  while (Date.now() < deadline) {
    if (A.writable) { try { await A.set('__hb__', Date.now()) } catch {} }
    await A.base.update().catch(() => {}); await D.base.update().catch(() => {})
    await sleep(200)
    const got = await A.list('world:players:')
    if (got['world:players:D']) break
  }
  const aSees = await A.list('world:players:')
  ok(aSees['world:players:D'] != null, '3. D ADMITTED via keet-writer manifest proof (write in A view)')

  // NEGATIVE: E is not in the roster -> admission must be denied
  const E = new StateBaseV3(path.join(ROOT, 'E'), A._roomKey, opt(crypto.randomBytes(32), wE))
  await E.ready()
  await E.set('world:players:E', { pos: [9, 9, 9] })
  const d2 = Date.now() + 12000
  while (Date.now() < d2) { await A.base.update().catch(() => {}); await E.base.update().catch(() => {}); await sleep(200) }
  const aSees2 = await A.list('world:players:')
  ok(aSees2['world:players:E'] == null, '4. E (NOT a member) DENIED — write never lands in A view')

  console.log('\n[sb-admission] pass=' + pass + ' fail=' + fail)
  Bare.exit(fail ? 1 : 0)
}
main().catch(e => { console.error('THREW', e && e.stack ? e.stack : e); Bare.exit(2) })