A social knowledge tool for researchers built on ATProto
1export interface AuthTokens {
2 accessToken: string | null;
3 refreshToken: string | null;
4}
5
6export interface TokenStorage {
7 getTokens(): AuthTokens;
8 setTokens(accessToken: string, refreshToken: string): Promise<void>;
9 clearTokens(): void;
10}
11
12export interface TokenRefresher {
13 refreshTokens(
14 refreshToken: string,
15 ): Promise<{ accessToken: string; refreshToken: string }>;
16}
17
18export class TokenManager {
19 private isRefreshing = false;
20 private refreshPromise: Promise<boolean> | null = null;
21 private failedRequestsQueue: Array<{
22 resolve: (value: any) => void;
23 reject: (error: any) => void;
24 request: () => Promise<any>;
25 }> = [];
26
27 constructor(
28 private storage: TokenStorage,
29 private refresher: TokenRefresher,
30 ) {}
31
32 async getAccessToken(): Promise<string | null> {
33 const { accessToken } = this.storage.getTokens();
34 return accessToken;
35 }
36
37 async getRefreshToken(): Promise<string | null> {
38 const { refreshToken } = this.storage.getTokens();
39 return refreshToken;
40 }
41
42 async handleAuthError<T>(originalRequest: () => Promise<T>): Promise<T> {
43 // If already refreshing, queue this request
44 if (this.isRefreshing) {
45 return new Promise((resolve, reject) => {
46 this.failedRequestsQueue.push({
47 resolve,
48 reject,
49 request: originalRequest,
50 });
51 });
52 }
53
54 // Start refresh process
55 this.isRefreshing = true;
56
57 try {
58 // Use existing refresh promise or create new one
59 if (!this.refreshPromise) {
60 this.refreshPromise = this.performRefresh();
61 }
62
63 const refreshSuccess = await this.refreshPromise;
64
65 if (refreshSuccess) {
66 // Process queued requests
67 const queuedRequests = [...this.failedRequestsQueue];
68 this.failedRequestsQueue = [];
69
70 // Retry all queued requests
71 queuedRequests.forEach(async ({ resolve, reject, request }) => {
72 try {
73 const result = await request();
74 resolve(result);
75 } catch (error) {
76 reject(error);
77 }
78 });
79
80 // Retry original request
81 return await originalRequest();
82 } else {
83 // Refresh failed, reject all queued requests
84 const queuedRequests = [...this.failedRequestsQueue];
85 this.failedRequestsQueue = [];
86
87 const refreshError = new Error('Token refresh failed');
88 queuedRequests.forEach(({ reject }) => reject(refreshError));
89
90 if (typeof window !== 'undefined') {
91 window.location.href = '/login';
92 }
93 throw refreshError;
94 }
95 } finally {
96 this.isRefreshing = false;
97 this.refreshPromise = null;
98 }
99 }
100
101 private async performRefresh(): Promise<boolean> {
102 const { refreshToken } = this.storage.getTokens();
103 if (!refreshToken) return false;
104
105 try {
106 const newTokens = await this.refresher.refreshTokens(refreshToken);
107 await this.storage.setTokens(
108 newTokens.accessToken,
109 newTokens.refreshToken,
110 );
111 return true;
112 } catch (error) {
113 console.error('Token refresh failed:', error);
114 this.storage.clearTokens();
115 return false;
116 }
117 }
118}