A Deno-compatible AT Protocol OAuth client that serves as a drop-in replacement for @atproto/oauth-client-node
0
fork

Configure Feed

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

Resolve DID and handle from token response for auth server URL flows

When authorizing via auth server URL, DID/handle aren't known upfront.
Extract DID from the token response sub claim and resolve handle/PDS
from the DID document.

+52 -7
+1 -1
deno.json
··· 1 1 { 2 2 "name": "@tijs/oauth-client-deno", 3 - "version": "4.1.1", 3 + "version": "4.1.2", 4 4 "description": "AT Protocol OAuth client for Deno - handle-focused alternative to @atproto/oauth-client-node with Web Crypto API compatibility", 5 5 "license": "MIT", 6 6 "repository": {
+20 -4
src/client.ts
··· 19 19 SessionNotFoundError, 20 20 TokenExchangeError, 21 21 } from "./errors.ts"; 22 - import { createDefaultResolver, discoverOAuthEndpointsFromPDS } from "./resolvers.ts"; 22 + import { 23 + createDefaultResolver, 24 + discoverOAuthEndpointsFromPDS, 25 + resolveDidDocument, 26 + } from "./resolvers.ts"; 23 27 import { generateCodeChallenge, generateCodeVerifier } from "./pkce.ts"; 24 28 import { exchangeCodeForTokens, refreshTokens } from "./token-exchange.ts"; 25 29 import type { Logger } from "./logger.ts"; ··· 312 316 this.logger, 313 317 ); 314 318 319 + // Resolve DID, handle, and PDS from token response when not available 320 + // (auth server URL flow skips handle resolution, so these come from `sub`) 321 + let { did, handle, pdsUrl } = pkceData; 322 + if (!did && tokens.sub) { 323 + did = tokens.sub; 324 + this.logger.debug("Using DID from token response sub claim", { did }); 325 + const resolved = await resolveDidDocument(did); 326 + handle = resolved.handle; 327 + pdsUrl = resolved.pdsUrl; 328 + this.logger.debug("Resolved DID document", { handle, pdsUrl }); 329 + } 330 + 315 331 // Create session 316 332 const sessionData: SessionData = { 317 - did: pkceData.did, 318 - handle: pkceData.handle, 319 - pdsUrl: pkceData.pdsUrl, 333 + did, 334 + handle, 335 + pdsUrl, 320 336 accessToken: tokens.access_token, 321 337 refreshToken: tokens.refresh_token ?? "", 322 338 dpopPrivateKeyJWK: dpopKeys.privateKeyJWK,
+29 -2
src/resolvers.ts
··· 297 297 * Resolve PDS URL from DID by fetching DID document 298 298 */ 299 299 async function resolvePdsFromDid(did: string): Promise<string> { 300 + const result = await resolveDidDocument(did); 301 + return result.pdsUrl; 302 + } 303 + 304 + /** 305 + * Resolve DID document to extract PDS URL and handle. 306 + * 307 + * Fetches the DID document from PLC directory and extracts the PDS service 308 + * endpoint and handle from alsoKnownAs. Used during auth server URL flows 309 + * to populate session data after the token exchange. 310 + * 311 + * @param did - DID to resolve (e.g., "did:plc:...") 312 + * @returns Promise resolving to PDS URL and handle 313 + * @throws {PDSDiscoveryError} When DID document cannot be fetched or parsed 314 + */ 315 + export async function resolveDidDocument( 316 + did: string, 317 + ): Promise<{ pdsUrl: string; handle: string }> { 300 318 try { 301 319 const response = await fetch(`https://plc.directory/${encodeURIComponent(did)}`); 302 320 ··· 324 342 } 325 343 326 344 // Clean up PDS URL 327 - pdsUrl = pdsUrl.replace(/\/$/, ""); // Remove trailing slash 345 + pdsUrl = pdsUrl.replace(/\/$/, ""); 346 + 347 + // Extract handle from alsoKnownAs 348 + let handle = did; 349 + if (Array.isArray(didDocument.alsoKnownAs)) { 350 + const atUri = didDocument.alsoKnownAs.find((uri: string) => uri.startsWith("at://")); 351 + if (atUri) { 352 + handle = atUri.replace("at://", ""); 353 + } 354 + } 328 355 329 - return pdsUrl; 356 + return { pdsUrl, handle }; 330 357 } catch (error) { 331 358 throw new PDSDiscoveryError(did, error as Error); 332 359 }
+2
src/token-exchange.ts
··· 15 15 access_token: string; 16 16 refresh_token?: string; 17 17 expires_in: number; 18 + /** DID of the authenticated user (AT Protocol extension) */ 19 + sub?: string; 18 20 } 19 21 20 22 /**