// ⛔ WHICH NETWORKS THIS IDENTITY PUBLISHES TO — a preference, stored in its own feed.
//
// ⚠ NOT a bridge, and not knowledge of one. `favela-markdown/lib/reach.js` defines
// NETWORKS = ['favela', 'bsky', 'fediverse'] and favela-peer already depends on it, so these are
// favela's OWN network names. What the peer must never hold is the projection — the schemas, the
// signing, the wire format — and it holds none of that. `boundary.test.js` is the arbiter and it
// still passes.
//
// ⛔ IN THE FEED, not on disk. A person who turns on bsky publishing on their laptop has said
// something about their identity, not about that laptop, so it has to replicate to their phone the
// way a profile does. It is the same shape as setProfile for the same reason.
//
// ⛔ OFF BY DEFAULT, AND OFF MEANS OFF. An absent record must never read as "everything enabled":
// publication is the one setting where a missing value has to fail closed.
import test from 'brittle'
import fs from '#node:fs'
import os from '#node:os'
import { join } from '#node:path'
import Corestore from 'corestore'
import { RecordStore } from '../lib/record-store.js'
import { Feed } from '../lib/feed-base.js'
import { Peer } from '../lib/peer.js'
import { identity } from './helpers.js'
const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'favela-reach-'))
async function mkPeer () {
const id = await identity()
const d = await id.device()
const f = new Feed(new Corestore(tmp()), null, { store: new RecordStore(tmp()) })
await f.ready()
await f.addDevice(d.bind(f.deviceKey))
await f.update()
return new Peer(f).ready()
}
test('⛔ a node publishes NOWHERE until it is told to', async (t) => {
const p = await mkPeer()
t.teardown(() => p.close())
const r = await p.reach()
t.alike(r.networks, [], 'no record means no publication, not all publication')
})
test('★ turning a network on, and off again', async (t) => {
const p = await mkPeer()
t.teardown(() => p.close())
await p.setReach({ networks: ['bsky'] })
t.alike((await p.reach()).networks, ['bsky'])
await p.setReach({ networks: ['bsky', 'fediverse'] })
t.alike((await p.reach()).networks.sort(), ['bsky', 'fediverse'])
// ⛔ An empty list is a real answer — "publish nowhere" — and must be storable. If it were treated
// as "unset" there would be no way to turn everything off once anything was on.
await p.setReach({ networks: [] })
t.alike((await p.reach()).networks, [])
})
test('⛔ an unknown network is REFUSED, not stored', async (t) => {
// Storing one would put a name in the feed that nothing can resolve to a bridge, and it would sit
// there looking enabled.
const p = await mkPeer()
t.teardown(() => p.close())
await t.exception(() => p.setReach({ networks: ['bsky', 'twitter'] }), /twitter|network/i)
t.alike((await p.reach()).networks, [], 'and nothing was written')
})
test('⛔ favela cannot be switched off — it is the substrate, not a destination', async (t) => {
// reach.js §5.4: "local is the substrate; there is no way to opt out of it". Accepting it here
// would imply a post could exist without existing locally.
const p = await mkPeer()
t.teardown(() => p.close())
await t.exception(() => p.setReach({ networks: ['favela'] }), /favela|substrate/i)
})
test('★★ the designated credential travels with it', async (t) => {
// identity.md §5.1: a user has at most ONE credential marked for atproto, and with none there is
// no projection. The peer stores WHICH identifier is designated; it does not know what the
// identifier means or hold anything that can sign with it.
const p = await mkPeer()
t.teardown(() => p.close())
await p.setReach({ networks: ['bsky'], credential: 'did:example:abc' })
const r = await p.reach()
t.is(r.credential, 'did:example:abc')
// Clearing it is how you stop projecting under an identifier without forgetting the preference.
await p.setReach({ credential: null })
t.is((await p.reach()).credential, null)
t.alike((await p.reach()).networks, ['bsky'], 'and the networks are untouched')
})
test('⚠ setReach merges — setting one field does not clear the other', async (t) => {
const p = await mkPeer()
t.teardown(() => p.close())
await p.setReach({ networks: ['bsky'], credential: 'did:example:abc' })
await p.setReach({ networks: ['fediverse'] })
t.is((await p.reach()).credential, 'did:example:abc', 'the credential survived a networks-only write')
})
test('⛔ set-reach requires an ATTESTED device, like posting does', async (t) => {
// Turning on a network publishes under this identity. That is an identity act, so it sits behind
// the same gate as a post — not behind the one that lets a browser change a display name.
const { dispatch } = await import('../lib/rpc.js')
const src = fs.readFileSync(new URL('../lib/rpc.js', import.meta.url).pathname, 'utf8')
const set = src.slice(src.indexOf('const MUTATIONS'), src.indexOf('async function requireLinked'))
t.ok(set.includes("'set-reach'"), 'set-reach is a mutation')
t.absent(set.includes("'set-profile'"), 'set-profile is not — a self-label asserts nothing')
t.ok(typeof dispatch === 'function')
})
// ⛔ WHICH BRIDGE CARRIES THE NAME TRAVELS WITH THE IDENTITY (hyper-social #82, I1). bridges.json is per DEVICE, so a
// second device of the same identity — or any device after a registration that predates bridges.json — could not
// tell which bridge to ask for its observation feed, and needed a manual re-claim. The bridge key is a public swarm
// address, so it rides in reach beside the name it carries, and every device of the identity learns it.
test('★ the bridge that carries the fediverse name is stored beside it, and merges like every other field', async (t) => {
const p = await mkPeer()
t.teardown(() => p.close())
const KEY = 'fe'.repeat(32)
await p.setReach({ fediverse: 'fleeky@ap.7rnx.net', fediverseBridge: KEY })
let r = await p.reach()
t.is(r.fediverse, 'fleeky@ap.7rnx.net')
t.is(r.fediverseBridge, KEY)
await p.setReach({ networks: ['fediverse'] })
t.is((await p.reach()).fediverseBridge, KEY, 'survives a networks-only write')
await p.setReach({ fediverseBridge: null })
r = await p.reach()
t.is(r.fediverseBridge, null, 'and can be cleared')
t.is(r.fediverse, 'fleeky@ap.7rnx.net', 'without touching the name')
})
test('⛔ a fediverseBridge that is not a 64-hex key is refused', async (t) => {
const p = await mkPeer()
t.teardown(() => p.close())
await t.exception(() => p.setReach({ fediverseBridge: 'not-a-key' }), /fediverseBridge/)
t.is((await p.reach()).fediverseBridge, null)
})