OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web / test / handles.test.mjs

119 lines · javascript ref:
// handle resolution — bidirectional verification, and refusing to render an unverified claim.
// Run: node test/handles.test.mjs
//
// The assertion that matters is the IMPERSONATION case: a DID that claims someone else's handle must
// not render as that person. alsoKnownAs is a claim by the DID, not proof, and our inbound lens retains
// records from arbitrary foreign DIDs discovered via a third-party index — so a UI that trusts the claim
// is an impersonation surface by construction.
import { createHandleResolver, claimedHandle, didFromTxt, displayHandle } from '../lib/handles.js'

let pass = 0
const ok = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log('  ok -', m); pass++ }

const DID = 'did:plc:y5caotnrle4kxlfez4azaf33'
const HANDLE = 'fleeky.bsky.social'
const OTHER = 'did:plc:impostor00000000000000'

// ── claim extraction ─────────────────────────────────────────────────────────────────────────────
ok(claimedHandle(['at://fleeky.bsky.social']) === HANDLE, 'at:// prefix stripped')
ok(claimedHandle(['at://P2P.7RNX.NET']) === 'p2p.7rnx.net', 'handles are lowercased — they are domain names')
ok(claimedHandle(['https://example.com', 'at://a.b']) === 'a.b', 'non-at:// entries are skipped; alsoKnownAs is a list of arbitrary URIs')
ok(claimedHandle([]) === null && claimedHandle(null) === null, 'absent is null')
ok(claimedHandle(['at://nodot']) === null, 'a handle must be a domain name')
ok(claimedHandle(['at://a.b/path']) === null, 'a URI with a path is not a handle')

// ── DNS TXT parsing ──────────────────────────────────────────────────────────────────────────────
ok(didFromTxt([['did=' + DID]]) === DID, 'chunked TXT arrays are joined before matching')
ok(didFromTxt(['did=' + DID]) === DID, 'flat strings work too')
ok(didFromTxt([['v=spf1 -all'], ['did=' + DID]]) === DID, 'the did= record is picked out of an unrelated set')
ok(didFromTxt([['notadid']]) === null, 'no did= record is null')
ok(didFromTxt(null) === null, 'garbage is null')

// ── verification: HTTPS well-known ───────────────────────────────────────────────────────────────
const plcFor = (did, aka) => ({ getData: async () => ({ did, alsoKnownAs: aka }) })
const okText = (t) => ({ ok: true, text: async () => t })

{
  let url = null
  const r = createHandleResolver({
    plc: plcFor(DID, ['at://' + HANDLE]),
    doFetch: async (u) => { url = u; return okText(DID) }
  })
  const got = await r.resolve(DID)
  ok(got.verified === true && got.via === 'well-known', 'a well-known that returns our DID verifies')
  ok(url === `https://${HANDLE}/.well-known/atproto-did`, 'probing the handle, not the DID')
  ok(displayHandle(got) === HANDLE, 'and a verified handle is what the UI shows')
}

// ── verification: DNS TXT (what *.bsky.social actually uses) ─────────────────────────────────────
{
  let queried = null
  const r = createHandleResolver({
    plc: plcFor(DID, ['at://' + HANDLE]),
    doFetch: async () => { throw new Error('no well-known here') },
    resolveTxt: async (n) => { queried = n; return [['did=' + DID]] }
  })
  const got = await r.resolve(DID)
  ok(got.verified === true && got.via === 'dns', 'DNS TXT verifies when the well-known path is absent')
  ok(queried === `_atproto.${HANDLE}`, 'querying _atproto.<handle>')
  ok(displayHandle(got) === HANDLE, 'shown')
}
{
  // Implementing only HTTPS would fail every bsky.social handle — half the network rendered invalid.
  const r = createHandleResolver({
    plc: plcFor(DID, ['at://' + HANDLE]),
    doFetch: async () => { throw new Error('unreachable') }
  })
  const got = await r.resolve(DID)
  ok(got.verified === false, 'with NO dns resolver wired, a DNS-backed handle cannot verify')
  ok(displayHandle(got) === 'handle.invalid', 'and it must not be rendered as the claim')
}

// ── THE IMPERSONATION CASE ───────────────────────────────────────────────────────────────────────
{
  // An impostor DID whose document claims someone else's handle. Both mechanisms answer with the REAL
  // owner's DID, which does not match the claimant.
  const r = createHandleResolver({
    plc: plcFor(OTHER, ['at://' + HANDLE]),
    doFetch: async () => okText(DID), // well-known says the handle belongs to the REAL did
    resolveTxt: async () => [['did=' + DID]] // so does DNS
  })
  const got = await r.resolve(OTHER)
  ok(got.handle === HANDLE, 'the claim is reported...')
  ok(got.verified === false, '...but NOT verified — the handle resolves back to a different DID')
  ok(displayHandle(got) === 'handle.invalid',
    'so the UI shows handle.invalid, never the claim: rendering it would let anyone appear as anyone')
}

// ── failure and cache behaviour ──────────────────────────────────────────────────────────────────
{
  const r = createHandleResolver({ plc: plcFor(DID, []), doFetch: async () => okText(DID) })
  ok((await r.resolve(DID)) === null, 'a DID with no handle claim resolves to null')
  ok(displayHandle(null) === null, 'and the UI falls back (to a short DID) rather than inventing a name')
}
{
  let calls = 0
  const r = createHandleResolver({
    plc: { getData: async () => { calls++; return { did: DID, alsoKnownAs: ['at://' + HANDLE] } } },
    doFetch: async () => okText(DID)
  })
  await r.resolve(DID); await r.resolve(DID)
  ok(calls === 1, 'resolution is cached — a timeline of N posts by one author must not be N lookups')
}
{
  let calls = 0
  const r = createHandleResolver({
    plc: { getData: async () => { calls++; throw new Error('502') } },
    doFetch: async () => okText(DID)
  })
  ok((await r.resolve(DID)) === null, 'a directory failure is null, not a throw — a name lookup must never break a render')
  await r.resolve(DID)
  ok(calls === 1, 'and negatives are cached briefly so a dead DID cannot hammer the directory per post')
}
{
  const r = createHandleResolver({ plc: plcFor(DID, ['at://' + HANDLE]), doFetch: async () => okText(DID) })
  ok((await r.resolve('not-a-did')) === null, 'non-DIDs resolve to null')
}

console.log(`\nHANDLES OK: ${pass} assertions`)