rehive 4.2.1 → 4.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/admin.d.mts +2 -2
  2. package/dist/admin.d.ts +2 -2
  3. package/dist/auth.d.mts +2 -2
  4. package/dist/auth.d.ts +2 -2
  5. package/dist/auth.js +1 -1
  6. package/dist/auth.mjs +1 -1
  7. package/dist/chunk-647XG2KN.js +1 -0
  8. package/dist/chunk-BAUHEOL5.mjs +1 -0
  9. package/dist/{create-api-client-CgvKBlQ_.d.ts → create-api-client-DzUaFuXN.d.ts} +1 -1
  10. package/dist/{create-api-client-Bkkri-AM.d.mts → create-api-client-JpNvOfh9.d.mts} +1 -1
  11. package/dist/{create-auth-ChOASbvo.d.mts → create-auth-D-3te_Jw.d.mts} +46 -1
  12. package/dist/{create-auth-ChOASbvo.d.ts → create-auth-D-3te_Jw.d.ts} +46 -1
  13. package/dist/extensions/alchemy.d.mts +2 -2
  14. package/dist/extensions/alchemy.d.ts +2 -2
  15. package/dist/extensions/app.d.mts +2 -2
  16. package/dist/extensions/app.d.ts +2 -2
  17. package/dist/extensions/billing.d.mts +2 -2
  18. package/dist/extensions/billing.d.ts +2 -2
  19. package/dist/extensions/bridge.d.mts +2 -2
  20. package/dist/extensions/bridge.d.ts +2 -2
  21. package/dist/extensions/builder.d.mts +2 -2
  22. package/dist/extensions/builder.d.ts +2 -2
  23. package/dist/extensions/business.d.mts +2 -2
  24. package/dist/extensions/business.d.ts +2 -2
  25. package/dist/extensions/conversion.d.mts +2 -2
  26. package/dist/extensions/conversion.d.ts +2 -2
  27. package/dist/extensions/mass-send.d.mts +2 -2
  28. package/dist/extensions/mass-send.d.ts +2 -2
  29. package/dist/extensions/notifications.d.mts +2 -2
  30. package/dist/extensions/notifications.d.ts +2 -2
  31. package/dist/extensions/payment-requests.d.mts +2 -2
  32. package/dist/extensions/payment-requests.d.ts +2 -2
  33. package/dist/extensions/products.d.mts +2 -2
  34. package/dist/extensions/products.d.ts +2 -2
  35. package/dist/extensions/rain.d.mts +2 -2
  36. package/dist/extensions/rain.d.ts +2 -2
  37. package/dist/extensions/rewards.d.mts +2 -2
  38. package/dist/extensions/rewards.d.ts +2 -2
  39. package/dist/extensions/stellar-testnet.d.mts +2 -2
  40. package/dist/extensions/stellar-testnet.d.ts +2 -2
  41. package/dist/extensions/stellar.d.mts +2 -2
  42. package/dist/extensions/stellar.d.ts +2 -2
  43. package/dist/index.d.mts +2 -2
  44. package/dist/index.d.ts +2 -2
  45. package/dist/index.js +1 -1
  46. package/dist/index.mjs +1 -1
  47. package/dist/react.d.mts +11 -3
  48. package/dist/react.d.ts +11 -3
  49. package/dist/react.js +1 -1
  50. package/dist/react.mjs +1 -1
  51. package/dist/user.d.mts +3 -3
  52. package/dist/user.d.ts +3 -3
  53. package/package.json +3 -3
  54. package/src/auth/create-auth.ts +774 -170
  55. package/src/auth/index.ts +25 -2
  56. package/src/auth/types/index.ts +48 -0
  57. package/dist/chunk-OV77OD2G.js +0 -1
  58. package/dist/chunk-RO2QGTSG.mjs +0 -1
@@ -14,10 +14,16 @@ import type {
14
14
  import { WebStorageAdapter, MemoryStorageAdapter } from './core/storage-adapters.js';
15
15
  import { ApiError, normalizeFetch } from '../shared/api-utils.js';
16
16
  import type {
17
+ AuthEvent,
18
+ AuthEventListener,
19
+ AuthRecoveryState,
17
20
  AuthSession,
21
+ AuthSnapshot,
18
22
  AuthState,
19
- SessionListener,
23
+ AuthStateListener,
24
+ AuthStatus,
20
25
  ErrorListener,
26
+ SessionListener,
21
27
  StorageAdapter,
22
28
  } from './types/index.js';
23
29
 
@@ -43,6 +49,21 @@ export type RegisterParams = {
43
49
 
44
50
  export type RegisterCompanyParams = RegisterCompanyRequestWritable;
45
51
 
52
+ export interface ImportTokenOptions {
53
+ company?: string;
54
+ expires?: number;
55
+ sessionDuration?: number;
56
+ }
57
+
58
+ export interface ValidateSessionOptions {
59
+ retryCount?: number;
60
+ retryDelayMs?: number;
61
+ }
62
+
63
+ export type SessionPatch =
64
+ | Partial<AuthSession>
65
+ | ((session: AuthSession) => AuthSession);
66
+
46
67
  export interface AuthConfig {
47
68
  baseUrl?: string;
48
69
  storage?: 'local' | 'memory' | StorageAdapter;
@@ -61,11 +82,25 @@ export interface Auth {
61
82
  getActiveSession(): AuthSession | null;
62
83
  getSessions(): AuthSession[];
63
84
  getSessionsByCompany(company: string): AuthSession[];
85
+ getState(): AuthSnapshot;
86
+ getStatus(): AuthStatus;
87
+ getRecoveryState(): AuthRecoveryState;
64
88
  switchToSession(userId: string, company?: string): Promise<AuthSession | null>;
65
89
  clearAllSessions(): Promise<void>;
66
90
  deleteChallenge(challengeId: string): Promise<void>;
91
+ importToken(token: string, options?: ImportTokenOptions): Promise<AuthSession>;
92
+ validateActiveSession(options?: ValidateSessionOptions): Promise<boolean>;
93
+ syncActiveSessionUser(): Promise<AuthSession | null>;
94
+ updateSession(
95
+ userId: string,
96
+ company: string | undefined,
97
+ patch: SessionPatch,
98
+ ): Promise<AuthSession | null>;
99
+ expireActiveSession(): Promise<AuthRecoveryState>;
67
100
  subscribe(listener: SessionListener): () => void;
68
101
  subscribeToErrors(listener: ErrorListener): () => void;
102
+ subscribeToState(listener: AuthStateListener): () => void;
103
+ subscribeToEvents(listener: AuthEventListener): () => void;
69
104
  readonly baseUrl: string;
70
105
  }
71
106
 
@@ -106,46 +141,195 @@ function errorHandlingFetch(baseFetch: typeof fetch): typeof fetch {
106
141
  };
107
142
  }
108
143
 
144
+ function sleep(ms: number): Promise<void> {
145
+ return new Promise((resolve) => setTimeout(resolve, ms));
146
+ }
147
+
148
+ function getSessionCompany(session: AuthSession): string | undefined {
149
+ return session.company ?? session.user?.company;
150
+ }
151
+
152
+ function cloneSessions(sessions: AuthSession[]): AuthSession[] {
153
+ return [...sessions];
154
+ }
155
+
109
156
  export function createAuth(config: AuthConfig = {}): Auth {
110
157
  const baseUrl = config.baseUrl || 'https://api.rehive.com';
111
158
  const storage = resolveStorage(config.storage);
112
159
  const permanentToken = config.token;
113
160
  const enableCrossTabSync = config.enableCrossTabSync ?? true;
114
161
  const storageKey = 'rehive_auth_state';
162
+ const baseFetch = normalizeFetch(globalThis.fetch);
115
163
 
116
164
  const client = createClient({
117
165
  baseUrl,
118
166
  responseStyle: 'data' as const,
119
- fetch: errorHandlingFetch(normalizeFetch(globalThis.fetch)),
167
+ fetch: errorHandlingFetch(baseFetch),
120
168
  });
121
169
 
122
170
  let sessions: AuthSession[] = [];
123
171
  let activeSessionIndex = -1;
124
172
  let sessionListeners: SessionListener[] = [];
125
173
  let errorListeners: ErrorListener[] = [];
174
+ let stateListeners: AuthStateListener[] = [];
175
+ let eventListeners: AuthEventListener[] = [];
126
176
  let refreshPromise: Promise<void> | null = null;
127
177
  let isRefreshing = false;
128
178
  let loadAuthStatePromise: Promise<AuthState> | null = null;
129
179
  let initialized = false;
180
+ let initializePromise: Promise<void> | null = null;
181
+ let cachedSnapshot: AuthSnapshot | null = null;
182
+ let lastError: Error | null = null;
183
+ let lastExpiredSession: AuthSession | null = null;
130
184
 
131
185
  function isTokenExpired(expires: number): boolean {
132
186
  return Date.now() >= expires - 30 * 1000;
133
187
  }
134
188
 
135
- function getActiveSession(): AuthSession | null {
136
- if (activeSessionIndex >= 0 && activeSessionIndex < sessions.length) {
137
- return sessions[activeSessionIndex];
189
+ function getActiveSessionFromState(state: AuthState): AuthSession | null {
190
+ if (
191
+ state.activeSessionIndex >= 0 &&
192
+ state.activeSessionIndex < state.sessions.length
193
+ ) {
194
+ return state.sessions[state.activeSessionIndex];
138
195
  }
139
196
  return null;
140
197
  }
141
198
 
142
- function notifySessionListeners(): void {
143
- const session = getActiveSession();
144
- sessionListeners.forEach((listener) => listener(session));
199
+ function getCurrentState(): AuthState {
200
+ return {
201
+ sessions: cloneSessions(sessions),
202
+ activeSessionIndex,
203
+ };
204
+ }
205
+
206
+ function getActiveSession(): AuthSession | null {
207
+ return getActiveSessionFromState(getCurrentState());
208
+ }
209
+
210
+ function buildRecoveryState(state: AuthState = getCurrentState()): AuthRecoveryState {
211
+ const session = getActiveSessionFromState(state);
212
+ const pending = !permanentToken && !session && state.sessions.length > 0;
213
+
214
+ return {
215
+ pending,
216
+ expiredSession: pending ? lastExpiredSession : null,
217
+ remainingSessions: pending ? cloneSessions(state.sessions) : [],
218
+ };
219
+ }
220
+
221
+ function getStatus(state: AuthState = getCurrentState()): AuthStatus {
222
+ if (!initialized && !permanentToken) {
223
+ return 'loading';
224
+ }
225
+ if (isRefreshing) {
226
+ return 'refreshing';
227
+ }
228
+ if (permanentToken) {
229
+ return 'authenticated';
230
+ }
231
+ if (getActiveSessionFromState(state)) {
232
+ return 'authenticated';
233
+ }
234
+ if (state.sessions.length > 0) {
235
+ return 'recoverable';
236
+ }
237
+ return 'unauthenticated';
145
238
  }
146
239
 
147
- function notifyErrorListeners(error: Error | null): void {
148
- errorListeners.forEach((listener) => listener(error));
240
+ function getState(): AuthSnapshot {
241
+ if (cachedSnapshot) {
242
+ return cachedSnapshot;
243
+ }
244
+ const state = getCurrentState();
245
+ cachedSnapshot = {
246
+ status: getStatus(state),
247
+ session: getActiveSessionFromState(state),
248
+ sessions: state.sessions,
249
+ activeSessionIndex: state.activeSessionIndex,
250
+ isRefreshing,
251
+ initialized: initialized || !!permanentToken,
252
+ error: lastError,
253
+ recovery: buildRecoveryState(state),
254
+ };
255
+ return cachedSnapshot;
256
+ }
257
+
258
+ function invalidateSnapshot(): void {
259
+ cachedSnapshot = null;
260
+ }
261
+
262
+ function emit(event: Omit<AuthEvent, 'snapshot'>): void {
263
+ const snapshot = getState();
264
+ const payload: AuthEvent = {
265
+ ...event,
266
+ snapshot,
267
+ };
268
+ eventListeners.forEach((listener) => listener(payload));
269
+ }
270
+
271
+ function notifyAll(event?: Omit<AuthEvent, 'snapshot'>): void {
272
+ invalidateSnapshot();
273
+ const snapshot = getState();
274
+ sessionListeners.forEach((listener) => listener(snapshot.session));
275
+ errorListeners.forEach((listener) => listener(snapshot.error));
276
+ stateListeners.forEach((listener) => listener(snapshot));
277
+
278
+ if (event) {
279
+ emit(event);
280
+ }
281
+ }
282
+
283
+ function setError(error: Error | null): void {
284
+ lastError = error;
285
+ }
286
+
287
+ async function persistState(state: AuthState): Promise<void> {
288
+ if (permanentToken) {
289
+ return;
290
+ }
291
+
292
+ await storage.setItem(storageKey, JSON.stringify(state));
293
+ }
294
+
295
+ async function applyState(
296
+ state: AuthState,
297
+ options: {
298
+ clearExpiredSession?: boolean;
299
+ expiredSession?: AuthSession | null;
300
+ error?: Error | null;
301
+ event?: Omit<AuthEvent, 'snapshot'>;
302
+ } = {},
303
+ ): Promise<void> {
304
+ sessions = cloneSessions(state.sessions);
305
+ activeSessionIndex =
306
+ state.activeSessionIndex >= 0 && state.activeSessionIndex < sessions.length
307
+ ? state.activeSessionIndex
308
+ : -1;
309
+
310
+ if (options.clearExpiredSession || activeSessionIndex >= 0 || sessions.length === 0) {
311
+ lastExpiredSession = null;
312
+ }
313
+ if (Object.prototype.hasOwnProperty.call(options, 'expiredSession')) {
314
+ lastExpiredSession = options.expiredSession ?? null;
315
+ }
316
+ if (Object.prototype.hasOwnProperty.call(options, 'error')) {
317
+ setError(options.error ?? null);
318
+ }
319
+
320
+ try {
321
+ await persistState({
322
+ sessions,
323
+ activeSessionIndex,
324
+ });
325
+ } catch (error) {
326
+ console.error('Failed to save auth state:', error);
327
+ if (!lastError && error instanceof Error) {
328
+ setError(error);
329
+ }
330
+ }
331
+
332
+ notifyAll(options.event);
149
333
  }
150
334
 
151
335
  async function loadAuthState(): Promise<AuthState> {
@@ -168,9 +352,17 @@ export function createAuth(config: AuthConfig = {}): Auth {
168
352
  } catch (error) {
169
353
  console.error('Failed to load auth state:', error);
170
354
  }
355
+
171
356
  sessions = state.sessions;
172
- activeSessionIndex = state.activeSessionIndex;
173
- return state;
357
+ activeSessionIndex =
358
+ state.activeSessionIndex >= 0 && state.activeSessionIndex < state.sessions.length
359
+ ? state.activeSessionIndex
360
+ : -1;
361
+
362
+ return {
363
+ sessions: cloneSessions(sessions),
364
+ activeSessionIndex,
365
+ };
174
366
  })();
175
367
 
176
368
  try {
@@ -180,51 +372,133 @@ export function createAuth(config: AuthConfig = {}): Auth {
180
372
  }
181
373
  }
182
374
 
183
- async function saveAuthState(state: AuthState): Promise<void> {
184
- try {
185
- await storage.setItem(storageKey, JSON.stringify(state));
186
- sessions = state.sessions;
187
- activeSessionIndex = state.activeSessionIndex;
188
- notifySessionListeners();
189
- } catch (error) {
190
- console.error('Failed to save auth state:', error);
191
- }
192
- }
193
-
194
375
  function setupCrossTabSync(): void {
195
376
  if (typeof window !== 'undefined') {
196
377
  window.addEventListener('storage', (event: StorageEvent) => {
197
- if (event.key === storageKey && event.newValue) {
198
- try {
199
- const newState = JSON.parse(event.newValue);
200
- sessions = Array.isArray(newState.sessions) ? newState.sessions : [];
201
- activeSessionIndex =
202
- typeof newState.activeSessionIndex === 'number'
203
- ? newState.activeSessionIndex
204
- : -1;
205
- notifySessionListeners();
206
- } catch (error) {
207
- console.error('Failed to sync auth state from storage event:', error);
208
- }
378
+ if (event.key !== storageKey) {
379
+ return;
380
+ }
381
+
382
+ if (!event.newValue) {
383
+ void applyState(
384
+ { sessions: [], activeSessionIndex: -1 },
385
+ { clearExpiredSession: true },
386
+ );
387
+ return;
388
+ }
389
+
390
+ try {
391
+ const newState = JSON.parse(event.newValue);
392
+ void applyState(
393
+ {
394
+ sessions: Array.isArray(newState.sessions) ? newState.sessions : [],
395
+ activeSessionIndex:
396
+ typeof newState.activeSessionIndex === 'number'
397
+ ? newState.activeSessionIndex
398
+ : -1,
399
+ },
400
+ {},
401
+ );
402
+ } catch (error) {
403
+ console.error('Failed to sync auth state from storage event:', error);
209
404
  }
210
405
  });
211
406
  }
212
407
  }
213
408
 
214
409
  async function initialize(): Promise<void> {
215
- if (initialized) return;
216
- initialized = true;
410
+ if (initialized) {
411
+ return;
412
+ }
413
+ if (initializePromise) {
414
+ return initializePromise;
415
+ }
217
416
 
218
- if (!permanentToken) {
219
- if (enableCrossTabSync) {
220
- setupCrossTabSync();
417
+ initializePromise = (async () => {
418
+ if (!permanentToken) {
419
+ if (enableCrossTabSync) {
420
+ setupCrossTabSync();
421
+ }
422
+ await loadAuthState();
221
423
  }
222
- await loadAuthState();
223
- notifySessionListeners();
424
+ initialized = true;
425
+ notifyAll({ type: 'initialized' });
426
+ })();
427
+
428
+ return initializePromise;
429
+ }
430
+
431
+ async function fetchUserForToken(token: string): Promise<any> {
432
+ const response = await baseFetch(`${baseUrl.replace(/\/$/, '')}/3/user/`, {
433
+ headers: {
434
+ Authorization: `Token ${token}`,
435
+ 'Content-Type': 'application/json',
436
+ },
437
+ });
438
+
439
+ if (!response.ok) {
440
+ const errorText = await response.text();
441
+ let errorJson: any = null;
442
+ try {
443
+ errorJson = JSON.parse(errorText);
444
+ } catch {
445
+ // not JSON
446
+ }
447
+
448
+ throw new ApiError({
449
+ status: response.status,
450
+ error: errorJson || errorText,
451
+ message:
452
+ errorJson?.error ||
453
+ errorJson?.message ||
454
+ 'A server error occurred. HTTPStatus: ' + response.status,
455
+ });
224
456
  }
457
+
458
+ const payload = await response.json();
459
+ return payload?.data ?? payload;
460
+ }
461
+
462
+ async function upsertSession(
463
+ newSession: AuthSession,
464
+ eventType: AuthEvent['type'],
465
+ ): Promise<AuthSession> {
466
+ await initialize();
467
+ const currentState = await loadAuthState();
468
+ const sessionCompany = getSessionCompany(newSession);
469
+ const existingIdx = currentState.sessions.findIndex(
470
+ (session) =>
471
+ session.user.id === newSession.user.id &&
472
+ getSessionCompany(session) === sessionCompany,
473
+ );
474
+
475
+ let nextState: AuthState;
476
+ if (existingIdx >= 0) {
477
+ const updatedSessions = cloneSessions(currentState.sessions);
478
+ updatedSessions[existingIdx] = newSession;
479
+ nextState = {
480
+ sessions: updatedSessions,
481
+ activeSessionIndex: existingIdx,
482
+ };
483
+ } else {
484
+ nextState = {
485
+ sessions: [...currentState.sessions, newSession],
486
+ activeSessionIndex: currentState.sessions.length,
487
+ };
488
+ }
489
+
490
+ await applyState(nextState, {
491
+ clearExpiredSession: true,
492
+ error: null,
493
+ event: {
494
+ type: eventType,
495
+ session: newSession,
496
+ },
497
+ });
498
+
499
+ return newSession;
225
500
  }
226
501
 
227
- // Kick off initialization (non-blocking)
228
502
  if (!permanentToken) {
229
503
  initialize().catch(console.error);
230
504
  }
@@ -247,40 +521,25 @@ export function createAuth(config: AuthConfig = {}): Auth {
247
521
  const result: any = await authLogin({ client, body, throwOnError: true });
248
522
  const data = result?.data ?? result;
249
523
 
250
- const newSession: AuthSession = {
251
- user: data.user,
252
- token: data.token,
253
- refresh_token: data.refresh_token,
254
- challenges: data.challenges,
255
- expires: data.expires,
256
- session_duration: sessionDuration ?? 900,
257
- company: params.company,
258
- };
259
-
260
- const currentState = await loadAuthState();
261
- const existingIdx = currentState.sessions.findIndex(
262
- (s: AuthSession) =>
263
- s.user.id === newSession.user.id && s.company === newSession.company,
524
+ return await upsertSession(
525
+ {
526
+ user: data.user,
527
+ token: data.token,
528
+ refresh_token: data.refresh_token,
529
+ challenges: data.challenges,
530
+ expires: data.expires,
531
+ session_duration: sessionDuration ?? 900,
532
+ company: params.company,
533
+ },
534
+ 'login',
264
535
  );
265
-
266
- let newState: AuthState;
267
- if (existingIdx !== -1) {
268
- const updated = [...currentState.sessions];
269
- updated[existingIdx] = newSession;
270
- newState = { ...currentState, sessions: updated, activeSessionIndex: existingIdx };
271
- } else {
272
- newState = {
273
- sessions: [...currentState.sessions, newSession],
274
- activeSessionIndex: currentState.sessions.length,
275
- };
276
- }
277
-
278
- await saveAuthState(newState);
279
- notifyErrorListeners(null);
280
- return newSession;
281
536
  } catch (e) {
282
537
  const error = e instanceof ApiError ? e : new Error('Login failed');
283
- notifyErrorListeners(error);
538
+ setError(error);
539
+ notifyAll({
540
+ type: 'error',
541
+ error,
542
+ });
284
543
  throw e;
285
544
  }
286
545
  }
@@ -309,28 +568,25 @@ export function createAuth(config: AuthConfig = {}): Auth {
309
568
  const result: any = await authRegister({ client, body, throwOnError: true });
310
569
  const data = result?.data ?? result;
311
570
 
312
- const newSession: AuthSession = {
313
- user: data.user,
314
- token: data.token,
315
- refresh_token: data.refresh_token,
316
- challenges: data.challenges,
317
- expires: data.expires,
318
- session_duration: sessionDuration ?? 900,
319
- company: params.company,
320
- };
321
-
322
- const currentState = await loadAuthState();
323
- const newState: AuthState = {
324
- sessions: [...currentState.sessions, newSession],
325
- activeSessionIndex: currentState.sessions.length,
326
- };
327
-
328
- await saveAuthState(newState);
329
- notifyErrorListeners(null);
330
- return newSession;
571
+ return await upsertSession(
572
+ {
573
+ user: data.user,
574
+ token: data.token,
575
+ refresh_token: data.refresh_token,
576
+ challenges: data.challenges,
577
+ expires: data.expires,
578
+ session_duration: sessionDuration ?? 900,
579
+ company: params.company,
580
+ },
581
+ 'register',
582
+ );
331
583
  } catch (e) {
332
584
  const error = e instanceof ApiError ? e : new Error('Registration failed');
333
- notifyErrorListeners(error);
585
+ setError(error);
586
+ notifyAll({
587
+ type: 'error',
588
+ error,
589
+ });
334
590
  throw e;
335
591
  }
336
592
  }
@@ -338,47 +594,45 @@ export function createAuth(config: AuthConfig = {}): Auth {
338
594
  async function registerCompanyFn(params: RegisterCompanyParams): Promise<AuthSession> {
339
595
  await initialize();
340
596
  try {
341
- const body: RegisterCompanyRequestWritable = params;
342
- const result: any = await authRegisterCompany({ client, body, throwOnError: true });
597
+ const result: any = await authRegisterCompany({
598
+ client,
599
+ body: params,
600
+ throwOnError: true,
601
+ });
343
602
  const data = result?.data ?? result;
344
-
345
- const newSession: AuthSession = {
346
- user: data.user,
347
- token: data.token,
348
- refresh_token: data.refresh_token,
349
- challenges: data.challenges,
350
- expires: data.expires,
351
- session_duration: 900,
352
- company: typeof params.company === 'string' ? params.company : (params.company as any)?.id,
353
- };
354
-
355
- const currentState = await loadAuthState();
356
- const newState: AuthState = {
357
- sessions: [...currentState.sessions, newSession],
358
- activeSessionIndex: currentState.sessions.length,
359
- };
360
-
361
- await saveAuthState(newState);
362
- notifyErrorListeners(null);
363
- return newSession;
603
+ const company =
604
+ typeof params.company === 'string'
605
+ ? params.company
606
+ : (params.company as any)?.id;
607
+
608
+ return await upsertSession(
609
+ {
610
+ user: data.user,
611
+ token: data.token,
612
+ refresh_token: data.refresh_token,
613
+ challenges: data.challenges,
614
+ expires: data.expires,
615
+ session_duration: 900,
616
+ company,
617
+ },
618
+ 'register-company',
619
+ );
364
620
  } catch (e) {
365
- const error = e instanceof ApiError ? e : new Error('Company registration failed');
366
- notifyErrorListeners(error);
621
+ const error =
622
+ e instanceof ApiError ? e : new Error('Company registration failed');
623
+ setError(error);
624
+ notifyAll({
625
+ type: 'error',
626
+ error,
627
+ });
367
628
  throw e;
368
629
  }
369
630
  }
370
631
 
371
632
  async function logout(): Promise<void> {
633
+ await initialize();
372
634
  const currentState = await loadAuthState();
373
- const session = currentState.sessions[currentState.activeSessionIndex];
374
-
375
- const newState: AuthState = {
376
- ...currentState,
377
- sessions: currentState.sessions.filter(
378
- (_: AuthSession, i: number) => i !== currentState.activeSessionIndex,
379
- ),
380
- activeSessionIndex: -1,
381
- };
635
+ const session = getActiveSessionFromState(currentState);
382
636
 
383
637
  if (session?.token) {
384
638
  try {
@@ -389,19 +643,34 @@ export function createAuth(config: AuthConfig = {}): Auth {
389
643
  throwOnError: true,
390
644
  });
391
645
  } catch {
392
- // Continue with local logout even if API call fails
646
+ // Continue with local logout even if API call fails.
393
647
  }
394
648
  }
395
649
 
396
- await saveAuthState(newState);
397
- notifyErrorListeners(null);
650
+ await applyState(
651
+ {
652
+ sessions: currentState.sessions.filter(
653
+ (_session, index) => index !== currentState.activeSessionIndex,
654
+ ),
655
+ activeSessionIndex: -1,
656
+ },
657
+ {
658
+ clearExpiredSession: true,
659
+ error: null,
660
+ event: {
661
+ type: 'logout',
662
+ session,
663
+ },
664
+ },
665
+ );
398
666
  }
399
667
 
400
668
  async function logoutAll(): Promise<void> {
669
+ await initialize();
401
670
  const currentState = await loadAuthState();
402
671
 
403
672
  await Promise.all(
404
- currentState.sessions.map(async (session: AuthSession) => {
673
+ currentState.sessions.map(async (session) => {
405
674
  try {
406
675
  await authLogout({
407
676
  client,
@@ -410,26 +679,44 @@ export function createAuth(config: AuthConfig = {}): Auth {
410
679
  throwOnError: true,
411
680
  });
412
681
  } catch {
413
- // Log but continue
682
+ // Continue clearing local sessions even if API logout fails.
414
683
  }
415
684
  }),
416
685
  );
417
686
 
418
- await saveAuthState({ sessions: [], activeSessionIndex: -1 });
419
- notifyErrorListeners(null);
687
+ await applyState(
688
+ {
689
+ sessions: [],
690
+ activeSessionIndex: -1,
691
+ },
692
+ {
693
+ clearExpiredSession: true,
694
+ error: null,
695
+ event: {
696
+ type: 'logout-all',
697
+ },
698
+ },
699
+ );
420
700
  }
421
701
 
422
702
  async function refresh(): Promise<void> {
423
- if (refreshPromise) return refreshPromise;
703
+ if (refreshPromise) {
704
+ return refreshPromise;
705
+ }
424
706
 
425
707
  refreshPromise = (async () => {
426
708
  isRefreshing = true;
709
+ notifyAll();
710
+
711
+ let sessionBeforeRefresh: AuthSession | null = null;
712
+
427
713
  try {
428
714
  const currentState = await loadAuthState();
429
715
  const idx = currentState.activeSessionIndex;
430
- const session = currentState.sessions[idx];
716
+ const session = getActiveSessionFromState(currentState);
717
+ sessionBeforeRefresh = session;
431
718
 
432
- if (!session?.token || !session?.refresh_token) {
719
+ if (idx < 0 || !session?.token || !session.refresh_token) {
433
720
  throw new Error('No active session, token, or refresh token found');
434
721
  }
435
722
 
@@ -442,33 +729,60 @@ export function createAuth(config: AuthConfig = {}): Auth {
442
729
  });
443
730
  const data = result?.data ?? result;
444
731
 
445
- const updatedSessions = [...currentState.sessions];
732
+ const updatedSessions = cloneSessions(currentState.sessions);
446
733
  updatedSessions[idx] = {
447
734
  ...session,
448
- refresh_token: data.refresh_token,
449
- expires: data.expires,
450
- session_duration: refreshDuration,
451
- company: session.company,
735
+ token: data.token ?? session.token,
736
+ refresh_token: data.refresh_token ?? session.refresh_token,
737
+ expires: data.expires ?? session.expires,
738
+ session_duration: session.session_duration ?? 900,
739
+ company: getSessionCompany(session),
452
740
  };
453
741
 
454
- await saveAuthState({ ...currentState, sessions: updatedSessions });
455
- notifyErrorListeners(null);
742
+ await applyState(
743
+ {
744
+ sessions: updatedSessions,
745
+ activeSessionIndex: idx,
746
+ },
747
+ {
748
+ clearExpiredSession: true,
749
+ error: null,
750
+ event: {
751
+ type: 'refresh',
752
+ session: updatedSessions[idx],
753
+ },
754
+ },
755
+ );
456
756
  } catch (e) {
457
757
  const error = e instanceof ApiError ? e : new Error('Refresh failed');
458
- notifyErrorListeners(error);
459
-
460
758
  const currentState = await loadAuthState();
461
- await saveAuthState({
462
- ...currentState,
463
- sessions: currentState.sessions.filter(
464
- (_: AuthSession, i: number) => i !== currentState.activeSessionIndex,
465
- ),
466
- activeSessionIndex: -1,
467
- });
759
+ const expiredSession =
760
+ sessionBeforeRefresh ??
761
+ getActiveSessionFromState(currentState);
762
+ const nextSessions = currentState.sessions.filter(
763
+ (_session, index) => index !== currentState.activeSessionIndex,
764
+ );
765
+
766
+ await applyState(
767
+ {
768
+ sessions: nextSessions,
769
+ activeSessionIndex: -1,
770
+ },
771
+ {
772
+ expiredSession: expiredSession ?? null,
773
+ error,
774
+ event: {
775
+ type: 'session-expired',
776
+ session: expiredSession,
777
+ error,
778
+ },
779
+ },
780
+ );
468
781
  throw e;
469
782
  } finally {
470
783
  isRefreshing = false;
471
784
  refreshPromise = null;
785
+ notifyAll();
472
786
  }
473
787
  })();
474
788
 
@@ -476,11 +790,15 @@ export function createAuth(config: AuthConfig = {}): Auth {
476
790
  }
477
791
 
478
792
  async function getToken(): Promise<string | undefined> {
479
- if (permanentToken) return permanentToken;
793
+ if (permanentToken) {
794
+ return permanentToken;
795
+ }
480
796
 
481
797
  await initialize();
482
798
  const session = getActiveSession();
483
- if (!session) return undefined;
799
+ if (!session) {
800
+ return undefined;
801
+ }
484
802
 
485
803
  if (session.expires && isTokenExpired(session.expires)) {
486
804
  try {
@@ -498,17 +816,34 @@ export function createAuth(config: AuthConfig = {}): Auth {
498
816
  userId: string,
499
817
  company?: string,
500
818
  ): Promise<AuthSession | null> {
819
+ await initialize();
501
820
  const currentState = await loadAuthState();
502
821
  const idx = currentState.sessions.findIndex(
503
- (s: AuthSession) => s.user.id === userId && s.company === company,
822
+ (session) =>
823
+ session.user.id === userId && getSessionCompany(session) === company,
504
824
  );
505
825
 
506
- if (idx === -1) return null;
826
+ if (idx === -1) {
827
+ return null;
828
+ }
507
829
 
508
- const session = currentState.sessions[idx];
509
- await saveAuthState({ ...currentState, activeSessionIndex: idx });
830
+ await applyState(
831
+ {
832
+ sessions: currentState.sessions,
833
+ activeSessionIndex: idx,
834
+ },
835
+ {
836
+ clearExpiredSession: true,
837
+ error: null,
838
+ event: {
839
+ type: 'session-switched',
840
+ session: currentState.sessions[idx],
841
+ },
842
+ },
843
+ );
510
844
 
511
- if (session.expires && isTokenExpired(session.expires)) {
845
+ const session = getActiveSession();
846
+ if (session?.expires && isTokenExpired(session.expires)) {
512
847
  await refresh();
513
848
  return getActiveSession();
514
849
  }
@@ -517,23 +852,272 @@ export function createAuth(config: AuthConfig = {}): Auth {
517
852
  }
518
853
 
519
854
  async function clearAllSessions(): Promise<void> {
520
- await saveAuthState({ sessions: [], activeSessionIndex: -1 });
521
- notifyErrorListeners(null);
855
+ await initialize();
856
+ await applyState(
857
+ {
858
+ sessions: [],
859
+ activeSessionIndex: -1,
860
+ },
861
+ {
862
+ clearExpiredSession: true,
863
+ error: null,
864
+ event: {
865
+ type: 'session-cleared',
866
+ },
867
+ },
868
+ );
522
869
  }
523
870
 
524
871
  async function deleteChallenge(challengeId: string): Promise<void> {
872
+ await initialize();
525
873
  const currentState = await loadAuthState();
526
874
  const idx = currentState.activeSessionIndex;
527
- if (idx < 0 || idx >= currentState.sessions.length) return;
875
+
876
+ if (idx < 0 || idx >= currentState.sessions.length) {
877
+ return;
878
+ }
528
879
 
529
880
  const session = currentState.sessions[idx];
530
- const updatedSessions = [...currentState.sessions];
881
+ const updatedSessions = cloneSessions(currentState.sessions);
531
882
  updatedSessions[idx] = {
532
883
  ...session,
533
- challenges: session.challenges?.filter((c: any) => c.id !== challengeId) || [],
884
+ challenges:
885
+ session.challenges?.filter((challenge: any) => challenge.id !== challengeId) ||
886
+ [],
887
+ };
888
+
889
+ await applyState(
890
+ {
891
+ sessions: updatedSessions,
892
+ activeSessionIndex: idx,
893
+ },
894
+ {
895
+ error: null,
896
+ },
897
+ );
898
+ }
899
+
900
+ async function importToken(
901
+ token: string,
902
+ options: ImportTokenOptions = {},
903
+ ): Promise<AuthSession> {
904
+ await initialize();
905
+ const user = await fetchUserForToken(token);
906
+ const sessionDuration = options.sessionDuration ?? 900;
907
+
908
+ return upsertSession(
909
+ {
910
+ user,
911
+ token,
912
+ refresh_token: '',
913
+ challenges: [],
914
+ expires: options.expires ?? Date.now() + 30 * 24 * 60 * 60 * 1000,
915
+ session_duration: sessionDuration,
916
+ company: options.company ?? user?.company,
917
+ },
918
+ 'session-imported',
919
+ );
920
+ }
921
+
922
+ async function validateActiveSession(
923
+ options: ValidateSessionOptions = {},
924
+ ): Promise<boolean> {
925
+ await initialize();
926
+ if (!getActiveSession()?.token) {
927
+ return false;
928
+ }
929
+
930
+ const retryCount = options.retryCount ?? 1;
931
+ const retryDelayMs = options.retryDelayMs ?? 400;
932
+ let refreshAttempted = false;
933
+
934
+ for (let attempt = 0; attempt <= retryCount; attempt += 1) {
935
+ const activeSession = getActiveSession();
936
+
937
+ if (!activeSession?.token) {
938
+ return false;
939
+ }
940
+
941
+ try {
942
+ await fetchUserForToken(activeSession.token);
943
+ return true;
944
+ } catch (error) {
945
+ const status =
946
+ error instanceof ApiError
947
+ ? error.status
948
+ : typeof error === 'object' &&
949
+ error &&
950
+ 'status' in error &&
951
+ typeof (error as any).status === 'number'
952
+ ? (error as any).status
953
+ : undefined;
954
+
955
+ if (status === 401 || status === 403) {
956
+ if (!refreshAttempted && activeSession.refresh_token) {
957
+ refreshAttempted = true;
958
+
959
+ try {
960
+ await refresh();
961
+ } catch {
962
+ await expireActiveSession();
963
+ return false;
964
+ }
965
+
966
+ const refreshedSession = getActiveSession();
967
+ if (!refreshedSession?.token) {
968
+ return false;
969
+ }
970
+
971
+ try {
972
+ await fetchUserForToken(refreshedSession.token);
973
+ return true;
974
+ } catch {
975
+ await expireActiveSession();
976
+ return false;
977
+ }
978
+ }
979
+
980
+ if (attempt < retryCount) {
981
+ await sleep(retryDelayMs);
982
+ continue;
983
+ }
984
+ await expireActiveSession();
985
+ return false;
986
+ }
987
+
988
+ if (attempt < retryCount) {
989
+ await sleep(retryDelayMs);
990
+ continue;
991
+ }
992
+
993
+ return false;
994
+ }
995
+ }
996
+
997
+ return false;
998
+ }
999
+
1000
+ async function syncActiveSessionUser(): Promise<AuthSession | null> {
1001
+ await initialize();
1002
+ const currentState = await loadAuthState();
1003
+ const idx = currentState.activeSessionIndex;
1004
+ const session = getActiveSessionFromState(currentState);
1005
+
1006
+ if (idx < 0 || !session?.token) {
1007
+ return null;
1008
+ }
1009
+
1010
+ const user = await fetchUserForToken(session.token);
1011
+ const updatedSession: AuthSession = {
1012
+ ...session,
1013
+ user: {
1014
+ ...session.user,
1015
+ ...user,
1016
+ },
1017
+ company: user?.company ?? getSessionCompany(session),
1018
+ };
1019
+
1020
+ const updatedSessions = cloneSessions(currentState.sessions);
1021
+ updatedSessions[idx] = updatedSession;
1022
+
1023
+ await applyState(
1024
+ {
1025
+ sessions: updatedSessions,
1026
+ activeSessionIndex: idx,
1027
+ },
1028
+ {
1029
+ error: null,
1030
+ event: {
1031
+ type: 'session-updated',
1032
+ session: updatedSession,
1033
+ },
1034
+ },
1035
+ );
1036
+
1037
+ return updatedSession;
1038
+ }
1039
+
1040
+ async function updateSession(
1041
+ userId: string,
1042
+ company: string | undefined,
1043
+ patch: SessionPatch,
1044
+ ): Promise<AuthSession | null> {
1045
+ await initialize();
1046
+ const currentState = await loadAuthState();
1047
+ const idx = currentState.sessions.findIndex(
1048
+ (session) =>
1049
+ session.user.id === userId && getSessionCompany(session) === company,
1050
+ );
1051
+
1052
+ if (idx === -1) {
1053
+ return null;
1054
+ }
1055
+
1056
+ const currentSession = currentState.sessions[idx];
1057
+ const updatedSession =
1058
+ typeof patch === 'function'
1059
+ ? patch(currentSession)
1060
+ : {
1061
+ ...currentSession,
1062
+ ...patch,
1063
+ user: patch.user
1064
+ ? {
1065
+ ...currentSession.user,
1066
+ ...patch.user,
1067
+ }
1068
+ : currentSession.user,
1069
+ company:
1070
+ patch.company ??
1071
+ patch.user?.company ??
1072
+ getSessionCompany(currentSession),
1073
+ };
1074
+
1075
+ const updatedSessions = cloneSessions(currentState.sessions);
1076
+ updatedSessions[idx] = updatedSession;
1077
+
1078
+ await applyState(
1079
+ {
1080
+ sessions: updatedSessions,
1081
+ activeSessionIndex: currentState.activeSessionIndex,
1082
+ },
1083
+ {
1084
+ error: null,
1085
+ event: {
1086
+ type: 'session-updated',
1087
+ session: updatedSession,
1088
+ },
1089
+ },
1090
+ );
1091
+
1092
+ return updatedSession;
1093
+ }
1094
+
1095
+ async function expireActiveSession(): Promise<AuthRecoveryState> {
1096
+ await initialize();
1097
+ const currentState = await loadAuthState();
1098
+ const session = getActiveSessionFromState(currentState);
1099
+
1100
+ if (!session) {
1101
+ return buildRecoveryState(currentState);
1102
+ }
1103
+
1104
+ const nextState: AuthState = {
1105
+ sessions: currentState.sessions.filter(
1106
+ (_entry, index) => index !== currentState.activeSessionIndex,
1107
+ ),
1108
+ activeSessionIndex: -1,
534
1109
  };
535
1110
 
536
- await saveAuthState({ ...currentState, sessions: updatedSessions });
1111
+ await applyState(nextState, {
1112
+ expiredSession: session,
1113
+ error: null,
1114
+ event: {
1115
+ type: 'session-expired',
1116
+ session,
1117
+ },
1118
+ });
1119
+
1120
+ return buildRecoveryState();
537
1121
  }
538
1122
 
539
1123
  return {
@@ -545,22 +1129,42 @@ export function createAuth(config: AuthConfig = {}): Auth {
545
1129
  refresh,
546
1130
  getToken,
547
1131
  getActiveSession,
548
- getSessions: () => [...sessions],
1132
+ getSessions: () => cloneSessions(sessions),
549
1133
  getSessionsByCompany: (company: string) =>
550
- sessions.filter((s) => s.company === company),
1134
+ sessions.filter((session) => getSessionCompany(session) === company),
1135
+ getState,
1136
+ getStatus: () => getStatus(),
1137
+ getRecoveryState: () => buildRecoveryState(),
551
1138
  switchToSession,
552
1139
  clearAllSessions,
553
1140
  deleteChallenge,
1141
+ importToken,
1142
+ validateActiveSession,
1143
+ syncActiveSessionUser,
1144
+ updateSession,
1145
+ expireActiveSession,
554
1146
  subscribe(listener: SessionListener): () => void {
555
1147
  sessionListeners.push(listener);
556
1148
  return () => {
557
- sessionListeners = sessionListeners.filter((l) => l !== listener);
1149
+ sessionListeners = sessionListeners.filter((entry) => entry !== listener);
558
1150
  };
559
1151
  },
560
1152
  subscribeToErrors(listener: ErrorListener): () => void {
561
1153
  errorListeners.push(listener);
562
1154
  return () => {
563
- errorListeners = errorListeners.filter((l) => l !== listener);
1155
+ errorListeners = errorListeners.filter((entry) => entry !== listener);
1156
+ };
1157
+ },
1158
+ subscribeToState(listener: AuthStateListener): () => void {
1159
+ stateListeners.push(listener);
1160
+ return () => {
1161
+ stateListeners = stateListeners.filter((entry) => entry !== listener);
1162
+ };
1163
+ },
1164
+ subscribeToEvents(listener: AuthEventListener): () => void {
1165
+ eventListeners.push(listener);
1166
+ return () => {
1167
+ eventListeners = eventListeners.filter((entry) => entry !== listener);
564
1168
  };
565
1169
  },
566
1170
  get baseUrl() {