diff --git a/.changeset/sync-get-latest-commit.md b/.changeset/sync-get-latest-commit.md new file mode 100644 index 0000000..28b4b63 --- /dev/null +++ b/.changeset/sync-get-latest-commit.md @@ -0,0 +1,7 @@ +--- +"@getcirrus/pds": minor +--- + +Implement `com.atproto.sync.getLatestCommit`. + +This sync XRPC endpoint was previously unimplemented, so requests fell through to the XRPC proxy and returned `501 MethodNotImplemented`. Relays call `getLatestCommit` during their crawl bootstrap, so a freshly created repo could never be indexed by a fresh `requestCrawl`. The endpoint now returns the repo's head commit as `{ cid, rev }` (sourced from the same `rpcGetRepoStatus` data used by `getRepoStatus`/`listRepos`). diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index eb75f09..fe6a67f 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -212,6 +212,9 @@ app.get("/xrpc/com.atproto.sync.getRepo", (c) => app.get("/xrpc/com.atproto.sync.getRepoStatus", (c) => sync.getRepoStatus(c, getAccountDO(c.env)), ); +app.get("/xrpc/com.atproto.sync.getLatestCommit", (c) => + sync.getLatestCommit(c, getAccountDO(c.env)), +); app.get("/xrpc/com.atproto.sync.getBlocks", (c) => sync.getBlocks(c, getAccountDO(c.env)), ); diff --git a/packages/pds/src/xrpc/sync.ts b/packages/pds/src/xrpc/sync.ts index 40861da..709626a 100644 --- a/packages/pds/src/xrpc/sync.ts +++ b/packages/pds/src/xrpc/sync.ts @@ -117,6 +117,55 @@ export async function listRepos( }); } +export async function getLatestCommit( + c: Context, + accountDO: DurableObjectStub, +): Promise { + const did = c.req.query("did"); + + if (!did) { + return c.json( + { + error: "InvalidRequest", + message: "Missing required parameter: did", + }, + 400, + ); + } + + // Validate DID format + if (!isDid(did)) { + return c.json( + { error: "InvalidRequest", message: "Invalid DID format" }, + 400, + ); + } + + if (did !== c.env.DID) { + return c.json( + { + error: "RepoNotFound", + message: `Repository not found for DID: ${did}`, + }, + 404, + ); + } + + const [data, active] = await Promise.all([ + accountDO.rpcGetRepoStatus(), + accountDO.rpcGetActive(), + ]); + + if (!active) { + return c.json( + { error: "RepoDeactivated", message: "Repository has been deactivated" }, + 400, + ); + } + + return c.json({ cid: data.head, rev: data.rev }); +} + export async function listBlobs( c: Context, _accountDO: DurableObjectStub, diff --git a/packages/pds/test/xrpc.test.ts b/packages/pds/test/xrpc.test.ts index c01d311..6b5f1cc 100644 --- a/packages/pds/test/xrpc.test.ts +++ b/packages/pds/test/xrpc.test.ts @@ -1481,6 +1481,46 @@ describe("XRPC Endpoints", () => { expect(data.status).toBeUndefined(); }); + it("should get latest commit", async () => { + const response = await worker.fetch( + new Request( + `http://pds.test/xrpc/com.atproto.sync.getLatestCommit?did=${env.DID}`, + ), + env, + ); + expect(response.status).toBe(200); + + const data = (await response.json()) as Record; + expect(data).toMatchObject({ + cid: expect.any(String), + rev: expect.any(String), + }); + }); + + it("should reject getLatestCommit without a did", async () => { + const response = await worker.fetch( + new Request("http://pds.test/xrpc/com.atproto.sync.getLatestCommit"), + env, + ); + expect(response.status).toBe(400); + + const data = (await response.json()) as Record; + expect(data.error).toBe("InvalidRequest"); + }); + + it("should return RepoNotFound for an unknown did on getLatestCommit", async () => { + const response = await worker.fetch( + new Request( + "http://pds.test/xrpc/com.atproto.sync.getLatestCommit?did=did:web:unknown.example.com", + ), + env, + ); + expect(response.status).toBe(404); + + const data = (await response.json()) as Record; + expect(data.error).toBe("RepoNotFound"); + }); + it("should return deactivated status when account is inactive", async () => { const deactivateResponse = await worker.fetch( new Request(