open-source, lexicon-agnostic PDS for AI agents. welcome-mat enrollment, AT Proto federation.
agents atprotocol pds cloudflare
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add head and rev fields to listRepos for AT Protocol spec compliance

Fan out to Account DOs via rpcGetLatestCommit() to populate the
required head (CID) and rev (TID) fields on each repo object.
Cursor is based on D1 row count to avoid pagination gaps if any
DO returns null.

+40 -15
+26 -11
src/worker.ts
··· 304 304 305 305 const cursor = c.req.query("cursor"); 306 306 307 - let rows: { did: string; active: number }[]; 307 + let rows: { did: string; active: number; do_id: string }[]; 308 308 if (cursor) { 309 309 const result = await c.env.DIRECTORY.prepare( 310 - "SELECT did, active FROM accounts WHERE active = 1 AND did > ? ORDER BY did ASC LIMIT ?", 311 - ).bind(cursor, limit).all<{ did: string; active: number }>(); 310 + "SELECT did, active, do_id FROM accounts WHERE active = 1 AND did > ? ORDER BY did ASC LIMIT ?", 311 + ).bind(cursor, limit).all<{ did: string; active: number; do_id: string }>(); 312 312 rows = result.results; 313 313 } else { 314 314 const result = await c.env.DIRECTORY.prepare( 315 - "SELECT did, active FROM accounts WHERE active = 1 ORDER BY did ASC LIMIT ?", 316 - ).bind(limit).all<{ did: string; active: number }>(); 315 + "SELECT did, active, do_id FROM accounts WHERE active = 1 ORDER BY did ASC LIMIT ?", 316 + ).bind(limit).all<{ did: string; active: number; do_id: string }>(); 317 317 rows = result.results; 318 318 } 319 319 320 - const repos = rows.map((row) => ({ 321 - did: row.did, 322 - active: row.active === 1, 323 - })); 320 + const repos = ( 321 + await Promise.all(rows.map(async (row) => { 322 + const stub = c.env.ACCOUNT.get(c.env.ACCOUNT.idFromString(row.do_id)); 323 + const commit = await stub.rpcGetLatestCommit(); 324 + if (!commit) return null; 325 + 326 + return { 327 + did: row.did, 328 + head: commit.cid, 329 + rev: commit.rev, 330 + active: row.active === 1, 331 + }; 332 + })) 333 + ).filter((repo): repo is { 334 + did: string; 335 + head: string; 336 + rev: string; 337 + active: boolean; 338 + } => repo !== null); 324 339 325 340 const response: { repos: typeof repos; cursor?: string } = { repos }; 326 - if (repos.length === limit) { 327 - response.cursor = repos[repos.length - 1].did; 341 + if (rows.length === limit) { 342 + response.cursor = rows[rows.length - 1].did; 328 343 } 329 344 330 345 return c.json(response);
+14 -4
test/directory.test.ts
··· 103 103 "http://localhost/xrpc/com.atproto.sync.listRepos", 104 104 ); 105 105 expect(res.status).toBe(200); 106 - const body = await res.json() as { repos: Array<{ did: string }> }; 106 + const body = await res.json() as { 107 + repos: Array<{ did: string; head: string; rev: string; active: boolean }>; 108 + }; 107 109 expect(Array.isArray(body.repos)).toBe(true); 108 110 for (let i = 1; i < body.repos.length; i++) { 109 111 expect(body.repos[i].did > body.repos[i - 1].did).toBe(true); 112 + } 113 + for (const repo of body.repos) { 114 + expect(typeof repo.head).toBe("string"); 115 + expect(typeof repo.rev).toBe("string"); 110 116 } 111 117 }); 112 118 ··· 129 135 ); 130 136 expect(res1.status).toBe(200); 131 137 const body1 = await res1.json() as { 132 - repos: Array<{ did: string }>; 138 + repos: Array<{ did: string; head: string; rev: string }>; 133 139 cursor?: string; 134 140 }; 135 141 expect(body1.repos.length).toBe(1); ··· 139 145 `http://localhost/xrpc/com.atproto.sync.listRepos?limit=1&cursor=${body1.cursor}`, 140 146 ); 141 147 expect(res2.status).toBe(200); 142 - const body2 = await res2.json() as { repos: Array<{ did: string }> }; 148 + const body2 = await res2.json() as { 149 + repos: Array<{ did: string; head: string; rev: string }>; 150 + }; 143 151 expect(body2.repos.length).toBe(1); 144 152 expect(body2.repos[0].did).not.toBe(body1.repos[0].did); 153 + expect(typeof body2.repos[0].head).toBe("string"); 154 + expect(typeof body2.repos[0].rev).toBe("string"); 145 155 expect(body2.repos[0].did > body1.repos[0].did).toBe(true); 146 156 }); 147 157 ··· 151 161 ); 152 162 expect(res.status).toBe(200); 153 163 const body = await res.json() as { 154 - repos: Array<{ did: string }>; 164 + repos: Array<{ did: string; head: string; rev: string }>; 155 165 cursor?: string; 156 166 }; 157 167 if (body.repos.length < 1000) {