+59
CHANGELOG.md
+59
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
+
## [3.0.0] - 2025-01-11
9
+
10
+
### Changed
11
+
12
+
- **BREAKING**: `restore()` method now throws typed errors instead of returning `null` on failure
13
+
- Throws `SessionNotFoundError` when session doesn't exist in storage
14
+
- Throws `RefreshTokenExpiredError` when refresh token has expired
15
+
- Throws `RefreshTokenRevokedError` when refresh token has been revoked
16
+
- Throws `NetworkError` for transient network failures
17
+
- Throws `TokenExchangeError` for other token refresh failures
18
+
- Throws `SessionError` for unexpected session restoration failures
19
+
20
+
### Added
21
+
22
+
- **New Error Types**: Added specific error classes for better error handling and debugging
23
+
- `SessionNotFoundError`: Session not found in storage
24
+
- `RefreshTokenExpiredError`: Refresh token has expired
25
+
- `RefreshTokenRevokedError`: Refresh token has been revoked
26
+
- `NetworkError`: Network-related failures (retryable)
27
+
- **Detailed Error Logging**: Added comprehensive logging throughout session restoration and token refresh flows
28
+
- Logs session lookup attempts
29
+
- Logs token refresh operations
30
+
- Logs all error conditions with context
31
+
32
+
### Improved
33
+
34
+
- **Error Visibility**: Session restoration failures now provide detailed error information instead of silent null returns
35
+
- **Error Classification**: Automatic classification of token exchange errors into specific error types
36
+
- **Debugging**: Enhanced logging makes it easier to diagnose OAuth session issues in production
37
+
38
+
### Migration Guide
39
+
40
+
Applications using `restore()` must now handle errors instead of checking for `null`:
41
+
42
+
**Before (v2.x):**
43
+
```typescript
44
+
const session = await client.restore("session-id");
45
+
if (!session) {
46
+
// Handle failure - but why did it fail?
47
+
console.log("Session not found");
48
+
}
49
+
```
50
+
51
+
**After (v3.x):**
52
+
```typescript
53
+
try {
54
+
const session = await client.restore("session-id");
55
+
// Use session
56
+
} catch (error) {
57
+
if (error instanceof SessionNotFoundError) {
58
+
// User needs to log in again
59
+
} else if (error instanceof RefreshTokenExpiredError) {
60
+
// Refresh token expired - re-authenticate required
61
+
} else if (error instanceof NetworkError) {
62
+
// Temporary network issue - retry may help
63
+
}
64
+
}
65
+
```
66
+
8
67
## [2.1.0] - 2025-01-17
9
68
10
69
### Added
+2
-2
deno.json
+2
-2
deno.json
···
1
1
{
2
2
"name": "@tijs/oauth-client-deno",
3
-
"version": "2.1.0",
3
+
"version": "3.0.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": {
···
64
64
"exactOptionalPropertyTypes": true,
65
65
"noImplicitOverride": false
66
66
}
67
-
}
67
+
}
+84
-6
src/client.ts
+84
-6
src/client.ts
···
17
17
AuthorizationError,
18
18
InvalidHandleError,
19
19
InvalidStateError,
20
+
NetworkError,
20
21
OAuthError,
22
+
RefreshTokenExpiredError,
23
+
RefreshTokenRevokedError,
24
+
SessionError,
25
+
SessionNotFoundError,
21
26
TokenExchangeError,
22
27
} from "./errors.ts";
23
28
import { createDefaultResolver, discoverOAuthEndpointsFromPDS } from "./resolvers.ts";
···
314
319
try {
315
320
const sessionData = await this.storage.get<SessionData>(`session:${sessionId}`);
316
321
if (!sessionData) {
317
-
return null;
322
+
console.log(`Session not found in storage: ${sessionId}`);
323
+
throw new SessionNotFoundError(sessionId);
318
324
}
319
325
320
326
const session = Session.fromJSON(sessionData);
321
327
322
328
// Auto-refresh if needed
323
329
if (session.isExpired) {
324
-
const refreshedSession = await this.refresh(session);
325
-
await this.storage.set(`session:${sessionId}`, refreshedSession.toJSON());
326
-
return refreshedSession;
330
+
console.log(`Session expired, attempting token refresh for: ${sessionId}`);
331
+
try {
332
+
const refreshedSession = await this.refresh(session);
333
+
await this.storage.set(`session:${sessionId}`, refreshedSession.toJSON());
334
+
console.log(`Token refresh successful for: ${sessionId}`);
335
+
return refreshedSession;
336
+
} catch (error) {
337
+
console.error(`Token refresh failed for ${sessionId}:`, error);
338
+
// Re-throw with proper error classification
339
+
if (error instanceof TokenExchangeError) {
340
+
// Check for specific refresh token error responses
341
+
if (error.errorCode === "invalid_grant") {
342
+
throw new RefreshTokenExpiredError(error);
343
+
}
344
+
throw error;
345
+
}
346
+
if (error instanceof NetworkError) {
347
+
throw error;
348
+
}
349
+
// Wrap unknown errors as generic token exchange errors
350
+
throw new TokenExchangeError(
351
+
"Token refresh failed",
352
+
undefined,
353
+
error as Error,
354
+
);
355
+
}
327
356
}
328
357
329
358
return session;
330
-
} catch {
331
-
return null;
359
+
} catch (error) {
360
+
// Log all errors for debugging
361
+
console.error(`Session restoration failed for ${sessionId}:`, error);
362
+
363
+
// Re-throw typed errors as-is
364
+
if (
365
+
error instanceof SessionNotFoundError ||
366
+
error instanceof RefreshTokenExpiredError ||
367
+
error instanceof RefreshTokenRevokedError ||
368
+
error instanceof NetworkError ||
369
+
error instanceof TokenExchangeError
370
+
) {
371
+
throw error;
372
+
}
373
+
374
+
// Wrap unexpected errors
375
+
throw new SessionError(
376
+
`Failed to restore session: ${sessionId}`,
377
+
error as Error,
378
+
);
332
379
} finally {
333
380
// Always cleanup the lock when done
334
381
this.refreshLocks.delete(sessionId);
···
381
428
* ```
382
429
*/
383
430
async refresh(session: Session): Promise<Session> {
431
+
console.log(`Refreshing tokens for session with DID: ${session.did}`);
432
+
384
433
try {
434
+
// Discover OAuth endpoints from PDS
385
435
const oauthEndpoints = await discoverOAuthEndpointsFromPDS(session.pdsUrl);
436
+
console.log(`Token endpoint discovered: ${oauthEndpoints.tokenEndpoint}`);
437
+
386
438
const refreshedTokens = await this.refreshTokens(
387
439
oauthEndpoints.tokenEndpoint,
388
440
session.refreshToken,
···
392
444
393
445
// Update session with new tokens
394
446
session.updateTokens(refreshedTokens);
447
+
console.log(`Token refresh successful for DID: ${session.did}`);
395
448
return session;
396
449
} catch (error) {
450
+
console.error(`Token refresh failed for DID ${session.did}:`, error);
451
+
452
+
// Classify the error based on type
453
+
if (error instanceof TokenExchangeError) {
454
+
// Already a TokenExchangeError, check for specific grant errors
455
+
if (error.errorCode === "invalid_grant") {
456
+
throw new RefreshTokenExpiredError(error);
457
+
}
458
+
throw error;
459
+
}
460
+
461
+
// Check for network-related errors
462
+
if (error instanceof Error) {
463
+
const errorMessage = error.message.toLowerCase();
464
+
if (
465
+
errorMessage.includes("network") ||
466
+
errorMessage.includes("timeout") ||
467
+
errorMessage.includes("connection") ||
468
+
errorMessage.includes("fetch")
469
+
) {
470
+
throw new NetworkError("Failed to reach token endpoint", error);
471
+
}
472
+
}
473
+
474
+
// Default to generic token exchange error
397
475
throw new TokenExchangeError("Token refresh failed", undefined, error as Error);
398
476
}
399
477
}
+117
src/errors.ts
+117
src/errors.ts
···
326
326
this.name = "AuthorizationError";
327
327
}
328
328
}
329
+
330
+
/**
331
+
* Thrown when a session cannot be found in storage.
332
+
*
333
+
* This error occurs during session restoration when the requested session
334
+
* ID does not exist in storage, indicating the user needs to re-authenticate.
335
+
*
336
+
* @example
337
+
* ```ts
338
+
* try {
339
+
* const session = await client.restore("unknown-session-id");
340
+
* } catch (error) {
341
+
* if (error instanceof SessionNotFoundError) {
342
+
* console.log("Session expired or doesn't exist - please log in again");
343
+
* }
344
+
* }
345
+
* ```
346
+
*/
347
+
export class SessionNotFoundError extends SessionError {
348
+
/**
349
+
* Create a new session not found error.
350
+
*
351
+
* @param sessionId - The session ID that was not found
352
+
*/
353
+
constructor(sessionId: string) {
354
+
super(`Session not found: ${sessionId}`);
355
+
this.name = "SessionNotFoundError";
356
+
}
357
+
}
358
+
359
+
/**
360
+
* Thrown when a refresh token has expired and cannot be used.
361
+
*
362
+
* Refresh tokens have a limited lifetime. This error occurs when attempting
363
+
* to use an expired refresh token, requiring the user to re-authenticate.
364
+
*
365
+
* @example
366
+
* ```ts
367
+
* try {
368
+
* const session = await client.restore("session-id");
369
+
* } catch (error) {
370
+
* if (error instanceof RefreshTokenExpiredError) {
371
+
* console.log("Refresh token expired - please log in again");
372
+
* }
373
+
* }
374
+
* ```
375
+
*/
376
+
export class RefreshTokenExpiredError extends TokenExchangeError {
377
+
/**
378
+
* Create a new refresh token expired error.
379
+
*
380
+
* @param cause - Optional underlying error from the token endpoint
381
+
*/
382
+
constructor(cause?: Error) {
383
+
super("Refresh token has expired", "invalid_grant", cause);
384
+
this.name = "RefreshTokenExpiredError";
385
+
}
386
+
}
387
+
388
+
/**
389
+
* Thrown when a refresh token has been revoked by the authorization server.
390
+
*
391
+
* This error occurs when attempting to use a refresh token that has been
392
+
* explicitly revoked, requiring the user to re-authenticate.
393
+
*
394
+
* @example
395
+
* ```ts
396
+
* try {
397
+
* const session = await client.restore("session-id");
398
+
* } catch (error) {
399
+
* if (error instanceof RefreshTokenRevokedError) {
400
+
* console.log("Access has been revoked - please log in again");
401
+
* }
402
+
* }
403
+
* ```
404
+
*/
405
+
export class RefreshTokenRevokedError extends TokenExchangeError {
406
+
/**
407
+
* Create a new refresh token revoked error.
408
+
*
409
+
* @param cause - Optional underlying error from the token endpoint
410
+
*/
411
+
constructor(cause?: Error) {
412
+
super("Refresh token has been revoked", "invalid_grant", cause);
413
+
this.name = "RefreshTokenRevokedError";
414
+
}
415
+
}
416
+
417
+
/**
418
+
* Thrown when network operations fail during OAuth operations.
419
+
*
420
+
* This error indicates a transient network failure that may be retryable,
421
+
* such as connection timeouts, DNS failures, or network unavailability.
422
+
*
423
+
* @example
424
+
* ```ts
425
+
* try {
426
+
* const session = await client.restore("session-id");
427
+
* } catch (error) {
428
+
* if (error instanceof NetworkError) {
429
+
* console.log("Network error - retrying may help:", error.message);
430
+
* }
431
+
* }
432
+
* ```
433
+
*/
434
+
export class NetworkError extends OAuthError {
435
+
/**
436
+
* Create a new network error.
437
+
*
438
+
* @param message - Error message describing the network failure
439
+
* @param cause - Optional underlying error from the network operation
440
+
*/
441
+
constructor(message: string, cause?: Error) {
442
+
super(`Network error: ${message}`, cause);
443
+
this.name = "NetworkError";
444
+
}
445
+
}