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.

Accept authorization server URLs in authorize()

The authorize method now accepts https:// URLs (e.g. https://bsky.social)
in addition to AT Protocol handles. When a URL is provided, handle
resolution is skipped and OAuth endpoints are discovered directly from
the server.

+60 -20
+11
CHANGELOG.md
··· 5 5 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 6 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 7 8 + ## [4.1.0] - 2026-02-15 9 + 10 + ### Added 11 + 12 + - **Authorization server URL support in `authorize()`**: The `authorize` method 13 + now accepts authorization server URLs (e.g., `https://bsky.social`) in addition 14 + to AT Protocol handles. When a URL is provided, handle resolution is skipped 15 + and OAuth endpoints are discovered directly from the server. This enables 16 + "Connect with Bluesky" flows that redirect users to a specific auth server 17 + without requiring handle entry. 18 + 8 19 ## [4.0.2] - 2025-11-27 9 20 10 21 ### Fixed
+1 -1
deno.json
··· 1 1 { 2 2 "name": "@tijs/oauth-client-deno", 3 - "version": "4.0.2", 3 + "version": "4.1.0", 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": {
+48 -19
src/client.ts
··· 122 122 } 123 123 124 124 /** 125 - * Initiate OAuth authorization flow for an AT Protocol handle. 125 + * Initiate OAuth authorization flow for an AT Protocol handle or auth server URL. 126 126 * 127 127 * Resolves the handle to a DID and PDS, discovers OAuth endpoints, generates 128 128 * PKCE parameters, and creates a Pushed Authorization Request (PAR). Returns 129 129 * the authorization URL where users should be redirected to complete authentication. 130 130 * 131 - * @param handle - AT Protocol handle (e.g., "alice.bsky.social") 131 + * When an authorization server URL is provided (e.g., "https://bsky.social"), 132 + * handle resolution is skipped and OAuth endpoints are discovered directly 133 + * from the server. This enables "Connect with Bluesky" flows. 134 + * 135 + * @param input - AT Protocol handle (e.g., "alice.bsky.social") or authorization server URL (e.g., "https://bsky.social") 132 136 * @param options - Additional authorization options 133 137 * @returns Promise resolving to authorization URL for user redirection 134 138 * @throws {InvalidHandleError} When handle format is invalid ··· 148 152 * ``` 149 153 */ 150 154 async authorize( 151 - handle: string, 155 + input: string, 152 156 options?: AuthorizeOptions, 153 157 ): Promise<URL> { 154 - if (!isValidHandle(handle)) { 155 - this.logger.error("Invalid handle format", { handle }); 156 - throw new InvalidHandleError(handle); 158 + const isAuthServerUrl = input.startsWith("https://"); 159 + 160 + if (!isAuthServerUrl && !isValidHandle(input)) { 161 + this.logger.error("Invalid handle format", { handle: input }); 162 + throw new InvalidHandleError(input); 157 163 } 158 164 159 - this.logger.info("Starting authorization flow", { handle }); 165 + this.logger.info("Starting authorization flow", { input }); 160 166 161 167 try { 162 - // Resolve handle to get user's PDS and DID 163 - this.logger.debug("Resolving handle to DID and PDS", { handle }); 164 - const resolved = await this.handleResolver(handle); 165 - this.logger.debug("Handle resolved", { did: resolved.did, pdsUrl: resolved.pdsUrl }); 168 + let authServer: string; 169 + let did: string; 170 + let pdsUrl: string; 171 + let handle: string; 172 + 173 + if (isAuthServerUrl) { 174 + // Authorization server URL provided directly — skip handle resolution 175 + authServer = input.replace(/\/$/, ""); 176 + pdsUrl = authServer; 177 + did = ""; 178 + handle = ""; 179 + this.logger.debug("Using authorization server URL directly", { authServer }); 180 + 181 + // Discover OAuth endpoints to verify this is a valid auth server 182 + const oauthEndpoints = await discoverOAuthEndpointsFromPDS(authServer); 183 + authServer = this.extractAuthServer(oauthEndpoints.authorizationEndpoint); 184 + this.logger.debug("OAuth endpoints discovered", { authServer }); 185 + } else { 186 + // Resolve handle to get user's PDS and DID 187 + handle = input; 188 + this.logger.debug("Resolving handle to DID and PDS", { handle }); 189 + const resolved = await this.handleResolver(handle); 190 + this.logger.debug("Handle resolved", { did: resolved.did, pdsUrl: resolved.pdsUrl }); 166 191 167 - // Discover OAuth endpoints from the PDS 168 - const oauthEndpoints = await discoverOAuthEndpointsFromPDS(resolved.pdsUrl); 169 - const authServer = this.extractAuthServer(oauthEndpoints.authorizationEndpoint); 170 - this.logger.debug("OAuth endpoints discovered", { authServer }); 192 + did = resolved.did; 193 + pdsUrl = resolved.pdsUrl; 194 + 195 + // Discover OAuth endpoints from the PDS 196 + const oauthEndpoints = await discoverOAuthEndpointsFromPDS(pdsUrl); 197 + authServer = this.extractAuthServer(oauthEndpoints.authorizationEndpoint); 198 + this.logger.debug("OAuth endpoints discovered", { authServer }); 199 + } 171 200 172 201 // Generate PKCE parameters 173 202 const codeVerifier = generateCodeVerifier(); ··· 178 207 await this.storage.set(`pkce:${state}`, { 179 208 codeVerifier, 180 209 authServer, 181 - handle: handle, 182 - did: resolved.did, 183 - pdsUrl: resolved.pdsUrl, 210 + handle, 211 + did, 212 + pdsUrl, 184 213 }, { ttl: PKCE_STATE_TTL }); 185 214 186 215 this.logger.debug("PKCE state stored", { state }); ··· 192 221 codeChallenge, 193 222 state, 194 223 scope: options?.scope ?? "atproto transition:generic", 195 - loginHint: options?.loginHint ?? handle, 224 + loginHint: options?.loginHint ?? input, 196 225 }, 197 226 ); 198 227