Skip to content

Commit 3f7afaa

Browse files
ascorbicclaude
andauthored
fix(pds): add handle verification and identity events (#13)
- Add /.well-known/atproto-did endpoint for handle verification - Add alsoKnownAs field to DID document for bidirectional verification - Add identity event emission for handle cache refresh - Add admin endpoint to trigger identity events 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent eca3634 commit 3f7afaa

2 files changed

Lines changed: 61 additions & 0 deletions

File tree

packages/pds/src/account-do.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,52 @@ export class AccountDurableObject extends DurableObject<Env> {
770770
console.error("WebSocket error:", error);
771771
}
772772

773+
/**
774+
* Emit an identity event to notify downstream services to refresh identity cache.
775+
*/
776+
async rpcEmitIdentityEvent(handle: string): Promise<{ seq: number }> {
777+
await this.ensureStorageInitialized();
778+
779+
const time = new Date().toISOString();
780+
781+
// Get next sequence number
782+
const result = this.ctx.storage.sql
783+
.exec(
784+
`INSERT INTO firehose_events (event_type, payload)
785+
VALUES ('identity', ?)
786+
RETURNING seq`,
787+
new Uint8Array(0), // Empty payload, we just need seq
788+
)
789+
.one();
790+
const seq = result.seq as number;
791+
792+
// Build identity event frame
793+
const header = { op: 1, t: "#identity" };
794+
const body = {
795+
seq,
796+
did: this.env.DID,
797+
time,
798+
handle,
799+
};
800+
801+
const headerBytes = cborEncode(header as unknown as import("@atproto/lex-cbor").LexValue);
802+
const bodyBytes = cborEncode(body as unknown as import("@atproto/lex-cbor").LexValue);
803+
const frame = new Uint8Array(headerBytes.length + bodyBytes.length);
804+
frame.set(headerBytes, 0);
805+
frame.set(bodyBytes, headerBytes.length);
806+
807+
// Broadcast to all connected clients
808+
for (const ws of this.ctx.getWebSockets()) {
809+
try {
810+
ws.send(frame);
811+
} catch (e) {
812+
console.error("Error broadcasting identity event:", e);
813+
}
814+
}
815+
816+
return { seq };
817+
}
818+
773819
/**
774820
* HTTP fetch handler for WebSocket upgrades.
775821
* This is used instead of RPC to avoid WebSocket serialization errors.

packages/pds/src/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ app.get("/.well-known/did.json", (c) => {
8787
"https://w3id.org/security/suites/secp256k1-2019/v1",
8888
],
8989
id: c.env.DID,
90+
alsoKnownAs: [`at://${c.env.HANDLE}`],
9091
verificationMethod: [
9192
{
9293
id: `${c.env.DID}#atproto`,
@@ -106,6 +107,13 @@ app.get("/.well-known/did.json", (c) => {
106107
return c.json(didDocument);
107108
});
108109

110+
// Handle verification for AT Protocol
111+
app.get("/.well-known/atproto-did", (c) => {
112+
return new Response(c.env.DID, {
113+
headers: { "Content-Type": "text/plain" },
114+
});
115+
});
116+
109117
// Health check
110118
app.get("/health", (c) =>
111119
c.json({
@@ -212,6 +220,13 @@ app.get("/xrpc/app.bsky.ageassurance.getState", requireAuth, (c) => {
212220
});
213221
});
214222

223+
// Admin: Emit identity event to refresh handle verification
224+
app.post("/admin/emit-identity", requireAuth, async (c) => {
225+
const accountDO = getAccountDO(c.env);
226+
const result = await accountDO.rpcEmitIdentityEvent(c.env.HANDLE);
227+
return c.json(result);
228+
});
229+
215230
// Proxy unhandled XRPC requests to Bluesky services
216231
app.all("/xrpc/*", async (c) => {
217232
const url = new URL(c.req.url);

0 commit comments

Comments
 (0)