OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-identity / test / p2p-only-genesis.test.js

85 lines · javascript ref:
// A P2P-ONLY GENESIS — an identity that names nobody's infrastructure.
//
// Creating an identity and joining someone's infrastructure are different acts, and conflating them is
// what made minting undecidable: the genesis op carried a handle and a PDS endpoint, so you could not
// create a did:plc without first answering "whose server are you on?". The answer (hyper-social #54 §8)
// is that you should not have to. Genesis is P2P-only — no handle, no PDS — and accepting an invite
// from atproto-bridge or ap-bridge is a LATER PLC UPDATE op that adds the domain handle and the service
// endpoint. Creation stays free; joining stays a separate, reversible act.
//
// ⛔ WHY THIS IS NOT COSMETIC. A PLC genesis is a permanent append to a global log. Before this,
// buildGenesisOp unconditionally wrote `alsoKnownAs: ['at://' + handle]` and
// `services.atproto_pds = { endpoint: pdsUrl }`, so minting without them produced the literal strings
// `at://undefined` and an endpoint of `undefined` — for ever, unfixable, in public.
//
// The directory does not require either field: checkGenesisOp (lib/plc-directory.js, the local mirror of
// the directory's §6.2 validation) asks only for a plc_operation with prev:null, non-empty rotationKeys,
// a verificationMethods.atproto, and a signature by one of its own rotation keys. So the shape below is
// valid — it just could not be built.

import test from 'brittle'
import { buildGenesisOp, signOp, createSwarmIdentity, deriveAtprotoKey, ATPROTO_SIGNING_PATH, ATPROTO_RECOVERY_PATH } from '../lib/atproto-identity.js'
import { checkGenesisOp } from '../lib/plc-directory.js'

const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
const HANDLE = 'someone.example.com'
const PDS = 'https://pds.example.com'

test('a genesis with no handle and no PDS omits both fields entirely', (t) => {
  const signing = deriveAtprotoKey(MNEMONIC, ATPROTO_SIGNING_PATH)
  const op = buildGenesisOp({ signingDidKey: signing.didKey })

  t.alike(op.alsoKnownAs, [], 'alsoKnownAs is empty — no handle is claimed')
  t.alike(op.services, {}, 'services is empty — no PDS is named')
  t.is(op.verificationMethods.atproto, signing.didKey, 'the signing key is still there — this is a real identity')
  t.is(op.prev, null, 'still a genesis')
})

test('⛔ never mints the string "undefined" into a permanent public log', (t) => {
  const signing = deriveAtprotoKey(MNEMONIC, ATPROTO_SIGNING_PATH)
  const encoded = JSON.stringify(buildGenesisOp({ signingDidKey: signing.didKey }))
  t.absent(encoded.includes('undefined'), 'no "undefined" anywhere in the op')
  t.absent(encoded.includes('at://undefined'), 'specifically not the handle')
})

test('the P2P-only genesis is VALID — the directory asks for neither field', (t) => {
  const recovery = deriveAtprotoKey(MNEMONIC, ATPROTO_RECOVERY_PATH)
  const signing = deriveAtprotoKey(MNEMONIC, ATPROTO_SIGNING_PATH)
  const unsigned = buildGenesisOp({ signingDidKey: signing.didKey, rotationDidKeys: [recovery.didKey] })
  const signedOp = { ...unsigned, sig: signOp(unsigned, recovery.privateKey) }

  const chk = checkGenesisOp(signedOp, null)
  t.ok(chk.ok, 'passes the directory\'s genesis validation: ' + (chk.error || 'ok'))
  t.ok(chk.did && chk.did.startsWith('did:plc:'), 'and yields a real did:plc — ' + chk.did)
})

test('createSwarmIdentity mints P2P-only when given neither', (t) => {
  const id = createSwarmIdentity(MNEMONIC)
  t.alike(id.signedOp.alsoKnownAs, [], 'no handle')
  t.alike(id.signedOp.services, {}, 'no PDS')
  t.ok(checkGenesisOp(id.signedOp, id.did).ok, 'and the op the phone would submit is valid')
  // The custody split still holds — this is the property the whole revocation story rests on.
  const recovery = deriveAtprotoKey(MNEMONIC, ATPROTO_RECOVERY_PATH)
  t.alike(id.signedOp.rotationKeys, [recovery.didKey], 'rotation authority is still the root recovery key alone')
})

// ── the invite is what adds them, later ──────────────────────────────────────────────────────────
// Nothing here submits anything; this only fixes what the op CAN say. But the DID must differ from the
// fronted one, or "P2P-only then join" would be indistinguishable from "join at birth" — and the whole
// point is that the first is a decision you have not yet made.
test('naming infrastructure at genesis produces a DIFFERENT identity', (t) => {
  const free = createSwarmIdentity(MNEMONIC)
  const fronted = createSwarmIdentity(MNEMONIC, { handle: HANDLE, pdsUrl: PDS })
  t.not(free.did, fronted.did, 'the DID is the hash of the op, so the two are different identities')
})

// ── regression: the existing shape must be untouched ─────────────────────────────────────────────
// Every identity already minted was built by this function. If passing a handle and PDS stopped
// producing byte-identical ops, every existing DID would fail to re-derive from its own mnemonic.
test('given a handle and PDS, the op is exactly what it always was', (t) => {
  const signing = deriveAtprotoKey(MNEMONIC, ATPROTO_SIGNING_PATH)
  const op = buildGenesisOp({ signingDidKey: signing.didKey, handle: HANDLE, pdsUrl: PDS })
  t.alike(op.alsoKnownAs, ['at://' + HANDLE], 'handle unchanged')
  t.alike(op.services, { atproto_pds: { type: 'AtprotoPersonalDataServer', endpoint: PDS } }, 'PDS unchanged')
})