import assert from 'node:assert/strict'; import { test } from 'node:test'; import { isPureWant } from '../src/gateway/git.ts'; import { memoryCache } from '../src/gateway/index.ts'; import { handleRequest, type RouterEnv } from '../src/router.ts'; import { upstream } from '../src/upstream.ts'; const ALICE = 'did:plc:aaaaaaaaaaaaaaaaaaaaaaaa'; const REPO = 'did:plc:rrrrrrrrrrrrrrrrrrrrrrrr'; const CID1 = 'bafyreie5737gdxlw5i64vzichcalba3z2v5n6icifvx5xytvske7mr3hpm'; const CID2 = 'bafyreib2rxk3rybk3aobmv5cjuql3bm2twh4jo5uxgf5x5hhvugd5rj2sq'; const BLOB = 'bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku'; const SHA = 'a'.repeat(40); /** The network the gateway sees: a PDS, PLC, Slingshot, Constellation, DNS, and a knot, each a * function tests can swap, with every request logged as `host path?query`. */ function world() { const calls: string[] = []; let record: { cid: string; value: unknown } = { cid: CID1, value: { text: 'first' } }; const services: Record Response | Promise> = { 'slingshot.test': (url) => { if (url.pathname === '/xrpc/blue.microcosm.identity.resolveMiniDoc') { const id = url.searchParams.get('identifier'); return id === ALICE || id === 'alice.test' ? Response.json({ did: ALICE, handle: 'alice.test', pds: 'https://pds.test' }) : Response.json({ error: 'NotFound' }, { status: 400 }); } if (url.pathname === '/xrpc/blue.microcosm.repo.getRecordByUri') { return Response.json({ uri: url.searchParams.get('at_uri'), cid: record.cid, value: record.value }); } return new Response('{}', { status: 404 }); }, 'pds.test': (url) => { switch (url.pathname) { case '/xrpc/com.atproto.repo.getRecord': { const uri = `at://${url.searchParams.get('repo')}/${url.searchParams.get('collection')}/${url.searchParams.get('rkey')}`; // Like some PDSes, this one ignores `cid` and answers with the current version. return Response.json({ uri, cid: record.cid, value: record.value }); } case '/xrpc/com.atproto.repo.listRecords': return Response.json({ records: [{ uri: `at://${ALICE}/dev.nibling.project/3la1`, cid: record.cid, value: record.value }] }); case '/xrpc/com.atproto.repo.describeRepo': return Response.json({ did: ALICE, handle: 'alice.test', collections: ['dev.nibling.project'] }); case '/xrpc/com.atproto.sync.getBlob': return new Response(new TextEncoder().encode(''), { headers: { 'content-type': 'text/html' } }); default: return new Response('{}', { status: 404 }); } }, 'plc.directory': (url) => { const did = url.pathname.slice(1); if (did === ALICE) { return Response.json({ id: ALICE, alsoKnownAs: ['at://alice.test'], service: [{ id: '#atproto_pds', type: 'AtprotoPersonalDataServer', serviceEndpoint: 'https://pds.test' }], }); } if (did === REPO) { return Response.json({ id: REPO, service: [{ id: '#tangled_knot', type: 'TangledKnot', serviceEndpoint: 'https://knot.test' }], }); } return new Response('not found', { status: 404 }); }, 'cloudflare-dns.com': (url) => url.searchParams.get('name') === '_atproto.alice.test' ? Response.json({ Answer: [{ data: `"did=${ALICE}"` }] }) : Response.json({ Answer: [] }), 'constellation.test': (url) => url.pathname.endsWith('getBacklinksCount') ? Response.json({ total: 3 }) : Response.json({ total: 1, records: [{ did: ALICE, collection: 'dev.nibling.feed.like', rkey: '3l' }] }), 'knot.test': async (url, init) => { if (url.pathname === `/${REPO}/info/refs`) { return new Response('001e# service=git-upload-pack\n0000', { headers: { 'content-type': 'application/x-git-upload-pack-advertisement' } }); } if (url.pathname === `/${REPO}/git-upload-pack`) { const body = new TextDecoder().decode(init.body as Uint8Array); if (body.includes('b'.repeat(40))) return new Response('0019ERR not our ref bbbb\n', { headers: { 'content-type': 'application/x-git-upload-pack-result' } }); return new Response('0008NAK\nPACK', { headers: { 'content-type': 'application/x-git-upload-pack-result' } }); } return new Response('nope', { status: 404 }); }, }; const fetch = upstream(async (input: string | URL | Request, init?: RequestInit) => { const url = new URL(input instanceof Request ? input.url : input.toString()); calls.push(`${url.host} ${url.pathname}${decodeURIComponent(url.search)}`); const service = services[url.host]; if (!service) throw new TypeError(`fetch failed: ${url.host}`); return service(url, init ?? {}); }); return { calls, services, setRecord: (next: typeof record) => (record = next), fetch, }; } function gateway() { const net = world(); let clock = Date.parse('2026-11-06T00:00:00.000Z'); const pending: Promise[] = []; const env: RouterEnv = { INDEX: { getByName: () => ({ getRecent: async () => ({ projects: [] }), getTrending: async () => ({ projects: [] }), getKnown: async () => [] }) }, APP_ORIGIN: 'https://nibling.run', SLINGSHOT_URL: 'https://slingshot.test', CONSTELLATION_URL: 'https://constellation.test', fetch: net.fetch, cache: memoryCache(() => clock), waitUntil: (promise) => void pending.push(promise), now: () => clock, }; return { net, tick: (ms: number) => (clock += ms), /** Runs a request, then waits for the cache writes and refreshes it started. */ async get(path: string, init?: RequestInit) { const res = await handleRequest(new Request(`https://api.nibling.run${path}`, init), env); const body = new Uint8Array(await res.arrayBuffer()); await Promise.all(pending.splice(0)); return { status: res.status, headers: res.headers, text: new TextDecoder().decode(body), json: () => JSON.parse(new TextDecoder().decode(body)) }; }, }; } const GET_RECORD = `/xrpc/com.atproto.repo.getRecord?repo=${ALICE}&collection=dev.nibling.project&rkey=3la1`; test('getRecord is answered from the PDS once, then from the cache', async () => { const g = gateway(); const first = await g.get(GET_RECORD); const second = await g.get(GET_RECORD); assert.deepEqual([first.status, first.headers.get('x-nib-cache'), second.headers.get('x-nib-cache')], [200, 'miss', 'hit']); assert.deepEqual(second.json(), { uri: `at://${ALICE}/dev.nibling.project/3la1`, cid: CID1, value: { text: 'first' } }); assert.equal(second.headers.get('access-control-allow-origin'), '*'); assert.equal(second.headers.get('cache-control'), 'no-cache'); assert.deepEqual(g.net.calls, [ `slingshot.test /xrpc/blue.microcosm.identity.resolveMiniDoc?identifier=${ALICE}`, `pds.test /xrpc/com.atproto.repo.getRecord?repo=${ALICE}&collection=dev.nibling.project&rkey=3la1`, ]); }); test('a handle repo is resolved and forwarded as its DID, sharing the DID entry', async () => { const g = gateway(); await g.get(GET_RECORD); const byHandle = await g.get('/xrpc/com.atproto.repo.getRecord?repo=alice.test&collection=dev.nibling.project&rkey=3la1'); assert.equal(byHandle.headers.get('x-nib-cache'), 'hit'); assert.deepEqual(g.net.calls.slice(2), ['slingshot.test /xrpc/blue.microcosm.identity.resolveMiniDoc?identifier=alice.test']); }); test('after 15 s the current version is served stale while it refreshes, then the refresh is a hit', async () => { const g = gateway(); await g.get(GET_RECORD); g.net.setRecord({ cid: CID2, value: { text: 'second' } }); g.tick(16_000); const stale = await g.get(GET_RECORD); assert.deepEqual([stale.headers.get('x-nib-cache'), stale.json().value], ['stale', { text: 'first' }]); const fresh = await g.get(GET_RECORD); assert.deepEqual([fresh.headers.get('x-nib-cache'), fresh.json().value], ['hit', { text: 'second' }]); g.tick(15_000 + 60_000 + 1); const expired = await g.get(GET_RECORD); assert.equal(expired.headers.get('x-nib-cache'), 'miss'); }); test('x-nib-fresh: 1 skips the cached answer and stores the new one for everyone', async () => { const g = gateway(); await g.get(GET_RECORD); g.net.setRecord({ cid: CID2, value: { text: 'saved' } }); const own = await g.get(GET_RECORD, { headers: { 'x-nib-fresh': '1' } }); assert.deepEqual([own.headers.get('x-nib-cache'), own.json().value], ['bypass', { text: 'saved' }]); const other = await g.get(GET_RECORD); assert.deepEqual([other.headers.get('x-nib-cache'), other.json().value], ['hit', { text: 'saved' }]); }); test('getRecord with a cid is cached for a year, and only when the PDS answers with that cid', async () => { const g = gateway(); const pinned = await g.get(`${GET_RECORD}&cid=${CID1}`); assert.deepEqual([pinned.status, pinned.headers.get('cache-control')], [200, 'public, max-age=31536000, immutable']); g.tick(30 * 24 * 3600 * 1000); assert.equal((await g.get(`${GET_RECORD}&cid=${CID1}`)).headers.get('x-nib-cache'), 'hit'); const gone = await g.get(`${GET_RECORD}&cid=${CID2}`); assert.deepEqual([gone.status, gone.json().error], [400, 'RecordNotFound']); assert.equal((await g.get(`${GET_RECORD}&cid=${CID2}`)).headers.get('x-nib-cache'), 'miss'); }); test('params are validated at the boundary, and unknown params never reach the PDS', async () => { const g = gateway(); const cases = [ ['/xrpc/com.atproto.repo.getRecord?repo=nope&collection=dev.nibling.project&rkey=3la1', 'repo must be a handle or DID'], [`/xrpc/com.atproto.repo.getRecord?repo=${ALICE}&collection=not_an_nsid&rkey=3la1`, 'collection must be an NSID'], [`/xrpc/com.atproto.repo.getRecord?repo=${ALICE}&collection=dev.nibling.project&rkey=..`, 'rkey must be a record key'], [`/xrpc/com.atproto.repo.getRecord?repo=${ALICE}&collection=dev.nibling.project`, 'rkey is required'], [`/xrpc/com.atproto.repo.listRecords?repo=${ALICE}&collection=dev.nibling.project&limit=500`, 'limit must be an integer from 1 to 100'], [`/xrpc/com.atproto.sync.getBlob?did=alice.test&cid=${BLOB}`, 'did must be a DID'], [`/xrpc/com.atproto.sync.getBlob?did=${ALICE}&cid=nope`, 'cid must be a CID'], ]; for (const [path, message] of cases) { const res = await g.get(path!); assert.deepEqual([res.status, res.json()], [400, { error: 'InvalidRequest', message }], path); } assert.deepEqual(g.net.calls, []); await g.get(`/xrpc/com.atproto.repo.listRecords?repo=${ALICE}&collection=dev.nibling.project&limit=10&evil=1`); assert.equal(g.net.calls.at(-1), `pds.test /xrpc/com.atproto.repo.listRecords?repo=${ALICE}&collection=dev.nibling.project&limit=10`); }); test('getBlob serves the bytes sandboxed and immutable', async () => { const g = gateway(); const res = await g.get(`/xrpc/com.atproto.sync.getBlob?did=${ALICE}&cid=${BLOB}`); assert.deepEqual( [res.status, res.text, res.headers.get('content-type'), res.headers.get('content-security-policy'), res.headers.get('x-content-type-options'), res.headers.get('cache-control')], [200, '', 'text/html', "default-src 'none'; sandbox", 'nosniff', 'public, max-age=31536000, immutable'], ); }); test('a blob or a JSON answer over 5 MB is refused with a 502', async () => { const g = gateway(); const big = new Uint8Array(5_000_001); g.net.services['pds.test'] = () => new Response(big); const blob = await g.get(`/xrpc/com.atproto.sync.getBlob?did=${ALICE}&cid=${BLOB}`); const record = await g.get(GET_RECORD); assert.deepEqual([blob.status, blob.json().error, record.status, record.json().error], [502, 'UpstreamFailure', 502, 'UpstreamFailure']); }); test('a failing upstream becomes a 5xx so the SDK falls back, and is not cached', async () => { const g = gateway(); g.net.services['pds.test'] = () => new Response('oops', { status: 503 }); const res = await g.get(GET_RECORD); assert.deepEqual([res.status, res.json()], [502, { error: 'UpstreamFailure', message: 'pds.test answered 503' }]); delete g.net.services['pds.test']; const down = await g.get(GET_RECORD); assert.deepEqual([down.status, down.json().error], [502, 'UpstreamFailure']); }); test('a PDS that redirects inward is not followed', async () => { const g = gateway(); g.net.services['pds.test'] = () => new Response(null, { status: 302, headers: { location: 'http://169.254.169.254/latest' } }); const res = await g.get(GET_RECORD); assert.deepEqual([res.status, res.json().message], [502, 'refusing to fetch http://169.254.169.254/latest']); assert.ok(!g.net.calls.some((call) => call.startsWith('169.254.169.254'))); }); test('a DID that names a private PDS is never fetched', async () => { const g = gateway(); g.net.services['slingshot.test'] = () => Response.json({ did: ALICE, handle: 'alice.test', pds: 'http://10.0.0.1' }); g.net.services['plc.directory'] = () => Response.json({ id: ALICE, service: [{ id: '#atproto_pds', type: 'AtprotoPersonalDataServer', serviceEndpoint: 'http://10.0.0.1' }] }); const res = await g.get(`/xrpc/com.atproto.repo.describeRepo?repo=${ALICE}`); assert.deepEqual([res.status, res.json()], [400, { error: 'RepoNotFound', message: `${ALICE} names no public https PDS` }]); assert.ok(!g.net.calls.some((call) => call.startsWith('10.0.0.1'))); }); test('resolveMiniDoc falls back from Slingshot to PLC and DNS, checks the handle, and caches for 5 minutes', async () => { const g = gateway(); g.net.services['slingshot.test'] = () => new Response('down', { status: 500 }); const res = await g.get('/xrpc/blue.microcosm.identity.resolveMiniDoc?identifier=@Alice.Test'); assert.deepEqual([res.status, res.json()], [200, { did: ALICE, handle: 'alice.test', pds: 'https://pds.test' }]); assert.deepEqual(g.net.calls, [ 'slingshot.test /xrpc/blue.microcosm.identity.resolveMiniDoc?identifier=alice.test', 'cloudflare-dns.com /dns-query?name=_atproto.alice.test&type=TXT', `plc.directory /${ALICE}`, ]); g.tick(299_000); assert.equal((await g.get('/xrpc/blue.microcosm.identity.resolveMiniDoc?identifier=alice.test')).headers.get('x-nib-cache'), 'hit'); g.tick(2_000); assert.equal((await g.get('/xrpc/blue.microcosm.identity.resolveMiniDoc?identifier=alice.test')).headers.get('x-nib-cache'), 'miss'); }); test('a DID whose handle does not point back is reported as handle.invalid', async () => { const g = gateway(); g.net.services['slingshot.test'] = () => new Response('down', { status: 500 }); g.net.services['cloudflare-dns.com'] = () => Response.json({ Answer: [] }); const res = await g.get(`/xrpc/blue.microcosm.identity.resolveMiniDoc?identifier=${ALICE}`); assert.deepEqual(res.json(), { did: ALICE, handle: 'handle.invalid', pds: 'https://pds.test' }); }); test('resolveDid serves the DID document', async () => { const g = gateway(); const res = await g.get(`/xrpc/com.atproto.identity.resolveDid?did=${REPO}`); assert.deepEqual(res.json(), { didDoc: { id: REPO, service: [{ id: '#tangled_knot', type: 'TangledKnot', serviceEndpoint: 'https://knot.test' }] } }); const missing = await g.get('/xrpc/com.atproto.identity.resolveDid?did=did:plc:zzzzzzzzzzzzzzzzzzzzzzzz'); assert.deepEqual([missing.status, missing.json().error], [400, 'DidNotFound']); }); test('backlinks, counts, and record-by-URI pass through to microcosm and cache for 15 s', async () => { const g = gateway(); const subject = encodeURIComponent(`at://${ALICE}/dev.nibling.project/3la1`); const count = await g.get(`/xrpc/blue.microcosm.links.getBacklinksCount?subject=${subject}&source=dev.nibling.feed.like:subject.uri`); const links = await g.get(`/xrpc/blue.microcosm.links.getBacklinks?subject=${subject}&source=dev.nibling.feed.like:subject.uri&limit=100`); const byUri = await g.get(`/xrpc/blue.microcosm.repo.getRecordByUri?at_uri=${subject}`); assert.deepEqual([count.json(), links.json().total, byUri.json().cid], [{ total: 3 }, 1, CID1]); assert.equal(count.headers.get('cache-control'), 'public, max-age=15'); g.tick(14_000); assert.equal((await g.get(`/xrpc/blue.microcosm.links.getBacklinksCount?subject=${subject}&source=dev.nibling.feed.like:subject.uri`)).headers.get('x-nib-cache'), 'hit'); g.tick(2_000); assert.equal((await g.get(`/xrpc/blue.microcosm.links.getBacklinksCount?subject=${subject}&source=dev.nibling.feed.like:subject.uri`)).headers.get('x-nib-cache'), 'miss'); assert.deepEqual(g.net.calls.filter((c) => c.startsWith('constellation')).length, 3); }); const pkt = (line: string) => (line.length + 4).toString(16).padStart(4, '0') + line; const wantBody = (sha: string, extra = '') => `${pkt(`want ${sha} side-band-64k agent=test\n`)}${pkt('deepen 1\n')}0000${extra}${pkt('done\n')}`; test('isPureWant accepts wants, deepen, and done, and nothing that depends on what the client has', () => { assert.equal(isPureWant(new TextEncoder().encode(wantBody(SHA))), true); assert.equal(isPureWant(new TextEncoder().encode(wantBody(SHA, pkt(`have ${'c'.repeat(40)}\n`)))), false); assert.equal(isPureWant(new TextEncoder().encode(`${pkt(`want ${SHA}\n`)}${pkt(`shallow ${SHA}\n`)}0000${pkt('done\n')}`)), false); assert.equal(isPureWant(new TextEncoder().encode(`${pkt(`want ${SHA}\n`)}0000`)), false); assert.equal(isPureWant(new TextEncoder().encode('zzzz')), false); }); test('git reads go only to the knot the repo DID names; pushes are refused', async () => { const g = gateway(); const refs = await g.get(`/git/${REPO}/info/refs?service=git-upload-pack`); assert.deepEqual( [refs.status, refs.text, refs.headers.get('cache-control'), refs.headers.get('access-control-allow-origin')], [200, '001e# service=git-upload-pack\n0000', 'no-store', '*'], ); const push = await g.get(`/git/${REPO}/info/refs?service=git-receive-pack`); const pushPost = await g.get(`/git/${REPO}/git-receive-pack`, { method: 'POST', body: '0000' }); assert.deepEqual([push.status, pushPost.status], [403, 403]); const noKnot = await g.get(`/git/${ALICE}/info/refs?service=git-upload-pack`); assert.deepEqual([noKnot.status, noKnot.json().message], [404, `${ALICE} names no public https #tangled_knot`]); assert.deepEqual(g.net.calls, [`plc.directory /${REPO}`, `knot.test /${REPO}/info/refs?service=git-upload-pack`, `plc.directory /${ALICE}`]); }); test('a pure-want upload-pack is cached by its body; one with haves, or an ERR answer, is not', async () => { const g = gateway(); const post = (body: string) => g.get(`/git/${REPO}/git-upload-pack`, { method: 'POST', body, headers: { 'content-type': 'application/x-git-upload-pack-request' } }); const first = await post(wantBody(SHA)); const second = await post(wantBody(SHA)); assert.deepEqual([first.text, first.headers.get('x-nib-cache'), second.text, second.headers.get('x-nib-cache')], ['0008NAK\nPACK', 'miss', '0008NAK\nPACK', 'hit']); const withHave = wantBody(SHA, pkt(`have ${'c'.repeat(40)}\n`)); await post(withHave); const again = await post(withHave); assert.deepEqual([again.headers.get('x-nib-cache'), again.headers.get('cache-control')], [null, 'no-store']); const missing = await post(wantBody('b'.repeat(40))); const missingAgain = await post(wantBody('b'.repeat(40))); assert.deepEqual([missing.text, missingAgain.headers.get('x-nib-cache')], ['0019ERR not our ref bbbb\n', 'miss']); assert.equal(g.net.calls.filter((c) => c.endsWith('git-upload-pack')).length, 5); }); test('a preflight allows the cache bypass and git headers from any origin', async () => { const g = gateway(); const res = await g.get(GET_RECORD, { method: 'OPTIONS' }); assert.deepEqual( [res.status, res.headers.get('access-control-allow-origin'), res.headers.get('access-control-allow-headers')], [204, '*', 'content-type, git-protocol, x-nib-fresh'], ); });