A Deno-compatible AT Protocol OAuth client that serves as a drop-in replacement for @atproto/oauth-client-node
at main 13 kB view raw
1/** 2 * @fileoverview Custom error classes for OAuth client operations 3 * @module 4 */ 5 6/** 7 * Base OAuth error class for all OAuth-related errors. 8 * 9 * Provides a common base class for all OAuth client errors with optional 10 * error chaining support. All other OAuth errors extend from this class. 11 * 12 * @example 13 * ```ts 14 * try { 15 * await client.authorize("invalid-handle"); 16 * } catch (error) { 17 * if (error instanceof OAuthError) { 18 * console.log("OAuth operation failed:", error.message); 19 * if (error.cause) { 20 * console.log("Underlying cause:", error.cause.message); 21 * } 22 * } 23 * } 24 * ``` 25 */ 26export class OAuthError extends Error { 27 /** Optional underlying error that caused this OAuth error */ 28 public readonly cause?: Error; 29 30 /** 31 * Create a new OAuth error. 32 * 33 * @param message - Error message describing what went wrong 34 * @param cause - Optional underlying error that caused this OAuth error 35 */ 36 constructor(message: string, cause?: Error) { 37 super(message); 38 this.name = "OAuthError"; 39 if (cause) { 40 this.cause = cause; 41 } 42 } 43} 44 45/** 46 * Thrown when an AT Protocol handle has invalid format. 47 * 48 * AT Protocol handles must follow specific formatting rules. This error 49 * is thrown when a handle doesn't match the expected format. 50 * 51 * @example 52 * ```ts 53 * try { 54 * await client.authorize("invalid-handle-format!!!"); 55 * } catch (error) { 56 * if (error instanceof InvalidHandleError) { 57 * console.log("Please provide a valid handle like 'alice.bsky.social'"); 58 * } 59 * } 60 * ``` 61 */ 62export class InvalidHandleError extends OAuthError { 63 /** 64 * Create a new invalid handle error. 65 * 66 * @param handle - The invalid handle that was provided 67 */ 68 constructor(handle: string) { 69 super(`Invalid AT Protocol handle: ${handle}`); 70 this.name = "InvalidHandleError"; 71 } 72} 73 74/** 75 * Thrown when a handle cannot be resolved to a DID and PDS URL. 76 * 77 * This error occurs during the handle resolution process when the handle 78 * cannot be found in the AT Protocol directory or when the resolution 79 * service is unavailable. 80 * 81 * @example 82 * ```ts 83 * try { 84 * await client.authorize("nonexistent.handle.social"); 85 * } catch (error) { 86 * if (error instanceof HandleResolutionError) { 87 * console.log("Handle not found or resolution service unavailable"); 88 * } 89 * } 90 * ``` 91 */ 92export class HandleResolutionError extends OAuthError { 93 /** 94 * Create a new handle resolution error. 95 * 96 * @param handle - The handle that failed to resolve 97 * @param cause - Optional underlying error that caused the resolution failure 98 */ 99 constructor(handle: string, cause?: Error) { 100 super(`Failed to resolve handle ${handle} to DID and PDS`, cause); 101 this.name = "HandleResolutionError"; 102 } 103} 104 105/** 106 * Thrown when OAuth endpoints cannot be discovered from a PDS. 107 * 108 * This error occurs when the PDS doesn't expose the required OAuth 109 * configuration endpoints or when the endpoints are malformed. 110 * 111 * @example 112 * ```ts 113 * try { 114 * await client.authorize("user.custom-pds.com"); 115 * } catch (error) { 116 * if (error instanceof PDSDiscoveryError) { 117 * console.log("PDS doesn't support OAuth or endpoints are unavailable"); 118 * } 119 * } 120 * ``` 121 */ 122export class PDSDiscoveryError extends OAuthError { 123 /** 124 * Create a new PDS discovery error. 125 * 126 * @param pdsUrl - The PDS URL where discovery failed 127 * @param cause - Optional underlying error that caused the discovery failure 128 */ 129 constructor(pdsUrl: string, cause?: Error) { 130 super(`Failed to discover OAuth endpoints for PDS: ${pdsUrl}`, cause); 131 this.name = "PDSDiscoveryError"; 132 } 133} 134 135/** 136 * Thrown when the authentication server cannot be discovered from a PDS. 137 * 138 * This error typically occurs with custom domain setups where the OAuth 139 * authorization server is separate from the PDS. It indicates that the 140 * authentication server URL couldn't be determined from the PDS configuration. 141 * 142 * @example 143 * ```ts 144 * try { 145 * await client.authorize("user.custom-domain.com"); 146 * } catch (error) { 147 * if (error instanceof AuthServerDiscoveryError) { 148 * console.log("Custom domain OAuth setup may be misconfigured"); 149 * } 150 * } 151 * ``` 152 */ 153export class AuthServerDiscoveryError extends OAuthError { 154 /** 155 * Create a new auth server discovery error. 156 * 157 * @param pdsUrl - The PDS URL where auth server discovery failed 158 * @param cause - Optional underlying error that caused the discovery failure 159 */ 160 constructor(pdsUrl: string, cause?: Error) { 161 super( 162 `Failed to discover authentication server from PDS: ${pdsUrl}. This may be a custom domain setup issue.`, 163 cause, 164 ); 165 this.name = "AuthServerDiscoveryError"; 166 } 167} 168 169/** 170 * Thrown when OAuth token exchange operations fail. 171 * 172 * This error occurs during authorization code exchange or token refresh 173 * operations when the OAuth server rejects the request or returns an error. 174 * 175 * @example 176 * ```ts 177 * try { 178 * const { session } = await client.callback(params); 179 * } catch (error) { 180 * if (error instanceof TokenExchangeError) { 181 * console.log("Token exchange failed:", error.message); 182 * if (error.errorCode) { 183 * console.log("OAuth error code:", error.errorCode); 184 * } 185 * if (error.errorDescription) { 186 * console.log("OAuth error description:", error.errorDescription); 187 * } 188 * } 189 * } 190 * ``` 191 */ 192export class TokenExchangeError extends OAuthError { 193 /** OAuth error code from the server (e.g., "invalid_grant") */ 194 public readonly errorCode?: string; 195 196 /** OAuth error_description from the server (e.g., "Refresh token replayed") */ 197 public readonly errorDescription?: string; 198 199 /** 200 * Create a new token exchange error. 201 * 202 * @param message - Error message describing what went wrong 203 * @param errorCode - Optional OAuth error code from the server 204 * @param cause - Optional underlying error that caused the token exchange failure 205 * @param errorDescription - Optional OAuth error_description from the server 206 */ 207 constructor(message: string, errorCode?: string, cause?: Error, errorDescription?: string) { 208 super(`Token exchange failed: ${message}`, cause); 209 this.name = "TokenExchangeError"; 210 if (errorCode) { 211 this.errorCode = errorCode; 212 } 213 if (errorDescription) { 214 this.errorDescription = errorDescription; 215 } 216 } 217} 218 219/** 220 * Thrown when DPoP (Demonstration of Proof-of-Possession) operations fail. 221 * 222 * DPoP is used for secure token binding in OAuth flows. This error occurs 223 * when DPoP key generation, proof creation, or validation fails. 224 * 225 * @example 226 * ```ts 227 * try { 228 * await session.makeRequest("GET", "/xrpc/endpoint"); 229 * } catch (error) { 230 * if (error instanceof DPoPError) { 231 * console.log("DPoP authentication failed:", error.message); 232 * } 233 * } 234 * ``` 235 */ 236export class DPoPError extends OAuthError { 237 /** 238 * Create a new DPoP error. 239 * 240 * @param message - Error message describing the DPoP operation failure 241 * @param cause - Optional underlying error that caused the DPoP failure 242 */ 243 constructor(message: string, cause?: Error) { 244 super(`DPoP operation failed: ${message}`, cause); 245 this.name = "DPoPError"; 246 } 247} 248 249/** 250 * Thrown when session operations encounter errors. 251 * 252 * This error occurs during session management operations like token refresh, 253 * request signing, or session restoration when the session state is invalid 254 * or operations fail. 255 * 256 * @example 257 * ```ts 258 * try { 259 * const session = await client.restore("session-id"); 260 * await session.makeRequest("GET", "/api/endpoint"); 261 * } catch (error) { 262 * if (error instanceof SessionError) { 263 * console.log("Session operation failed:", error.message); 264 * } 265 * } 266 * ``` 267 */ 268export class SessionError extends OAuthError { 269 /** 270 * Create a new session error. 271 * 272 * @param message - Error message describing the session operation failure 273 * @param cause - Optional underlying error that caused the session failure 274 */ 275 constructor(message: string, cause?: Error) { 276 super(`Session error: ${message}`, cause); 277 this.name = "SessionError"; 278 } 279} 280 281/** 282 * Thrown when the OAuth state parameter is invalid or expired. 283 * 284 * The state parameter is used for CSRF protection in OAuth flows. This error 285 * occurs when the state parameter in the callback doesn't match the expected 286 * value or has expired. 287 * 288 * @example 289 * ```ts 290 * try { 291 * const { session } = await client.callback(params); 292 * } catch (error) { 293 * if (error instanceof InvalidStateError) { 294 * console.log("OAuth state validation failed - possible CSRF attack"); 295 * } 296 * } 297 * ``` 298 */ 299export class InvalidStateError extends OAuthError { 300 /** 301 * Create a new invalid state error. 302 */ 303 constructor() { 304 super("Invalid or expired OAuth state parameter"); 305 this.name = "InvalidStateError"; 306 } 307} 308 309/** 310 * Thrown when OAuth authorization fails at the authorization server. 311 * 312 * This error occurs when the authorization server returns an error during 313 * the OAuth flow, typically due to user denial, invalid client configuration, 314 * or server-side issues. 315 * 316 * @example 317 * ```ts 318 * try { 319 * const { session } = await client.callback(params); 320 * } catch (error) { 321 * if (error instanceof AuthorizationError) { 322 * console.log("Authorization was denied or failed:", error.message); 323 * } 324 * } 325 * ``` 326 */ 327export class AuthorizationError extends OAuthError { 328 /** 329 * Create a new authorization error. 330 * 331 * @param error - OAuth error code from the authorization server 332 * @param description - Optional human-readable error description 333 */ 334 constructor(error: string, description?: string) { 335 super(`Authorization failed: ${error}${description ? ` - ${description}` : ""}`); 336 this.name = "AuthorizationError"; 337 } 338} 339 340/** 341 * Thrown when a session cannot be found in storage. 342 * 343 * This error occurs during session restoration when the requested session 344 * ID does not exist in storage, indicating the user needs to re-authenticate. 345 * 346 * @example 347 * ```ts 348 * try { 349 * const session = await client.restore("unknown-session-id"); 350 * } catch (error) { 351 * if (error instanceof SessionNotFoundError) { 352 * console.log("Session expired or doesn't exist - please log in again"); 353 * } 354 * } 355 * ``` 356 */ 357export class SessionNotFoundError extends SessionError { 358 /** 359 * Create a new session not found error. 360 * 361 * @param sessionId - The session ID that was not found 362 */ 363 constructor(sessionId: string) { 364 super(`Session not found: ${sessionId}`); 365 this.name = "SessionNotFoundError"; 366 } 367} 368 369/** 370 * Thrown when a refresh token has expired and cannot be used. 371 * 372 * Refresh tokens have a limited lifetime. This error occurs when attempting 373 * to use an expired refresh token, requiring the user to re-authenticate. 374 * 375 * @example 376 * ```ts 377 * try { 378 * const session = await client.restore("session-id"); 379 * } catch (error) { 380 * if (error instanceof RefreshTokenExpiredError) { 381 * console.log("Refresh token expired - please log in again"); 382 * } 383 * } 384 * ``` 385 */ 386export class RefreshTokenExpiredError extends TokenExchangeError { 387 /** 388 * Create a new refresh token expired error. 389 * 390 * @param cause - Optional underlying error from the token endpoint 391 */ 392 constructor(cause?: Error) { 393 super("Refresh token has expired", "invalid_grant", cause); 394 this.name = "RefreshTokenExpiredError"; 395 } 396} 397 398/** 399 * Thrown when a refresh token has been revoked by the authorization server. 400 * 401 * This error occurs when attempting to use a refresh token that has been 402 * explicitly revoked, requiring the user to re-authenticate. 403 * 404 * @example 405 * ```ts 406 * try { 407 * const session = await client.restore("session-id"); 408 * } catch (error) { 409 * if (error instanceof RefreshTokenRevokedError) { 410 * console.log("Access has been revoked - please log in again"); 411 * } 412 * } 413 * ``` 414 */ 415export class RefreshTokenRevokedError extends TokenExchangeError { 416 /** 417 * Create a new refresh token revoked error. 418 * 419 * @param cause - Optional underlying error from the token endpoint 420 */ 421 constructor(cause?: Error) { 422 super("Refresh token has been revoked", "invalid_grant", cause); 423 this.name = "RefreshTokenRevokedError"; 424 } 425} 426 427/** 428 * Thrown when network operations fail during OAuth operations. 429 * 430 * This error indicates a transient network failure that may be retryable, 431 * such as connection timeouts, DNS failures, or network unavailability. 432 * 433 * @example 434 * ```ts 435 * try { 436 * const session = await client.restore("session-id"); 437 * } catch (error) { 438 * if (error instanceof NetworkError) { 439 * console.log("Network error - retrying may help:", error.message); 440 * } 441 * } 442 * ``` 443 */ 444export class NetworkError extends OAuthError { 445 /** 446 * Create a new network error. 447 * 448 * @param message - Error message describing the network failure 449 * @param cause - Optional underlying error from the network operation 450 */ 451 constructor(message: string, cause?: Error) { 452 super(`Network error: ${message}`, cause); 453 this.name = "NetworkError"; 454 } 455}