Framework-agnostic OAuth integration for AT Protocol (Bluesky) applications.
at main 86 lines 2.3 kB view raw
1/** 2 * Generate ATProto OAuth client metadata 3 */ 4 5import type { ATProtoOAuthConfig, ClientMetadata } from "./types.ts"; 6 7/** 8 * Check if a URL is a loopback/localhost address for local development. 9 */ 10export function isLoopbackUrl(url: string): boolean { 11 try { 12 const parsed = new URL(url); 13 const host = parsed.hostname; 14 return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || 15 host === "::1"; 16 } catch { 17 return false; 18 } 19} 20 21/** 22 * Build a loopback redirect URI from a localhost base URL. 23 * Replaces "localhost" with "127.0.0.1" per the AT Protocol OAuth spec. 24 */ 25export function buildLoopbackRedirectUri(baseUrl: string): string { 26 const parsed = new URL(baseUrl); 27 parsed.hostname = "127.0.0.1"; 28 const origin = parsed.origin; // includes port 29 return `${origin}/oauth/callback`; 30} 31 32/** 33 * Build a loopback client_id per the AT Protocol OAuth spec. 34 * Format: http://localhost?redirect_uri=<encoded>&scope=<encoded> 35 */ 36export function buildLoopbackClientId( 37 redirectUri: string, 38 scope: string, 39): string { 40 const params = new URLSearchParams(); 41 params.set("redirect_uri", redirectUri); 42 params.set("scope", scope); 43 return `http://localhost?${params.toString()}`; 44} 45 46/** 47 * Generate ATProto OAuth client metadata for the /.well-known/oauth-client endpoint 48 */ 49export function generateClientMetadata( 50 config: ATProtoOAuthConfig, 51): ClientMetadata { 52 const baseUrl = config.baseUrl.replace(/\/$/, ""); 53 const scope = config.scope || "atproto transition:generic"; 54 const loopback = isLoopbackUrl(baseUrl); 55 56 const redirectUri = loopback 57 ? buildLoopbackRedirectUri(baseUrl) 58 : `${baseUrl}/oauth/callback`; 59 60 const clientId = loopback 61 ? buildLoopbackClientId(redirectUri, scope) 62 : `${baseUrl}/oauth-client-metadata.json`; 63 64 const metadata: ClientMetadata = { 65 client_name: config.appName, 66 client_id: clientId, 67 client_uri: baseUrl, 68 redirect_uris: [redirectUri], 69 scope, 70 grant_types: ["authorization_code", "refresh_token"], 71 response_types: ["code"], 72 application_type: "web", 73 token_endpoint_auth_method: "none", 74 dpop_bound_access_tokens: true, 75 }; 76 77 if (config.logoUri) { 78 metadata.logo_uri = config.logoUri; 79 } 80 81 if (config.policyUri) { 82 metadata.policy_uri = config.policyUri; 83 } 84 85 return metadata; 86}