OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-identity / test / hosting-attach.test.js

102 lines · javascript ref:
// Attaching an identity to hosting — and detaching it again.
//
// This is the second half of the split in hyper-social #54 §8. Genesis is P2P-only: an identity names
// nobody's infrastructure and needs nobody's permission to exist. Accepting an invite from a membrane
// (pds.7rnx.net) or a fediverse bridge (ap.7rnx.net) is a LATER PLC UPDATE that adds a domain handle and
// a service endpoint. Creation stays free; joining stays a separate, deliberate, REVERSIBLE act.
//
// ⛔ THE BRIDGE CANNOT DO THIS FOR YOU, AND THAT IS THE POINT. A PLC update must be signed by a CURRENT
// rotation key (checkUpdateOp verifies against the head op's rotationKeys), and createSwarmIdentity puts
// exactly one there: the root recovery key, which never leaves its owner. So an operator can grant
// infrastructure and can front a DID, but cannot alter someone's identity document. The ceremony is
// two-sided by construction, not by policy.
//
// Built as a primitive rather than a script because an atproto-bridge web UI should be able to drive it:
// the UI issues the invite and knows the handle + endpoint, the HOLDER signs. These functions are the
// holder's half, and they are offline — nothing here touches the network.
//
// Run: brittle-node test/hosting-attach.test.js
import test from 'brittle'
import { createSwarmIdentity, deriveAtprotoKey, ATPROTO_RECOVERY_PATH, ATPROTO_SIGNING_PATH } from '../lib/atproto-identity.js'
import { checkUpdateOp } from '../lib/plc-directory.js'
import { attachHosting, detachHosting } from '../lib/hosting.js'

const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
const HANDLE = 'flaky.7rnx.net'
const PDS = 'https://pds.7rnx.net'

const recovery = () => deriveAtprotoKey(MNEMONIC, ATPROTO_RECOVERY_PATH)
const p2pOnly = () => createSwarmIdentity(MNEMONIC) // no handle, no pds — what mint produces

test('attaching adds the handle and the PDS, and nothing else changes', (t) => {
  const id = p2pOnly()
  const op = attachHosting({ prevSignedOp: id.signedOp, handle: HANDLE, pdsUrl: PDS, signerPrivateKey: recovery().privateKey })

  t.alike(op.alsoKnownAs, ['at://' + HANDLE], 'the handle is claimed')
  t.is(op.services.atproto_pds.endpoint, PDS, 'the PDS endpoint is named')
  t.is(op.services.atproto_pds.type, 'AtprotoPersonalDataServer', 'with the right service type')

  // A PLC op REPLACES the whole document state — it is not a patch. Carrying the rest forward is the
  // whole correctness requirement, and getting it wrong silently drops keys.
  t.alike(op.rotationKeys, id.signedOp.rotationKeys, 'rotation authority is carried forward untouched')
  t.alike(op.verificationMethods, id.signedOp.verificationMethods, 'the signing key is carried forward')
  t.ok(typeof op.prev === 'string' && op.prev.length > 0, 'it chains onto the genesis (CID verified by checkUpdateOp below)')
})

test('the op the holder produces is one the directory will accept', (t) => {
  const id = p2pOnly()
  const op = attachHosting({ prevSignedOp: id.signedOp, handle: HANDLE, pdsUrl: PDS, signerPrivateKey: recovery().privateKey })
  const chk = checkUpdateOp(op, id.signedOp)
  t.ok(chk.ok, 'chains onto the genesis and is signed by a current rotation key: ' + (chk.error || 'ok'))
})

test('⛔ refuses a signer that is not a current rotation key', (t) => {
  const id = p2pOnly()
  // The SIGNING key. It authors commits and has zero PLC authority — device-node.js is explicit that a
  // device never holds the root. Signing an update with it produces an op the directory rejects, and
  // failing here beats discovering that against a permanent public log.
  const signing = deriveAtprotoKey(MNEMONIC, ATPROTO_SIGNING_PATH)
  t.exception(() => attachHosting({ prevSignedOp: id.signedOp, handle: HANDLE, pdsUrl: PDS, signerPrivateKey: signing.privateKey }),
    /rotation key/i, 'the #atproto signing key cannot rewrite the identity document')
})

test('⛔ refuses a no-op — a PLC append is permanent and public', (t) => {
  const id = p2pOnly()
  const once = attachHosting({ prevSignedOp: id.signedOp, handle: HANDLE, pdsUrl: PDS, signerPrivateKey: recovery().privateKey })
  t.exception(() => attachHosting({ prevSignedOp: once, handle: HANDLE, pdsUrl: PDS, signerPrivateKey: recovery().privateKey }),
    /already/i, 're-attaching identical hosting is refused rather than appended')
})

test('joining is REVERSIBLE — detaching returns it to P2P-only', (t) => {
  const id = p2pOnly()
  const joined = attachHosting({ prevSignedOp: id.signedOp, handle: HANDLE, pdsUrl: PDS, signerPrivateKey: recovery().privateKey })
  const left = detachHosting({ prevSignedOp: joined, signerPrivateKey: recovery().privateKey })

  t.alike(left.alsoKnownAs, [], 'the handle is released')
  t.alike(left.services, {}, 'and the PDS is no longer named')
  t.ok(checkUpdateOp(left, joined).ok, 'and the directory would accept it')
  t.alike(left.verificationMethods, id.signedOp.verificationMethods, 'the identity itself is unchanged — same keys, same DID')
})

test('other services survive both directions', (t) => {
  // A community DID exports a feed generator and a labeler. Attaching a PDS must not silently drop them,
  // and detaching the PDS must not take them with it.
  const id = createSwarmIdentity(MNEMONIC, { feedGenEndpoint: 'https://feeds.example', labelerEndpoint: 'https://labels.example' })
  const joined = attachHosting({ prevSignedOp: id.signedOp, handle: HANDLE, pdsUrl: PDS, signerPrivateKey: recovery().privateKey })
  t.is(joined.services.bsky_fg.endpoint, 'https://feeds.example', 'feed generator survives attach')
  t.is(joined.services.atproto_labeler.endpoint, 'https://labels.example', 'labeler survives attach')

  const left = detachHosting({ prevSignedOp: joined, signerPrivateKey: recovery().privateKey })
  t.is(left.services.bsky_fg.endpoint, 'https://feeds.example', 'and survives detach')
  t.absent(left.services.atproto_pds, 'while the PDS is gone')
})

test('moving to a different host is one op, not two', (t) => {
  const id = p2pOnly()
  const a = attachHosting({ prevSignedOp: id.signedOp, handle: HANDLE, pdsUrl: PDS, signerPrivateKey: recovery().privateKey })
  const b = attachHosting({ prevSignedOp: a, handle: 'flaky.example.org', pdsUrl: 'https://pds.example.org', signerPrivateKey: recovery().privateKey })
  t.alike(b.alsoKnownAs, ['at://flaky.example.org'], 'the new handle replaces the old')
  t.is(b.services.atproto_pds.endpoint, 'https://pds.example.org', 'and the new endpoint replaces the old')
  t.ok(checkUpdateOp(b, a).ok, 'chained onto the previous update, not the genesis')
})