Skip to content

Commit f5479ba

Browse files
committed
feat: add TokenRefreshError with userId and sessionId for debugging
Add custom error classes matching authkit-session pattern: - AuthKitError base class with optional data field - TokenRefreshError with userId/sessionId context for debugging - getSessionErrorContext helper to extract context from sessions Update session refresh failure to include user/session context.
1 parent 29b3c78 commit f5479ba

File tree

4 files changed

+173
-3
lines changed

4 files changed

+173
-3
lines changed

src/errors.spec.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { AuthKitError, TokenRefreshError, getSessionErrorContext } from './errors.js';
2+
import type { Session } from './interfaces.js';
3+
import type { User } from '@workos-inc/node';
4+
5+
describe('AuthKitError', () => {
6+
it('creates error with message', () => {
7+
const error = new AuthKitError('Test error');
8+
9+
expect(error.message).toBe('Test error');
10+
expect(error.name).toBe('AuthKitError');
11+
expect(error).toBeInstanceOf(Error);
12+
});
13+
14+
it('creates error with cause and data', () => {
15+
const originalError = new Error('Original error');
16+
const data = { userId: '123' };
17+
const error = new AuthKitError('Test error', originalError, data);
18+
19+
expect(error.cause).toBe(originalError);
20+
expect(error.data).toEqual(data);
21+
});
22+
});
23+
24+
describe('TokenRefreshError', () => {
25+
it('creates error with correct name and inheritance', () => {
26+
const error = new TokenRefreshError('Refresh failed');
27+
28+
expect(error.name).toBe('TokenRefreshError');
29+
expect(error.message).toBe('Refresh failed');
30+
expect(error).toBeInstanceOf(AuthKitError);
31+
expect(error).toBeInstanceOf(Error);
32+
});
33+
34+
it('creates error with cause and context', () => {
35+
const originalError = new Error('Network error');
36+
const error = new TokenRefreshError('Refresh failed', originalError, {
37+
userId: 'user_123',
38+
sessionId: 'session_456',
39+
});
40+
41+
expect(error.cause).toBe(originalError);
42+
expect(error.userId).toBe('user_123');
43+
expect(error.sessionId).toBe('session_456');
44+
});
45+
46+
it('has undefined properties when no context provided', () => {
47+
const error = new TokenRefreshError('Refresh failed');
48+
49+
expect(error.userId).toBeUndefined();
50+
expect(error.sessionId).toBeUndefined();
51+
});
52+
});
53+
54+
describe('getSessionErrorContext', () => {
55+
function createTestJwt(payload: Record<string, unknown>): string {
56+
const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
57+
const payloadStr = btoa(JSON.stringify(payload));
58+
return `${header}.${payloadStr}.test-signature`;
59+
}
60+
61+
it('returns empty object for missing session', () => {
62+
expect(getSessionErrorContext(null)).toEqual({});
63+
expect(getSessionErrorContext(undefined)).toEqual({});
64+
});
65+
66+
it('extracts userId and sessionId from session', () => {
67+
const session: Session = {
68+
accessToken: createTestJwt({ sid: 'session_123' }),
69+
refreshToken: 'refresh_token',
70+
user: {
71+
id: 'user_456',
72+
email: 'test@example.com',
73+
emailVerified: true,
74+
profilePictureUrl: null,
75+
firstName: null,
76+
lastName: null,
77+
createdAt: '2024-01-01',
78+
updatedAt: '2024-01-01',
79+
object: 'user',
80+
} as User,
81+
};
82+
83+
const context = getSessionErrorContext(session);
84+
expect(context.userId).toBe('user_456');
85+
expect(context.sessionId).toBe('session_123');
86+
});
87+
88+
it('handles invalid JWT gracefully', () => {
89+
const session: Session = {
90+
accessToken: 'invalid-jwt',
91+
refreshToken: 'refresh_token',
92+
user: {
93+
id: 'user_123',
94+
email: 'test@example.com',
95+
emailVerified: true,
96+
profilePictureUrl: null,
97+
firstName: null,
98+
lastName: null,
99+
createdAt: '2024-01-01',
100+
updatedAt: '2024-01-01',
101+
object: 'user',
102+
} as User,
103+
};
104+
105+
const context = getSessionErrorContext(session);
106+
expect(context.userId).toBe('user_123');
107+
expect(context.sessionId).toBeUndefined();
108+
});
109+
});

src/errors.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { Session } from './interfaces.js';
2+
import { decodeJwt } from './jwt.js';
3+
4+
export class AuthKitError extends Error {
5+
data?: Record<string, unknown>;
6+
7+
constructor(message: string, cause?: unknown, data?: Record<string, unknown>) {
8+
super(message);
9+
this.name = 'AuthKitError';
10+
this.cause = cause;
11+
this.data = data;
12+
}
13+
}
14+
15+
export interface TokenRefreshErrorContext {
16+
userId?: string;
17+
sessionId?: string;
18+
}
19+
20+
export class TokenRefreshError extends AuthKitError {
21+
readonly userId?: string;
22+
readonly sessionId?: string;
23+
24+
constructor(message: string, cause?: unknown, context?: TokenRefreshErrorContext) {
25+
super(message, cause);
26+
this.name = 'TokenRefreshError';
27+
this.userId = context?.userId;
28+
this.sessionId = context?.sessionId;
29+
}
30+
}
31+
32+
export function getSessionErrorContext(session?: Session | null): TokenRefreshErrorContext {
33+
if (!session) {
34+
return {};
35+
}
36+
37+
const context: TokenRefreshErrorContext = {};
38+
39+
if (session.user?.id) {
40+
context.userId = session.user.id;
41+
}
42+
43+
if (session.accessToken) {
44+
try {
45+
const { payload } = decodeJwt(session.accessToken);
46+
if (payload.sid) {
47+
context.sessionId = payload.sid;
48+
}
49+
} catch {
50+
// JWT decode can fail for malformed tokens
51+
}
52+
}
53+
54+
return context;
55+
}

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getSignInUrl, getSignUpUrl, signOut, switchToOrganization } from './auth.js';
22
import { handleAuth } from './authkit-callback-route.js';
3+
import { AuthKitError, TokenRefreshError } from './errors.js';
34
import { authkit, authkitMiddleware } from './middleware.js';
45
import { getTokenClaims, refreshSession, saveSession, withAuth } from './session.js';
56
import { validateApiKey } from './validate-api-key.js';
@@ -8,6 +9,8 @@ import { getWorkOS } from './workos.js';
89
export * from './interfaces.js';
910

1011
export {
12+
AuthKitError,
13+
TokenRefreshError,
1114
authkit,
1215
authkitMiddleware,
1316
getSignInUrl,

src/session.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { redirect } from 'next/navigation';
77
import { NextRequest, NextResponse } from 'next/server';
88
import { getCookieOptions, getJwtCookie } from './cookie.js';
99
import { WORKOS_CLIENT_ID, WORKOS_COOKIE_NAME, WORKOS_COOKIE_PASSWORD, WORKOS_REDIRECT_URI } from './env-variables.js';
10+
import { TokenRefreshError, getSessionErrorContext } from './errors.js';
1011
import { getAuthorizationUrl } from './get-authorization-url.js';
1112
import {
1213
AccessToken,
@@ -406,9 +407,11 @@ async function refreshSession({
406407
organizationId: nextOrganizationId ?? organizationIdFromAccessToken,
407408
});
408409
} catch (error) {
409-
throw new Error(`Failed to refresh session: ${error instanceof Error ? error.message : String(error)}`, {
410-
cause: error,
411-
});
410+
throw new TokenRefreshError(
411+
`Failed to refresh session: ${error instanceof Error ? error.message : String(error)}`,
412+
error,
413+
getSessionErrorContext(session),
414+
);
412415
}
413416

414417
const headersList = await headers();

0 commit comments

Comments
 (0)