Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/drop-non-pds-endpoints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@getcirrus/pds": minor
---

Remove `com.atproto.identity.resolveDid`, `com.atproto.identity.resolveIdentity`, and `com.atproto.sync.listReposByCollection` handlers. These lexicons are implemented by the directory and relay layers, not the PDS — the reference @atproto PDS doesn't expose them either. Requests for these methods now fall through to the AppView proxy like any other unknown XRPC call.
109 changes: 1 addition & 108 deletions apps/check/src/checks/identity.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import {
ComAtprotoIdentityResolveDid,
ComAtprotoIdentityResolveHandle,
ComAtprotoIdentityResolveIdentity,
} from "@atcute/atproto";
import { ComAtprotoIdentityResolveHandle } from "@atcute/atproto";
import { getPdsEndpoint } from "@atcute/identity";
import { isDid, isHandle, type Did, type Handle } from "@atcute/lexicons/syntax";
import { didDocResolver, handleResolver } from "../lib/resolvers";
import { publicClient, validateLexicon } from "../lib/xrpc";
import type { Check, CheckOutcome } from "../types";

let pdsResolveHandleBody: ComAtprotoIdentityResolveHandle.$output | undefined;
let pdsResolveDidBody: ComAtprotoIdentityResolveDid.$output | undefined;
let pdsResolveIdentityBody: ComAtprotoIdentityResolveIdentity.$output | undefined;

const parseInput: Check = {
id: "identity.parse-input",
Expand Down Expand Up @@ -229,112 +223,11 @@ const pdsResolveHandleValidates: Check = {
},
};

const pdsResolveDid: Check = {
id: "identity.pds-resolve-did",
category: "identity",
label: "PDS resolves DID to document",
requires: ["pds", "did"],
run: async (ctx): Promise<CheckOutcome> => {
pdsResolveDidBody = undefined;
const client = publicClient(ctx.pds!);
const res = await client.get("com.atproto.identity.resolveDid", {
params: { did: ctx.did as Did },
});
if (!res.ok) {
return {
status: "fail",
message: `${res.data.error}: ${res.data.message ?? ""}`.trim(),
evidence: { response: { status: res.status, body: res.data } },
};
}
pdsResolveDidBody = res.data;
return {
status: "pass",
message: `didDoc with ${res.data.didDoc ? "didDoc" : "no didDoc"}`,
evidence: { response: { body: res.data } },
};
},
};

const pdsResolveDidValidates: Check = {
id: "identity.pds-resolve-did.validates",
category: "identity",
label: "PDS resolveDid response matches lexicon",
requires: ["pds", "did"],
run: async (): Promise<CheckOutcome> => {
if (!pdsResolveDidBody) {
return { status: "skip", message: "no response to validate" };
}
return validateLexicon(
ComAtprotoIdentityResolveDid.mainSchema.output.schema,
pdsResolveDidBody,
);
},
};

const pdsResolveIdentity: Check = {
id: "identity.pds-resolve-identity",
category: "identity",
label: "PDS resolves identity (combined)",
requires: ["pds"],
run: async (ctx): Promise<CheckOutcome> => {
pdsResolveIdentityBody = undefined;
const identifier = ctx.handle ?? ctx.did;
if (!identifier) {
return { status: "skip", message: "no handle or DID in context" };
}
const client = publicClient(ctx.pds!);
const res = await client.get("com.atproto.identity.resolveIdentity", {
params: { identifier: identifier as Handle | Did },
});
if (!res.ok) {
return {
status: "fail",
message: `${res.data.error}: ${res.data.message ?? ""}`.trim(),
evidence: { response: { status: res.status, body: res.data } },
};
}
pdsResolveIdentityBody = res.data;
if (ctx.did && res.data.did !== ctx.did) {
return {
status: "fail",
message: `did mismatch: expected ${ctx.did}, got ${res.data.did}`,
evidence: { expected: ctx.did, actual: res.data.did },
};
}
return {
status: "pass",
message: `${res.data.did}`,
evidence: { response: { body: res.data } },
};
},
};

const pdsResolveIdentityValidates: Check = {
id: "identity.pds-resolve-identity.validates",
category: "identity",
label: "PDS resolveIdentity response matches lexicon",
requires: ["pds"],
run: async (): Promise<CheckOutcome> => {
if (!pdsResolveIdentityBody) {
return { status: "skip", message: "no response to validate" };
}
return validateLexicon(
ComAtprotoIdentityResolveIdentity.mainSchema.output.schema,
pdsResolveIdentityBody,
);
},
};

export const identityChecks: Check[] = [
parseInput,
resolveHandle,
fetchDidDocument,
extractPdsEndpoint,
pdsResolveHandle,
pdsResolveHandleValidates,
pdsResolveDid,
pdsResolveDidValidates,
pdsResolveIdentity,
pdsResolveIdentityValidates,
];
78 changes: 1 addition & 77 deletions apps/check/src/checks/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,19 @@ import {
ComAtprotoSyncGetBlocks,
ComAtprotoSyncGetLatestCommit,
ComAtprotoSyncGetRepoStatus,
ComAtprotoSyncListReposByCollection,
} from "@atcute/atproto";
import type { Did, Nsid } from "@atcute/lexicons/syntax";
import type { Did } from "@atcute/lexicons/syntax";
import { publicClient, validateLexicon } from "../lib/xrpc";
import type { Check, CheckOutcome } from "../types";

let getLatestCommitResponse: ComAtprotoSyncGetLatestCommit.$output | undefined;
let getRepoStatusResponse: ComAtprotoSyncGetRepoStatus.$output | undefined;
let getBlocksResponseBytes: Uint8Array | undefined;
let listReposByCollectionResponse:
| ComAtprotoSyncListReposByCollection.$output
| undefined;

function reset() {
getLatestCommitResponse = undefined;
getRepoStatusResponse = undefined;
getBlocksResponseBytes = undefined;
listReposByCollectionResponse = undefined;
}

function xrpcUrl(
Expand Down Expand Up @@ -241,82 +236,11 @@ const getBlocksValidates: Check = {
},
};

const listReposByCollection: Check = {
id: "sync.list-repos-by-collection",
category: "sync",
label: "listReposByCollection",
requires: ["pds", "did"],
run: async (ctx): Promise<CheckOutcome> => {
const pds = ctx.pds!;
const collection = "app.bsky.actor.profile";
const url = xrpcUrl(pds, "com.atproto.sync.listReposByCollection", {
collection,
limit: "5",
});
try {
const res = await publicClient(pds).get(
"com.atproto.sync.listReposByCollection",
{ params: { collection: collection as Nsid, limit: 5 } },
);
if (!res.ok) {
return {
status: "fail",
message:
`${res.data.error}: ${res.data.message ?? ""}`.trim(),
evidence: {
request: { method: "GET", url },
response: { status: res.status, body: res.data },
},
};
}
listReposByCollectionResponse = res.data;
return {
status: "pass",
message: `${res.data.repos.length} repos for ${collection}`,
evidence: {
request: { method: "GET", url },
response: { status: res.status, body: res.data },
},
};
} catch (error) {
return {
status: "fail",
message: error instanceof Error ? error.message : String(error),
evidence: {
request: { method: "GET", url },
error: String(error),
},
};
}
},
};

const listReposByCollectionValidates: Check = {
id: "sync.list-repos-by-collection.validates",
category: "sync",
label: "listReposByCollection response matches lexicon",
requires: ["pds", "did"],
run: async (): Promise<CheckOutcome> => {
if (!listReposByCollectionResponse) {
return {
status: "skip",
message: "listReposByCollection did not succeed",
};
}
return validateLexicon(
ComAtprotoSyncListReposByCollection.mainSchema.output.schema,
listReposByCollectionResponse,
);
},
};

export const syncChecks: Check[] = [
getLatestCommit,
getLatestCommitValidates,
getRepoStatus,
getRepoStatusValidates,
getBlocks,
getBlocksValidates,
listReposByCollection,
listReposByCollectionValidates,
];
20 changes: 0 additions & 20 deletions packages/pds/src/account-do.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,26 +249,6 @@ export class AccountDurableObject extends DurableObject<PDSEnv> {
};
}

/**
* RPC method: Check whether the repo contains any records in a collection.
*/
async rpcHasRecordsInCollection(collection: string): Promise<boolean> {
const repo = await this.getRepo();
const storage = await this.getStorage();

if (!storage.hasCollections() && (await storage.getRoot())) {
const seen = new Set<string>();
for await (const record of repo.walkRecords()) {
if (!seen.has(record.collection)) {
seen.add(record.collection);
storage.addCollection(record.collection);
}
}
}

return storage.getCollections().includes(collection);
}

/**
* RPC method: Get a single record
*/
Expand Down
12 changes: 0 additions & 12 deletions packages/pds/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,6 @@ app.get("/xrpc/com.atproto.sync.getBlob", (c) =>
app.get("/xrpc/com.atproto.sync.listRepos", (c) =>
sync.listRepos(c, getAccountDO(c.env)),
);
app.get("/xrpc/com.atproto.sync.listReposByCollection", (c) =>
sync.listReposByCollection(c, getAccountDO(c.env)),
);
app.get("/xrpc/com.atproto.sync.listBlobs", (c) =>
sync.listBlobs(c, getAccountDO(c.env)),
);
Expand Down Expand Up @@ -290,15 +287,6 @@ app.use("/xrpc/com.atproto.identity.resolveHandle", async (c, next) => {
await next();
});

// DID resolution - serve our DID doc for our DID, others fall through to proxy
app.use("/xrpc/com.atproto.identity.resolveDid", identity.resolveDid);

// Identity resolution - serve our identity for our DID/handle, others fall through
app.use(
"/xrpc/com.atproto.identity.resolveIdentity",
identity.resolveIdentity,
);

// Recommended PLC credentials for inbound migration
app.get(
"/xrpc/com.atproto.identity.getRecommendedDidCredentials",
Expand Down
62 changes: 2 additions & 60 deletions packages/pds/src/xrpc/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type { Context } from "hono";
import { Secp256k1Keypair } from "@atproto/crypto";
import { encode } from "@atcute/cbor";
import { base64url } from "jose";
import type { AppEnv, AuthedAppEnv, PDSEnv } from "../types";
import type { AuthedAppEnv, PDSEnv } from "../types";
import {
createMigrationToken,
validateMigrationToken,
Expand All @@ -27,8 +27,7 @@ const PLC_DIRECTORY = "https://plc.directory";
/**
* Build the DID document for the local account.
*
* Shared between /.well-known/did.json and the
* com.atproto.identity.resolveDid / resolveIdentity endpoints.
* Served by /.well-known/did.json.
*/
export function buildDidDocument(env: PDSEnv) {
return {
Expand Down Expand Up @@ -57,63 +56,6 @@ export function buildDidDocument(env: PDSEnv) {
};
}

/**
* Resolve a DID to its DID document.
*
* For our local DID we serve the document directly. Any other DID is
* passed through to the next handler (the AppView proxy).
*
* Endpoint: GET com.atproto.identity.resolveDid
*/
export async function resolveDid(
c: Context<AppEnv>,
next: () => Promise<void>,
): Promise<Response | void> {
const did = c.req.query("did");
if (!did) {
return c.json(
{ error: "InvalidRequest", message: "Missing required parameter: did" },
400,
);
}
if (did === c.env.DID) {
return c.json({ didDoc: buildDidDocument(c.env) });
}
await next();
}

/**
* Resolve an identifier (handle or DID) to its full identity.
*
* For our local handle or DID we serve the response directly. Anything
* else falls through to the AppView proxy.
*
* Endpoint: GET com.atproto.identity.resolveIdentity
*/
export async function resolveIdentity(
c: Context<AppEnv>,
next: () => Promise<void>,
): Promise<Response | void> {
const identifier = c.req.query("identifier");
if (!identifier) {
return c.json(
{
error: "InvalidRequest",
message: "Missing required parameter: identifier",
},
400,
);
}
if (identifier === c.env.DID || identifier === c.env.HANDLE) {
return c.json({
did: c.env.DID,
handle: c.env.HANDLE,
didDoc: buildDidDocument(c.env),
});
}
await next();
}

/**
* Return recommended PLC credentials for the current account.
*
Expand Down
Loading
Loading