// Accepting a bridge's handle — the step that makes a minted DID resolvable at all.
//
// ⛔ WHY IT WAS MISSING AND WHY THAT MATTERED. A mint is P2P-only by design: `buildGenesisOp` writes
// `alsoKnownAs: []` and `services: {}` when given no handle or PDS, and hpp's mint says both "arrive
// LATER, as a PLC update op, when an invite from atproto-bridge or ap-bridge is accepted". Nothing
// performed that op, so a published DID had NO server for anyone to fetch a repo from — it could be
// minted, published to the global directory, and still followable by nobody.
//
// ⚠ The submit itself is not tested: it is a second permanent append to a live global log, and a
// test identity is forever. What IS tested is every refusal in front of it, which is where a mistake
// would be silent rather than loud.
import test from 'brittle'
import fs from 'bare-fs'
import os from 'bare-os'
import process from 'bare-process'
import { join } from 'bare-path'
import { inProcessClient, recoveryVaultPath } from '../index.js'
const PASS = 'a passphrase long enough'
function fixture (t, name) {
const root = join(os.tmpdir(), 'fh-adopt-' + name + '-' + Date.now() + '-' + Math.random().toString(16).slice(2))
const home = join(root, 'home')
const storage = join(root, 'storage')
fs.mkdirSync(home, { recursive: true })
fs.mkdirSync(storage, { recursive: true })
const realHome = process.env.HOME
process.env.HOME = home
t.teardown(() => { process.env.HOME = realHome })
return { storage, node: { storage }, client: inProcessClient({ storage }) }
}
const ok = { passphrase: PASS, handle: 'alice.7rnx.net', pdsUrl: 'https://pds.7rnx.net' }
test('⛔ it refuses without the passphrase — the rotation key is the only thing that may change a DID', async (t) => {
const { client } = fixture(t, 'nopass')
await client.call('did-mint', { passphrase: PASS })
await t.exception(() => client.call('did-adopt', { ...ok, passphrase: '' }), /passphrase/)
})
test('⛔ a wrong passphrase is refused, and says nothing about which half was wrong', async (t) => {
const { client } = fixture(t, 'badpass')
await client.call('did-mint', { passphrase: PASS })
await t.exception(() => client.call('did-adopt', { ...ok, passphrase: 'not it' }), /could not open the recovery vault/)
})
test('⛔ a node with no identity has nothing to adopt onto', async (t) => {
const { client } = fixture(t, 'noid')
await t.exception(() => client.call('did-adopt', ok), /no identity/)
})
test('⛔ garbage handles and non-https endpoints are refused before anything irreversible', async (t) => {
const { client } = fixture(t, 'garbage')
await client.call('did-mint', { passphrase: PASS })
await t.exception(() => client.call('did-adopt', { ...ok, handle: 'not a handle' }), /usable handle/)
await t.exception(() => client.call('did-adopt', { ...ok, pdsUrl: 'http://insecure.example' }), /https/)
await t.exception(() => client.call('did-adopt', { ...ok, pdsUrl: '' }), /https/)
})
// ⛔ The vault is what holds the rotation key. Without it this device can hold a DID and sign its
// records and still be unable to change the identity — which is the correct, documented split, and
// the message has to say so rather than failing as a missing file.
test('⛔ a device with no recovery vault says why it cannot do this', async (t) => {
const { node, client } = fixture(t, 'novault')
const r = await client.call('did-mint', { passphrase: PASS })
fs.unlinkSync(recoveryVaultPath(node, r.did))
await t.exception(() => client.call('did-adopt', ok), /no recovery vault/)
})