OverviewFilesCommitsIssuesDocsReleasesPeers

ap-bridge / test / registrations.test.js

129 lines · javascript ref:
// registrations.test.js — who may have a name on this bridge.
//
// ⛔ A BRIDGE IS AN OWNED RESOURCE, NOT A COMMONS, and that is the whole reason this differs from
// favela's NameRegistry. There, `opAuthorised` binds release to the claim's own publisher, so a
// squatted name is permanent — filed as pear-git #43 after hyper-social #59 §3 found it. A bridge
// runs on somebody's domain, so the operator owns the namespace under it and gets the arbiter the
// commons cannot have. First come, first served; either the holder OR the operator may release.
//
// ⛔ AND A RELEASE MUST BURN THE IDENTITY, not just free the string. AP object ids are
// `/ap/users/<name>/…`. If alice registers, federates fifty posts, releases, and bob then claims
// `alice`, bob inherits alice's actor URI and every one of her object ids — and every remote server
// that cached her now follows him. That is impersonation by name reuse, and the only thing that
// stops it is refusing to hand a reused name the previous holder's audience.
//
// Registration policy (operator's decision, hyper-social #65 §8 preserved):
//   open    request + chain proof  → registered
//   invite  bridge issues invite → invite + chain proof → registered
// Control is proved the same way in both. Open registration replaces the PER-USER invite with a
// STANDING POLICY consent; it never drops the control half, which is what §8 forbids.

import test from 'brittle'
import fs from 'bare-fs'
import { join } from 'bare-path'
import { Registrations, MemoryRegistrations } from '../lib/registrations.js'

const ROOT_A = 'a'.repeat(64)
const ROOT_B = 'b'.repeat(64)
let n = 0
const tmp = () => join('/tmp/claude-1000/-storage-projects-code-hypercore-kb/c3f731e9-2bec-4052-9d87-28d0c39fbacf/scratchpad', 'reg' + (n++) + '-' + Date.now(), 'reg.json')

test('★★ open registration: a name is first come, first served', async (t) => {
  const r = new MemoryRegistrations({ policy: 'open' })

  t.alike(await r.claim({ username: 'alice', root: ROOT_A }), { ok: true, username: 'alice', generation: 1 })
  const second = await r.claim({ username: 'alice', root: ROOT_B })
  t.absent(second.ok, 'the second claimant is refused')
  t.is(second.error, 'taken')
  t.is((await r.get('alice')).root, ROOT_A, 'and the first still holds it')
})

test('⛔ invite-only registration refuses a bare request', async (t) => {
  const r = new MemoryRegistrations({ policy: 'invite' })

  const bare = await r.claim({ username: 'alice', root: ROOT_A })
  t.absent(bare.ok)
  t.is(bare.error, 'invite required', 'closed means closed — the bridge must send one')

  const token = await r.invite()
  t.ok(token, 'the operator can mint one')
  t.ok((await r.claim({ username: 'alice', root: ROOT_A, invite: token })).ok)
})

test('⛔ an invite is single-use', async (t) => {
  const r = new MemoryRegistrations({ policy: 'invite' })
  const token = await r.invite()

  t.ok((await r.claim({ username: 'alice', root: ROOT_A, invite: token })).ok)
  const reuse = await r.claim({ username: 'bob', root: ROOT_B, invite: token })
  t.absent(reuse.ok, 'a second name cannot ride the same invite')
})

test('★ the holder may release their own name', async (t) => {
  const r = new MemoryRegistrations({ policy: 'open' })
  await r.claim({ username: 'alice', root: ROOT_A })

  t.absent((await r.release('alice', { root: ROOT_B })).ok, 'a stranger cannot')
  t.ok((await r.release('alice', { root: ROOT_A })).ok, 'the holder can')
  t.absent(await r.get('alice'))
})

// The arbiter favela's registry deliberately lacks. The operator owns the domain, so they own the
// names under it — otherwise a squatted name on your own bridge is permanent.
test('★ the operator may release any name', async (t) => {
  const r = new MemoryRegistrations({ policy: 'open' })
  await r.claim({ username: 'alice', root: ROOT_A })

  t.ok((await r.release('alice', { operator: true })).ok)
  t.absent(await r.get('alice'))
})

// ⛔ THE ONE THAT MATTERS. Freeing the string must not hand the next holder the last one's audience.
test('⛔⛔ a reused name does NOT inherit the previous holder\'s identity', async (t) => {
  const r = new MemoryRegistrations({ policy: 'open' })
  await r.claim({ username: 'alice', root: ROOT_A })
  await r.release('alice', { operator: true })

  const again = await r.claim({ username: 'alice', root: ROOT_B })
  t.ok(again.ok, 'the name is free again')

  const rec = await r.get('alice')
  t.is(rec.root, ROOT_B)
  t.ok(rec.generation > 1, 'and it is a NEW generation (' + rec.generation + ')')
  t.not(rec.generation, 1, 'so ids derived from it cannot collide with the previous holder\'s')
})

test('⛔ a burned name records that it was burned', async (t) => {
  const r = new MemoryRegistrations({ policy: 'open' })
  await r.claim({ username: 'alice', root: ROOT_A })
  await r.release('alice', { operator: true })

  const tomb = await r.released('alice')
  t.ok(tomb, 'the release is remembered, not forgotten')
  t.is(tomb.root, ROOT_A, 'including WHO held it')
  // Absence would make "never registered" and "registered then released" identical, and the second
  // is the one a federated peer may still hold references into.
})

test('★ registrations survive a restart', async (t) => {
  const path = tmp()
  await new Registrations({ path, policy: 'open' }).claim({ username: 'alice', root: ROOT_A })

  const reopened = new Registrations({ path, policy: 'open' })
  t.is((await reopened.get('alice')).root, ROOT_A)
  t.absent((await reopened.claim({ username: 'alice', root: ROOT_B })).ok, 'still taken after a restart')
})

test('⛔ a name must be a usable path segment', async (t) => {
  const r = new MemoryRegistrations({ policy: 'open' })
  for (const bad of ['', 'a/b', '.', '..', 'A B', null, 'x'.repeat(200)]) {
    t.absent((await r.claim({ username: bad, root: ROOT_A })).ok, JSON.stringify(bad))
  }
})

test('⛔ a claim without an identity root is refused', async (t) => {
  const r = new MemoryRegistrations({ policy: 'open' })
  t.absent((await r.claim({ username: 'alice' })).ok, 'open registration still proves CONTROL')
  t.absent((await r.claim({ username: 'alice', root: 'not-a-root' })).ok)
})