Update selected item color in Pass menu
[ProtonMail-WebClient.git] / packages / pass / lib / auth / service.ts
blob502b9103864c0c8ac816a923318d324f1416c81e
1 import { c } from 'ttag';
3 import type { CreateNotificationOptions } from '@proton/components';
4 import { DEFAULT_LOCK_TTL } from '@proton/pass/constants';
5 import { PassErrorCode } from '@proton/pass/lib/api/errors';
6 import { type RefreshSessionData } from '@proton/pass/lib/api/refresh';
7 import { getOfflineComponents, getOfflineVerifier } from '@proton/pass/lib/cache/crypto';
8 import type { Maybe, MaybeNull, MaybePromise } from '@proton/pass/types';
9 import { type Api } from '@proton/pass/types';
10 import { NotificationKey } from '@proton/pass/types/worker/notification';
11 import { getErrorMessage } from '@proton/pass/utils/errors/get-error-message';
12 import { pipe, tap } from '@proton/pass/utils/fp/pipe';
13 import { asyncLock } from '@proton/pass/utils/fp/promises';
14 import { withCallCount } from '@proton/pass/utils/fp/with-call-count';
15 import { logger } from '@proton/pass/utils/logger';
16 import { getEpoch } from '@proton/pass/utils/time/epoch';
17 import { revoke, setLocalKey } from '@proton/shared/lib/api/auth';
18 import { getApiError, getApiErrorMessage } from '@proton/shared/lib/api/helpers/apiErrorHelper';
19 import { generateClientKey } from '@proton/shared/lib/authentication/clientKey';
20 import type { LocalKeyResponse } from '@proton/shared/lib/authentication/interface';
21 import { stringToUint8Array } from '@proton/shared/lib/helpers/encoding';
22 import { loadCryptoWorker } from '@proton/shared/lib/helpers/setupCryptoWorker';
23 import noop from '@proton/utils/noop';
25 import type { PullForkCall } from './fork';
26 import {
27     type ConsumeForkPayload,
28     type RequestForkOptions,
29     type RequestForkResult,
30     consumeFork,
31     requestFork,
32 } from './fork';
33 import { checkSessionLock } from './lock/session/lock.requests';
34 import type { Lock, LockAdapter, LockCreateDTO } from './lock/types';
35 import { LockMode } from './lock/types';
36 import {
37     PasswordVerification,
38     getPasswordVerification,
39     registerExtraPassword,
40     removeExtraPassword,
41     verifyExtraPassword,
42     verifyOfflinePassword,
43     verifyPassword,
44 } from './password';
45 import {
46     type AuthSession,
47     type EncryptedAuthSession,
48     type ResumeSessionResult,
49     encryptPersistedSessionWithKey,
50     getPersistedSessionKey,
51     migrateSession,
52     resumeSession,
53     syncAuthSession,
54 } from './session';
55 import { type AuthStore } from './store';
57 export type AuthOptions = {
58     /** `forceLock` will locally lock the session upon resuming */
59     forceLock?: boolean;
60     /** If `true`, will re-persist session on login */
61     forcePersist?: boolean;
62     /** If `true`, session resuming should be retried */
63     retryable?: boolean;
64     /** If `true`, the session is considered unlocked */
65     unlocked?: boolean;
68 export interface AuthServiceConfig {
69     api: Api;
70     /** Store holding the active session data */
71     authStore: AuthStore;
73     /** Override the default pull fork call. This is mostly
74      * required for safari extensions which will not include
75      * cookies set by account when requesting a fork sw-side. */
76     pullFork?: PullForkCall;
78     /** The in-memory session is used to store the session data securely.
79      * It allows resuming a session without any API calls to re-authenticate.
80      * In most cases you can omit the implementation and rely on the `authStore` */
81     getMemorySession?: () => MaybePromise<any>;
82     /** The persisted session will be parsed and decrypted to extract the
83      * session data. Requires an API call to retrieve the local key. */
84     getPersistedSession: (localID: Maybe<number>) => MaybePromise<MaybeNull<EncryptedAuthSession>>;
85     /**  Implement any service initialization logic in this hook. Should return
86      * a boolean flag indicating wether user was authorized or not. */
87     onInit: (options: AuthOptions) => Promise<boolean>;
88     /** Called when authorization sequence starts: this can happen when consuming a
89      * session fork or when trying to resume a session. */
90     onLoginStart?: () => void;
91     /** Called whenever a user is successfully authenticated. This can happen
92      * after consuming a fork or resuming a session.  */
93     onLoginComplete?: (userID: string, localID: Maybe<number>) => void;
94     /** Called when logout sequence starts before the authentication store is cleared */
95     onLogoutStart?: () => void;
96     /** Called whenever a user is unauthenticated. This will be triggered any time
97      * the `logout` function is called (either via user action or when an inactive
98      * session is detected). The `broadcast` flag indicates wether we should
99      * broadcast the unauthorized session to other clients. */
100     onLogoutComplete?: (userID: Maybe<string>, localID: Maybe<number>, broadcast: boolean) => void;
101     /** Called immediately after a fork has been successfully consumed. At this
102      * point the user is not fully logged in yet. */
103     onForkConsumed?: (session: AuthSession, payload: ConsumeForkPayload) => MaybePromise<void>;
104     /** Called when a fork could not be successfully consumed. This can happen
105      * if the fork data is invalid */
106     onForkInvalid?: () => void;
107     /** Handle the result of a fork request call. Can be used to redirect the
108      * user automatically when requesting a fork from account. */
109     onForkRequest?: (result: RequestForkResult) => void;
110     /** Called when an invalid persistent session error is thrown during a
111      * session resuming sequence. It will get called with the invalid session
112      * and the localID being resumed for retry mechanisms */
113     onSessionInvalid?: (
114         error: unknown,
115         data: {
116             localID: Maybe<number>;
117             invalidSession: EncryptedAuthSession;
118             retry: (session: EncryptedAuthSession) => Promise<ResumeSessionResult>;
119         }
120     ) => MaybePromise<ResumeSessionResult>;
121     /* Called when no persisted session or in-memory session can be used to
122      * resume a session. */
123     onSessionEmpty?: () => void;
124     /** Called when a session is locked either through user action or when a
125      * locked session is detected. The `broadcast` flag indicates wether we should
126      * broadcast the locked session to other clients. */
127     onLocked?: (mode: LockMode, localID: Maybe<number>, broadcast: boolean, userInitiated: boolean) => void;
128     /** Callback when session lock is created, updated or deleted */
129     onLockUpdate?: (lock: Lock, localID: Maybe<number>, broadcast: boolean) => MaybePromise<void>;
130     /** Called with the `sessionLockToken` when session is successfully unlocked */
131     onUnlocked?: (mode: LockMode, token: Maybe<string>, localID: Maybe<number>) => Promise<void>;
132     /** Implement encrypted local session persistence using this hook. Called on every
133      * successful consumed fork or unlocked session. */
134     onSessionPersist?: (encryptedSession: string) => MaybePromise<void>;
135     /** Called when resuming the session failed for any reason excluding inactive
136      * session error. */
137     onSessionFailure?: (options: AuthOptions) => MaybePromise<void>;
138     /** Called when session tokens have been refreshed. The`broadcast` flag indicates
139      * wether we should broadcast the refresh session data to other clients. */
140     onSessionRefresh?: (localId: Maybe<number>, data: RefreshSessionData, broadcast: boolean) => MaybePromise<void>;
141     /** Implement how you want to handle notifications emitted from the service */
142     onNotification?: (notification: CreateNotificationOptions) => void;
143     /** Triggered when extra password is required */
144     onMissingScope?: () => void;
147 export const createAuthService = (config: AuthServiceConfig) => {
148     const { api, authStore } = config;
150     const adapters = new Map<LockMode, LockAdapter>();
152     const getLockAdapter = (mode: LockMode): LockAdapter => {
153         const adapter = adapters.get(mode);
154         if (!adapter) throw new Error(`Lock adapter not found for "${mode}"`);
155         return adapter;
156     };
158     const authService = {
159         init: asyncLock(async (options: AuthOptions) => {
160             logger.info(`[AuthService] Initialization start`);
161             return config.onInit(options).catch((err) => {
162                 logger.warn(`[AuthService] Initialization failure`, err);
163                 config.onNotification?.({ type: 'error', text: getErrorMessage(err) });
164                 return false;
165             });
166         }),
168         /** Stores the initial configuration object passed to the
169          * auth service factory function. Useful if you want to trigger
170          * certain handlers outside of the auth service flow. */
171         config,
173         registerLockAdapter: (mode: LockMode, adapter: LockAdapter) => adapters.set(mode, adapter),
175         login: async (session: AuthSession, options: AuthOptions) => {
176             config.onLoginStart?.();
178             try {
179                 if (!authStore.validSession(session)) {
180                     authStore.clear();
181                     throw new Error('Invalid session');
182                 }
184                 authStore.setSession(session);
185                 await api.reset();
187                 const migrated = await migrateSession(authStore);
188                 if (migrated || options.forcePersist) await authService.persistSession().catch(noop);
190                 const lockMode = authStore.getLockMode();
192                 if (options?.forceLock && lockMode !== LockMode.NONE) {
193                     await authService.lock(lockMode, { soft: true, broadcast: false });
194                     return false;
195                 }
197                 if (!options?.unlocked) {
198                     const sessionLock = await checkSessionLock();
199                     const sessionLockRegistered = sessionLock.mode === LockMode.SESSION;
200                     const sessionLocked = sessionLock.locked;
202                     const hasToken = authStore.getLockToken() !== undefined;
203                     const needsToken = sessionLockRegistered && !hasToken;
204                     const overrideLock = sessionLockRegistered ? lockMode !== sessionLock.mode : false;
205                     const shouldLockSession = overrideLock || sessionLocked || needsToken;
207                     if (shouldLockSession) {
208                         logger.info(`[AuthService] Locked session [locked=${sessionLocked},token=${hasToken}]`);
209                         await authService.lock(LockMode.SESSION, { soft: true, broadcast: false });
210                         return false;
211                     }
212                 }
213             } catch (err) {
214                 const { code } = getApiError(err);
215                 if (code === PassErrorCode.MISSING_SCOPE) return false;
217                 logger.warn(`[AuthService] Logging in session failed`, err);
218                 config.onNotification?.({ text: c('Warning').t`Your session could not be resumed.`, type: 'error' });
219                 await config?.onSessionFailure?.({ forceLock: true, retryable: true });
220                 return false;
221             }
223             logger.info(`[AuthService] User is authorized`);
224             config.onLoginComplete?.(authStore.getUserID()!, authStore.getLocalID());
226             return true;
227         },
229         logout: async (options: { soft: boolean; broadcast?: boolean }) => {
230             config.onLogoutStart?.();
232             const localID = authStore.getLocalID();
233             const userID = authStore.getUserID();
235             if (!options?.soft) await api({ ...revoke(), silence: true }).catch(noop);
236             logger.info(`[AuthService] User is not authorized`);
238             await api.reset();
239             authStore.clear();
240             authService.resumeSession.resetCount();
242             config.onLogoutComplete?.(userID, localID, options.broadcast ?? true);
244             return true;
245         },
247         consumeFork: async (payload: ConsumeForkPayload, apiUrl?: string): Promise<boolean> => {
248             try {
249                 config.onLoginStart?.();
250                 const { session, Scopes } = await consumeFork({ api, apiUrl, payload, pullFork: config.pullFork });
251                 const validScope = Scopes.includes('pass');
253                 if (!validScope) {
254                     /** If the scope is invalid then the user must unlock with his
255                      * pass extra password in order to continue the login sequence.
256                      * Clear any offline components provided by account as they will
257                      * need to be recomputed against the extra password */
258                     delete session.offlineConfig;
259                     delete session.offlineKD;
260                 }
262                 await config.onForkConsumed?.(session, payload);
264                 const loggedIn = validScope && (await authService.login(session, {}));
266                 if (!validScope) {
267                     authStore.setSession(session);
268                     authStore.setExtraPassword(true);
269                     config.onMissingScope?.();
270                 }
272                 const locked = authStore.getLocked();
274                 /** Persist the session only on successful login. If the forked session is
275                  * locked, persist eitherway to avoid requiring a new fork consumption if
276                  * user does not unlock immediately (reset api state for persisting). */
277                 if (locked) await api.reset();
278                 if (loggedIn || locked) await authService.persistSession({ regenerateClientKey: true });
280                 return true;
281             } catch (error: unknown) {
282                 const reason = error instanceof Error ? ` (${getApiErrorMessage(error) ?? error?.message})` : '';
284                 config.onNotification?.({
285                     text: c('Warning').t`Your session could not be authorized.` + reason,
286                     type: 'error',
287                 });
289                 config.onForkInvalid?.();
290                 await authService.logout({ soft: true, broadcast: false });
292                 throw error;
293             }
294         },
296         requestFork: (options: RequestForkOptions): RequestForkResult => {
297             const result = requestFork(options);
298             config.onForkRequest?.(result);
300             return result;
301         },
303         createLock: async (payload: LockCreateDTO) => {
304             if (payload.mode === LockMode.NONE) return;
306             const adapter = getLockAdapter(payload.mode);
307             const localID = authStore.getLocalID();
308             const sessionLockRegistered = authStore.getLockMode() === LockMode.SESSION;
310             /** If we're creating a new lock over an
311              * active API session lock - delete it first */
312             const onBeforeCreate = sessionLockRegistered
313                 ? async () => {
314                       if (!payload.current) throw new Error('Invalid lock creation');
315                       const lock = await getLockAdapter(LockMode.SESSION).delete(payload.current.secret);
316                       void config.onLockUpdate?.(lock, localID, false);
317                   }
318                 : undefined;
320             const lock = await adapter.create(payload, onBeforeCreate);
321             void config.onLockUpdate?.(lock, localID, true);
322         },
324         deleteLock: async (mode: LockMode, secret: string) => {
325             if (mode === LockMode.NONE) return;
327             const adapter = getLockAdapter(mode);
328             const lock = await adapter.delete(secret);
329             const localID = authStore.getLocalID();
331             void config.onLockUpdate?.(lock, localID, true);
332         },
334         lock: async (
335             mode: LockMode,
336             options: { broadcast?: boolean; soft: boolean; userInitiated?: boolean }
337         ): Promise<Lock> => {
338             const adapter = getLockAdapter(mode);
339             const localID = authStore.getLocalID();
340             const broadcast = options.broadcast ?? false;
342             config.onLocked?.(mode, localID, broadcast, options.userInitiated ?? false);
343             const lock = await adapter.lock(options);
345             return lock;
346         },
348         unlock: async (mode: LockMode, secret: string): Promise<void> => {
349             if (mode === LockMode.NONE) return;
351             try {
352                 const adapter = getLockAdapter(mode);
353                 const token = await adapter.unlock(secret);
354                 const localID = authStore.getLocalID();
355                 await adapter.check();
357                 await config.onUnlocked?.(mode, token, localID);
358             } catch (error) {
359                 /** error is thrown for clients to consume */
360                 logger.warn(`[AuthService] Unlock failure [mode=${mode}]`, error);
361                 throw error;
362             }
363         },
365         checkLock: async (): Promise<Lock> => {
366             const mode = authStore.getLockMode();
367             if (mode === LockMode.NONE) return { mode: LockMode.NONE, locked: false };
369             /** If we have a TTL and lastExtendTime - check early
370              * if the TTL has been reached and lock accordingly */
371             const ttl = authStore.getLockTTL();
372             const lastExtendTime = authStore.getLockLastExtendTime();
374             if (ttl && lastExtendTime) {
375                 const now = getEpoch();
376                 const diff = now - (lastExtendTime ?? 0);
377                 if (diff > ttl) return authService.lock(mode, { soft: true, broadcast: true });
378             }
380             const adapter = getLockAdapter(mode);
381             const lock = await adapter.check();
382             const localID = authStore.getLocalID();
384             await config.onLockUpdate?.(lock, localID, false);
385             return lock;
386         },
388         /** Passing the `regenerateClientKey` option will generate
389          * a new local key and update it back-end side. Ideally, this
390          * should only happen after consuming a fork. */
391         persistSession: async (options?: { regenerateClientKey: boolean }) => {
392             try {
393                 const session = authStore.getSession();
394                 if (!authStore.validSession(session)) throw new Error('Trying to persist invalid session');
396                 const clientKey = await (async () => {
397                     if (options?.regenerateClientKey) {
398                         const { serializedData, key } = await generateClientKey();
399                         await api<LocalKeyResponse>(setLocalKey(serializedData));
400                         authStore.setClientKey(serializedData);
401                         return key;
402                     }
404                     return getPersistedSessionKey(api, authStore);
405                 })();
407                 logger.info('[AuthService] Persisting session');
409                 /* If the clientKey resolution sequence triggered a refresh,
410                  * make sure we persist the session with the new tokens */
411                 session.lastUsedAt = getEpoch();
412                 const syncedSession = syncAuthSession(session, authStore);
413                 const encryptedSession = await encryptPersistedSessionWithKey(syncedSession, clientKey);
415                 await config.onSessionPersist?.(encryptedSession);
416             } catch (error) {
417                 logger.warn(`[AuthService] Persisting session failure`, error);
418             }
419         },
421         resumeSession: withCallCount(
422             pipe(
423                 async (localID: Maybe<number>, options: AuthOptions): Promise<boolean> => {
424                     try {
425                         const memorySession = await config.getMemorySession?.();
426                         const persistedSession = await config.getPersistedSession(localID);
428                         /** If we have an in-memory decrypted AuthSession - use it to
429                          * login without making any other API requests. Authorizing
430                          * from in-memory session does not account for force lock, rather
431                          * when locking the in-memory session should be cleared */
432                         if (memorySession && authStore.validSession(memorySession)) {
433                             logger.info(`[AuthService] Resuming in-memory session [lock=${options.forceLock}]`);
434                             return await authService.login(memorySession, {});
435                         }
437                         /** If we have no persisted session to resume from, exit early */
438                         if (!persistedSession) {
439                             logger.info(`[AuthService] No persisted session found`);
440                             config.onSessionEmpty?.();
441                             return false;
442                         }
444                         logger.info(`[AuthService] Resuming persisted session [lock=${options.forceLock ?? false}]`);
445                         config.onLoginStart?.();
447                         /** Partially configure the auth store before resume sequence. `keyPassword`
448                          * and `sessionLockToken` may be still encrypted at this point */
449                         authStore.setSession(persistedSession);
450                         await api.reset();
452                         const result = await resumeSession(persistedSession, localID, config, options);
454                         logger.info(`[AuthService] Session successfully resumed`);
455                         options.forcePersist = options.forcePersist || result.repersist;
456                         return await authService.login(result.session, options);
457                     } catch (error: unknown) {
458                         if (error instanceof Error) {
459                             const message = getApiErrorMessage(error) ?? error?.message;
460                             const reason = message ? ` (${message})` : '';
461                             const text = c('Warning').t`Your session could not be resumed.` + reason;
462                             logger.warn(`[AuthService] Resuming session failed ${reason}`);
463                             config.onNotification?.({ text, type: 'error' });
464                         }
466                         /** If a session fails to resume due to reasons other than being locked,
467                          * inactive, or offline, the sessionFailure callback should trigger the
468                          * resuming process. Session errors will be managed by the API listener. */
469                         const { sessionLocked, sessionInactive } = api.getState();
470                         const sessionFailure = !(sessionLocked || sessionInactive);
471                         if (sessionFailure) await config.onSessionFailure?.(options);
473                         return false;
474                     }
475                 },
476                 tap((resumed) => {
477                     /** Reset the internal resume session count when session
478                      * resuming succeeds */
479                     if (resumed) authService.resumeSession.resetCount();
480                 })
481             )
482         ),
484         /** Password confirmation can either be verified offline or online.
485          * In `srp` mode, we will verify the user's password through SRP
486          * (two-password mode not supported yet). If the user has an offline
487          * config, we compare the `offlineKD` with the derived argon2 hash */
488         confirmPassword: async (password: string, mode?: PasswordVerification): Promise<boolean> => {
489             try {
490                 await loadCryptoWorker();
492                 switch (mode ?? getPasswordVerification(authStore)) {
493                     case PasswordVerification.LOCAL: {
494                         const offlineConfig = authStore.getOfflineConfig()!;
495                         const offlineVerifier = authStore.getOfflineVerifier()!;
496                         return await verifyOfflinePassword(password, { offlineConfig, offlineVerifier });
497                     }
499                     case PasswordVerification.EXTRA_PASSWORD: {
500                         await verifyExtraPassword({ password });
502                         const { offlineConfig, offlineKD } = await getOfflineComponents(password);
504                         authStore.setOfflineConfig(offlineConfig);
505                         authStore.setOfflineKD(offlineKD);
506                         authStore.setOfflineVerifier(await getOfflineVerifier(stringToUint8Array(offlineKD)));
508                         /** Online extra password verification will happen on
509                          * first login after a successful fork. At this point
510                          * we can enable the password lock automatically. */
511                         if (
512                             !EXTENSION_BUILD &&
513                             [LockMode.NONE, LockMode.BIOMETRICS].includes(authStore.getLockMode())
514                         ) {
515                             authStore.setLockMode(LockMode.PASSWORD);
516                             authStore.setLockTTL(DEFAULT_LOCK_TTL);
517                         }
519                         await authService.persistSession({ regenerateClientKey: true });
521                         return true;
522                     }
524                     case PasswordVerification.SRP: {
525                         return await verifyPassword({ password });
526                     }
527                 }
528             } catch (error) {
529                 logger.warn(`[AuthService] failed password confirmation (${getErrorMessage(error)})`);
530                 return false;
531             }
532         },
534         registerExtraPassword: async (password: string): Promise<boolean> => {
535             /** Compute the offline components in order to update the auth store on successful
536              * extra password registration : this will affect any password locks or offline mode
537              * setting. Users will now have to unlock the client with the extra password */
538             const { offlineConfig, offlineKD } = await getOfflineComponents(password);
539             await registerExtraPassword({ password });
541             /* Clear biometrics */
542             if (authStore.getLockMode() === LockMode.BIOMETRICS) {
543                 authStore.setEncryptedOfflineKD(undefined);
544                 authStore.setLockMode(LockMode.PASSWORD);
545                 await authService.config.onLockUpdate?.(
546                     { mode: LockMode.PASSWORD, locked: false, ttl: authStore.getLockTTL() },
547                     authStore.getLocalID(),
548                     true
549                 );
550             }
552             authStore.setExtraPassword(true);
553             authStore.setOfflineConfig(offlineConfig);
554             authStore.setOfflineKD(offlineKD);
555             authStore.setOfflineVerifier(await getOfflineVerifier(stringToUint8Array(offlineKD)));
557             await authService.persistSession();
558             return true;
559         },
561         removeExtraPassword: async (password: string) => {
562             // Clear biometrics
563             if (authStore.getLockMode() === LockMode.BIOMETRICS) {
564                 authStore.setEncryptedOfflineKD(undefined);
565                 await authService.config.onLockUpdate?.(
566                     { mode: LockMode.PASSWORD, locked: false },
567                     authStore.getLocalID(),
568                     true
569                 );
570             }
572             await verifyExtraPassword({ password });
573             await removeExtraPassword();
574             await authService.logout({ soft: true, broadcast: true });
575         },
576     };
578     api.subscribe(async (event) => {
579         /** Ensure we have an active session before processing API events*/
580         if (authStore.hasSession()) {
581             switch (event.type) {
582                 case 'session': {
583                     if (event.status === 'inactive') {
584                         if (!event.silent) {
585                             config.onNotification?.({
586                                 text: c('Warning').t`Your session is inactive.`,
587                                 type: 'error',
588                             });
589                         }
590                         await authService.logout({ soft: true, broadcast: true });
591                     }
593                     if (event.status === 'locked') {
594                         const locked = authStore.getLocked();
596                         if (!locked) {
597                             config.onNotification?.({
598                                 key: NotificationKey.LOCK,
599                                 text: c('Warning').t`Your session was locked.`,
600                                 type: 'info',
601                             });
602                         }
604                         await authService.lock(LockMode.SESSION, { soft: true, broadcast: true });
605                     }
607                     if (event.status === 'not-allowed') {
608                         if (event.error?.includes('two-factor-authentication-2fa')) {
609                             config.onNotification?.({
610                                 text: '',
611                                 key: NotificationKey.ORG_MISSING_2FA,
612                                 type: 'error',
613                                 expiration: -1,
614                             });
615                         }
617                         await authService.logout({ soft: true, broadcast: true });
618                     }
620                     if (event.status === 'missing-scope') config.onMissingScope?.();
622                     break;
623                 }
625                 case 'refresh': {
626                     const { data } = event;
627                     if (authStore.getUID() === data.UID) {
628                         /** The `onSessionRefresh` callback is invoked to persist the new tokens.
629                          * If this callback throws an error, it is crucial to avoid updating the
630                          * authentication store data. This precaution prevents potential inconsistencies
631                          * between the store and persisted data. The provisional refresh token is confirmed
632                          * only upon the initial use of the new access token. */
633                         await config.onSessionRefresh?.(authStore.getLocalID(), data, true);
634                         authStore.setSession(data);
635                     }
637                     break;
638                 }
639             }
640         }
641     });
643     return authService;
646 export type AuthService = ReturnType<typeof createAuthService>;