1 import type { AuthSession } from '@proton/pass/lib/auth/session';
2 import type { ApiAuth, ApiCallFn, ApiOptions, Maybe, MaybePromise } from '@proton/pass/types';
3 import { AuthMode } from '@proton/pass/types';
4 import { asyncLock } from '@proton/pass/utils/fp/promises';
5 import { logger } from '@proton/pass/utils/logger';
6 import { setRefreshCookies as refreshTokens, setRefreshCookies } from '@proton/shared/lib/api/auth';
7 import { InactiveSessionError } from '@proton/shared/lib/api/helpers/errors';
8 import { retryHandler } from '@proton/shared/lib/api/helpers/retryHandler';
9 import type { RefreshSessionResponse } from '@proton/shared/lib/authentication/interface';
10 import { OFFLINE_RETRY_ATTEMPTS_MAX, OFFLINE_RETRY_DELAY, RETRY_ATTEMPTS_MAX } from '@proton/shared/lib/constants';
11 import { HTTP_ERROR_CODES } from '@proton/shared/lib/errors';
12 import { withAuthHeaders, withUIDHeaders } from '@proton/shared/lib/fetch/headers';
13 import { getDateHeader } from '@proton/shared/lib/fetch/helpers';
14 import { wait } from '@proton/shared/lib/helpers/promise';
15 import randomIntFromInterval from '@proton/utils/randomIntFromInterval';
17 type RefreshCookieResponse = { LocalID?: number; RefreshCounter: number; RefreshTime: number; UID: string };
19 export type RefreshSessionData = Pick<AuthSession, 'UID' | 'AccessToken' | 'RefreshToken' | 'RefreshTime' | 'cookies'>;
20 export type RefreshHandler = (response: Response, options: ApiOptions) => Promise<void>;
21 export type OnRefreshCallback = (response: RefreshSessionData) => MaybePromise<void>;
23 export type DynamicRefreshResult =
24 | { type: AuthMode.COOKIE; response: { json: () => Promise<RefreshCookieResponse> } }
25 | { type: AuthMode.TOKEN; response: { json: () => Promise<RefreshSessionResponse> } };
27 type CreateRefreshHandlerConfig = { call: ApiCallFn; getAuth: () => Maybe<ApiAuth>; onRefresh: OnRefreshCallback };
29 /** Handle refresh token. Happens when the access token has expired.
30 * Multiple calls can fail, so this ensures the refresh route is called once.
31 * Needs to re-handle errors here for that reason. */
32 const refreshFactory = (call: ApiCallFn, getAuth: () => Maybe<ApiAuth>) => {
33 const next = async (maxAttempts: number, attempt: number = 1): Promise<DynamicRefreshResult> => {
34 const auth = getAuth();
35 if (auth === undefined) throw InactiveSessionError();
41 auth.type === AuthMode.COOKIE
42 ? withUIDHeaders(auth.UID, setRefreshCookies())
46 refreshTokens({ RefreshToken: auth.RefreshToken })
50 } catch (error: any) {
51 if (attempt >= maxAttempts) throw error;
52 const { status, name } = error;
54 if (['OfflineError', 'TimeoutError'].includes(name)) {
55 if (attempt > OFFLINE_RETRY_ATTEMPTS_MAX) throw error;
56 await wait(name === 'OfflineError' ? OFFLINE_RETRY_DELAY : 0);
57 return next(OFFLINE_RETRY_ATTEMPTS_MAX, attempt + 1);
60 if (status === HTTP_ERROR_CODES.TOO_MANY_REQUESTS) {
61 await retryHandler(error);
62 return next(maxAttempts, attempt + 1);
72 export const refreshHandlerFactory = (config: CreateRefreshHandlerConfig) =>
73 asyncLock<RefreshHandler>(
74 async (response, options) => {
75 /** Override the default `getAuth` if an `auth` option was passed
76 * to the underlying API request that triggered the refresh. */
77 const getAuth = () => options.auth ?? config.getAuth();
78 const refresh = refreshFactory(config.call, getAuth);
80 const responseDate = getDateHeader(response.headers);
81 const lastRefreshDate = getAuth()?.RefreshTime;
83 if (lastRefreshDate === undefined || +(responseDate ?? new Date()) > lastRefreshDate) {
84 const result = await refresh(RETRY_ATTEMPTS_MAX);
85 const RefreshTime = +(getDateHeader(response.headers) ?? new Date());
87 const refreshData = await (async (): Promise<RefreshSessionData> => {
88 switch (result.type) {
89 case AuthMode.TOKEN: {
90 const { AccessToken, RefreshToken, UID } = await result.response.json();
91 return { UID, AccessToken, RefreshToken, RefreshTime };
93 case AuthMode.COOKIE: {
94 const { UID } = await result.response.json();
95 return { UID, AccessToken: '', RefreshToken: '', RefreshTime, cookies: true };
100 logger.info('[API] Successfully refreshed session tokens');
102 await config.onRefresh(refreshData);
103 await wait(randomIntFromInterval(500, 2000));
106 { key: (_, options) => options.auth?.UID ?? config.getAuth()?.UID ?? '' }