// Booting the atproto identity — the step that makes a device a WRITER of its DID.
//
// ⛔ WHY THIS EXISTS. The worklet reached exactly two hpp entry points, mint and submit, so a phone
// could create an identity, publish it to the global directory, and then author nothing under it.
// The swarm replicates a DID's repo to every device that holds it; a device that never boots the DID
// holds nothing. `repair-store-backend.js` was copied into the app assuming this boot existed — and
// had no callers, which is how it stayed missing.
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, bootAtproto, atprotoConfigPath } from '../index.js'
const PASS = 'a passphrase long enough'
function fixture (t, name) {
const root = join(os.tmpdir(), 'fh-boot-' + 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
const realCfg = process.env.PEER_CONFIG
const realOverlay = process.env.ATPROTO_OVERLAY_ROOT
process.env.HOME = home
t.teardown(() => {
process.env.HOME = realHome
// ⚠ Assigned, never deleted: bare-process's env is a proxy whose deleteProperty trap refuses,
// throwing "trap returned falsish". An empty string is what the code under test treats as unset.
process.env.PEER_CONFIG = realCfg || ''
process.env.ATPROTO_OVERLAY_ROOT = realOverlay || ''
})
const node = { storage }
// A booted node watches for posts to project; a test must not leave that running past its hpp.
t.teardown(() => { try { if (node.projection) node.projection.stop() } catch {} })
return { storage, node, client: inProcessClient(node) }
}
test('a node with no identity boots no atproto, and says so by returning null', async (t) => {
const { node } = fixture(t, 'none')
t.is(await bootAtproto(node), null, 'not a failure — a node with no DID is the normal state')
})
test('after minting, the DID boots and this device becomes a writer of it', async (t) => {
const { node, client } = fixture(t, 'up')
const r = await client.call('did-mint', { passphrase: PASS })
const said = []
const hpp = await bootAtproto(node, { log: (m, d) => said.push([m, d]) })
t.teardown(async () => { try { await hpp.close() } catch {} })
t.ok(hpp, 'it booted')
t.is(hpp.nodes.length, 1, 'exactly the one identity that was minted')
// ⛔ Reported, because favela-android printed the CONFIG's count and said "signing enabled" while
// zero nodes had come up — an hour lost diagnosing an empty appview when there was no node at all.
const up = said.find(([m]) => m === 'atproto up')
t.ok(up, 'it reported what BOOTED')
t.is(up[1].dids, 1)
// The one value another machine needs to admit this phone to the DID's roster, and unreadable from
// adb on a release-signed app.
const key = said.find(([m]) => m === 'atproto device key')
t.ok(key && /^[0-9a-f]{64}$/.test(key[1].devicePublicKey), 'and named this device: ' + (key && key[1].devicePublicKey || 'MISSING'))
t.is(key[1].did, r.did)
})
test('it points hpp at the config under node storage, not a home directory', async (t) => {
const { node, client } = fixture(t, 'cfgpath')
await client.call('did-mint', { passphrase: PASS })
const hpp = await bootAtproto(node)
t.teardown(async () => { try { await hpp.close() } catch {} })
t.is(process.env.PEER_CONFIG, atprotoConfigPath(node), 'PEER_CONFIG is the file our mint wrote')
t.absent(fs.existsSync(join(process.env.HOME, '.config', 'hyper-proto-peer', 'config.json')),
'and nothing was read from or written to $HOME')
})
// ⛔ ONE APP ON A MACHINE THAT ALREADY RUNS hyper-proto-peer. The servers hold wj62 and loser-club in
// hpp's own config and data (~/.config/hyper-proto-peer). A desktop app that booted them from
// `<favela storage>/atproto` instead would open a SECOND store for the same devices — a second writer for
// the same identity, which is a fork (hyper-social #77 §5, change 1). So the embedder can hand bootAtproto
// hpp's existing config and storage, and every atproto op on the node then reads that same config.
test('given hpp\'s own config and storage, it boots THOSE and writes nothing under node storage', async (t) => {
const { node } = fixture(t, 'shared')
const hppHome = join(os.tmpdir(), 'fh-hpp-' + Date.now() + '-' + Math.random().toString(16).slice(2))
const configFile = join(hppHome, 'config.json')
const storage = join(hppHome, 'data')
fs.mkdirSync(storage, { recursive: true })
node.atprotoConfigFile = configFile // what boot() sets from the embedder's options
const client = inProcessClient(node)
const r = await client.call('did-mint', { passphrase: PASS })
t.ok(fs.existsSync(configFile), 'the mint wrote hpp\'s config, not one under node storage')
const hpp = await bootAtproto(node, { configFile, storage })
t.teardown(async () => { try { await hpp.close() } catch {} })
t.is(hpp && hpp.nodes.length, 1, 'booted the identity from hpp\'s config')
t.ok(fs.readdirSync(storage).length > 0, 'hpp\'s data directory is the one in use')
t.absent(fs.existsSync(join(node.storage, 'atproto', 'config.json')), 'no second config under node storage')
t.absent(fs.existsSync(join(node.storage, 'atproto', 'statebase')), 'no second store under node storage')
t.is((await client.call('did-status')).did, r.did, 'did-status reads the same config')
})
// ⛔ THE PHONE NEVER PROJECTED A POST ON ITS OWN. The desktop entry started watchProjection after boot;
// the Android worklet boots through bootAtproto too and never did, so a post from the phone reached bsky
// only when someone pressed publish in Settings (2026-10-14). The watch now starts HERE, the one boot
// both shells share, so neither can be without it.
test('booting the identity starts the bsky projection watch — one pass at boot', async (t) => {
const { node, client } = fixture(t, 'watch')
await client.call('did-mint', { passphrase: PASS })
const runs = []
const hpp = await bootAtproto(node, { project: async (n, h, opts) => { runs.push(opts.send) } })
t.teardown(async () => { try { await hpp.close() } catch {} })
t.ok(node.projection && node.projection.watching, 'the node is watching for posts to project')
await new Promise((resolve) => setTimeout(resolve, 50))
t.alike(runs, [true], 'and ran one SENDING pass at boot')
})
test('a node with no identity starts no watch', async (t) => {
const { node } = fixture(t, 'nowatch')
await bootAtproto(node)
t.absent(node.projection, 'nothing to project, nothing watching')
})