A Deno-compatible AT Protocol OAuth client that serves as a drop-in replacement for @atproto/oauth-client-node
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}