// The phone's half of the invite ceremony — unlocking the vault mint sealed.
//
// The PLC half is proven in atproto-identity (hosting-attach.test.js) and hyper-proto-peer
// (hosting-cli.test.mjs) and is not re-tested here. What this file covers is the part that only exists
// on a handset: finding the vault mint wrote, opening it with a passphrase, and getting back the key
// that can actually sign a PLC update.
//
// ⛔ THE ROUND TRIP IS THE POINT. mint seals; this opens. If those two ever disagree about where the
// vault lives or what it contains, the failure appears months later when someone is trying to recover an
// identity — the worst possible time to discover a filename mismatch.
//
// Run: bare test/hosting.test.mjs
import 'favela-web/lib/bare-globals.js'
import fs from 'bare-fs'
import os from 'bare-os'
import path from 'bare-path'
import { mintIdentity } from '../lib/mint.js'
import { vaultPathFor, resolveSigner } from '../lib/hosting.js'
import { sealVault } from 'atproto-identity/lib/vault.js'
import { deriveAtprotoKey, ATPROTO_RECOVERY_PATH } from 'atproto-identity'
let pass = 0
const ok = (c, m) => { if (!c) throw new Error('FAIL: ' + m); console.log(' ok -', m); pass++ }
const tmp = (n) => {
const d = path.join(os.tmpdir(), 'favela-hosting-' + n + '-' + Date.now())
fs.mkdirSync(d, { recursive: true })
return d
}
// ── the path agrees with what mint writes ────────────────────────────────────────────────────────
{
const p = vaultPathFor('/data/hpp', 'did:plc:zl5loetx46p6xusgc47h3mia')
ok(p === '/data/hpp/recovery/zl5loetx46p6xusgc47h3mia.vault.json', 'vault path matches mint\'s layout: ' + p)
// Per-DID, not a shared vault.json — a device may hold several identities and the last mint must not
// silently overwrite an earlier one's recovery secret.
ok(vaultPathFor('/d', 'did:plc:aaa') !== vaultPathFor('/d', 'did:plc:bbb'), 'two identities get two vaults')
let threw = null
try { vaultPathFor('/d', '') } catch (e) { threw = e }
ok(threw, 'a missing did is refused rather than producing a wrong path')
}
// ── mint seals; hosting opens; they must agree ───────────────────────────────────────────────────
{
const hpp = tmp('roundtrip')
const r = await mintIdentity({ configFile: path.join(hpp, 'config.json') })
const PASS = 'correct horse battery staple'
// Exactly what lib/node.js's mint handler does.
const dir = path.join(hpp, 'recovery')
fs.mkdirSync(dir, { recursive: true })
fs.writeFileSync(vaultPathFor(hpp, r.did), JSON.stringify(sealVault({ secret: r.rootMnemonic, passphrase: PASS, did: r.did })) + '\n')
// And exactly what the hosting handler does.
const envelope = JSON.parse(fs.readFileSync(vaultPathFor(hpp, r.did), 'utf8'))
const signer = resolveSigner({ vault: envelope, passphrase: PASS, did: r.did })
const expected = deriveAtprotoKey(r.rootMnemonic, ATPROTO_RECOVERY_PATH)
ok(signer.didKey === expected.didKey, 'the unlocked key is the RECOVERY key — the one a PLC update needs')
// ⛔ AND IT IS A ROTATION KEY OF THIS IDENTITY. Deriving *a* key proves nothing; the directory only
// accepts an update signed by a key already in the head op's rotationKeys.
ok(r.signedOp.rotationKeys.includes(signer.didKey), 'and it is in the identity\'s own rotationKeys, so the directory would accept it')
}
// ── the refusals ─────────────────────────────────────────────────────────────────────────────────
{
const hpp = tmp('refuse')
const r = await mintIdentity({ configFile: path.join(hpp, 'config.json') })
const envelope = sealVault({ secret: r.rootMnemonic, passphrase: 'right', did: r.did })
const bad = (fn, m) => { try { fn(); ok(false, m) } catch { ok(true, m) } }
bad(() => resolveSigner({ vault: envelope, passphrase: 'wrong', did: r.did }), 'a wrong passphrase does not unlock it')
bad(() => resolveSigner({ vault: envelope, passphrase: 'right', did: 'did:plc:someoneelse' }), 'nor does the wrong identity')
bad(() => resolveSigner({ vault: envelope, did: r.did }), 'nor no passphrase at all')
}
console.log('\n' + pass + ' assertions passed')