Framework-agnostic OAuth integration for AT Protocol (Bluesky) applications.

Remove unused mobile OAuth support (v2.0.0)

Breaking change: Removes mobile-specific features that were unused by the
iOS app (which uses cookie-based auth via WebView instead):
- mobileScheme config option
- mobile and code_challenge query params on /login
- Mobile callback with session_token redirect
- Bearer token auth in getSessionFromRequest()

Now focuses solely on cookie-based session management.
Updates @tijs/atproto-sessions to 2.0.0.

+23
CHANGELOG.md
··· 2 2 3 3 All notable changes to this project will be documented in this file. 4 4 5 + ## [2.0.0] - 2025-11-29 6 + 7 + ### Breaking Changes 8 + 9 + - **Removed mobile OAuth support**: The following features have been removed as 10 + they were unused (the Anchor iOS app uses cookie-based auth via a WebView): 11 + - `mobileScheme` config option - No longer needed 12 + - `mobile` query parameter on `/login` - Removed 13 + - `code_challenge` query parameter on `/login` - Removed 14 + - Mobile callback with `session_token` - Removed 15 + - Bearer token authentication in `getSessionFromRequest()` - Removed (now 16 + cookie-only) 17 + - **Removed types**: `MobileOAuthStartRequest`, `MobileOAuthStartResponse` 18 + - **Simplified `OAuthState`**: Removed `mobile` and `codeChallenge` fields 19 + 20 + The library now focuses solely on cookie-based session management for web 21 + applications. Mobile apps should use app-specific WebView flows with cookie 22 + authentication. 23 + 24 + ### Changed 25 + 26 + - Updated `@tijs/atproto-sessions` dependency to 2.0.0 27 + 5 28 ## [1.1.1] - 2025-11-28 6 29 7 30 ### Fixed
+2 -2
deno.json
··· 1 1 { 2 2 "$schema": "https://jsr.io/schema/config-file.v1.json", 3 3 "name": "@tijs/atproto-oauth", 4 - "version": "1.1.1", 4 + "version": "2.0.0", 5 5 "license": "MIT", 6 6 "exports": "./mod.ts", 7 7 "publish": { ··· 11 11 "imports": { 12 12 "@std/assert": "jsr:@std/assert@1.0.16", 13 13 "@tijs/oauth-client-deno": "jsr:@tijs/oauth-client-deno@4.0.2", 14 - "@tijs/atproto-sessions": "jsr:@tijs/atproto-sessions@1.0.1", 14 + "@tijs/atproto-sessions": "jsr:@tijs/atproto-sessions@2.0.0", 15 15 "@tijs/atproto-storage": "jsr:@tijs/atproto-storage@0.1.1", 16 16 "@atproto/syntax": "npm:@atproto/syntax@0.3.0" 17 17 },
-2
mod.ts
··· 43 43 ATProtoOAuthInstance, 44 44 ClientMetadata, 45 45 Logger, 46 - MobileOAuthStartRequest, 47 - MobileOAuthStartResponse, 48 46 OAuthClientInterface, 49 47 OAuthSessionFromRequestResult, 50 48 OAuthSessionsInterface,
-5
src/oauth.ts
··· 19 19 /** Default session TTL: 7 days in seconds */ 20 20 const DEFAULT_SESSION_TTL = 60 * 60 * 24 * 7; 21 21 22 - /** Default mobile callback scheme */ 23 - const DEFAULT_MOBILE_SCHEME = "app://auth-callback"; 24 - 25 22 /** 26 23 * Create a complete ATProto OAuth integration for any framework. 27 24 * ··· 108 105 // Normalize baseUrl 109 106 const baseUrl = config.baseUrl.replace(/\/$/, ""); 110 107 const sessionTtl = config.sessionTtl ?? DEFAULT_SESSION_TTL; 111 - const mobileScheme = config.mobileScheme ?? DEFAULT_MOBILE_SCHEME; 112 108 const logger: Logger = config.logger ?? noopLogger; 113 109 114 110 // Create OAuth client (Logger interfaces now match) ··· 143 139 oauthSessions, 144 140 storage: config.storage, 145 141 sessionTtl, 146 - mobileScheme, 147 142 logger, 148 143 }); 149 144
+11 -111
src/routes.ts
··· 26 26 oauthSessions: OAuthSessions; 27 27 storage: OAuthStorage; 28 28 sessionTtl: number; 29 - mobileScheme: string; 30 29 logger: Logger; 31 30 } 32 31 ··· 48 47 oauthSessions, 49 48 storage, 50 49 sessionTtl, 51 - mobileScheme, 52 50 logger, 53 51 } = config; 54 52 ··· 57 55 * 58 56 * Query parameters: 59 57 * - handle: User's AT Protocol handle (required) 60 - * - redirect: Relative path to redirect after web OAuth (optional) 61 - * - mobile: "true" to enable mobile flow with configured mobileScheme redirect (optional) 62 - * - code_challenge: PKCE code_challenge from mobile client (optional, for future use) 63 - * 64 - * Security: Mobile redirects always use the server-configured mobileScheme. 65 - * Client-specified redirect schemes are NOT allowed to prevent OAuth redirect attacks. 58 + * - redirect: Relative path to redirect after OAuth (optional) 66 59 */ 67 60 async function handleLogin(request: Request): Promise<Response> { 68 61 const url = new URL(request.url); 69 62 const handle = url.searchParams.get("handle"); 70 63 const redirect = url.searchParams.get("redirect"); 71 - const mobile = url.searchParams.get("mobile") === "true"; 72 - const codeChallenge = url.searchParams.get("code_challenge"); 73 64 74 65 if (!handle || typeof handle !== "string") { 75 66 return new Response("Invalid handle", { status: 400 }); ··· 85 76 timestamp: Date.now(), 86 77 }; 87 78 88 - // Mobile flow configuration 89 - if (mobile) { 90 - state.mobile = true; 91 - logger.info(`Starting mobile OAuth flow for handle: ${handle}`); 92 - 93 - // Store PKCE code_challenge for mobile (library generates its own, but 94 - // in the future we could support external challenges for native apps) 95 - if (codeChallenge) { 96 - state.codeChallenge = codeChallenge; 97 - } 98 - } else { 99 - // Web flow - store redirect path (validate it's a relative path) 100 - if (redirect) { 101 - // Security: Only allow relative paths starting with / 102 - if (redirect.startsWith("/") && !redirect.startsWith("//")) { 103 - state.redirectPath = redirect; 104 - } else { 105 - logger.warn(`Invalid redirect path ignored: ${redirect}`); 106 - } 79 + // Store redirect path (validate it's a relative path) 80 + if (redirect) { 81 + // Security: Only allow relative paths starting with / 82 + if (redirect.startsWith("/") && !redirect.startsWith("//")) { 83 + state.redirectPath = redirect; 84 + } else { 85 + logger.warn(`Invalid redirect path ignored: ${redirect}`); 107 86 } 108 87 } 109 88 ··· 167 146 lastAccessed: now, 168 147 }); 169 148 170 - // Handle mobile callback 171 - if (state.mobile) { 172 - const sealedToken = await sessionManager.sealToken({ did }); 173 - 174 - // Always use server-configured mobileScheme for security 175 - const mobileCallbackUrl = new URL(mobileScheme); 176 - mobileCallbackUrl.searchParams.set("session_token", sealedToken); 177 - mobileCallbackUrl.searchParams.set("did", did); 178 - mobileCallbackUrl.searchParams.set("handle", state.handle); 179 - 180 - if (oauthSession.accessToken) { 181 - mobileCallbackUrl.searchParams.set( 182 - "access_token", 183 - oauthSession.accessToken, 184 - ); 185 - } 186 - if (oauthSession.refreshToken) { 187 - mobileCallbackUrl.searchParams.set( 188 - "refresh_token", 189 - oauthSession.refreshToken, 190 - ); 191 - } 192 - 193 - logger.info( 194 - `Mobile OAuth callback complete for ${did}, redirecting to ${mobileScheme}`, 195 - ); 196 - 197 - return new Response(null, { 198 - status: 302, 199 - headers: { 200 - Location: mobileCallbackUrl.toString(), 201 - "Set-Cookie": setCookieHeader, 202 - }, 203 - }); 204 - } 205 - 206 - // Web callback - redirect to stored path or home 149 + // Redirect to stored path or home 207 150 const redirectPath = state.redirectPath || "/"; 208 151 209 152 return new Response(null, { ··· 254 197 } 255 198 256 199 /** 257 - * Get OAuth session from request (cookie or Bearer token) 200 + * Get OAuth session from request (cookie-based) 258 201 */ 259 202 async function getSessionFromRequest( 260 203 request: Request, 261 204 ): Promise<OAuthSessionFromRequestResult> { 262 - // Check for Bearer token first (mobile) 263 - const authHeader = request.headers.get("Authorization"); 264 - if (authHeader && authHeader.startsWith("Bearer ")) { 265 - const tokenResult = await sessionManager.validateBearerToken(authHeader); 266 - if (tokenResult.data?.did) { 267 - try { 268 - const oauthSession = await oauthSessions.getOAuthSession( 269 - tokenResult.data.did, 270 - ); 271 - if (oauthSession) { 272 - return { session: oauthSession }; 273 - } 274 - return { 275 - session: null, 276 - error: { 277 - type: "SESSION_EXPIRED", 278 - message: "OAuth session not found in storage", 279 - }, 280 - }; 281 - } catch (error) { 282 - return { 283 - session: null, 284 - error: { 285 - type: "OAUTH_ERROR", 286 - message: error instanceof Error 287 - ? error.message 288 - : "OAuth session restore failed", 289 - details: error, 290 - }, 291 - }; 292 - } 293 - } 294 - return { 295 - session: null, 296 - error: tokenResult.error 297 - ? { 298 - type: "INVALID_COOKIE", 299 - message: tokenResult.error.message, 300 - } 301 - : { type: "INVALID_COOKIE", message: "Invalid token" }, 302 - }; 303 - } 304 - 305 - // Check for session cookie (web) 205 + // Check for session cookie 306 206 const sessionResult = await sessionManager.getSessionFromRequest(request); 307 207 if (!sessionResult.data?.did) { 308 208 return {
+1 -25
src/types.ts
··· 110 110 /** Display name for OAuth consent screen */ 111 111 appName: string; 112 112 113 - /** Custom URL scheme for mobile app callbacks (default: "app://auth-callback") */ 114 - mobileScheme?: string; 115 - 116 113 /** URL to app logo for OAuth consent screen */ 117 114 logoUri?: string; 118 115 ··· 167 164 valid: boolean; 168 165 did?: string; 169 166 handle?: string; 170 - } 171 - 172 - /** 173 - * Mobile OAuth start request 174 - */ 175 - export interface MobileOAuthStartRequest { 176 - handle: string; 177 - code_challenge: string; 178 - } 179 - 180 - /** 181 - * Mobile OAuth start response 182 - */ 183 - export interface MobileOAuthStartResponse { 184 - success: boolean; 185 - authUrl?: string; 186 - error?: string; 187 167 } 188 168 189 169 /** ··· 318 298 export interface OAuthState { 319 299 handle: string; 320 300 timestamp: number; 321 - /** Whether this is a mobile OAuth flow */ 322 - mobile?: boolean; 323 - /** PKCE code_challenge for mobile flows (generated by mobile client) */ 324 - codeChallenge?: string; 325 - /** Redirect path for web flows */ 301 + /** Redirect path after successful web OAuth */ 326 302 redirectPath?: string; 327 303 } 328 304