···11import { createRoute } from "honox/factory";
22import { setSignedCookie } from "hono/cookie";
33-import { getOAuthClient } from "@/auth/client.js";
33+import { getOAuthClient, resolveDidToHandle } from "@/auth/client.js";
44import { COOKIE_NAME } from "@/auth/middleware.js";
55import { config } from "@/config.js";
66import { db } from "@/db/index.js";
···5252 const { session, state } = await client.callback(params);
53535454 const did = session.did;
5555- const handle = state || did;
5555+ const handle = await resolveDidToHandle(state || did);
56565757 // Upsert user
5858 await db.insert(users).values({ did, handle, createdAt: new Date() }).onConflictDoUpdate({
+36
lib/auth/client.ts
···5959}
60606161/**
6262+ * Resolve a DID to a handle. In local dev, queries the local PDS.
6363+ * In production, queries the PLC directory.
6464+ */
6565+export async function resolveDidToHandle(did: string): Promise<string> {
6666+ if (!did.startsWith("did:")) return did;
6767+6868+ if (pdsUrl) {
6969+ try {
7070+ const res = await fetch(
7171+ `${pdsUrl}/xrpc/com.atproto.repo.describeRepo?repo=${encodeURIComponent(did)}`,
7272+ );
7373+ if (res.ok) {
7474+ const data = (await res.json()) as { handle: string };
7575+ if (data.handle) return data.handle;
7676+ }
7777+ } catch {
7878+ // fall through
7979+ }
8080+ }
8181+8282+ // Production: resolve via PLC directory
8383+ try {
8484+ const res = await fetch(`https://plc.directory/${did}`);
8585+ if (res.ok) {
8686+ const data = (await res.json()) as { alsoKnownAs?: string[] };
8787+ const atUri = data.alsoKnownAs?.find((u) => u.startsWith("at://"));
8888+ if (atUri) return atUri.replace("at://", "");
8989+ }
9090+ } catch {
9191+ // fall through
9292+ }
9393+9494+ return did;
9595+}
9696+9797+/**
6298 * Rewrite a URL targeting the PDS public hostname to the local PDS.
6399 * The local PDS registers DIDs with its public hostname (e.g. "pds.dev")
64100 * which isn't reachable — this maps those URLs to the actual PDS_URL.