@mapnests/gateway-web-sdk 1.0.6 → 1.0.8
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.
- package/README.md +250 -33
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/package.json +3 -2
- package/src/SessionManager.d.ts +14 -0
package/README.md
CHANGED
|
@@ -8,9 +8,10 @@ A lightweight, production-ready session token management SDK for React and Next.
|
|
|
8
8
|
- HttpOnly cookie support for secure token storage
|
|
9
9
|
- React hook (`useSession`) for seamless integration
|
|
10
10
|
- Singleton pattern ensuring a single session instance across your app
|
|
11
|
-
- Zero dependencies (
|
|
11
|
+
- Zero runtime dependencies (`react` and `react-dom` as peer dependencies)
|
|
12
12
|
- Next.js compatible (SSR-safe)
|
|
13
|
-
- Built-in Fetch and Axios interceptors
|
|
13
|
+
- Built-in Fetch and Axios interceptors with automatic session readiness gating and 401 handling
|
|
14
|
+
- Resilient to browser close/reopen — stale cookies are cleared and tokens refresh before any API call
|
|
14
15
|
- Full TypeScript definitions included
|
|
15
16
|
|
|
16
17
|
## Installation
|
|
@@ -25,9 +26,11 @@ npm install @mapnests/gateway-web-sdk
|
|
|
25
26
|
|
|
26
27
|
Choose one of the three approaches below based on your preferred HTTP client. All three approaches share the same **Step 1** (environment setup) and **Step 2** (session initialization).
|
|
27
28
|
|
|
29
|
+
> **Next.js users:** Skip the Common Setup below and go directly to the [Next.js Integration](#nextjs-integration) section, which provides its own Steps 1–2. Then return here for Approach A, B, or C.
|
|
30
|
+
|
|
28
31
|
---
|
|
29
32
|
|
|
30
|
-
### Common Setup (
|
|
33
|
+
### Common Setup (Vite / CRA / React Apps)
|
|
31
34
|
|
|
32
35
|
#### Step 1 — Environment Variables
|
|
33
36
|
|
|
@@ -45,8 +48,6 @@ VITE_TOKEN_COOKIE_NAME=token
|
|
|
45
48
|
| `VITE_BOOTSTRAP_PATH` | Path to the session bootstrap endpoint |
|
|
46
49
|
| `VITE_TOKEN_COOKIE_NAME` | Name of the token cookie set by your server |
|
|
47
50
|
|
|
48
|
-
> If you are using Next.js, prefix with `NEXT_PUBLIC_` instead of `VITE_`.
|
|
49
|
-
|
|
50
51
|
Then create a config helper to read these values:
|
|
51
52
|
|
|
52
53
|
```js
|
|
@@ -210,6 +211,9 @@ import { API_BASE_URL } from './config.js';
|
|
|
210
211
|
const sm = SessionManager.getInstance();
|
|
211
212
|
|
|
212
213
|
async function request(url, init = {}) {
|
|
214
|
+
// Ensure session is bootstrapped before sending (handles browser close/reopen, tab wake, etc.)
|
|
215
|
+
await sm.ensureReady();
|
|
216
|
+
|
|
213
217
|
const opts = {
|
|
214
218
|
...init,
|
|
215
219
|
credentials: 'include',
|
|
@@ -227,20 +231,17 @@ async function request(url, init = {}) {
|
|
|
227
231
|
|
|
228
232
|
let res = await fetch(url, opts);
|
|
229
233
|
|
|
230
|
-
// Handle 401
|
|
234
|
+
// Handle 401 with the configured invalidSessionError
|
|
231
235
|
if (res.status === 401) {
|
|
232
236
|
const cloned = res.clone();
|
|
233
237
|
try {
|
|
234
238
|
const body = await cloned.json();
|
|
235
|
-
if (body.error_msg ===
|
|
239
|
+
if (body.error_msg === sm.config.invalidSessionError) {
|
|
236
240
|
// Wait for any in-progress refresh, or trigger a new one
|
|
237
241
|
if (sm.isRefreshing()) {
|
|
238
242
|
await sm.waitForRefresh();
|
|
239
243
|
} else {
|
|
240
|
-
|
|
241
|
-
if (!existingToken) {
|
|
242
|
-
await sm.refreshToken();
|
|
243
|
-
}
|
|
244
|
+
await sm.refreshToken();
|
|
244
245
|
}
|
|
245
246
|
|
|
246
247
|
// Update headers with refreshed session
|
|
@@ -297,46 +298,153 @@ export default function Dashboard() {
|
|
|
297
298
|
|
|
298
299
|
## Next.js Integration
|
|
299
300
|
|
|
300
|
-
|
|
301
|
+
> **Note:** For Next.js, use `NEXT_PUBLIC_` prefixed environment variables instead of `VITE_`, and replace the common **Step 1** config helper and **Step 2** initialization with the Next.js-specific setup below.
|
|
302
|
+
|
|
303
|
+
### Step 1 — Environment Variables
|
|
304
|
+
|
|
305
|
+
```env
|
|
306
|
+
NEXT_PUBLIC_API_BASE_URL=https://your-gateway.example.com
|
|
307
|
+
NEXT_PUBLIC_BOOTSTRAP_PATH=/api/session/bootstrap
|
|
308
|
+
NEXT_PUBLIC_TOKEN_COOKIE_NAME=token
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Step 2 — Config Helper
|
|
312
|
+
|
|
313
|
+
```js
|
|
314
|
+
// src/config.js
|
|
315
|
+
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;
|
|
316
|
+
const BOOTSTRAP_PATH = process.env.NEXT_PUBLIC_BOOTSTRAP_PATH;
|
|
317
|
+
const TOKEN_COOKIE_NAME = process.env.NEXT_PUBLIC_TOKEN_COOKIE_NAME;
|
|
318
|
+
|
|
319
|
+
if (!API_BASE_URL) throw new Error('NEXT_PUBLIC_API_BASE_URL is not defined');
|
|
320
|
+
if (!BOOTSTRAP_PATH) throw new Error('NEXT_PUBLIC_BOOTSTRAP_PATH is not defined');
|
|
321
|
+
if (!TOKEN_COOKIE_NAME) throw new Error('NEXT_PUBLIC_TOKEN_COOKIE_NAME is not defined');
|
|
322
|
+
|
|
323
|
+
export { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME };
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### App Router
|
|
327
|
+
|
|
328
|
+
#### Step 3 — Create a Session Provider
|
|
329
|
+
|
|
330
|
+
Create a client component that initializes the session. This keeps the root layout as a Server Component, preserving the benefits of React Server Components.
|
|
301
331
|
|
|
302
332
|
```jsx
|
|
333
|
+
// app/providers/SessionProvider.jsx
|
|
303
334
|
'use client';
|
|
304
335
|
|
|
305
336
|
import { useEffect } from 'react';
|
|
306
337
|
import { SessionManager } from '@mapnests/gateway-web-sdk';
|
|
338
|
+
import { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME } from '@/src/config';
|
|
307
339
|
|
|
308
|
-
|
|
340
|
+
// Configure at module level so the SDK is ready before any child component mounts.
|
|
341
|
+
// configure() only sets config values — no browser APIs needed, safe during SSR.
|
|
342
|
+
const sessionManager = SessionManager.getInstance();
|
|
343
|
+
sessionManager.configure({
|
|
344
|
+
bootstrapUrl: `${API_BASE_URL}${BOOTSTRAP_PATH}`,
|
|
345
|
+
tokenCookieName: TOKEN_COOKIE_NAME,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
export default function SessionProvider({ children }) {
|
|
349
|
+
// initialize() calls the bootstrap API — must run in useEffect (client-only)
|
|
309
350
|
useEffect(() => {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
tokenCookieName: process.env.NEXT_PUBLIC_TOKEN_COOKIE_NAME,
|
|
314
|
-
});
|
|
315
|
-
sessionManager.initialize();
|
|
351
|
+
sessionManager.initialize().catch(err =>
|
|
352
|
+
console.error('Failed to initialize session:', err)
|
|
353
|
+
);
|
|
316
354
|
}, []);
|
|
317
355
|
|
|
356
|
+
return children;
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
#### Step 4 — Add the Provider to the Root Layout
|
|
361
|
+
|
|
362
|
+
The root layout stays as a Server Component — do **not** add `'use client'` here.
|
|
363
|
+
|
|
364
|
+
```jsx
|
|
365
|
+
// app/layout.js
|
|
366
|
+
import SessionProvider from './providers/SessionProvider';
|
|
367
|
+
|
|
368
|
+
export default function RootLayout({ children }) {
|
|
318
369
|
return (
|
|
319
370
|
<html lang="en">
|
|
320
|
-
<body>
|
|
371
|
+
<body>
|
|
372
|
+
<SessionProvider>{children}</SessionProvider>
|
|
373
|
+
</body>
|
|
321
374
|
</html>
|
|
322
375
|
);
|
|
323
376
|
}
|
|
324
377
|
```
|
|
325
378
|
|
|
326
|
-
|
|
379
|
+
#### Step 5 — Create an API Layer and Use in a Page
|
|
380
|
+
|
|
381
|
+
After your router setup is complete, create an API layer using any of **Approach A / B / C** from the [Implementation Guide](#implementation-guide) above. The only change needed is to replace `import.meta.env.VITE_*` references with imports from your `src/config.js`.
|
|
382
|
+
|
|
383
|
+
For example, using Approach A (Fetch Interceptor):
|
|
384
|
+
|
|
385
|
+
```js
|
|
386
|
+
// src/api.js
|
|
387
|
+
import { fetchInterceptor } from '@mapnests/gateway-web-sdk';
|
|
388
|
+
import { API_BASE_URL } from './config';
|
|
389
|
+
|
|
390
|
+
export const getUser = () =>
|
|
391
|
+
fetchInterceptor(`${API_BASE_URL}/api/user`);
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Then use it in a page component. Page components that use hooks must be client components.
|
|
327
395
|
|
|
328
396
|
```jsx
|
|
397
|
+
// app/dashboard/page.jsx
|
|
398
|
+
'use client';
|
|
399
|
+
|
|
400
|
+
import { useEffect, useState } from 'react';
|
|
401
|
+
import { useSession } from '@mapnests/gateway-web-sdk';
|
|
402
|
+
import { getUser } from '@/src/api';
|
|
403
|
+
|
|
404
|
+
export default function DashboardPage() {
|
|
405
|
+
const { isInitialized, isLoading, error } = useSession();
|
|
406
|
+
const [user, setUser] = useState(null);
|
|
407
|
+
|
|
408
|
+
useEffect(() => {
|
|
409
|
+
if (!isInitialized) return;
|
|
410
|
+
|
|
411
|
+
getUser()
|
|
412
|
+
.then(res => res.json())
|
|
413
|
+
.then(setUser)
|
|
414
|
+
.catch(err => console.error('Failed to fetch user:', err));
|
|
415
|
+
}, [isInitialized]);
|
|
416
|
+
|
|
417
|
+
if (isLoading) return <p>Loading session...</p>;
|
|
418
|
+
if (error) return <p>Session error: {error}</p>;
|
|
419
|
+
if (!user) return <p>Loading data...</p>;
|
|
420
|
+
|
|
421
|
+
return <pre>{JSON.stringify(user, null, 2)}</pre>;
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### Pages Router
|
|
426
|
+
|
|
427
|
+
#### Step 3 — Initialize in `_app.js`
|
|
428
|
+
|
|
429
|
+
```jsx
|
|
430
|
+
// pages/_app.js
|
|
329
431
|
import { useEffect } from 'react';
|
|
330
432
|
import { SessionManager } from '@mapnests/gateway-web-sdk';
|
|
433
|
+
import { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME } from '../src/config';
|
|
434
|
+
|
|
435
|
+
// Configure at module level — runs once when the module is first imported.
|
|
436
|
+
const sessionManager = SessionManager.getInstance();
|
|
437
|
+
sessionManager.configure({
|
|
438
|
+
bootstrapUrl: `${API_BASE_URL}${BOOTSTRAP_PATH}`,
|
|
439
|
+
tokenCookieName: TOKEN_COOKIE_NAME,
|
|
440
|
+
});
|
|
331
441
|
|
|
332
442
|
function MyApp({ Component, pageProps }) {
|
|
443
|
+
// initialize() needs the browser — must run in useEffect
|
|
333
444
|
useEffect(() => {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
tokenCookieName: process.env.NEXT_PUBLIC_TOKEN_COOKIE_NAME,
|
|
338
|
-
});
|
|
339
|
-
sessionManager.initialize();
|
|
445
|
+
sessionManager.initialize().catch(err =>
|
|
446
|
+
console.error('Failed to initialize session:', err)
|
|
447
|
+
);
|
|
340
448
|
}, []);
|
|
341
449
|
|
|
342
450
|
return <Component {...pageProps} />;
|
|
@@ -345,6 +453,37 @@ function MyApp({ Component, pageProps }) {
|
|
|
345
453
|
export default MyApp;
|
|
346
454
|
```
|
|
347
455
|
|
|
456
|
+
#### Step 4 — Create an API Layer and Use in a Page
|
|
457
|
+
|
|
458
|
+
Same as the App Router — create an `src/api.js` using any of **Approach A / B / C**, then use it in your page:
|
|
459
|
+
|
|
460
|
+
```jsx
|
|
461
|
+
// pages/dashboard.jsx
|
|
462
|
+
import { useEffect, useState } from 'react';
|
|
463
|
+
import { useSession } from '@mapnests/gateway-web-sdk';
|
|
464
|
+
import { getUser } from '../src/api';
|
|
465
|
+
|
|
466
|
+
export default function Dashboard() {
|
|
467
|
+
const { isInitialized, isLoading, error } = useSession();
|
|
468
|
+
const [user, setUser] = useState(null);
|
|
469
|
+
|
|
470
|
+
useEffect(() => {
|
|
471
|
+
if (!isInitialized) return;
|
|
472
|
+
|
|
473
|
+
getUser()
|
|
474
|
+
.then(res => res.json())
|
|
475
|
+
.then(setUser)
|
|
476
|
+
.catch(err => console.error('Failed to fetch user:', err));
|
|
477
|
+
}, [isInitialized]);
|
|
478
|
+
|
|
479
|
+
if (isLoading) return <p>Loading session...</p>;
|
|
480
|
+
if (error) return <p>Session error: {error}</p>;
|
|
481
|
+
if (!user) return <p>Loading data...</p>;
|
|
482
|
+
|
|
483
|
+
return <pre>{JSON.stringify(user, null, 2)}</pre>;
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
348
487
|
---
|
|
349
488
|
|
|
350
489
|
## API Reference
|
|
@@ -366,9 +505,10 @@ React hook for session management.
|
|
|
366
505
|
isInitialized: boolean;
|
|
367
506
|
isLoading: boolean;
|
|
368
507
|
error: string | null;
|
|
369
|
-
lastRefreshTime: number;
|
|
370
|
-
nextRefreshTime: number;
|
|
371
|
-
timeUntilRefresh: number;
|
|
508
|
+
lastRefreshTime: number | null;
|
|
509
|
+
nextRefreshTime: number | null;
|
|
510
|
+
timeUntilRefresh: number | null;
|
|
511
|
+
initializationFailed: boolean;
|
|
372
512
|
refresh: () => Promise<void>;
|
|
373
513
|
initialize: () => Promise<void>;
|
|
374
514
|
}
|
|
@@ -423,7 +563,33 @@ Returns the token value from the named cookie, or `null`.
|
|
|
423
563
|
|
|
424
564
|
#### `shouldUseTokenHeader()`
|
|
425
565
|
|
|
426
|
-
Returns `true` if the token should be sent as a request header (
|
|
566
|
+
Returns `true` if the token should be sent as a request header (i.e. when the app is served over HTTP, not HTTPS).
|
|
567
|
+
|
|
568
|
+
#### `ensureReady()`
|
|
569
|
+
|
|
570
|
+
Ensures the session is ready before making API calls. This is the recommended way to gate requests on session readiness.
|
|
571
|
+
|
|
572
|
+
- If initialized and token is fresh, resolves immediately (zero overhead).
|
|
573
|
+
- If initialization is in progress, waits for it to complete.
|
|
574
|
+
- If not initialized but configured, triggers `initialize()` and waits.
|
|
575
|
+
- If not configured or permanently failed, resolves and lets the request proceed (the 401 handler will deal with it).
|
|
576
|
+
|
|
577
|
+
```javascript
|
|
578
|
+
await sessionManager.ensureReady();
|
|
579
|
+
// Session is now ready — safe to make API calls
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
> **Note:** `fetchInterceptor` and `setupAxiosInterceptor` call `ensureReady()` automatically before every request. You only need to call it explicitly when building a manual fetch wrapper (Approach C).
|
|
583
|
+
|
|
584
|
+
#### `needsRefresh()`
|
|
585
|
+
|
|
586
|
+
Returns `true` if the session is initialized but the token has likely expired. Useful for proactive checks.
|
|
587
|
+
|
|
588
|
+
```javascript
|
|
589
|
+
if (sessionManager.needsRefresh()) {
|
|
590
|
+
await sessionManager.refreshToken();
|
|
591
|
+
}
|
|
592
|
+
```
|
|
427
593
|
|
|
428
594
|
#### `isRefreshing()`
|
|
429
595
|
|
|
@@ -437,6 +603,22 @@ Returns a promise that resolves when the in-progress refresh completes.
|
|
|
437
603
|
|
|
438
604
|
Returns the current session state object.
|
|
439
605
|
|
|
606
|
+
```typescript
|
|
607
|
+
{
|
|
608
|
+
isInitialized: boolean;
|
|
609
|
+
isLoading: boolean;
|
|
610
|
+
lastRefreshTime: number | null;
|
|
611
|
+
nextRefreshTime: number | null;
|
|
612
|
+
tokenExpiry: number | null;
|
|
613
|
+
error: string | null;
|
|
614
|
+
errorCode: string | null;
|
|
615
|
+
initializationFailed: boolean;
|
|
616
|
+
timeUntilRefresh: number | null;
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
> **Note:** `getSessionStatus()` returns `tokenExpiry` and `errorCode` which are not exposed by the `useSession` hook. Use this method directly if you need those fields.
|
|
621
|
+
|
|
440
622
|
#### `subscribe(listener)`
|
|
441
623
|
|
|
442
624
|
Subscribe to session state changes. Returns an unsubscribe function.
|
|
@@ -453,7 +635,7 @@ Clean up timers, listeners, and cookies. Resets the session manager.
|
|
|
453
635
|
|
|
454
636
|
### `fetchInterceptor(url, options?)`
|
|
455
637
|
|
|
456
|
-
Drop-in `fetch` wrapper. Automatically attaches session headers and retries on 401 `INVALID_SESSION`.
|
|
638
|
+
Drop-in `fetch` wrapper. Automatically gates on session readiness via `ensureReady()`, attaches session headers, and retries on 401 `INVALID_SESSION`. Safe to call even before the session is initialized — the request will wait for bootstrap to complete.
|
|
457
639
|
|
|
458
640
|
```javascript
|
|
459
641
|
import { fetchInterceptor } from '@mapnests/gateway-web-sdk';
|
|
@@ -462,7 +644,7 @@ const response = await fetchInterceptor('/api/data');
|
|
|
462
644
|
|
|
463
645
|
### `setupAxiosInterceptor(axiosInstance)`
|
|
464
646
|
|
|
465
|
-
Attaches request/response interceptors to an Axios instance. Returns the same instance.
|
|
647
|
+
Attaches request/response interceptors to an Axios instance. Returns the same instance. Every request automatically gates on session readiness via `ensureReady()`, attaches session headers, and retries on 401 `INVALID_SESSION`.
|
|
466
648
|
|
|
467
649
|
```javascript
|
|
468
650
|
import axios from 'axios';
|
|
@@ -470,6 +652,35 @@ import { setupAxiosInterceptor } from '@mapnests/gateway-web-sdk';
|
|
|
470
652
|
const api = setupAxiosInterceptor(axios.create({ baseURL: '/api' }));
|
|
471
653
|
```
|
|
472
654
|
|
|
655
|
+
### Error Classes
|
|
656
|
+
|
|
657
|
+
The SDK exports custom error classes for typed error handling:
|
|
658
|
+
|
|
659
|
+
```javascript
|
|
660
|
+
import {
|
|
661
|
+
SessionError, // Base error class (code, details)
|
|
662
|
+
ConfigurationError, // Invalid configuration (code: 'CONFIGURATION_ERROR')
|
|
663
|
+
BootstrapError, // Bootstrap API failure (code: 'BOOTSTRAP_ERROR')
|
|
664
|
+
NetworkError, // Network-level failure (code: 'NETWORK_ERROR')
|
|
665
|
+
SSRError, // Called in non-browser environment (code: 'SSR_ERROR')
|
|
666
|
+
} from '@mapnests/gateway-web-sdk';
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
All errors extend `SessionError`, which provides `code` (string) and `details` (object) properties.
|
|
670
|
+
|
|
671
|
+
### `logger` and `LOG_LEVELS`
|
|
672
|
+
|
|
673
|
+
The SDK's internal logger is exported for advanced use (e.g. setting log level independently of `configure()`):
|
|
674
|
+
|
|
675
|
+
```javascript
|
|
676
|
+
import { logger, LOG_LEVELS } from '@mapnests/gateway-web-sdk';
|
|
677
|
+
|
|
678
|
+
logger.setLevel('DEBUG'); // or logger.setLevel(LOG_LEVELS.DEBUG)
|
|
679
|
+
logger.info('Custom log'); // [SessionManager] Custom log
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
Available levels: `NONE` (0), `ERROR` (1), `WARN` (2, default), `INFO` (3), `DEBUG` (4).
|
|
683
|
+
|
|
473
684
|
---
|
|
474
685
|
|
|
475
686
|
## Security Notice
|
|
@@ -507,6 +718,12 @@ This SDK prioritizes **server-set HttpOnly cookies** for maximum security. The S
|
|
|
507
718
|
- The SDK uses singleton pattern, but ensure you're not calling `initialize()` multiple times
|
|
508
719
|
- Use `autoInitialize: false` in `useSession()` if you want manual control
|
|
509
720
|
|
|
721
|
+
### 401 errors after browser close and reopen
|
|
722
|
+
- The SDK automatically clears stale cookies and re-bootstraps on fresh page loads
|
|
723
|
+
- Both `fetchInterceptor` and `setupAxiosInterceptor` call `ensureReady()` before every request, which waits for bootstrap to complete
|
|
724
|
+
- If using a manual fetch wrapper (Approach C), ensure you call `await sm.ensureReady()` before each request
|
|
725
|
+
- Verify your 401 handler compares against `sm.config.invalidSessionError` (default: `'INVALID-GW-SESSION'`), not a hardcoded string
|
|
726
|
+
|
|
510
727
|
### Next.js SSR errors
|
|
511
728
|
- The SDK detects SSR environments and prevents initialization
|
|
512
729
|
- Always wrap initialization in `useEffect` or client components (`'use client'`)
|
package/dist/index.cjs.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("react");class t extends Error{constructor(e,i,s={}){super(e),this.name="SessionError",this.code=i,this.details=s,Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor),Object.setPrototypeOf(this,t.prototype)}}class i extends t{constructor(e,t){super(e,"CONFIGURATION_ERROR",t),this.name="ConfigurationError"}}class s extends t{constructor(e,t){super(e,"BOOTSTRAP_ERROR",t),this.name="BootstrapError"}}class r extends t{constructor(e,t){super(e,"NETWORK_ERROR",t),this.name="NetworkError"}}class o extends t{constructor(e,t={}){super(e,"SSR_ERROR",t),this.name="SSRError"}}const n={NONE:0,ERROR:1,WARN:2,INFO:3,DEBUG:4};const a=new class{constructor(){this.level=n.WARN}setLevel(e){this.level="string"==typeof e?n[e.toUpperCase()]??n.WARN:e}error(...e){this.level>=n.ERROR&&console.error("[SessionManager]",...e)}warn(...e){this.level>=n.WARN&&console.warn("[SessionManager]",...e)}info(...e){this.level>=n.INFO&&console.info("[SessionManager]",...e)}debug(...e){this.level>=n.DEBUG&&console.debug("[SessionManager]",...e)}};class l{constructor(){if(l.instance)return l.instance;this.config={bootstrapUrl:"/session/bootstrap",refreshInterval:15e5,tokenExpiry:18e5,maxRetries:3,tokenCookieName:"token"},this.state={isInitialized:!1,isLoading:!1,lastRefreshTime:null,error:null,errorCode:null,nextRefreshTime:null,tokenExpiry:null,cfSessionId:null,initializationFailed:!1},this.refreshTimerId=null,this.tickTimerId=null,this.listeners=new Set,this.initializationPromise=null,l.instance=this}configure(e={}){if(!e.bootstrapUrl||"string"!=typeof e.bootstrapUrl||!e.bootstrapUrl.trim())throw new i("bootstrapUrl is required and must be a non-empty string",{bootstrapUrl:e.bootstrapUrl});if(void 0!==e.refreshInterval){if("number"!=typeof e.refreshInterval||!isFinite(e.refreshInterval))throw new i("refreshInterval must be a finite number",{refreshInterval:e.refreshInterval});if(e.refreshInterval<=0)throw new i("refreshInterval must be positive",{refreshInterval:e.refreshInterval})}if(void 0!==e.tokenExpiry){if("number"!=typeof e.tokenExpiry||!isFinite(e.tokenExpiry))throw new i("tokenExpiry must be a finite number",{tokenExpiry:e.tokenExpiry});if(e.tokenExpiry<=0)throw new i("tokenExpiry must be positive",{tokenExpiry:e.tokenExpiry})}this.config={...this.config,...e,credentials:void 0===e.credentials||e.credentials,logLevel:e.logLevel||"WARN"},a.setLevel(this.config.logLevel),this.config.refreshInterval&&this.config.tokenExpiry&&this.config.refreshInterval>=this.config.tokenExpiry&&a.warn("refreshInterval should be less than tokenExpiry to prevent race conditions")}async initialize(e=!1){if("undefined"==typeof window)throw new o("Cannot initialize in non-browser environment (SSR)");if(this.state.initializationFailed)throw a.warn("Initialization previously failed. Reload page to retry."),new s("Initialization failed after max retries. Reload page to retry.");if(this.initializationPromise)return a.debug("Initialization already in progress, waiting..."),this.initializationPromise;if(!e){if("true"===this.getCookie("session_initialized")){a.info("Session already initialized, skipping bootstrap API call"),this.state.isInitialized=!0,this.state.tokenExpiry=this.config.tokenExpiry;const e=this.getCookie("cf-session-id");e?this.state.cfSessionId=e:this.setCfSessionId(this.generateSessionId());const t=parseInt(this.getCookie("session_refresh_at"),10);let i;return i=t&&!isNaN(t)?Math.max(0,t-Date.now()):this.config.refreshInterval,this.state.lastRefreshTime=Date.now(),this.state.nextRefreshTime=Date.now()+i,this.startTokenRefresh(i),this.notifyListeners(),{token:this.getToken(this.config.tokenCookieName)}}}this.state.isLoading=!0,this.state.error=null,this.notifyListeners(),this.initializationPromise=(async()=>{let e;for(let t=1;t<=this.config.maxRetries;t++)try{const e=this.generateSessionId();this.setCfSessionId(e),a.info(`Calling bootstrap API (attempt ${t}/${this.config.maxRetries}):`,this.config.bootstrapUrl);const i=await fetch(this.config.bootstrapUrl,{method:"GET",credentials:this.config.credentials?"include":"same-origin",headers:{"Content-Type":"application/json","cf-session-id":this.state.cfSessionId,"x-client-platform":"web",...this.config.headers}});if(!i.ok)throw new s(`Bootstrap failed: ${i.status} ${i.statusText}`,{status:i.status,statusText:i.statusText,url:this.config.bootstrapUrl});let r;try{r=await i.json()}catch(e){throw new s("Bootstrap response is not valid JSON",{originalError:e.message})}a.info("Bootstrap successful");const o=i.headers.get("set-cookie"),n=o&&o.includes(this.config.tokenCookieName),l=r.refresh_time?1e3*r.refresh_time:this.config.refreshInterval,h=r.expire_time?1e3*r.expire_time:this.config.tokenExpiry;return r.token&&!n?(a.debug("Server did not set cookie, setting from SDK"),this.setCookie(this.config.tokenCookieName,r.token,h/1e3)):n&&a.debug("Server set cookie, skipping SDK cookie logic"),this.setCfSessionId(this.state.cfSessionId,h/1e3),this.state.isInitialized=!0,this.state.isLoading=!1,this.state.lastRefreshTime=Date.now(),this.state.nextRefreshTime=Date.now()+l,this.state.tokenExpiry=h,this.state.error=null,this.setCookie("session_initialized","true",l/1e3),this.setCookie("session_refresh_at",String(Date.now()+l),l/1e3),this.startTokenRefresh(l),this.notifyListeners(),r}catch(i){if(e=i instanceof Error?i:new r("Unknown error occurred",{error:i}),a.error(`Bootstrap attempt ${t} failed:`,e.message),t<this.config.maxRetries){const e=Math.min(1e3*Math.pow(2,t-1),5e3);a.info(`Retrying in ${e}ms...`),await new Promise(t=>setTimeout(t,e))}}throw a.error("All bootstrap attempts failed"),this.state.isLoading=!1,this.state.error=e?.message||"Unknown error",this.state.errorCode=e?.code||"UNKNOWN_ERROR",this.state.initializationFailed=!0,this.notifyListeners(),e})();try{return await this.initializationPromise}finally{this.initializationPromise=null}}isRefreshing(){return null!==this.initializationPromise}async waitForRefresh(){return this.initializationPromise?(a.debug("Waiting for ongoing refresh to complete"),this.initializationPromise):Promise.resolve()}setCfSessionId(e,t){this.state.cfSessionId=e;const i=t||this.config.tokenExpiry/1e3;this.setCookie("cf-session-id",e,i)}generateSessionId(){return"undefined"!=typeof crypto&&crypto.randomUUID?crypto.randomUUID():`${Date.now()}-${Math.random().toString(36).substr(2,9)}`}getSessionId(){return this.state.cfSessionId}shouldUseTokenHeader(){return"undefined"!=typeof window&&"http:"===window.location.protocol}getCookie(e){if("undefined"==typeof document)return null;const t=`; ${document.cookie}`.split(`; ${e}=`);if(2===t.length)try{return decodeURIComponent(t.pop().split(";").shift())}catch(e){return a.error("Failed to decode cookie value:",e instanceof Error?e.message:String(e)),null}return null}getToken(e=this.config.tokenCookieName){return this.getCookie(e)}setCookie(e,t,i){if("undefined"==typeof document)return void a.warn("Cannot set cookie in non-browser environment");const s=e.replace(/[^a-zA-Z0-9_-]/g,"");if(!s)return void a.error("Invalid cookie name provided");const r=new Date(Date.now()+1e3*i).toUTCString(),o="undefined"!=typeof window&&"https:"===window.location.protocol,n=o?"; Secure":"",l=encodeURIComponent(t);document.cookie=`${s}=${l}; path=/; expires=${r}; SameSite=Lax${n}`,a.debug(`Cookie set: ${s}, expires in ${i}s${o?" (Secure)":""} [WARNING: Not HttpOnly]`)}startTokenRefresh(e=this.config.refreshInterval){this.stopTokenRefresh(),a.info(`Scheduling token refresh in ${e/1e3} seconds`),this.refreshTimerId=setTimeout(async()=>{a.info("Auto-refreshing token...");try{await this.refreshToken()}catch(e){const t=e instanceof Error?e.message:String(e),i=e?.code||"UNKNOWN_ERROR";a.error("Auto-refresh failed:",t),this.state.error=t,this.state.errorCode=i,this.notifyListeners()}},e),this.startTickTimer()}stopTokenRefresh(){this.refreshTimerId&&(clearTimeout(this.refreshTimerId),this.refreshTimerId=null,a.debug("Token refresh timer stopped")),this.stopTickTimer()}startTickTimer(){this.stopTickTimer(),this.tickTimerId=setInterval(()=>{this.notifyListeners()},1e3)}stopTickTimer(){this.tickTimerId&&(clearInterval(this.tickTimerId),this.tickTimerId=null)}async refreshToken(){return a.info("Manual token refresh triggered"),this.state.initializationFailed=!1,this.initialize(!0)}getSessionStatus(){return{isInitialized:this.state.isInitialized,isLoading:this.state.isLoading,lastRefreshTime:this.state.lastRefreshTime,nextRefreshTime:this.state.nextRefreshTime,tokenExpiry:this.state.tokenExpiry,error:this.state.error,errorCode:this.state.errorCode,initializationFailed:this.state.initializationFailed,timeUntilRefresh:this.state.nextRefreshTime?Math.max(0,this.state.nextRefreshTime-Date.now()):null}}subscribe(e){if("function"!=typeof e)throw new TypeError("Listener must be a function");return this.listeners.add(e),()=>{this.listeners.delete(e)}}notifyListeners(){const e=this.getSessionStatus();Array.from(this.listeners).forEach(t=>{try{t(e)}catch(e){const t=e instanceof Error?e.message:String(e);a.error("Listener error:",t)}})}destroy(){this.stopTokenRefresh(),this.listeners.clear(),this.state={isInitialized:!1,isLoading:!1,lastRefreshTime:null,error:null,errorCode:null,nextRefreshTime:null,tokenExpiry:null,cfSessionId:null,initializationFailed:!1},this.setCookie("cf-session-id","",-1),this.setCookie("session_initialized","",-1),this.setCookie("session_refresh_at","",-1),a.info("Destroyed")}static getInstance(){return l.instance||(l.instance=new l),l.instance}}const h="web";exports.BootstrapError=s,exports.CLIENT_PLATFORM=h,exports.ConfigurationError=i,exports.LOG_LEVELS=n,exports.NetworkError=r,exports.SSRError=o,exports.SessionError=t,exports.SessionManager=l,exports.default=l,exports.fetchInterceptor=async function(e,t={}){const i=l.getInstance();if(t.credentials=t.credentials||"include",t.headers={...t.headers,"cf-session-id":i.getSessionId(),"x-client-platform":h},i.shouldUseTokenHeader()){const e=i.getToken(i.config.tokenCookieName);e&&(t.headers[i.config.tokenCookieName]=e)}let s=await fetch(e,t);if(401===s.status){const r=s.clone();try{if("INVALID_SESSION"===(await r.json()).error_msg){if(i.isRefreshing())await i.waitForRefresh();else{i.getToken(i.config.tokenCookieName)||await i.refreshToken()}if(t.headers["cf-session-id"]=i.getSessionId(),t.headers["x-client-platform"]=h,i.shouldUseTokenHeader()){const e=i.getToken(i.config.tokenCookieName);e&&(t.headers[i.config.tokenCookieName]=e)}s=await fetch(e,t)}}catch(e){}}return s},exports.logger=a,exports.setupAxiosInterceptor=function(e){const t=l.getInstance();return e.interceptors.request.use(e=>{if(e.headers["cf-session-id"]=t.getSessionId(),e.headers["x-client-platform"]=h,t.shouldUseTokenHeader()){const i=t.getToken(t.config.tokenCookieName);i&&(e.headers[t.config.tokenCookieName]=i)}return e},e=>Promise.reject(e)),e.interceptors.response.use(e=>e,async i=>{const s=i.config;if(401===i.response?.status&&!s._retry&&"INVALID_SESSION"===i.response?.data?.error_msg){if(s._retry=!0,t.isRefreshing())await t.waitForRefresh();else{t.getToken(t.config.tokenCookieName)||await t.refreshToken()}if(s.headers["cf-session-id"]=t.getSessionId(),s.headers["x-client-platform"]=h,t.shouldUseTokenHeader()){const e=t.getToken(t.config.tokenCookieName);e&&(s.headers[t.config.tokenCookieName]=e)}return e(s)}return Promise.reject(i)}),e},exports.useSession=function(t={}){const{autoInitialize:i=!0}=t,s=l.getInstance(),[r,o]=e.useState(()=>s.getSessionStatus());e.useEffect(()=>s.subscribe(e=>{o(e)}),[]),e.useEffect(()=>{!i||r.isInitialized||r.isLoading||r.initializationFailed||s.initialize().catch(e=>{a.error("Auto-initialization failed:",e)})},[i,r.isInitialized,r.isLoading,r.initializationFailed]);const n=e.useCallback(async()=>{try{await s.refreshToken()}catch(e){throw a.error("Manual refresh failed:",e),e}},[]),h=e.useCallback(async()=>{try{await s.initialize()}catch(e){throw a.error("Manual initialization failed:",e),e}},[]);return{isInitialized:r.isInitialized,isLoading:r.isLoading,error:r.error,lastRefreshTime:r.lastRefreshTime,nextRefreshTime:r.nextRefreshTime,timeUntilRefresh:r.timeUntilRefresh,initializationFailed:r.initializationFailed,refresh:n,initialize:h}};
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("react");class t extends Error{constructor(e,i,s={}){super(e),this.name="SessionError",this.code=i,this.details=s,Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor),Object.setPrototypeOf(this,t.prototype)}}class i extends t{constructor(e,t){super(e,"CONFIGURATION_ERROR",t),this.name="ConfigurationError"}}class s extends t{constructor(e,t){super(e,"BOOTSTRAP_ERROR",t),this.name="BootstrapError"}}class n extends t{constructor(e,t){super(e,"NETWORK_ERROR",t),this.name="NetworkError"}}class r extends t{constructor(e,t={}){super(e,"SSR_ERROR",t),this.name="SSRError"}}const o={NONE:0,ERROR:1,WARN:2,INFO:3,DEBUG:4};const a=new class{constructor(){this.level=o.WARN}setLevel(e){this.level="string"==typeof e?o[e.toUpperCase()]??o.WARN:e}error(...e){this.level>=o.ERROR&&console.error("[SessionManager]",...e)}warn(...e){this.level>=o.WARN&&console.warn("[SessionManager]",...e)}info(...e){this.level>=o.INFO&&console.info("[SessionManager]",...e)}debug(...e){this.level>=o.DEBUG&&console.debug("[SessionManager]",...e)}},l={bootstrapUrl:"/session/bootstrap",refreshInterval:15e5,tokenExpiry:18e5,maxRetries:3,bootstrapTimeout:3e4,tokenCookieName:"token",invalidSessionError:"INVALID-GW-SESSION"};class h{constructor(){if(h.instance)return h.instance;this.config={...l},this.state={isInitialized:!1,isLoading:!1,lastRefreshTime:null,error:null,errorCode:null,nextRefreshTime:null,tokenExpiry:null,cfSessionId:null,initializationFailed:!1},this.refreshTimerId=null,this.listeners=new Set,this.initializationPromise=null,this._configured=!1,this._boundHandleVisibilityChange=this._handleVisibilityChange.bind(this),this._boundHandlePageShow=this._handlePageShow.bind(this),this._boundHandleOnline=this._handleOnline.bind(this),h.instance=this}configure(e={}){if(!e.bootstrapUrl||"string"!=typeof e.bootstrapUrl||!e.bootstrapUrl.trim())throw new i("bootstrapUrl is required and must be a non-empty string",{bootstrapUrl:e.bootstrapUrl});if(void 0!==e.refreshInterval){if("number"!=typeof e.refreshInterval||!isFinite(e.refreshInterval))throw new i("refreshInterval must be a finite number",{refreshInterval:e.refreshInterval});if(e.refreshInterval<=0)throw new i("refreshInterval must be positive",{refreshInterval:e.refreshInterval})}if(void 0!==e.tokenExpiry){if("number"!=typeof e.tokenExpiry||!isFinite(e.tokenExpiry))throw new i("tokenExpiry must be a finite number",{tokenExpiry:e.tokenExpiry});if(e.tokenExpiry<=0)throw new i("tokenExpiry must be positive",{tokenExpiry:e.tokenExpiry})}if(void 0!==e.maxRetries&&("number"!=typeof e.maxRetries||!Number.isInteger(e.maxRetries)||e.maxRetries<1))throw new i("maxRetries must be a positive integer",{maxRetries:e.maxRetries});this.config={...this.config,...e,credentials:void 0===e.credentials||e.credentials,logLevel:e.logLevel||"WARN"},a.setLevel(this.config.logLevel),this._configured=!0,this.config.refreshInterval&&this.config.tokenExpiry&&this.config.refreshInterval>=this.config.tokenExpiry&&a.warn("refreshInterval should be less than tokenExpiry to prevent race conditions")}async initialize(e=!1){if("undefined"==typeof window)throw new r("Cannot initialize in non-browser environment (SSR)");if(this._configured||a.warn("initialize() called before configure(). Using default config."),this.state.initializationFailed)throw a.warn("Initialization previously failed. Reload page to retry."),new s("Initialization failed after max retries. Reload page to retry.");if(this.initializationPromise)return a.debug("Initialization already in progress, waiting..."),this.initializationPromise;if(this.state.isInitialized&&!e)return a.debug("Session already initialized"),{token:this.getToken(this.config.tokenCookieName)};this.state.isLoading=!0,this.state.error=null,this.notifyListeners(),this.initializationPromise=(async()=>{let e;for(let t=1;t<=this.config.maxRetries;t++)try{const e=this.generateSessionId();a.info(`Calling bootstrap API (attempt ${t}/${this.config.maxRetries}):`,this.config.bootstrapUrl);const i=new AbortController,r=setTimeout(()=>i.abort(),this.config.bootstrapTimeout);let o,l;try{o=await fetch(this.config.bootstrapUrl,{method:"GET",credentials:this.config.credentials?"include":"same-origin",signal:i.signal,headers:{"cf-session-id":e,"x-client-platform":"web",...this.config.headers}})}catch(e){if("AbortError"===e.name)throw new n(`Bootstrap request timed out after ${this.config.bootstrapTimeout}ms`,{timeout:this.config.bootstrapTimeout,url:this.config.bootstrapUrl});throw e}finally{clearTimeout(r)}if(!o.ok)throw new s(`Bootstrap failed: ${o.status} ${o.statusText}`,{status:o.status,statusText:o.statusText,url:this.config.bootstrapUrl});try{l=await o.json()}catch(e){throw new s("Bootstrap response is not valid JSON",{originalError:e.message})}a.info("Bootstrap successful");const h=l.refresh_time?1e3*l.refresh_time:this.config.refreshInterval,d=l.expire_time?1e3*l.expire_time:this.config.tokenExpiry,f=l[this.config.tokenCookieName];return f&&this.setCookie(this.config.tokenCookieName,f,d/1e3),this.setCfSessionId(e,d/1e3),this.state.isInitialized=!0,this.state.isLoading=!1,this.state.lastRefreshTime=Date.now(),this.state.nextRefreshTime=Date.now()+h,this.state.tokenExpiry=d,this.state.error=null,this.startTokenRefresh(h),this.notifyListeners(),l}catch(i){if(e=i instanceof Error?i:new n("Unknown error occurred",{error:i}),a.error(`Bootstrap attempt ${t} failed:`,e.message),t<this.config.maxRetries){const e=Math.min(1e3*Math.pow(2,t-1),5e3);a.info(`Retrying in ${e}ms...`),await new Promise(t=>setTimeout(t,e))}}throw a.error("All bootstrap attempts failed"),this.state.isLoading=!1,this.state.error=e?.message||"Unknown error",this.state.errorCode=e?.code||"UNKNOWN_ERROR",this.state.initializationFailed=!0,this.notifyListeners(),e})();try{return await this.initializationPromise}finally{this.initializationPromise=null}}isRefreshing(){return null!==this.initializationPromise}async waitForRefresh(){return this.initializationPromise?(a.debug("Waiting for ongoing refresh to complete"),this.initializationPromise):Promise.resolve()}_isTokenLikelyExpired(){return!this.state.lastRefreshTime||!this.state.tokenExpiry||Date.now()>=this.state.lastRefreshTime+this.state.tokenExpiry}async ensureReady(){if(!this.state.isInitialized||this._isTokenLikelyExpired())if(this.initializationPromise)try{await this.initializationPromise}catch(e){a.debug("ensureReady: in-flight initialization failed, proceeding with request")}else if(this._configured&&!this.state.initializationFailed)try{await this.initialize(this.state.isInitialized)}catch(e){a.debug("ensureReady: initialization failed, proceeding with request")}}needsRefresh(){return this.state.isInitialized&&this._isTokenLikelyExpired()}setCfSessionId(e,t){this.state.cfSessionId=e;const i=t||this.config.tokenExpiry/1e3;this.setCookie("cf-session-id",e,i)}generateSessionId(){return"undefined"!=typeof crypto&&crypto.randomUUID?crypto.randomUUID():`${Date.now()}-${Math.random().toString(36).substr(2,9)}`}getSessionId(){return this.state.cfSessionId}shouldUseTokenHeader(){return"undefined"!=typeof window&&"http:"===window.location.protocol}_sanitizeCookieName(e){return e.replace(/[^a-zA-Z0-9_-]/g,"")}getCookie(e){if("undefined"==typeof document)return null;const t=this._sanitizeCookieName(e);if(!t)return null;const i=`; ${document.cookie}`.split(`; ${t}=`);if(2===i.length)try{return decodeURIComponent(i.pop().split(";").shift())}catch(e){return a.error("Failed to decode cookie value:",e instanceof Error?e.message:String(e)),null}return null}getToken(e=this.config.tokenCookieName){return this.getCookie(e)}setCookie(e,t,i){if("undefined"==typeof document)return void a.warn("Cannot set cookie in non-browser environment");const s=this._sanitizeCookieName(e);if(!s)return void a.error("Invalid cookie name provided");const n=new Date(Date.now()+1e3*i).toUTCString(),r="undefined"!=typeof window&&"https:"===window.location.protocol,o=r?"; Secure":"",l=encodeURIComponent(t);document.cookie=`${s}=${l}; path=/; expires=${n}; SameSite=Lax${o}`,a.debug(`Cookie set: ${s}, expires in ${i}s${r?" (Secure)":""} [WARNING: Not HttpOnly]`)}startTokenRefresh(e=this.config.refreshInterval){this.stopTokenRefresh(),a.info(`Scheduling token refresh in ${e/1e3} seconds`),this.refreshTimerId=setTimeout(async()=>{a.info("Auto-refreshing token...");try{await this.refreshToken()}catch(t){const i=t instanceof Error?t.message:String(t),s=t?.code||"UNKNOWN_ERROR";a.error("Auto-refresh failed:",i),this.state.error=i,this.state.errorCode=s,this.notifyListeners();const n=Math.min(2*e,6e4);a.info(`Scheduling auto-refresh retry in ${n/1e3}s`),this.startTokenRefresh(n)}},e),this._startVisibilityListener(),this._startOnlineListener()}stopTokenRefresh(){this.refreshTimerId&&(clearTimeout(this.refreshTimerId),this.refreshTimerId=null,a.debug("Token refresh timer stopped")),this._stopVisibilityListener(),this._stopOnlineListener()}_startVisibilityListener(){"undefined"!=typeof document&&(document.addEventListener("visibilitychange",this._boundHandleVisibilityChange),"undefined"!=typeof window&&window.addEventListener("pageshow",this._boundHandlePageShow))}_stopVisibilityListener(){"undefined"!=typeof document&&(document.removeEventListener("visibilitychange",this._boundHandleVisibilityChange),"undefined"!=typeof window&&window.removeEventListener("pageshow",this._boundHandlePageShow))}_refreshIfOverdue(){if(!this.state.isInitialized||this.state.initializationFailed)return!1;if(this.isRefreshing())return!1;const e=Date.now(),{nextRefreshTime:t}=this.state;return!!(t&&e>=t)&&(a.info("Missed scheduled refresh — refreshing token now"),this.refreshToken().catch(e=>{const t=e instanceof Error?e.message:String(e);a.error("Triggered refresh failed:",t)}),!0)}_handleVisibilityChange(){"visible"===document.visibilityState&&this._refreshIfOverdue()}_handlePageShow(e){e.persisted&&this.state.isInitialized&&!this.state.initializationFailed&&(this.isRefreshing()||(a.info("Page restored from bfcache — forcing bootstrap"),this.refreshToken().catch(e=>{const t=e instanceof Error?e.message:String(e);a.error("bfcache-triggered refresh failed:",t)})))}_startOnlineListener(){"undefined"!=typeof window&&window.addEventListener("online",this._boundHandleOnline)}_stopOnlineListener(){"undefined"!=typeof window&&window.removeEventListener("online",this._boundHandleOnline)}_handleOnline(){this.state.initializationFailed&&(a.info("Network restored — retrying session initialization"),this.refreshToken().catch(e=>{const t=e instanceof Error?e.message:String(e);a.error("Online-triggered refresh failed:",t)}))}async refreshToken(){return a.info("Manual token refresh triggered"),this.state.initializationFailed=!1,this.initialize(!0)}getSessionStatus(){return{isInitialized:this.state.isInitialized,isLoading:this.state.isLoading,lastRefreshTime:this.state.lastRefreshTime,nextRefreshTime:this.state.nextRefreshTime,tokenExpiry:this.state.tokenExpiry,error:this.state.error,errorCode:this.state.errorCode,initializationFailed:this.state.initializationFailed,timeUntilRefresh:this.state.nextRefreshTime?Math.max(0,this.state.nextRefreshTime-Date.now()):null}}subscribe(e){if("function"!=typeof e)throw new TypeError("Listener must be a function");return this.listeners.add(e),()=>{this.listeners.delete(e)}}notifyListeners(){const e=this.getSessionStatus();Array.from(this.listeners).forEach(t=>{try{t(e)}catch(e){const t=e instanceof Error?e.message:String(e);a.error("Listener error:",t)}})}destroy(){this.stopTokenRefresh(),this.initializationPromise=null,this.listeners.clear(),this.setCookie("cf-session-id","",-1),this.setCookie(this.config.tokenCookieName,"",-1),this.config={...l},this.state={isInitialized:!1,isLoading:!1,lastRefreshTime:null,error:null,errorCode:null,nextRefreshTime:null,tokenExpiry:null,cfSessionId:null,initializationFailed:!1},this._configured=!1,a.info("Destroyed")}static getInstance(){return h.instance||(h.instance=new h),h.instance}}const d="web";exports.BootstrapError=s,exports.CLIENT_PLATFORM=d,exports.ConfigurationError=i,exports.LOG_LEVELS=o,exports.NetworkError=n,exports.SSRError=r,exports.SessionError=t,exports.SessionManager=h,exports.default=h,exports.fetchInterceptor=async function(e,t={}){const i=h.getInstance();await i.ensureReady();const s={...t,credentials:t.credentials||"include",headers:{...t.headers,"cf-session-id":i.getSessionId(),"x-client-platform":d}};if(i.shouldUseTokenHeader()){const e=i.getToken(i.config.tokenCookieName);e&&(s.headers[i.config.tokenCookieName]=e)}let n=await fetch(e,s);if(401===n.status){const t=n.clone();try{if((await t.json()).error_msg===i.config.invalidSessionError){if(i.isRefreshing()?await i.waitForRefresh():await i.refreshToken(),s.headers["cf-session-id"]=i.getSessionId(),i.shouldUseTokenHeader()){const e=i.getToken(i.config.tokenCookieName);e&&(s.headers[i.config.tokenCookieName]=e)}n=await fetch(e,s)}}catch(e){}}return n},exports.logger=a,exports.setupAxiosInterceptor=function(e){const t=h.getInstance();return void 0!==e._gwSessionRequestId&&e.interceptors.request.eject(e._gwSessionRequestId),void 0!==e._gwSessionResponseId&&e.interceptors.response.eject(e._gwSessionResponseId),e._gwSessionRequestId=e.interceptors.request.use(async e=>{if(await t.ensureReady(),t.config.credentials&&(e.withCredentials=!0),e.headers["cf-session-id"]=t.getSessionId(),e.headers["x-client-platform"]=d,t.shouldUseTokenHeader()){const i=t.getToken(t.config.tokenCookieName);i&&(e.headers[t.config.tokenCookieName]=i)}return e},e=>Promise.reject(e)),e._gwSessionResponseId=e.interceptors.response.use(e=>e,async i=>{const s=i.config;if(401===i.response?.status&&!s._retry&&i.response?.data?.error_msg===t.config.invalidSessionError){if(s._retry=!0,t.isRefreshing()?await t.waitForRefresh():await t.refreshToken(),s.headers["cf-session-id"]=t.getSessionId(),s.headers["x-client-platform"]=d,t.shouldUseTokenHeader()){const e=t.getToken(t.config.tokenCookieName);e&&(s.headers[t.config.tokenCookieName]=e)}return e(s)}return Promise.reject(i)}),e},exports.useSession=function(t={}){const{autoInitialize:i=!0}=t,s=h.getInstance(),[n,r]=e.useState(()=>s.getSessionStatus()),[o,l]=e.useState(()=>{const{nextRefreshTime:e}=s.getSessionStatus();return e?Math.max(0,e-Date.now()):null}),d=e.useRef(n.nextRefreshTime);e.useEffect(()=>s.subscribe(e=>{r(e),d.current=e.nextRefreshTime}),[]),e.useEffect(()=>{if(!n.nextRefreshTime)return void l(null);l(Math.max(0,n.nextRefreshTime-Date.now()));const e=setInterval(()=>{const e=d.current;l(e?Math.max(0,e-Date.now()):null)},1e3);return()=>clearInterval(e)},[n.nextRefreshTime]),e.useEffect(()=>{!i||n.isInitialized||n.isLoading||n.initializationFailed||s.initialize().catch(e=>{a.error("Auto-initialization failed:",e)})},[i,n.isInitialized,n.isLoading,n.initializationFailed]);const f=e.useCallback(async()=>{try{await s.refreshToken()}catch(e){throw a.error("Manual refresh failed:",e),e}},[]),c=e.useCallback(async()=>{try{await s.initialize()}catch(e){throw a.error("Manual initialization failed:",e),e}},[]);return{isInitialized:n.isInitialized,isLoading:n.isLoading,error:n.error,lastRefreshTime:n.lastRefreshTime,nextRefreshTime:n.nextRefreshTime,timeUntilRefresh:o,initializationFailed:n.initializationFailed,refresh:f,initialize:c}};
|
|
2
2
|
//# sourceMappingURL=index.cjs.js.map
|
package/dist/index.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs.js","sources":["../src/errors.js","../src/logger.js","../src/SessionManager.js","../src/interceptors.js","../src/hooks/useSession.js"],"sourcesContent":["/**\n * Custom error classes for SessionManager\n */\n\nexport class SessionError extends Error {\n constructor(message, code, details = {}) {\n super(message);\n this.name = 'SessionError';\n this.code = code;\n this.details = details;\n \n // Maintains proper stack trace for where error was thrown\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor);\n }\n \n // Set the prototype explicitly for proper instanceof checks\n Object.setPrototypeOf(this, SessionError.prototype);\n }\n}\n\nexport class ConfigurationError extends SessionError {\n constructor(message, details) {\n super(message, 'CONFIGURATION_ERROR', details);\n this.name = 'ConfigurationError';\n }\n}\n\nexport class BootstrapError extends SessionError {\n constructor(message, details) {\n super(message, 'BOOTSTRAP_ERROR', details);\n this.name = 'BootstrapError';\n }\n}\n\nexport class NetworkError extends SessionError {\n constructor(message, details) {\n super(message, 'NETWORK_ERROR', details);\n this.name = 'NetworkError';\n }\n}\n\nexport class SSRError extends SessionError {\n constructor(message, details = {}) {\n super(message, 'SSR_ERROR', details);\n this.name = 'SSRError';\n }\n}\n","/**\n * Logger utility with configurable log levels\n */\n\nconst LOG_LEVELS = {\n NONE: 0,\n ERROR: 1,\n WARN: 2,\n INFO: 3,\n DEBUG: 4\n};\n\nclass Logger {\n constructor() {\n this.level = LOG_LEVELS.WARN; // Default to WARN in production\n }\n\n setLevel(level) {\n if (typeof level === 'string') {\n this.level = LOG_LEVELS[level.toUpperCase()] ?? LOG_LEVELS.WARN;\n } else {\n this.level = level;\n }\n }\n\n error(...args) {\n if (this.level >= LOG_LEVELS.ERROR) {\n console.error('[SessionManager]', ...args);\n }\n }\n\n warn(...args) {\n if (this.level >= LOG_LEVELS.WARN) {\n console.warn('[SessionManager]', ...args);\n }\n }\n\n info(...args) {\n if (this.level >= LOG_LEVELS.INFO) {\n console.info('[SessionManager]', ...args);\n }\n }\n\n debug(...args) {\n if (this.level >= LOG_LEVELS.DEBUG) {\n console.debug('[SessionManager]', ...args);\n }\n }\n}\n\nexport const logger = new Logger();\nexport { LOG_LEVELS };\n","import {BootstrapError, ConfigurationError, NetworkError, SSRError} from './errors.js';\nimport {logger} from './logger.js';\nconst CLIENT_PLATFORM = 'web';\n/**\n * SessionManager - Core session token management class\n * Handles session bootstrap, automatic token refresh, and lifecycle management\n */\nclass SessionManager {\n constructor() {\n if (SessionManager.instance) {\n return SessionManager.instance;\n }\n\n this.config = {\n bootstrapUrl: '/session/bootstrap',\n refreshInterval: 25 * 60 * 1000, // 25 minutes in milliseconds\n tokenExpiry: 30 * 60 * 1000, // 30 minutes in milliseconds\n maxRetries: 3,\n tokenCookieName: 'token', // Cookie name to read token from\n };\n\n this.state = {\n isInitialized: false,\n isLoading: false,\n lastRefreshTime: null,\n error: null,\n errorCode: null,\n nextRefreshTime: null,\n tokenExpiry: null,\n cfSessionId: null,\n initializationFailed: false,\n };\n\n this.refreshTimerId = null;\n this.tickTimerId = null;\n this.listeners = new Set();\n this.initializationPromise = null;\n\n SessionManager.instance = this;\n }\n\n /**\n * Configure the session manager\n * @param {Object} config - Configuration options\n * @param {string} config.bootstrapUrl - URL for bootstrap API endpoint\n * @param {number} config.refreshInterval - Interval for token refresh in milliseconds (default: 25 mins)\n * @param {number} config.tokenExpiry - Token expiry time in milliseconds (default: 30 mins)\n * @param {number} config.maxRetries - Maximum retry attempts on failure (default: 3)\n * @param {Object} config.headers - Additional headers for API calls\n * @param {boolean} config.credentials - Include credentials in requests (default: true)\n * @param {string} config.tokenCookieName - Cookie name to read token from (default: 'token')\n */\n configure(config = {}) {\n if (!config.bootstrapUrl || typeof config.bootstrapUrl !== 'string' || !config.bootstrapUrl.trim()) {\n throw new ConfigurationError('bootstrapUrl is required and must be a non-empty string', { bootstrapUrl: config.bootstrapUrl });\n }\n \n if (config.refreshInterval !== undefined) {\n if (typeof config.refreshInterval !== 'number' || !isFinite(config.refreshInterval)) {\n throw new ConfigurationError('refreshInterval must be a finite number', { refreshInterval: config.refreshInterval });\n }\n if (config.refreshInterval <= 0) {\n throw new ConfigurationError('refreshInterval must be positive', { refreshInterval: config.refreshInterval });\n }\n }\n \n if (config.tokenExpiry !== undefined) {\n if (typeof config.tokenExpiry !== 'number' || !isFinite(config.tokenExpiry)) {\n throw new ConfigurationError('tokenExpiry must be a finite number', { tokenExpiry: config.tokenExpiry });\n }\n if (config.tokenExpiry <= 0) {\n throw new ConfigurationError('tokenExpiry must be positive', { tokenExpiry: config.tokenExpiry });\n }\n }\n\n this.config = {\n ...this.config,\n ...config,\n credentials: config.credentials !== undefined ? config.credentials : true,\n logLevel: config.logLevel || 'WARN'\n };\n\n logger.setLevel(this.config.logLevel);\n\n if (this.config.refreshInterval && this.config.tokenExpiry && \n this.config.refreshInterval >= this.config.tokenExpiry) {\n logger.warn('refreshInterval should be less than tokenExpiry to prevent race conditions');\n }\n }\n\n /**\n * Initialize session by calling bootstrap API\n * @param {boolean} skipCookieCheck - Skip session_initialized cookie check (for auto-refresh)\n * @returns {Promise<Object>} Bootstrap response\n */\n async initialize(skipCookieCheck = false) {\n if (typeof window === 'undefined') {\n throw new SSRError('Cannot initialize in non-browser environment (SSR)');\n }\n \n if (this.state.initializationFailed) {\n logger.warn('Initialization previously failed. Reload page to retry.');\n throw new BootstrapError('Initialization failed after max retries. Reload page to retry.');\n }\n \n // Return existing promise if refresh already in progress\n if (this.initializationPromise) {\n logger.debug('Initialization already in progress, waiting...');\n return this.initializationPromise;\n }\n\n // Check if session is already initialized via cookie (only on page load)\n if (!skipCookieCheck) {\n const sessionInitialized = this.getCookie('session_initialized');\n if (sessionInitialized === 'true') {\n logger.info('Session already initialized, skipping bootstrap API call');\n this.state.isInitialized = true;\n this.state.tokenExpiry = this.config.tokenExpiry;\n\n // Restore cf-session-id from cookie\n const storedSessionId = this.getCookie('cf-session-id');\n if (storedSessionId) {\n this.state.cfSessionId = storedSessionId;\n } else {\n this.setCfSessionId(this.generateSessionId());\n }\n\n // Calculate remaining time from stored refresh timestamp\n const refreshAt = parseInt(this.getCookie('session_refresh_at'), 10);\n let remaining;\n if (refreshAt && !isNaN(refreshAt)) {\n remaining = Math.max(0, refreshAt - Date.now());\n } else {\n remaining = this.config.refreshInterval;\n }\n\n this.state.lastRefreshTime = Date.now();\n this.state.nextRefreshTime = Date.now() + remaining;\n\n this.startTokenRefresh(remaining);\n this.notifyListeners();\n return { token: this.getToken(this.config.tokenCookieName) };\n }\n }\n\n this.state.isLoading = true;\n this.state.error = null;\n this.notifyListeners();\n\n this.initializationPromise = (async () => {\n let lastError;\n \n for (let attempt = 1; attempt <= this.config.maxRetries; attempt++) {\n try {\n // Generate new cf-session-id for each bootstrap call\n const newSessionId = this.generateSessionId();\n this.setCfSessionId(newSessionId);\n\n logger.info(`Calling bootstrap API (attempt ${attempt}/${this.config.maxRetries}):`, this.config.bootstrapUrl);\n\n const response = await fetch(this.config.bootstrapUrl, {\n method: 'GET',\n credentials: this.config.credentials ? 'include' : 'same-origin',\n headers: {\n 'Content-Type': 'application/json',\n 'cf-session-id': this.state.cfSessionId,\n 'x-client-platform': CLIENT_PLATFORM,\n ...this.config.headers,\n },\n });\n\n if (!response.ok) {\n throw new BootstrapError(`Bootstrap failed: ${response.status} ${response.statusText}`, {\n status: response.status,\n statusText: response.statusText,\n url: this.config.bootstrapUrl\n });\n }\n\n let data;\n try {\n data = await response.json();\n } catch (jsonError) {\n throw new BootstrapError('Bootstrap response is not valid JSON', { originalError: jsonError.message });\n }\n logger.info('Bootstrap successful');\n\n // Check if server set a cookie\n const setCookieHeader = response.headers.get('set-cookie');\n const serverSetCookie = setCookieHeader && setCookieHeader.includes(this.config.tokenCookieName);\n\n // Use refresh_time from response (in seconds) or fall back to config\n const refreshInterval = data.refresh_time \n ? data.refresh_time * 1000 \n : this.config.refreshInterval;\n\n // Calculate token expiry from response (in seconds) or fall back to config\n const tokenExpiry = data.expire_time\n ? data.expire_time * 1000\n : this.config.tokenExpiry;\n\n if (data.token && !serverSetCookie) {\n logger.debug('Server did not set cookie, setting from SDK');\n this.setCookie(this.config.tokenCookieName, data.token, tokenExpiry / 1000);\n } else if (serverSetCookie) {\n logger.debug('Server set cookie, skipping SDK cookie logic');\n }\n\n // Re-set cf-session-id cookie with actual expire_time from response\n this.setCfSessionId(this.state.cfSessionId, tokenExpiry / 1000);\n\n this.state.isInitialized = true;\n this.state.isLoading = false;\n this.state.lastRefreshTime = Date.now();\n this.state.nextRefreshTime = Date.now() + refreshInterval;\n this.state.tokenExpiry = tokenExpiry;\n this.state.error = null;\n\n // Set session_initialized cookie (expires at refresh interval time)\n this.setCookie('session_initialized', 'true', refreshInterval / 1000);\n\n // Store next refresh timestamp for reload recovery\n this.setCookie('session_refresh_at', String(Date.now() + refreshInterval), refreshInterval / 1000);\n\n // Start automatic refresh timer with dynamic interval\n this.startTokenRefresh(refreshInterval);\n\n this.notifyListeners();\n return data;\n } catch (error) {\n lastError = error instanceof Error ? error : new NetworkError('Unknown error occurred', { error });\n logger.error(`Bootstrap attempt ${attempt} failed:`, lastError.message);\n \n if (attempt < this.config.maxRetries) {\n const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);\n logger.info(`Retrying in ${delay}ms...`);\n await new Promise(resolve => setTimeout(resolve, delay));\n }\n }\n }\n \n logger.error('All bootstrap attempts failed');\n this.state.isLoading = false;\n this.state.error = lastError?.message || 'Unknown error';\n this.state.errorCode = lastError?.code || 'UNKNOWN_ERROR';\n this.state.initializationFailed = true;\n this.notifyListeners();\n throw lastError;\n })();\n\n try {\n return await this.initializationPromise;\n } finally {\n this.initializationPromise = null;\n }\n }\n\n /**\n * Check if token refresh is currently in progress\n * @returns {boolean} True if refresh is in progress\n */\n isRefreshing() {\n return this.initializationPromise !== null;\n }\n\n /**\n * Wait for ongoing refresh to complete\n * @returns {Promise<Object>} Bootstrap response from ongoing refresh\n */\n async waitForRefresh() {\n if (this.initializationPromise) {\n logger.debug('Waiting for ongoing refresh to complete');\n return this.initializationPromise;\n }\n return Promise.resolve();\n }\n\n /**\n * Set cf-session-id in cookie\n * @param {string} sessionId - Session ID\n */\n setCfSessionId(sessionId, maxAge) {\n this.state.cfSessionId = sessionId;\n const cookieMaxAge = maxAge || this.config.tokenExpiry / 1000;\n this.setCookie('cf-session-id', sessionId, cookieMaxAge);\n }\n\n /**\n * Generate unique session ID\n * @returns {string} Generated session ID\n */\n generateSessionId() {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n }\n\n /**\n * Get cf-session-id for requests\n * @returns {string} Current cf-session-id\n */\n getSessionId() {\n return this.state.cfSessionId;\n }\n\n /**\n * Check if token should be added to headers (HTTP or cross-origin)\n * @returns {boolean} True if token should be added to headers\n */\n shouldUseTokenHeader() {\n if (typeof window === 'undefined') return false;\n return window.location.protocol === 'http:';\n }\n\n /**\n * Get cookie value\n * @param {string} name - Cookie name\n * @returns {string|null} Cookie value or null\n */\n getCookie(name) {\n if (typeof document === 'undefined') return null;\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${name}=`);\n if (parts.length === 2) {\n try {\n return decodeURIComponent(parts.pop().split(';').shift());\n } catch (error) {\n logger.error('Failed to decode cookie value:', error instanceof Error ? error.message : String(error));\n return null;\n }\n }\n return null;\n }\n\n /**\n * Get token from cookie\n * @param {string} name - Cookie name (default: configured tokenCookieName)\n * @returns {string|null} Token value or null\n */\n getToken(name = this.config.tokenCookieName) {\n return this.getCookie(name);\n }\n\n /**\n * Set cookie from client-side (WARNING: Not HttpOnly, less secure than server-set cookies)\n * @param {string} name - Cookie name\n * @param {string} value - Cookie value\n * @param {number} maxAge - Max age in seconds\n */\n setCookie(name, value, maxAge) {\n if (typeof document === 'undefined') {\n logger.warn('Cannot set cookie in non-browser environment');\n return;\n }\n \n // Sanitize cookie name to prevent XSS\n const sanitizedName = name.replace(/[^a-zA-Z0-9_-]/g, '');\n if (!sanitizedName) {\n logger.error('Invalid cookie name provided');\n return;\n }\n \n const expires = new Date(Date.now() + maxAge * 1000).toUTCString();\n const isSecure = typeof window !== 'undefined' && window.location.protocol === 'https:';\n const secureFlag = isSecure ? '; Secure' : '';\n const encodedValue = encodeURIComponent(value);\n document.cookie = `${sanitizedName}=${encodedValue}; path=/; expires=${expires}; SameSite=Lax${secureFlag}`;\n logger.debug(`Cookie set: ${sanitizedName}, expires in ${maxAge}s${isSecure ? ' (Secure)' : ''} [WARNING: Not HttpOnly]`);\n }\n\n /**\n * Start automatic token refresh timer\n * @param {number} interval - Refresh interval in milliseconds\n */\n startTokenRefresh(interval = this.config.refreshInterval) {\n this.stopTokenRefresh();\n logger.info(`Scheduling token refresh in ${interval / 1000} seconds`);\n\n this.refreshTimerId = setTimeout(async () => {\n logger.info('Auto-refreshing token...');\n try {\n await this.refreshToken();\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n const errorCode = error?.code || 'UNKNOWN_ERROR';\n logger.error('Auto-refresh failed:', errorMessage);\n this.state.error = errorMessage;\n this.state.errorCode = errorCode;\n this.notifyListeners();\n }\n }, interval);\n\n this.startTickTimer();\n }\n\n /**\n * Stop automatic token refresh timer\n */\n stopTokenRefresh() {\n if (this.refreshTimerId) {\n clearTimeout(this.refreshTimerId);\n this.refreshTimerId = null;\n logger.debug('Token refresh timer stopped');\n }\n this.stopTickTimer();\n }\n\n /**\n * Start periodic tick timer to notify listeners for countdown updates\n */\n startTickTimer() {\n this.stopTickTimer();\n this.tickTimerId = setInterval(() => {\n this.notifyListeners();\n }, 1000);\n }\n\n /**\n * Stop periodic tick timer\n */\n stopTickTimer() {\n if (this.tickTimerId) {\n clearInterval(this.tickTimerId);\n this.tickTimerId = null;\n }\n }\n\n /**\n * Manually refresh the session token\n * @returns {Promise<Object>} Bootstrap response\n */\n async refreshToken() {\n logger.info('Manual token refresh triggered');\n this.state.initializationFailed = false;\n return this.initialize(true);\n }\n\n /**\n * Get current session status\n * @returns {Object} Current session state\n */\n getSessionStatus() {\n return {\n isInitialized: this.state.isInitialized,\n isLoading: this.state.isLoading,\n lastRefreshTime: this.state.lastRefreshTime,\n nextRefreshTime: this.state.nextRefreshTime,\n tokenExpiry: this.state.tokenExpiry,\n error: this.state.error,\n errorCode: this.state.errorCode,\n initializationFailed: this.state.initializationFailed,\n timeUntilRefresh: this.state.nextRefreshTime\n ? Math.max(0, this.state.nextRefreshTime - Date.now())\n : null,\n };\n }\n\n /**\n * Subscribe to session state changes\n * @param {Function} listener - Callback function to be called on state changes\n * @returns {Function} Unsubscribe function\n */\n subscribe(listener) {\n if (typeof listener !== 'function') {\n throw new TypeError('Listener must be a function');\n }\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Notify all listeners of state changes\n */\n notifyListeners() {\n const status = this.getSessionStatus();\n // Use Array.from to create a snapshot, preventing issues if listeners modify the Set\n Array.from(this.listeners).forEach(listener => {\n try {\n listener(status);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n logger.error('Listener error:', errorMessage);\n }\n });\n }\n\n /**\n * Clean up and reset the session manager\n */\n destroy() {\n this.stopTokenRefresh();\n this.listeners.clear();\n this.state = {\n isInitialized: false,\n isLoading: false,\n lastRefreshTime: null,\n error: null,\n errorCode: null,\n nextRefreshTime: null,\n tokenExpiry: null,\n cfSessionId: null,\n initializationFailed: false,\n };\n // Clear cookies\n this.setCookie('cf-session-id', '', -1);\n this.setCookie('session_initialized', '', -1);\n this.setCookie('session_refresh_at', '', -1);\n logger.info('Destroyed');\n }\n\n /**\n * Get the singleton instance\n * @returns {SessionManager} Singleton instance\n */\n static getInstance() {\n if (!SessionManager.instance) {\n SessionManager.instance = new SessionManager();\n }\n return SessionManager.instance;\n }\n}\n\nexport default SessionManager;\n","import SessionManager from './SessionManager.js';\nexport const CLIENT_PLATFORM = 'web';\n\n/**\n * Fetch interceptor with automatic token refresh on 401\n * @param {string} url - Request URL\n * @param {Object} options - Fetch options\n * @returns {Promise<Response>} Fetch response\n */\nexport async function fetchInterceptor(url, options = {}) {\n const sessionManager = SessionManager.getInstance();\n \n // Ensure credentials are included to send cookies\n options.credentials = options.credentials || 'include';\n \n // Add headers\n options.headers = {\n ...options.headers,\n 'cf-session-id': sessionManager.getSessionId(),\n 'x-client-platform': CLIENT_PLATFORM,\n };\n \n // Add token to header if HTTP (not HTTPS)\n if (sessionManager.shouldUseTokenHeader()) {\n const token = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (token) {\n options.headers[sessionManager.config.tokenCookieName] = token;\n }\n }\n \n let response = await fetch(url, options);\n \n // Retry once if unauthorized with INVALID_SESSION\n if (response.status === 401) {\n const clonedResponse = response.clone();\n try {\n const data = await clonedResponse.json();\n if (data.error_msg === 'INVALID_SESSION') {\n if (sessionManager.isRefreshing()) {\n await sessionManager.waitForRefresh();\n } else {\n const existingToken = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (!existingToken) {\n await sessionManager.refreshToken();\n }\n }\n options.headers['cf-session-id'] = sessionManager.getSessionId();\n options.headers['x-client-platform'] = CLIENT_PLATFORM;\n if (sessionManager.shouldUseTokenHeader()) {\n const newToken = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (newToken) {\n options.headers[sessionManager.config.tokenCookieName] = newToken;\n }\n }\n response = await fetch(url, options);\n }\n } catch (e) {\n // Not JSON or parsing failed, return original response\n }\n }\n \n return response;\n}\n\n/**\n * Axios interceptor with automatic token refresh on 401 and INVALID_SESSION\n * @param {Object} axiosInstance - Axios instance\n */\nexport function setupAxiosInterceptor(axiosInstance) {\n const sessionManager = SessionManager.getInstance();\n \n // Request interceptor to add cf-session-id and token\n axiosInstance.interceptors.request.use(\n (config) => {\n config.headers['cf-session-id'] = sessionManager.getSessionId();\n config.headers['x-client-platform'] = CLIENT_PLATFORM;\n if (sessionManager.shouldUseTokenHeader()) {\n const token = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (token) {\n config.headers[sessionManager.config.tokenCookieName] = token;\n }\n }\n return config;\n },\n (error) => Promise.reject(error)\n );\n \n // Response interceptor for token refresh\n axiosInstance.interceptors.response.use(\n (response) => response,\n async (error) => {\n const originalRequest = error.config;\n \n // Retry once if unauthorized with INVALID_SESSION\n if (error.response?.status === 401 && !originalRequest._retry) {\n if (error.response?.data?.error_msg === 'INVALID_SESSION') {\n originalRequest._retry = true;\n \n if (sessionManager.isRefreshing()) {\n await sessionManager.waitForRefresh();\n } else {\n const existingToken = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (!existingToken) {\n await sessionManager.refreshToken();\n }\n }\n \n originalRequest.headers['cf-session-id'] = sessionManager.getSessionId();\n originalRequest.headers['x-client-platform'] = CLIENT_PLATFORM;\n if (sessionManager.shouldUseTokenHeader()) {\n const newToken = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (newToken) {\n originalRequest.headers[sessionManager.config.tokenCookieName] = newToken;\n }\n }\n return axiosInstance(originalRequest);\n }\n }\n \n return Promise.reject(error);\n }\n );\n \n return axiosInstance;\n}\n","import { useState, useEffect, useCallback } from 'react';\nimport SessionManager from '../SessionManager.js';\nimport { logger } from '../logger.js';\n\n/**\n * React hook for session management\n * Provides session state and controls for React components\n * \n * @param {Object} options - Configuration options\n * @param {boolean} options.autoInitialize - Automatically initialize on mount (default: true)\n * @returns {Object} Session state and controls\n */\nexport function useSession(options = {}) {\n const { autoInitialize = true } = options;\n\n const sessionManager = SessionManager.getInstance();\n\n const [sessionState, setSessionState] = useState(() =>\n sessionManager.getSessionStatus()\n );\n\n // Subscribe to session state changes\n useEffect(() => {\n const unsubscribe = sessionManager.subscribe((newState) => {\n setSessionState(newState);\n });\n\n return unsubscribe;\n }, []); // sessionManager is a singleton, no need to include in deps\n\n // Auto-initialize if enabled\n useEffect(() => {\n if (autoInitialize && !sessionState.isInitialized && !sessionState.isLoading && !sessionState.initializationFailed) {\n sessionManager.initialize().catch(error => {\n logger.error('Auto-initialization failed:', error);\n });\n }\n }, [autoInitialize, sessionState.isInitialized, sessionState.isLoading, sessionState.initializationFailed]);\n\n // Manual refresh function\n const refresh = useCallback(async () => {\n try {\n await sessionManager.refreshToken();\n } catch (error) {\n logger.error('Manual refresh failed:', error);\n throw error;\n }\n }, []);\n\n // Initialize function (for manual control)\n const initialize = useCallback(async () => {\n try {\n await sessionManager.initialize();\n } catch (error) {\n logger.error('Manual initialization failed:', error);\n throw error;\n }\n }, []);\n\n return {\n // State\n isInitialized: sessionState.isInitialized,\n isLoading: sessionState.isLoading,\n error: sessionState.error,\n lastRefreshTime: sessionState.lastRefreshTime,\n nextRefreshTime: sessionState.nextRefreshTime,\n timeUntilRefresh: sessionState.timeUntilRefresh,\n initializationFailed: sessionState.initializationFailed,\n\n // Actions\n refresh,\n initialize,\n };\n}\n\nexport default useSession;\n"],"names":["SessionError","Error","constructor","message","code","details","super","this","name","captureStackTrace","Object","setPrototypeOf","prototype","ConfigurationError","BootstrapError","NetworkError","SSRError","LOG_LEVELS","NONE","ERROR","WARN","INFO","DEBUG","logger","level","setLevel","toUpperCase","error","args","console","warn","info","debug","SessionManager","instance","config","bootstrapUrl","refreshInterval","tokenExpiry","maxRetries","tokenCookieName","state","isInitialized","isLoading","lastRefreshTime","errorCode","nextRefreshTime","cfSessionId","initializationFailed","refreshTimerId","tickTimerId","listeners","Set","initializationPromise","configure","trim","undefined","isFinite","credentials","logLevel","initialize","skipCookieCheck","window","getCookie","storedSessionId","setCfSessionId","generateSessionId","refreshAt","parseInt","remaining","isNaN","Math","max","Date","now","startTokenRefresh","notifyListeners","token","getToken","lastError","attempt","newSessionId","response","fetch","method","headers","ok","status","statusText","url","data","json","jsonError","originalError","setCookieHeader","get","serverSetCookie","includes","refresh_time","expire_time","setCookie","String","delay","min","pow","Promise","resolve","setTimeout","isRefreshing","waitForRefresh","sessionId","maxAge","cookieMaxAge","crypto","randomUUID","random","toString","substr","getSessionId","shouldUseTokenHeader","location","protocol","document","parts","cookie","split","length","decodeURIComponent","pop","shift","value","sanitizedName","replace","expires","toUTCString","isSecure","secureFlag","encodedValue","encodeURIComponent","interval","stopTokenRefresh","async","refreshToken","errorMessage","startTickTimer","clearTimeout","stopTickTimer","setInterval","clearInterval","getSessionStatus","timeUntilRefresh","subscribe","listener","TypeError","add","delete","Array","from","forEach","destroy","clear","getInstance","CLIENT_PLATFORM","options","sessionManager","clonedResponse","clone","error_msg","newToken","e","axiosInstance","interceptors","request","use","reject","originalRequest","_retry","autoInitialize","sessionState","setSessionState","useState","useEffect","newState","catch","refresh","useCallback"],"mappings":"2FAIO,MAAMA,UAAqBC,MAChC,WAAAC,CAAYC,EAASC,EAAMC,EAAU,CAAA,GACnCC,MAAMH,GACNI,KAAKC,KAAO,eACZD,KAAKH,KAAOA,EACZG,KAAKF,QAAUA,EAGXJ,MAAMQ,mBACRR,MAAMQ,kBAAkBF,KAAMA,KAAKL,aAIrCQ,OAAOC,eAAeJ,KAAMP,EAAaY,UAC3C,EAGK,MAAMC,UAA2Bb,EACtC,WAAAE,CAAYC,EAASE,GACnBC,MAAMH,EAAS,sBAAuBE,GACtCE,KAAKC,KAAO,oBACd,EAGK,MAAMM,UAAuBd,EAClC,WAAAE,CAAYC,EAASE,GACnBC,MAAMH,EAAS,kBAAmBE,GAClCE,KAAKC,KAAO,gBACd,EAGK,MAAMO,UAAqBf,EAChC,WAAAE,CAAYC,EAASE,GACnBC,MAAMH,EAAS,gBAAiBE,GAChCE,KAAKC,KAAO,cACd,EAGK,MAAMQ,UAAiBhB,EAC5B,WAAAE,CAAYC,EAASE,EAAU,IAC7BC,MAAMH,EAAS,YAAaE,GAC5BE,KAAKC,KAAO,UACd,EC1CG,MAACS,EAAa,CACjBC,KAAM,EACNC,MAAO,EACPC,KAAM,EACNC,KAAM,EACNC,MAAO,GAyCG,MAACC,EAAS,IAtCtB,MACE,WAAArB,GACEK,KAAKiB,MAAQP,EAAWG,IAC1B,CAEA,QAAAK,CAASD,GAELjB,KAAKiB,MADc,iBAAVA,EACIP,EAAWO,EAAME,gBAAkBT,EAAWG,KAE9CI,CAEjB,CAEA,KAAAG,IAASC,GACHrB,KAAKiB,OAASP,EAAWE,OAC3BU,QAAQF,MAAM,sBAAuBC,EAEzC,CAEA,IAAAE,IAAQF,GACFrB,KAAKiB,OAASP,EAAWG,MAC3BS,QAAQC,KAAK,sBAAuBF,EAExC,CAEA,IAAAG,IAAQH,GACFrB,KAAKiB,OAASP,EAAWI,MAC3BQ,QAAQE,KAAK,sBAAuBH,EAExC,CAEA,KAAAI,IAASJ,GACHrB,KAAKiB,OAASP,EAAWK,OAC3BO,QAAQG,MAAM,sBAAuBJ,EAEzC,GCxCF,MAAMK,EACJ,WAAA/B,GACE,GAAI+B,EAAeC,SACjB,OAAOD,EAAeC,SAGxB3B,KAAK4B,OAAS,CACZC,aAAc,qBACdC,gBAAiB,KACjBC,YAAa,KACbC,WAAY,EACZC,gBAAiB,SAGnBjC,KAAKkC,MAAQ,CACXC,eAAe,EACfC,WAAW,EACXC,gBAAiB,KACjBjB,MAAO,KACPkB,UAAW,KACXC,gBAAiB,KACjBR,YAAa,KACbS,YAAa,KACbC,sBAAsB,GAGxBzC,KAAK0C,eAAiB,KACtB1C,KAAK2C,YAAc,KACnB3C,KAAK4C,UAAY,IAAIC,IACrB7C,KAAK8C,sBAAwB,KAE7BpB,EAAeC,SAAW3B,IAC5B,CAaA,SAAA+C,CAAUnB,EAAS,IACjB,IAAKA,EAAOC,cAA+C,iBAAxBD,EAAOC,eAA8BD,EAAOC,aAAamB,OAC1F,MAAM,IAAI1C,EAAmB,0DAA2D,CAAEuB,aAAcD,EAAOC,eAGjH,QAA+BoB,IAA3BrB,EAAOE,gBAA+B,CACxC,GAAsC,iBAA3BF,EAAOE,kBAAiCoB,SAAStB,EAAOE,iBACjE,MAAM,IAAIxB,EAAmB,0CAA2C,CAAEwB,gBAAiBF,EAAOE,kBAEpG,GAAIF,EAAOE,iBAAmB,EAC5B,MAAM,IAAIxB,EAAmB,mCAAoC,CAAEwB,gBAAiBF,EAAOE,iBAE/F,CAEA,QAA2BmB,IAAvBrB,EAAOG,YAA2B,CACpC,GAAkC,iBAAvBH,EAAOG,cAA6BmB,SAAStB,EAAOG,aAC7D,MAAM,IAAIzB,EAAmB,sCAAuC,CAAEyB,YAAaH,EAAOG,cAE5F,GAAIH,EAAOG,aAAe,EACxB,MAAM,IAAIzB,EAAmB,+BAAgC,CAAEyB,YAAaH,EAAOG,aAEvF,CAEA/B,KAAK4B,OAAS,IACT5B,KAAK4B,UACLA,EACHuB,iBAAoCF,IAAvBrB,EAAOuB,aAA4BvB,EAAOuB,YACvDC,SAAUxB,EAAOwB,UAAY,QAG/BpC,EAAOE,SAASlB,KAAK4B,OAAOwB,UAExBpD,KAAK4B,OAAOE,iBAAmB9B,KAAK4B,OAAOG,aAC3C/B,KAAK4B,OAAOE,iBAAmB9B,KAAK4B,OAAOG,aAC7Cf,EAAOO,KAAK,6EAEhB,CAOA,gBAAM8B,CAAWC,GAAkB,GACjC,GAAsB,oBAAXC,OACT,MAAM,IAAI9C,EAAS,sDAGrB,GAAIT,KAAKkC,MAAMO,qBAEb,MADAzB,EAAOO,KAAK,2DACN,IAAIhB,EAAe,kEAI3B,GAAIP,KAAK8C,sBAEP,OADA9B,EAAOS,MAAM,kDACNzB,KAAK8C,sBAId,IAAKQ,EAAiB,CAEpB,GAA2B,SADAtD,KAAKwD,UAAU,uBACP,CACjCxC,EAAOQ,KAAK,4DACZxB,KAAKkC,MAAMC,eAAgB,EAC3BnC,KAAKkC,MAAMH,YAAc/B,KAAK4B,OAAOG,YAGrC,MAAM0B,EAAkBzD,KAAKwD,UAAU,iBACnCC,EACFzD,KAAKkC,MAAMM,YAAciB,EAEzBzD,KAAK0D,eAAe1D,KAAK2D,qBAI3B,MAAMC,EAAYC,SAAS7D,KAAKwD,UAAU,sBAAuB,IACjE,IAAIM,EAYJ,OAVEA,EADEF,IAAcG,MAAMH,GACVI,KAAKC,IAAI,EAAGL,EAAYM,KAAKC,OAE7BnE,KAAK4B,OAAOE,gBAG1B9B,KAAKkC,MAAMG,gBAAkB6B,KAAKC,MAClCnE,KAAKkC,MAAMK,gBAAkB2B,KAAKC,MAAQL,EAE1C9D,KAAKoE,kBAAkBN,GACvB9D,KAAKqE,kBACE,CAAEC,MAAOtE,KAAKuE,SAASvE,KAAK4B,OAAOK,iBAC5C,CACF,CAEAjC,KAAKkC,MAAME,WAAY,EACvBpC,KAAKkC,MAAMd,MAAQ,KACnBpB,KAAKqE,kBAELrE,KAAK8C,sBAAwB,WAC3B,IAAI0B,EAEJ,IAAK,IAAIC,EAAU,EAAGA,GAAWzE,KAAK4B,OAAOI,WAAYyC,IACvD,IAEE,MAAMC,EAAe1E,KAAK2D,oBAC1B3D,KAAK0D,eAAegB,GAEpB1D,EAAOQ,KAAK,kCAAkCiD,KAAWzE,KAAK4B,OAAOI,eAAgBhC,KAAK4B,OAAOC,cAEjG,MAAM8C,QAAiBC,MAAM5E,KAAK4B,OAAOC,aAAc,CACrDgD,OAAQ,MACR1B,YAAanD,KAAK4B,OAAOuB,YAAc,UAAY,cACnD2B,QAAS,CACP,eAAgB,mBAChB,gBAAiB9E,KAAKkC,MAAMM,YAC5B,oBApKU,SAqKPxC,KAAK4B,OAAOkD,WAInB,IAAKH,EAASI,GACZ,MAAM,IAAIxE,EAAe,qBAAqBoE,EAASK,UAAUL,EAASM,aAAc,CACtFD,OAAQL,EAASK,OACjBC,WAAYN,EAASM,WACrBC,IAAKlF,KAAK4B,OAAOC,eAIrB,IAAIsD,EACJ,IACEA,QAAaR,EAASS,MACxB,CAAE,MAAOC,GACP,MAAM,IAAI9E,EAAe,uCAAwC,CAAE+E,cAAeD,EAAUzF,SAC9F,CACAoB,EAAOQ,KAAK,wBAGZ,MAAM+D,EAAkBZ,EAASG,QAAQU,IAAI,cACvCC,EAAkBF,GAAmBA,EAAgBG,SAAS1F,KAAK4B,OAAOK,iBAG1EH,EAAkBqD,EAAKQ,aACL,IAApBR,EAAKQ,aACL3F,KAAK4B,OAAOE,gBAGVC,EAAcoD,EAAKS,YACF,IAAnBT,EAAKS,YACL5F,KAAK4B,OAAOG,YA6BhB,OA3BIoD,EAAKb,QAAUmB,GACjBzE,EAAOS,MAAM,+CACbzB,KAAK6F,UAAU7F,KAAK4B,OAAOK,gBAAiBkD,EAAKb,MAAOvC,EAAc,MAC7D0D,GACTzE,EAAOS,MAAM,gDAIfzB,KAAK0D,eAAe1D,KAAKkC,MAAMM,YAAaT,EAAc,KAE1D/B,KAAKkC,MAAMC,eAAgB,EAC3BnC,KAAKkC,MAAME,WAAY,EACvBpC,KAAKkC,MAAMG,gBAAkB6B,KAAKC,MAClCnE,KAAKkC,MAAMK,gBAAkB2B,KAAKC,MAAQrC,EAC1C9B,KAAKkC,MAAMH,YAAcA,EACzB/B,KAAKkC,MAAMd,MAAQ,KAGnBpB,KAAK6F,UAAU,sBAAuB,OAAQ/D,EAAkB,KAGhE9B,KAAK6F,UAAU,qBAAsBC,OAAO5B,KAAKC,MAAQrC,GAAkBA,EAAkB,KAG7F9B,KAAKoE,kBAAkBtC,GAEvB9B,KAAKqE,kBACEc,CACT,CAAE,MAAO/D,GAIP,GAHAoD,EAAYpD,aAAiB1B,MAAQ0B,EAAQ,IAAIZ,EAAa,yBAA0B,CAAEY,UAC1FJ,EAAOI,MAAM,qBAAqBqD,YAAmBD,EAAU5E,SAE3D6E,EAAUzE,KAAK4B,OAAOI,WAAY,CACpC,MAAM+D,EAAQ/B,KAAKgC,IAAI,IAAOhC,KAAKiC,IAAI,EAAGxB,EAAU,GAAI,KACxDzD,EAAOQ,KAAK,eAAeuE,gBACrB,IAAIG,QAAQC,GAAWC,WAAWD,EAASJ,GACnD,CACF,CASF,MANA/E,EAAOI,MAAM,iCACbpB,KAAKkC,MAAME,WAAY,EACvBpC,KAAKkC,MAAMd,MAAQoD,GAAW5E,SAAW,gBACzCI,KAAKkC,MAAMI,UAAYkC,GAAW3E,MAAQ,gBAC1CG,KAAKkC,MAAMO,sBAAuB,EAClCzC,KAAKqE,kBACCG,CACP,EAnG4B,GAqG7B,IACE,aAAaxE,KAAK8C,qBACpB,CAAC,QACC9C,KAAK8C,sBAAwB,IAC/B,CACF,CAMA,YAAAuD,GACE,OAAsC,OAA/BrG,KAAK8C,qBACd,CAMA,oBAAMwD,GACJ,OAAItG,KAAK8C,uBACP9B,EAAOS,MAAM,2CACNzB,KAAK8C,uBAEPoD,QAAQC,SACjB,CAMA,cAAAzC,CAAe6C,EAAWC,GACxBxG,KAAKkC,MAAMM,YAAc+D,EACzB,MAAME,EAAeD,GAAUxG,KAAK4B,OAAOG,YAAc,IACzD/B,KAAK6F,UAAU,gBAAiBU,EAAWE,EAC7C,CAMA,iBAAA9C,GACE,MAAsB,oBAAX+C,QAA0BA,OAAOC,WACnCD,OAAOC,aAET,GAAGzC,KAAKC,SAASH,KAAK4C,SAASC,SAAS,IAAIC,OAAO,EAAG,IAC/D,CAMA,YAAAC,GACE,OAAO/G,KAAKkC,MAAMM,WACpB,CAMA,oBAAAwE,GACE,MAAsB,oBAAXzD,QACyB,UAA7BA,OAAO0D,SAASC,QACzB,CAOA,SAAA1D,CAAUvD,GACR,GAAwB,oBAAbkH,SAA0B,OAAO,KAC5C,MACMC,EADQ,KAAKD,SAASE,SACRC,MAAM,KAAKrH,MAC/B,GAAqB,IAAjBmH,EAAMG,OACR,IACE,OAAOC,mBAAmBJ,EAAMK,MAAMH,MAAM,KAAKI,QACnD,CAAE,MAAOtG,GAEP,OADAJ,EAAOI,MAAM,iCAAkCA,aAAiB1B,MAAQ0B,EAAMxB,QAAUkG,OAAO1E,IACxF,IACT,CAEF,OAAO,IACT,CAOA,QAAAmD,CAAStE,EAAOD,KAAK4B,OAAOK,iBAC1B,OAAOjC,KAAKwD,UAAUvD,EACxB,CAQA,SAAA4F,CAAU5F,EAAM0H,EAAOnB,GACrB,GAAwB,oBAAbW,SAET,YADAnG,EAAOO,KAAK,gDAKd,MAAMqG,EAAgB3H,EAAK4H,QAAQ,kBAAmB,IACtD,IAAKD,EAEH,YADA5G,EAAOI,MAAM,gCAIf,MAAM0G,EAAU,IAAI5D,KAAKA,KAAKC,MAAiB,IAATqC,GAAeuB,cAC/CC,EAA6B,oBAAXzE,QAAuD,WAA7BA,OAAO0D,SAASC,SAC5De,EAAaD,EAAW,WAAa,GACrCE,EAAeC,mBAAmBR,GACxCR,SAASE,OAAS,GAAGO,KAAiBM,sBAAiCJ,kBAAwBG,IAC/FjH,EAAOS,MAAM,eAAemG,iBAA6BpB,KAAUwB,EAAW,YAAc,6BAC9F,CAMA,iBAAA5D,CAAkBgE,EAAWpI,KAAK4B,OAAOE,iBACvC9B,KAAKqI,mBACLrH,EAAOQ,KAAK,+BAA+B4G,EAAW,eAEtDpI,KAAK0C,eAAiB0D,WAAWkC,UAC/BtH,EAAOQ,KAAK,4BACZ,UACQxB,KAAKuI,cACb,CAAE,MAAOnH,GACP,MAAMoH,EAAepH,aAAiB1B,MAAQ0B,EAAMxB,QAAUkG,OAAO1E,GAC/DkB,EAAYlB,GAAOvB,MAAQ,gBACjCmB,EAAOI,MAAM,uBAAwBoH,GACrCxI,KAAKkC,MAAMd,MAAQoH,EACnBxI,KAAKkC,MAAMI,UAAYA,EACvBtC,KAAKqE,iBACP,GACC+D,GAEHpI,KAAKyI,gBACP,CAKA,gBAAAJ,GACMrI,KAAK0C,iBACPgG,aAAa1I,KAAK0C,gBAClB1C,KAAK0C,eAAiB,KACtB1B,EAAOS,MAAM,gCAEfzB,KAAK2I,eACP,CAKA,cAAAF,GACEzI,KAAK2I,gBACL3I,KAAK2C,YAAciG,YAAY,KAC7B5I,KAAKqE,mBACJ,IACL,CAKA,aAAAsE,GACM3I,KAAK2C,cACPkG,cAAc7I,KAAK2C,aACnB3C,KAAK2C,YAAc,KAEvB,CAMA,kBAAM4F,GAGJ,OAFAvH,EAAOQ,KAAK,kCACZxB,KAAKkC,MAAMO,sBAAuB,EAC3BzC,KAAKqD,YAAW,EACzB,CAMA,gBAAAyF,GACE,MAAO,CACL3G,cAAenC,KAAKkC,MAAMC,cAC1BC,UAAWpC,KAAKkC,MAAME,UACtBC,gBAAiBrC,KAAKkC,MAAMG,gBAC5BE,gBAAiBvC,KAAKkC,MAAMK,gBAC5BR,YAAa/B,KAAKkC,MAAMH,YACxBX,MAAOpB,KAAKkC,MAAMd,MAClBkB,UAAWtC,KAAKkC,MAAMI,UACtBG,qBAAsBzC,KAAKkC,MAAMO,qBACjCsG,iBAAkB/I,KAAKkC,MAAMK,gBACzByB,KAAKC,IAAI,EAAGjE,KAAKkC,MAAMK,gBAAkB2B,KAAKC,OAC9C,KAER,CAOA,SAAA6E,CAAUC,GACR,GAAwB,mBAAbA,EACT,MAAM,IAAIC,UAAU,+BAGtB,OADAlJ,KAAK4C,UAAUuG,IAAIF,GACZ,KACLjJ,KAAK4C,UAAUwG,OAAOH,GAE1B,CAKA,eAAA5E,GACE,MAAMW,EAAShF,KAAK8I,mBAEpBO,MAAMC,KAAKtJ,KAAK4C,WAAW2G,QAAQN,IACjC,IACEA,EAASjE,EACX,CAAE,MAAO5D,GACP,MAAMoH,EAAepH,aAAiB1B,MAAQ0B,EAAMxB,QAAUkG,OAAO1E,GACrEJ,EAAOI,MAAM,kBAAmBoH,EAClC,GAEJ,CAKA,OAAAgB,GACExJ,KAAKqI,mBACLrI,KAAK4C,UAAU6G,QACfzJ,KAAKkC,MAAQ,CACXC,eAAe,EACfC,WAAW,EACXC,gBAAiB,KACjBjB,MAAO,KACPkB,UAAW,KACXC,gBAAiB,KACjBR,YAAa,KACbS,YAAa,KACbC,sBAAsB,GAGxBzC,KAAK6F,UAAU,gBAAiB,IAAI,GACpC7F,KAAK6F,UAAU,sBAAuB,IAAI,GAC1C7F,KAAK6F,UAAU,qBAAsB,IAAI,GACzC7E,EAAOQ,KAAK,YACd,CAMA,kBAAOkI,GAIL,OAHKhI,EAAeC,WAClBD,EAAeC,SAAW,IAAID,GAEzBA,EAAeC,QACxB,ECzgBU,MAACgI,EAAkB,gPAQxBrB,eAAgCpD,EAAK0E,EAAU,IACpD,MAAMC,EAAiBnI,EAAegI,cAatC,GAVAE,EAAQzG,YAAcyG,EAAQzG,aAAe,UAG7CyG,EAAQ9E,QAAU,IACb8E,EAAQ9E,QACX,gBAAiB+E,EAAe9C,eAChC,oBAAqB4C,GAInBE,EAAe7C,uBAAwB,CACzC,MAAM1C,EAAQuF,EAAetF,SAASsF,EAAejI,OAAOK,iBACxDqC,IACFsF,EAAQ9E,QAAQ+E,EAAejI,OAAOK,iBAAmBqC,EAE7D,CAEA,IAAIK,QAAiBC,MAAMM,EAAK0E,GAGhC,GAAwB,MAApBjF,EAASK,OAAgB,CAC3B,MAAM8E,EAAiBnF,EAASoF,QAChC,IAEE,GAAuB,2BADJD,EAAe1E,QACzB4E,UAAiC,CACxC,GAAIH,EAAexD,qBACXwD,EAAevD,qBAChB,CACiBuD,EAAetF,SAASsF,EAAejI,OAAOK,wBAE5D4H,EAAetB,cAEzB,CAGA,GAFAqB,EAAQ9E,QAAQ,iBAAmB+E,EAAe9C,eAClD6C,EAAQ9E,QAAQ,qBAAuB6E,EACnCE,EAAe7C,uBAAwB,CACzC,MAAMiD,EAAWJ,EAAetF,SAASsF,EAAejI,OAAOK,iBAC3DgI,IACFL,EAAQ9E,QAAQ+E,EAAejI,OAAOK,iBAAmBgI,EAE7D,CACAtF,QAAiBC,MAAMM,EAAK0E,EAC9B,CACF,CAAE,MAAOM,GAET,CACF,CAEA,OAAOvF,CACT,iDAMO,SAA+BwF,GACpC,MAAMN,EAAiBnI,EAAegI,cAsDtC,OAnDAS,EAAcC,aAAaC,QAAQC,IAChC1I,IAGC,GAFAA,EAAOkD,QAAQ,iBAAmB+E,EAAe9C,eACjDnF,EAAOkD,QAAQ,qBAAuB6E,EAClCE,EAAe7C,uBAAwB,CACzC,MAAM1C,EAAQuF,EAAetF,SAASsF,EAAejI,OAAOK,iBACxDqC,IACF1C,EAAOkD,QAAQ+E,EAAejI,OAAOK,iBAAmBqC,EAE5D,CACA,OAAO1C,GAERR,GAAU8E,QAAQqE,OAAOnJ,IAI5B+I,EAAcC,aAAazF,SAAS2F,IACjC3F,GAAaA,EACd2D,MAAOlH,IACL,MAAMoJ,EAAkBpJ,EAAMQ,OAG9B,GAA+B,MAA3BR,EAAMuD,UAAUK,SAAmBwF,EAAgBC,QACb,oBAApCrJ,EAAMuD,UAAUQ,MAAM6E,UAAiC,CAGzD,GAFAQ,EAAgBC,QAAS,EAErBZ,EAAexD,qBACXwD,EAAevD,qBAChB,CACiBuD,EAAetF,SAASsF,EAAejI,OAAOK,wBAE5D4H,EAAetB,cAEzB,CAIA,GAFAiC,EAAgB1F,QAAQ,iBAAmB+E,EAAe9C,eAC1DyD,EAAgB1F,QAAQ,qBAAuB6E,EAC3CE,EAAe7C,uBAAwB,CACzC,MAAMiD,EAAWJ,EAAetF,SAASsF,EAAejI,OAAOK,iBAC3DgI,IACFO,EAAgB1F,QAAQ+E,EAAejI,OAAOK,iBAAmBgI,EAErE,CACA,OAAOE,EAAcK,EACvB,CAGF,OAAOtE,QAAQqE,OAAOnJ,KAInB+I,CACT,qBChHO,SAAoBP,EAAU,IACjC,MAAMc,eAAEA,GAAiB,GAASd,EAE5BC,EAAiBnI,EAAegI,eAE/BiB,EAAcC,GAAmBC,WAAS,IAC7ChB,EAAef,oBAInBgC,EAAAA,UAAU,IACcjB,EAAeb,UAAW+B,IAC1CH,EAAgBG,KAIrB,IAGHD,EAAAA,UAAU,MACFJ,GAAmBC,EAAaxI,eAAkBwI,EAAavI,WAAcuI,EAAalI,sBAC1FoH,EAAexG,aAAa2H,MAAM5J,IAC9BJ,EAAOI,MAAM,8BAA+BA,MAGrD,CAACsJ,EAAgBC,EAAaxI,cAAewI,EAAavI,UAAWuI,EAAalI,uBAGrF,MAAMwI,EAAUC,EAAAA,YAAY5C,UACxB,UACUuB,EAAetB,cACzB,CAAE,MAAOnH,GAEL,MADAJ,EAAOI,MAAM,yBAA0BA,GACjCA,CACV,GACD,IAGGiC,EAAa6H,EAAAA,YAAY5C,UAC3B,UACUuB,EAAexG,YACzB,CAAE,MAAOjC,GAEL,MADAJ,EAAOI,MAAM,gCAAiCA,GACxCA,CACV,GACD,IAEH,MAAO,CAEHe,cAAewI,EAAaxI,cAC5BC,UAAWuI,EAAavI,UACxBhB,MAAOuJ,EAAavJ,MACpBiB,gBAAiBsI,EAAatI,gBAC9BE,gBAAiBoI,EAAapI,gBAC9BwG,iBAAkB4B,EAAa5B,iBAC/BtG,qBAAsBkI,EAAalI,qBAGnCwI,UACA5H,aAER"}
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":["../src/errors.js","../src/logger.js","../src/SessionManager.js","../src/interceptors.js","../src/hooks/useSession.js"],"sourcesContent":["/**\n * Custom error classes for SessionManager\n */\n\nexport class SessionError extends Error {\n constructor(message, code, details = {}) {\n super(message);\n this.name = 'SessionError';\n this.code = code;\n this.details = details;\n \n // Maintains proper stack trace for where error was thrown\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor);\n }\n \n // Set the prototype explicitly for proper instanceof checks\n Object.setPrototypeOf(this, SessionError.prototype);\n }\n}\n\nexport class ConfigurationError extends SessionError {\n constructor(message, details) {\n super(message, 'CONFIGURATION_ERROR', details);\n this.name = 'ConfigurationError';\n }\n}\n\nexport class BootstrapError extends SessionError {\n constructor(message, details) {\n super(message, 'BOOTSTRAP_ERROR', details);\n this.name = 'BootstrapError';\n }\n}\n\nexport class NetworkError extends SessionError {\n constructor(message, details) {\n super(message, 'NETWORK_ERROR', details);\n this.name = 'NetworkError';\n }\n}\n\nexport class SSRError extends SessionError {\n constructor(message, details = {}) {\n super(message, 'SSR_ERROR', details);\n this.name = 'SSRError';\n }\n}\n","/**\n * Logger utility with configurable log levels\n */\n\nconst LOG_LEVELS = {\n NONE: 0,\n ERROR: 1,\n WARN: 2,\n INFO: 3,\n DEBUG: 4\n};\n\nclass Logger {\n constructor() {\n this.level = LOG_LEVELS.WARN; // Default to WARN in production\n }\n\n setLevel(level) {\n if (typeof level === 'string') {\n this.level = LOG_LEVELS[level.toUpperCase()] ?? LOG_LEVELS.WARN;\n } else {\n this.level = level;\n }\n }\n\n error(...args) {\n if (this.level >= LOG_LEVELS.ERROR) {\n console.error('[SessionManager]', ...args);\n }\n }\n\n warn(...args) {\n if (this.level >= LOG_LEVELS.WARN) {\n console.warn('[SessionManager]', ...args);\n }\n }\n\n info(...args) {\n if (this.level >= LOG_LEVELS.INFO) {\n console.info('[SessionManager]', ...args);\n }\n }\n\n debug(...args) {\n if (this.level >= LOG_LEVELS.DEBUG) {\n console.debug('[SessionManager]', ...args);\n }\n }\n}\n\nexport const logger = new Logger();\nexport { LOG_LEVELS };\n","import {BootstrapError, ConfigurationError, NetworkError, SSRError} from './errors.js';\nimport {logger} from './logger.js';\nconst CLIENT_PLATFORM = 'web';\n\nconst DEFAULT_CONFIG = {\n bootstrapUrl: '/session/bootstrap',\n refreshInterval: 25 * 60 * 1000, // 25 minutes in milliseconds\n tokenExpiry: 30 * 60 * 1000, // 30 minutes in milliseconds\n maxRetries: 3,\n bootstrapTimeout: 30000, // 30 seconds timeout for bootstrap fetch\n tokenCookieName: 'token', // Must match server's session_header\n invalidSessionError: 'INVALID-GW-SESSION', // Must match server's error response\n};\n\n/**\n * SessionManager - Core session token management class\n * Handles session bootstrap, automatic token refresh, and lifecycle management\n */\nclass SessionManager {\n constructor() {\n if (SessionManager.instance) {\n return SessionManager.instance;\n }\n\n this.config = { ...DEFAULT_CONFIG };\n\n this.state = {\n isInitialized: false,\n isLoading: false,\n lastRefreshTime: null,\n error: null,\n errorCode: null,\n nextRefreshTime: null,\n tokenExpiry: null,\n cfSessionId: null,\n initializationFailed: false,\n };\n\n this.refreshTimerId = null;\n this.listeners = new Set();\n this.initializationPromise = null;\n this._configured = false;\n this._boundHandleVisibilityChange = this._handleVisibilityChange.bind(this);\n this._boundHandlePageShow = this._handlePageShow.bind(this);\n this._boundHandleOnline = this._handleOnline.bind(this);\n\n SessionManager.instance = this;\n }\n\n /**\n * Configure the session manager\n * @param {Object} config - Configuration options\n * @param {string} config.bootstrapUrl - URL for bootstrap API endpoint\n * @param {number} config.refreshInterval - Interval for token refresh in milliseconds (default: 25 mins)\n * @param {number} config.tokenExpiry - Token expiry time in milliseconds (default: 30 mins)\n * @param {number} config.maxRetries - Maximum retry attempts on failure (default: 3)\n * @param {number} config.bootstrapTimeout - Timeout for bootstrap fetch in milliseconds (default: 30s)\n * @param {Object} config.headers - Additional headers for API calls\n * @param {boolean} config.credentials - Include credentials in requests (default: true)\n * @param {string} config.tokenCookieName - Cookie name to read token from (default: 'token')\n */\n configure(config = {}) {\n if (!config.bootstrapUrl || typeof config.bootstrapUrl !== 'string' || !config.bootstrapUrl.trim()) {\n throw new ConfigurationError('bootstrapUrl is required and must be a non-empty string', { bootstrapUrl: config.bootstrapUrl });\n }\n\n if (config.refreshInterval !== undefined) {\n if (typeof config.refreshInterval !== 'number' || !isFinite(config.refreshInterval)) {\n throw new ConfigurationError('refreshInterval must be a finite number', { refreshInterval: config.refreshInterval });\n }\n if (config.refreshInterval <= 0) {\n throw new ConfigurationError('refreshInterval must be positive', { refreshInterval: config.refreshInterval });\n }\n }\n\n if (config.tokenExpiry !== undefined) {\n if (typeof config.tokenExpiry !== 'number' || !isFinite(config.tokenExpiry)) {\n throw new ConfigurationError('tokenExpiry must be a finite number', { tokenExpiry: config.tokenExpiry });\n }\n if (config.tokenExpiry <= 0) {\n throw new ConfigurationError('tokenExpiry must be positive', { tokenExpiry: config.tokenExpiry });\n }\n }\n\n if (config.maxRetries !== undefined) {\n if (typeof config.maxRetries !== 'number' || !Number.isInteger(config.maxRetries) || config.maxRetries < 1) {\n throw new ConfigurationError('maxRetries must be a positive integer', { maxRetries: config.maxRetries });\n }\n }\n\n this.config = {\n ...this.config,\n ...config,\n credentials: config.credentials !== undefined ? config.credentials : true,\n logLevel: config.logLevel || 'WARN'\n };\n\n logger.setLevel(this.config.logLevel);\n this._configured = true;\n\n if (this.config.refreshInterval && this.config.tokenExpiry &&\n this.config.refreshInterval >= this.config.tokenExpiry) {\n logger.warn('refreshInterval should be less than tokenExpiry to prevent race conditions');\n }\n }\n\n /**\n * Initialize session by calling bootstrap API\n * @param {boolean} forceRefresh - Force a new bootstrap call even if already initialized (used by refreshToken)\n * @returns {Promise<Object>} Bootstrap response\n */\n async initialize(forceRefresh = false) {\n if (typeof window === 'undefined') {\n throw new SSRError('Cannot initialize in non-browser environment (SSR)');\n }\n\n if (!this._configured) {\n logger.warn('initialize() called before configure(). Using default config.');\n }\n\n if (this.state.initializationFailed) {\n logger.warn('Initialization previously failed. Reload page to retry.');\n throw new BootstrapError('Initialization failed after max retries. Reload page to retry.');\n }\n\n // Return existing promise if refresh already in progress\n if (this.initializationPromise) {\n logger.debug('Initialization already in progress, waiting...');\n return this.initializationPromise;\n }\n\n // If already initialized in this page session and not a forced refresh, return current state\n if (this.state.isInitialized && !forceRefresh) {\n logger.debug('Session already initialized');\n return { token: this.getToken(this.config.tokenCookieName) };\n }\n\n this.state.isLoading = true;\n this.state.error = null;\n this.notifyListeners();\n\n this.initializationPromise = (async () => {\n let lastError;\n\n for (let attempt = 1; attempt <= this.config.maxRetries; attempt++) {\n try {\n const newSessionId = this.generateSessionId();\n\n logger.info(`Calling bootstrap API (attempt ${attempt}/${this.config.maxRetries}):`, this.config.bootstrapUrl);\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.bootstrapTimeout);\n\n let response;\n try {\n response = await fetch(this.config.bootstrapUrl, {\n method: 'GET',\n credentials: this.config.credentials ? 'include' : 'same-origin',\n signal: controller.signal,\n headers: {\n 'cf-session-id': newSessionId,\n 'x-client-platform': CLIENT_PLATFORM,\n ...this.config.headers,\n },\n });\n } catch (fetchError) {\n if (fetchError.name === 'AbortError') {\n throw new NetworkError(`Bootstrap request timed out after ${this.config.bootstrapTimeout}ms`, {\n timeout: this.config.bootstrapTimeout,\n url: this.config.bootstrapUrl\n });\n }\n throw fetchError;\n } finally {\n clearTimeout(timeoutId);\n }\n\n if (!response.ok) {\n throw new BootstrapError(`Bootstrap failed: ${response.status} ${response.statusText}`, {\n status: response.status,\n statusText: response.statusText,\n url: this.config.bootstrapUrl\n });\n }\n\n let data;\n try {\n data = await response.json();\n } catch (jsonError) {\n throw new BootstrapError('Bootstrap response is not valid JSON', { originalError: jsonError.message });\n }\n logger.info('Bootstrap successful');\n\n // Use refresh_time from response (in seconds) or fall back to config\n const refreshInterval = data.refresh_time\n ? data.refresh_time * 1000\n : this.config.refreshInterval;\n\n // Calculate token expiry from response (in seconds) or fall back to config\n const tokenExpiry = data.expire_time\n ? data.expire_time * 1000\n : this.config.tokenExpiry;\n\n // If server returned token in response body (non-cookie mode),\n // store it as a client-side cookie. Key matches server's session_header.\n const tokenFromResponse = data[this.config.tokenCookieName];\n if (tokenFromResponse) {\n this.setCookie(this.config.tokenCookieName, tokenFromResponse, tokenExpiry / 1000);\n }\n\n // Now safe to update cf-session-id — new token is stored/set\n this.setCfSessionId(newSessionId, tokenExpiry / 1000);\n\n this.state.isInitialized = true;\n this.state.isLoading = false;\n this.state.lastRefreshTime = Date.now();\n this.state.nextRefreshTime = Date.now() + refreshInterval;\n this.state.tokenExpiry = tokenExpiry;\n this.state.error = null;\n\n // Start automatic refresh timer with dynamic interval\n this.startTokenRefresh(refreshInterval);\n\n this.notifyListeners();\n return data;\n } catch (error) {\n lastError = error instanceof Error ? error : new NetworkError('Unknown error occurred', { error });\n logger.error(`Bootstrap attempt ${attempt} failed:`, lastError.message);\n\n if (attempt < this.config.maxRetries) {\n const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);\n logger.info(`Retrying in ${delay}ms...`);\n await new Promise(resolve => setTimeout(resolve, delay));\n }\n }\n }\n\n logger.error('All bootstrap attempts failed');\n this.state.isLoading = false;\n this.state.error = lastError?.message || 'Unknown error';\n this.state.errorCode = lastError?.code || 'UNKNOWN_ERROR';\n this.state.initializationFailed = true;\n this.notifyListeners();\n throw lastError;\n })();\n\n try {\n return await this.initializationPromise;\n } finally {\n this.initializationPromise = null;\n }\n }\n\n /**\n * Check if token refresh is currently in progress\n * @returns {boolean} True if refresh is in progress\n */\n isRefreshing() {\n return this.initializationPromise !== null;\n }\n\n /**\n * Wait for ongoing refresh to complete\n * @returns {Promise<Object>} Bootstrap response from ongoing refresh\n */\n async waitForRefresh() {\n if (this.initializationPromise) {\n logger.debug('Waiting for ongoing refresh to complete');\n return this.initializationPromise;\n }\n return Promise.resolve();\n }\n\n /**\n * Check if the token is likely expired based on in-memory timestamps.\n * After a full browser close + reopen, lastRefreshTime is null, so this returns true.\n * @returns {boolean} True if token appears expired or state is unknown\n */\n _isTokenLikelyExpired() {\n if (!this.state.lastRefreshTime || !this.state.tokenExpiry) return true;\n return Date.now() >= this.state.lastRefreshTime + this.state.tokenExpiry;\n }\n\n /**\n * Ensure the session is ready before making API calls.\n * - If initialized and token not expired, resolves immediately.\n * - If initialization is in progress, waits for it.\n * - If not initialized but configured, triggers initialization and waits.\n * - If not configured or permanently failed, resolves (lets request proceed;\n * the 401 handler in the interceptor will deal with it).\n * @returns {Promise<void>}\n */\n async ensureReady() {\n // Happy path: session is live and token hasn't expired\n if (this.state.isInitialized && !this._isTokenLikelyExpired()) {\n return;\n }\n\n // An initialization / refresh is already in flight — piggyback on it\n if (this.initializationPromise) {\n try {\n await this.initializationPromise;\n } catch (_) {\n // Initialization failed — let the request proceed, 401 handler will deal with it\n logger.debug('ensureReady: in-flight initialization failed, proceeding with request');\n }\n return;\n }\n\n // Not initialized / token expired, no call in flight, but we *can* bootstrap\n if (this._configured && !this.state.initializationFailed) {\n try {\n await this.initialize(this.state.isInitialized); // force refresh if already initialized but token expired\n } catch (_) {\n logger.debug('ensureReady: initialization failed, proceeding with request');\n }\n }\n\n // Not configured or permanently failed — nothing we can do here\n }\n\n /**\n * Check if the session needs a refresh (initialized but token expired).\n * Useful for interceptors to proactively refresh before sending a request.\n * @returns {boolean}\n */\n needsRefresh() {\n return this.state.isInitialized && this._isTokenLikelyExpired();\n }\n\n /**\n * Set cf-session-id in cookie\n * @param {string} sessionId - Session ID\n * @param {number} maxAge - Max age in seconds\n */\n setCfSessionId(sessionId, maxAge) {\n this.state.cfSessionId = sessionId;\n const cookieMaxAge = maxAge || this.config.tokenExpiry / 1000;\n this.setCookie('cf-session-id', sessionId, cookieMaxAge);\n }\n\n /**\n * Generate unique session ID\n * @returns {string} Generated session ID\n */\n generateSessionId() {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n }\n\n /**\n * Get cf-session-id for requests\n * @returns {string} Current cf-session-id\n */\n getSessionId() {\n return this.state.cfSessionId;\n }\n\n /**\n * Check if token should be added to headers (HTTP or cross-origin)\n * @returns {boolean} True if token should be added to headers\n */\n shouldUseTokenHeader() {\n if (typeof window === 'undefined') return false;\n return window.location.protocol === 'http:';\n }\n\n /**\n * Shared cookie name sanitizer used by both getCookie and setCookie\n * @param {string} name - Raw cookie name\n * @returns {string} Sanitized cookie name\n */\n _sanitizeCookieName(name) {\n return name.replace(/[^a-zA-Z0-9_-]/g, '');\n }\n\n /**\n * Get cookie value\n * @param {string} name - Cookie name\n * @returns {string|null} Cookie value or null\n */\n getCookie(name) {\n if (typeof document === 'undefined') return null;\n const sanitizedName = this._sanitizeCookieName(name);\n if (!sanitizedName) return null;\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${sanitizedName}=`);\n if (parts.length === 2) {\n try {\n return decodeURIComponent(parts.pop().split(';').shift());\n } catch (error) {\n logger.error('Failed to decode cookie value:', error instanceof Error ? error.message : String(error));\n return null;\n }\n }\n return null;\n }\n\n /**\n * Get token from cookie\n * @param {string} name - Cookie name (default: configured tokenCookieName)\n * @returns {string|null} Token value or null\n */\n getToken(name = this.config.tokenCookieName) {\n return this.getCookie(name);\n }\n\n /**\n * Set cookie from client-side (WARNING: Not HttpOnly, less secure than server-set cookies)\n * @param {string} name - Cookie name\n * @param {string} value - Cookie value\n * @param {number} maxAge - Max age in seconds\n */\n setCookie(name, value, maxAge) {\n if (typeof document === 'undefined') {\n logger.warn('Cannot set cookie in non-browser environment');\n return;\n }\n\n const sanitizedName = this._sanitizeCookieName(name);\n if (!sanitizedName) {\n logger.error('Invalid cookie name provided');\n return;\n }\n\n const expires = new Date(Date.now() + maxAge * 1000).toUTCString();\n const isSecure = typeof window !== 'undefined' && window.location.protocol === 'https:';\n const secureFlag = isSecure ? '; Secure' : '';\n const encodedValue = encodeURIComponent(value);\n document.cookie = `${sanitizedName}=${encodedValue}; path=/; expires=${expires}; SameSite=Lax${secureFlag}`;\n logger.debug(`Cookie set: ${sanitizedName}, expires in ${maxAge}s${isSecure ? ' (Secure)' : ''} [WARNING: Not HttpOnly]`);\n }\n\n /**\n * Start automatic token refresh timer\n * @param {number} interval - Refresh interval in milliseconds\n */\n startTokenRefresh(interval = this.config.refreshInterval) {\n this.stopTokenRefresh();\n logger.info(`Scheduling token refresh in ${interval / 1000} seconds`);\n\n this.refreshTimerId = setTimeout(async () => {\n logger.info('Auto-refreshing token...');\n try {\n await this.refreshToken();\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n const errorCode = error?.code || 'UNKNOWN_ERROR';\n logger.error('Auto-refresh failed:', errorMessage);\n this.state.error = errorMessage;\n this.state.errorCode = errorCode;\n this.notifyListeners();\n // Schedule retry instead of dying permanently\n const retryDelay = Math.min(interval * 2, 60000);\n logger.info(`Scheduling auto-refresh retry in ${retryDelay / 1000}s`);\n this.startTokenRefresh(retryDelay);\n }\n }, interval);\n\n // Listen for visibility changes (tab wake from sleep / bfcache restore)\n this._startVisibilityListener();\n // Listen for network recovery\n this._startOnlineListener();\n }\n\n /**\n * Stop automatic token refresh timer\n */\n stopTokenRefresh() {\n if (this.refreshTimerId) {\n clearTimeout(this.refreshTimerId);\n this.refreshTimerId = null;\n logger.debug('Token refresh timer stopped');\n }\n this._stopVisibilityListener();\n this._stopOnlineListener();\n }\n\n /**\n * Start listening for page visibility changes and bfcache restore\n */\n _startVisibilityListener() {\n if (typeof document === 'undefined') return;\n document.addEventListener('visibilitychange', this._boundHandleVisibilityChange);\n if (typeof window !== 'undefined') {\n window.addEventListener('pageshow', this._boundHandlePageShow);\n }\n }\n\n /**\n * Stop listening for page visibility changes\n */\n _stopVisibilityListener() {\n if (typeof document === 'undefined') return;\n document.removeEventListener('visibilitychange', this._boundHandleVisibilityChange);\n if (typeof window !== 'undefined') {\n window.removeEventListener('pageshow', this._boundHandlePageShow);\n }\n }\n\n /**\n * Check if token refresh is overdue and trigger refresh if so\n * @returns {boolean} True if refresh was triggered\n */\n _refreshIfOverdue() {\n if (!this.state.isInitialized || this.state.initializationFailed) return false;\n if (this.isRefreshing()) return false;\n\n const now = Date.now();\n const { nextRefreshTime } = this.state;\n\n if (nextRefreshTime && now >= nextRefreshTime) {\n logger.info('Missed scheduled refresh — refreshing token now');\n this.refreshToken().catch(error => {\n const errorMessage = error instanceof Error ? error.message : String(error);\n logger.error('Triggered refresh failed:', errorMessage);\n });\n return true;\n }\n return false;\n }\n\n /**\n * Handle visibility change — refresh if overdue when tab becomes visible\n */\n _handleVisibilityChange() {\n if (document.visibilityState !== 'visible') return;\n this._refreshIfOverdue();\n }\n\n /**\n * Handle pageshow — force bootstrap on bfcache restore (browser close + reopen)\n */\n _handlePageShow(event) {\n if (!event.persisted) return;\n // bfcache restore means browser was closed — always force a fresh bootstrap\n if (!this.state.isInitialized || this.state.initializationFailed) return;\n if (this.isRefreshing()) return;\n logger.info('Page restored from bfcache — forcing bootstrap');\n this.refreshToken().catch(error => {\n const errorMessage = error instanceof Error ? error.message : String(error);\n logger.error('bfcache-triggered refresh failed:', errorMessage);\n });\n }\n\n /**\n * Start listening for network recovery\n */\n _startOnlineListener() {\n if (typeof window === 'undefined') return;\n window.addEventListener('online', this._boundHandleOnline);\n }\n\n /**\n * Stop listening for network recovery\n */\n _stopOnlineListener() {\n if (typeof window === 'undefined') return;\n window.removeEventListener('online', this._boundHandleOnline);\n }\n\n /**\n * Handle online event — retry if initialization had failed due to network\n */\n _handleOnline() {\n if (!this.state.initializationFailed) return;\n logger.info('Network restored — retrying session initialization');\n this.refreshToken().catch(error => {\n const errorMessage = error instanceof Error ? error.message : String(error);\n logger.error('Online-triggered refresh failed:', errorMessage);\n });\n }\n\n /**\n * Manually refresh the session token\n * @returns {Promise<Object>} Bootstrap response\n */\n async refreshToken() {\n logger.info('Manual token refresh triggered');\n this.state.initializationFailed = false;\n return this.initialize(true);\n }\n\n /**\n * Get current session status\n * @returns {Object} Current session state\n */\n getSessionStatus() {\n return {\n isInitialized: this.state.isInitialized,\n isLoading: this.state.isLoading,\n lastRefreshTime: this.state.lastRefreshTime,\n nextRefreshTime: this.state.nextRefreshTime,\n tokenExpiry: this.state.tokenExpiry,\n error: this.state.error,\n errorCode: this.state.errorCode,\n initializationFailed: this.state.initializationFailed,\n timeUntilRefresh: this.state.nextRefreshTime\n ? Math.max(0, this.state.nextRefreshTime - Date.now())\n : null,\n };\n }\n\n /**\n * Subscribe to session state changes\n * @param {Function} listener - Callback function to be called on state changes\n * @returns {Function} Unsubscribe function\n */\n subscribe(listener) {\n if (typeof listener !== 'function') {\n throw new TypeError('Listener must be a function');\n }\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Notify all listeners of state changes\n */\n notifyListeners() {\n const status = this.getSessionStatus();\n Array.from(this.listeners).forEach(listener => {\n try {\n listener(status);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n logger.error('Listener error:', errorMessage);\n }\n });\n }\n\n /**\n * Clean up and reset the session manager\n */\n destroy() {\n this.stopTokenRefresh();\n this.initializationPromise = null;\n this.listeners.clear();\n this.setCookie('cf-session-id', '', -1);\n this.setCookie(this.config.tokenCookieName, '', -1);\n this.config = { ...DEFAULT_CONFIG };\n this.state = {\n isInitialized: false,\n isLoading: false,\n lastRefreshTime: null,\n error: null,\n errorCode: null,\n nextRefreshTime: null,\n tokenExpiry: null,\n cfSessionId: null,\n initializationFailed: false,\n };\n this._configured = false;\n logger.info('Destroyed');\n }\n\n /**\n * Get the singleton instance\n * @returns {SessionManager} Singleton instance\n */\n static getInstance() {\n if (!SessionManager.instance) {\n SessionManager.instance = new SessionManager();\n }\n return SessionManager.instance;\n }\n}\n\nexport default SessionManager;","import SessionManager from './SessionManager.js';\nexport const CLIENT_PLATFORM = 'web';\n\n/**\n * Fetch interceptor with automatic token refresh on 401\n * @param {string} url - Request URL\n * @param {Object} options - Fetch options\n * @returns {Promise<Response>} Fetch response\n */\nexport async function fetchInterceptor(url, options = {}) {\n const sessionManager = SessionManager.getInstance();\n\n // Gate on session readiness: if not initialized (e.g., browser was closed and\n // reopened), this triggers bootstrap and waits; if a refresh is in progress\n // (tab wake, online recovery, concurrent 401), it piggybacks on it.\n await sessionManager.ensureReady();\n\n // FIX #6: Shallow-clone options to avoid mutating the caller's object\n const requestOptions = {\n ...options,\n credentials: options.credentials || 'include',\n headers: {\n ...options.headers,\n 'cf-session-id': sessionManager.getSessionId(),\n 'x-client-platform': CLIENT_PLATFORM,\n },\n };\n\n // Add token to header if HTTP (not HTTPS)\n if (sessionManager.shouldUseTokenHeader()) {\n const token = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (token) {\n requestOptions.headers[sessionManager.config.tokenCookieName] = token;\n }\n }\n\n let response = await fetch(url, requestOptions);\n\n // Retry once if unauthorized with INVALID_SESSION\n if (response.status === 401) {\n const clonedResponse = response.clone();\n try {\n const data = await clonedResponse.json();\n if (data.error_msg === sessionManager.config.invalidSessionError) {\n if (sessionManager.isRefreshing()) {\n await sessionManager.waitForRefresh();\n } else {\n await sessionManager.refreshToken();\n }\n requestOptions.headers['cf-session-id'] = sessionManager.getSessionId();\n if (sessionManager.shouldUseTokenHeader()) {\n const newToken = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (newToken) {\n requestOptions.headers[sessionManager.config.tokenCookieName] = newToken;\n }\n }\n response = await fetch(url, requestOptions);\n }\n } catch (e) {\n // Not JSON or parsing failed, return original response\n }\n }\n\n return response;\n}\n\n/**\n * Axios interceptor with automatic token refresh on 401 and INVALID_SESSION\n * @param {Object} axiosInstance - Axios instance\n * @returns {Object} The same axios instance with interceptors attached\n */\nexport function setupAxiosInterceptor(axiosInstance) {\n const sessionManager = SessionManager.getInstance();\n\n // FIX #7: Eject previous interceptors if this instance was already set up\n if (axiosInstance._gwSessionRequestId !== undefined) {\n axiosInstance.interceptors.request.eject(axiosInstance._gwSessionRequestId);\n }\n if (axiosInstance._gwSessionResponseId !== undefined) {\n axiosInstance.interceptors.response.eject(axiosInstance._gwSessionResponseId);\n }\n\n // Request interceptor to add cf-session-id, token, and credentials\n axiosInstance._gwSessionRequestId = axiosInstance.interceptors.request.use(\n async (config) => {\n // Gate on session readiness before every request\n await sessionManager.ensureReady();\n if (sessionManager.config.credentials) {\n config.withCredentials = true;\n }\n config.headers['cf-session-id'] = sessionManager.getSessionId();\n config.headers['x-client-platform'] = CLIENT_PLATFORM;\n if (sessionManager.shouldUseTokenHeader()) {\n const token = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (token) {\n config.headers[sessionManager.config.tokenCookieName] = token;\n }\n }\n return config;\n },\n (error) => Promise.reject(error)\n );\n\n // Response interceptor for token refresh\n axiosInstance._gwSessionResponseId = axiosInstance.interceptors.response.use(\n (response) => response,\n async (error) => {\n const originalRequest = error.config;\n\n // Retry once if unauthorized with INVALID_SESSION\n if (error.response?.status === 401 && !originalRequest._retry) {\n if (error.response?.data?.error_msg === sessionManager.config.invalidSessionError) {\n originalRequest._retry = true;\n\n if (sessionManager.isRefreshing()) {\n await sessionManager.waitForRefresh();\n } else {\n await sessionManager.refreshToken();\n }\n\n originalRequest.headers['cf-session-id'] = sessionManager.getSessionId();\n originalRequest.headers['x-client-platform'] = CLIENT_PLATFORM;\n if (sessionManager.shouldUseTokenHeader()) {\n const newToken = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (newToken) {\n originalRequest.headers[sessionManager.config.tokenCookieName] = newToken;\n }\n }\n return axiosInstance(originalRequest);\n }\n }\n\n return Promise.reject(error);\n }\n );\n\n return axiosInstance;\n}","import { useState, useEffect, useCallback, useRef } from 'react';\nimport SessionManager from '../SessionManager.js';\nimport { logger } from '../logger.js';\n\n/**\n * React hook for session management\n * Provides session state and controls for React components\n *\n * @param {Object} options - Configuration options\n * @param {boolean} options.autoInitialize - Automatically initialize on mount (default: true)\n * @returns {Object} Session state and controls\n */\nexport function useSession(options = {}) {\n const { autoInitialize = true } = options;\n\n const sessionManager = SessionManager.getInstance();\n\n const [sessionState, setSessionState] = useState(() =>\n sessionManager.getSessionStatus()\n );\n\n // FIX #11: Separate countdown state — only this re-renders every second,\n // not the full session state which would cause unnecessary subscriber churn\n const [timeUntilRefresh, setTimeUntilRefresh] = useState(() => {\n const { nextRefreshTime } = sessionManager.getSessionStatus();\n return nextRefreshTime ? Math.max(0, nextRefreshTime - Date.now()) : null;\n });\n\n const nextRefreshTimeRef = useRef(sessionState.nextRefreshTime);\n\n // Subscribe to meaningful session state changes only\n useEffect(() => {\n const unsubscribe = sessionManager.subscribe((newState) => {\n setSessionState(newState);\n nextRefreshTimeRef.current = newState.nextRefreshTime;\n });\n\n return unsubscribe;\n }, []); // sessionManager is a singleton, no need to include in deps\n\n // Local tick timer for countdown display — avoids global tick in SessionManager\n useEffect(() => {\n if (!sessionState.nextRefreshTime) {\n setTimeUntilRefresh(null);\n return;\n }\n // Set immediately on mount / when nextRefreshTime changes\n setTimeUntilRefresh(Math.max(0, sessionState.nextRefreshTime - Date.now()));\n const tickId = setInterval(() => {\n const nrt = nextRefreshTimeRef.current;\n setTimeUntilRefresh(nrt ? Math.max(0, nrt - Date.now()) : null);\n }, 1000);\n return () => clearInterval(tickId);\n }, [sessionState.nextRefreshTime]);\n\n // Auto-initialize if enabled\n useEffect(() => {\n if (autoInitialize && !sessionState.isInitialized && !sessionState.isLoading && !sessionState.initializationFailed) {\n sessionManager.initialize().catch(error => {\n logger.error('Auto-initialization failed:', error);\n });\n }\n }, [autoInitialize, sessionState.isInitialized, sessionState.isLoading, sessionState.initializationFailed]);\n\n // Manual refresh function\n const refresh = useCallback(async () => {\n try {\n await sessionManager.refreshToken();\n } catch (error) {\n logger.error('Manual refresh failed:', error);\n throw error;\n }\n }, []);\n\n // Initialize function (for manual control)\n const initialize = useCallback(async () => {\n try {\n await sessionManager.initialize();\n } catch (error) {\n logger.error('Manual initialization failed:', error);\n throw error;\n }\n }, []);\n\n return {\n // State\n isInitialized: sessionState.isInitialized,\n isLoading: sessionState.isLoading,\n error: sessionState.error,\n lastRefreshTime: sessionState.lastRefreshTime,\n nextRefreshTime: sessionState.nextRefreshTime,\n timeUntilRefresh,\n initializationFailed: sessionState.initializationFailed,\n\n // Actions\n refresh,\n initialize,\n };\n}\n\nexport default useSession;"],"names":["SessionError","Error","constructor","message","code","details","super","this","name","captureStackTrace","Object","setPrototypeOf","prototype","ConfigurationError","BootstrapError","NetworkError","SSRError","LOG_LEVELS","NONE","ERROR","WARN","INFO","DEBUG","logger","level","setLevel","toUpperCase","error","args","console","warn","info","debug","DEFAULT_CONFIG","bootstrapUrl","refreshInterval","tokenExpiry","maxRetries","bootstrapTimeout","tokenCookieName","invalidSessionError","SessionManager","instance","config","state","isInitialized","isLoading","lastRefreshTime","errorCode","nextRefreshTime","cfSessionId","initializationFailed","refreshTimerId","listeners","Set","initializationPromise","_configured","_boundHandleVisibilityChange","_handleVisibilityChange","bind","_boundHandlePageShow","_handlePageShow","_boundHandleOnline","_handleOnline","configure","trim","undefined","isFinite","Number","isInteger","credentials","logLevel","initialize","forceRefresh","window","token","getToken","notifyListeners","lastError","attempt","newSessionId","generateSessionId","controller","AbortController","timeoutId","setTimeout","abort","response","data","fetch","method","signal","headers","fetchError","timeout","url","clearTimeout","ok","status","statusText","json","jsonError","originalError","refresh_time","expire_time","tokenFromResponse","setCookie","setCfSessionId","Date","now","startTokenRefresh","delay","Math","min","pow","Promise","resolve","isRefreshing","waitForRefresh","_isTokenLikelyExpired","ensureReady","_","needsRefresh","sessionId","maxAge","cookieMaxAge","crypto","randomUUID","random","toString","substr","getSessionId","shouldUseTokenHeader","location","protocol","_sanitizeCookieName","replace","getCookie","document","sanitizedName","parts","cookie","split","length","decodeURIComponent","pop","shift","String","value","expires","toUTCString","isSecure","secureFlag","encodedValue","encodeURIComponent","interval","stopTokenRefresh","async","refreshToken","errorMessage","retryDelay","_startVisibilityListener","_startOnlineListener","_stopVisibilityListener","_stopOnlineListener","addEventListener","removeEventListener","_refreshIfOverdue","catch","visibilityState","event","persisted","getSessionStatus","timeUntilRefresh","max","subscribe","listener","TypeError","add","delete","Array","from","forEach","destroy","clear","getInstance","CLIENT_PLATFORM","options","sessionManager","requestOptions","clonedResponse","clone","error_msg","newToken","e","axiosInstance","_gwSessionRequestId","interceptors","request","eject","_gwSessionResponseId","use","withCredentials","reject","originalRequest","_retry","autoInitialize","sessionState","setSessionState","useState","setTimeUntilRefresh","nextRefreshTimeRef","useRef","useEffect","newState","current","tickId","setInterval","nrt","clearInterval","refresh","useCallback"],"mappings":"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"}
|
package/dist/index.esm.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{useState as e,useEffect as t,useCallback as i}from"react";class s extends Error{constructor(e,t,i={}){super(e),this.name="SessionError",this.code=t,this.details=i,Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor),Object.setPrototypeOf(this,s.prototype)}}class r extends s{constructor(e,t){super(e,"CONFIGURATION_ERROR",t),this.name="ConfigurationError"}}class n extends s{constructor(e,t){super(e,"BOOTSTRAP_ERROR",t),this.name="BootstrapError"}}class o extends s{constructor(e,t){super(e,"NETWORK_ERROR",t),this.name="NetworkError"}}class a extends s{constructor(e,t={}){super(e,"SSR_ERROR",t),this.name="SSRError"}}const h={NONE:0,ERROR:1,WARN:2,INFO:3,DEBUG:4};const l=new class{constructor(){this.level=h.WARN}setLevel(e){this.level="string"==typeof e?h[e.toUpperCase()]??h.WARN:e}error(...e){this.level>=h.ERROR&&console.error("[SessionManager]",...e)}warn(...e){this.level>=h.WARN&&console.warn("[SessionManager]",...e)}info(...e){this.level>=h.INFO&&console.info("[SessionManager]",...e)}debug(...e){this.level>=h.DEBUG&&console.debug("[SessionManager]",...e)}};class c{constructor(){if(c.instance)return c.instance;this.config={bootstrapUrl:"/session/bootstrap",refreshInterval:15e5,tokenExpiry:18e5,maxRetries:3,tokenCookieName:"token"},this.state={isInitialized:!1,isLoading:!1,lastRefreshTime:null,error:null,errorCode:null,nextRefreshTime:null,tokenExpiry:null,cfSessionId:null,initializationFailed:!1},this.refreshTimerId=null,this.tickTimerId=null,this.listeners=new Set,this.initializationPromise=null,c.instance=this}configure(e={}){if(!e.bootstrapUrl||"string"!=typeof e.bootstrapUrl||!e.bootstrapUrl.trim())throw new r("bootstrapUrl is required and must be a non-empty string",{bootstrapUrl:e.bootstrapUrl});if(void 0!==e.refreshInterval){if("number"!=typeof e.refreshInterval||!isFinite(e.refreshInterval))throw new r("refreshInterval must be a finite number",{refreshInterval:e.refreshInterval});if(e.refreshInterval<=0)throw new r("refreshInterval must be positive",{refreshInterval:e.refreshInterval})}if(void 0!==e.tokenExpiry){if("number"!=typeof e.tokenExpiry||!isFinite(e.tokenExpiry))throw new r("tokenExpiry must be a finite number",{tokenExpiry:e.tokenExpiry});if(e.tokenExpiry<=0)throw new r("tokenExpiry must be positive",{tokenExpiry:e.tokenExpiry})}this.config={...this.config,...e,credentials:void 0===e.credentials||e.credentials,logLevel:e.logLevel||"WARN"},l.setLevel(this.config.logLevel),this.config.refreshInterval&&this.config.tokenExpiry&&this.config.refreshInterval>=this.config.tokenExpiry&&l.warn("refreshInterval should be less than tokenExpiry to prevent race conditions")}async initialize(e=!1){if("undefined"==typeof window)throw new a("Cannot initialize in non-browser environment (SSR)");if(this.state.initializationFailed)throw l.warn("Initialization previously failed. Reload page to retry."),new n("Initialization failed after max retries. Reload page to retry.");if(this.initializationPromise)return l.debug("Initialization already in progress, waiting..."),this.initializationPromise;if(!e){if("true"===this.getCookie("session_initialized")){l.info("Session already initialized, skipping bootstrap API call"),this.state.isInitialized=!0,this.state.tokenExpiry=this.config.tokenExpiry;const e=this.getCookie("cf-session-id");e?this.state.cfSessionId=e:this.setCfSessionId(this.generateSessionId());const t=parseInt(this.getCookie("session_refresh_at"),10);let i;return i=t&&!isNaN(t)?Math.max(0,t-Date.now()):this.config.refreshInterval,this.state.lastRefreshTime=Date.now(),this.state.nextRefreshTime=Date.now()+i,this.startTokenRefresh(i),this.notifyListeners(),{token:this.getToken(this.config.tokenCookieName)}}}this.state.isLoading=!0,this.state.error=null,this.notifyListeners(),this.initializationPromise=(async()=>{let e;for(let t=1;t<=this.config.maxRetries;t++)try{const e=this.generateSessionId();this.setCfSessionId(e),l.info(`Calling bootstrap API (attempt ${t}/${this.config.maxRetries}):`,this.config.bootstrapUrl);const i=await fetch(this.config.bootstrapUrl,{method:"GET",credentials:this.config.credentials?"include":"same-origin",headers:{"Content-Type":"application/json","cf-session-id":this.state.cfSessionId,"x-client-platform":"web",...this.config.headers}});if(!i.ok)throw new n(`Bootstrap failed: ${i.status} ${i.statusText}`,{status:i.status,statusText:i.statusText,url:this.config.bootstrapUrl});let s;try{s=await i.json()}catch(e){throw new n("Bootstrap response is not valid JSON",{originalError:e.message})}l.info("Bootstrap successful");const r=i.headers.get("set-cookie"),o=r&&r.includes(this.config.tokenCookieName),a=s.refresh_time?1e3*s.refresh_time:this.config.refreshInterval,h=s.expire_time?1e3*s.expire_time:this.config.tokenExpiry;return s.token&&!o?(l.debug("Server did not set cookie, setting from SDK"),this.setCookie(this.config.tokenCookieName,s.token,h/1e3)):o&&l.debug("Server set cookie, skipping SDK cookie logic"),this.setCfSessionId(this.state.cfSessionId,h/1e3),this.state.isInitialized=!0,this.state.isLoading=!1,this.state.lastRefreshTime=Date.now(),this.state.nextRefreshTime=Date.now()+a,this.state.tokenExpiry=h,this.state.error=null,this.setCookie("session_initialized","true",a/1e3),this.setCookie("session_refresh_at",String(Date.now()+a),a/1e3),this.startTokenRefresh(a),this.notifyListeners(),s}catch(i){if(e=i instanceof Error?i:new o("Unknown error occurred",{error:i}),l.error(`Bootstrap attempt ${t} failed:`,e.message),t<this.config.maxRetries){const e=Math.min(1e3*Math.pow(2,t-1),5e3);l.info(`Retrying in ${e}ms...`),await new Promise(t=>setTimeout(t,e))}}throw l.error("All bootstrap attempts failed"),this.state.isLoading=!1,this.state.error=e?.message||"Unknown error",this.state.errorCode=e?.code||"UNKNOWN_ERROR",this.state.initializationFailed=!0,this.notifyListeners(),e})();try{return await this.initializationPromise}finally{this.initializationPromise=null}}isRefreshing(){return null!==this.initializationPromise}async waitForRefresh(){return this.initializationPromise?(l.debug("Waiting for ongoing refresh to complete"),this.initializationPromise):Promise.resolve()}setCfSessionId(e,t){this.state.cfSessionId=e;const i=t||this.config.tokenExpiry/1e3;this.setCookie("cf-session-id",e,i)}generateSessionId(){return"undefined"!=typeof crypto&&crypto.randomUUID?crypto.randomUUID():`${Date.now()}-${Math.random().toString(36).substr(2,9)}`}getSessionId(){return this.state.cfSessionId}shouldUseTokenHeader(){return"undefined"!=typeof window&&"http:"===window.location.protocol}getCookie(e){if("undefined"==typeof document)return null;const t=`; ${document.cookie}`.split(`; ${e}=`);if(2===t.length)try{return decodeURIComponent(t.pop().split(";").shift())}catch(e){return l.error("Failed to decode cookie value:",e instanceof Error?e.message:String(e)),null}return null}getToken(e=this.config.tokenCookieName){return this.getCookie(e)}setCookie(e,t,i){if("undefined"==typeof document)return void l.warn("Cannot set cookie in non-browser environment");const s=e.replace(/[^a-zA-Z0-9_-]/g,"");if(!s)return void l.error("Invalid cookie name provided");const r=new Date(Date.now()+1e3*i).toUTCString(),n="undefined"!=typeof window&&"https:"===window.location.protocol,o=n?"; Secure":"",a=encodeURIComponent(t);document.cookie=`${s}=${a}; path=/; expires=${r}; SameSite=Lax${o}`,l.debug(`Cookie set: ${s}, expires in ${i}s${n?" (Secure)":""} [WARNING: Not HttpOnly]`)}startTokenRefresh(e=this.config.refreshInterval){this.stopTokenRefresh(),l.info(`Scheduling token refresh in ${e/1e3} seconds`),this.refreshTimerId=setTimeout(async()=>{l.info("Auto-refreshing token...");try{await this.refreshToken()}catch(e){const t=e instanceof Error?e.message:String(e),i=e?.code||"UNKNOWN_ERROR";l.error("Auto-refresh failed:",t),this.state.error=t,this.state.errorCode=i,this.notifyListeners()}},e),this.startTickTimer()}stopTokenRefresh(){this.refreshTimerId&&(clearTimeout(this.refreshTimerId),this.refreshTimerId=null,l.debug("Token refresh timer stopped")),this.stopTickTimer()}startTickTimer(){this.stopTickTimer(),this.tickTimerId=setInterval(()=>{this.notifyListeners()},1e3)}stopTickTimer(){this.tickTimerId&&(clearInterval(this.tickTimerId),this.tickTimerId=null)}async refreshToken(){return l.info("Manual token refresh triggered"),this.state.initializationFailed=!1,this.initialize(!0)}getSessionStatus(){return{isInitialized:this.state.isInitialized,isLoading:this.state.isLoading,lastRefreshTime:this.state.lastRefreshTime,nextRefreshTime:this.state.nextRefreshTime,tokenExpiry:this.state.tokenExpiry,error:this.state.error,errorCode:this.state.errorCode,initializationFailed:this.state.initializationFailed,timeUntilRefresh:this.state.nextRefreshTime?Math.max(0,this.state.nextRefreshTime-Date.now()):null}}subscribe(e){if("function"!=typeof e)throw new TypeError("Listener must be a function");return this.listeners.add(e),()=>{this.listeners.delete(e)}}notifyListeners(){const e=this.getSessionStatus();Array.from(this.listeners).forEach(t=>{try{t(e)}catch(e){const t=e instanceof Error?e.message:String(e);l.error("Listener error:",t)}})}destroy(){this.stopTokenRefresh(),this.listeners.clear(),this.state={isInitialized:!1,isLoading:!1,lastRefreshTime:null,error:null,errorCode:null,nextRefreshTime:null,tokenExpiry:null,cfSessionId:null,initializationFailed:!1},this.setCookie("cf-session-id","",-1),this.setCookie("session_initialized","",-1),this.setCookie("session_refresh_at","",-1),l.info("Destroyed")}static getInstance(){return c.instance||(c.instance=new c),c.instance}}function f(s={}){const{autoInitialize:r=!0}=s,n=c.getInstance(),[o,a]=e(()=>n.getSessionStatus());t(()=>n.subscribe(e=>{a(e)}),[]),t(()=>{!r||o.isInitialized||o.isLoading||o.initializationFailed||n.initialize().catch(e=>{l.error("Auto-initialization failed:",e)})},[r,o.isInitialized,o.isLoading,o.initializationFailed]);const h=i(async()=>{try{await n.refreshToken()}catch(e){throw l.error("Manual refresh failed:",e),e}},[]),f=i(async()=>{try{await n.initialize()}catch(e){throw l.error("Manual initialization failed:",e),e}},[]);return{isInitialized:o.isInitialized,isLoading:o.isLoading,error:o.error,lastRefreshTime:o.lastRefreshTime,nextRefreshTime:o.nextRefreshTime,timeUntilRefresh:o.timeUntilRefresh,initializationFailed:o.initializationFailed,refresh:h,initialize:f}}const d="web";async function u(e,t={}){const i=c.getInstance();if(t.credentials=t.credentials||"include",t.headers={...t.headers,"cf-session-id":i.getSessionId(),"x-client-platform":d},i.shouldUseTokenHeader()){const e=i.getToken(i.config.tokenCookieName);e&&(t.headers[i.config.tokenCookieName]=e)}let s=await fetch(e,t);if(401===s.status){const r=s.clone();try{if("INVALID_SESSION"===(await r.json()).error_msg){if(i.isRefreshing())await i.waitForRefresh();else{i.getToken(i.config.tokenCookieName)||await i.refreshToken()}if(t.headers["cf-session-id"]=i.getSessionId(),t.headers["x-client-platform"]=d,i.shouldUseTokenHeader()){const e=i.getToken(i.config.tokenCookieName);e&&(t.headers[i.config.tokenCookieName]=e)}s=await fetch(e,t)}}catch(e){}}return s}function g(e){const t=c.getInstance();return e.interceptors.request.use(e=>{if(e.headers["cf-session-id"]=t.getSessionId(),e.headers["x-client-platform"]=d,t.shouldUseTokenHeader()){const i=t.getToken(t.config.tokenCookieName);i&&(e.headers[t.config.tokenCookieName]=i)}return e},e=>Promise.reject(e)),e.interceptors.response.use(e=>e,async i=>{const s=i.config;if(401===i.response?.status&&!s._retry&&"INVALID_SESSION"===i.response?.data?.error_msg){if(s._retry=!0,t.isRefreshing())await t.waitForRefresh();else{t.getToken(t.config.tokenCookieName)||await t.refreshToken()}if(s.headers["cf-session-id"]=t.getSessionId(),s.headers["x-client-platform"]=d,t.shouldUseTokenHeader()){const e=t.getToken(t.config.tokenCookieName);e&&(s.headers[t.config.tokenCookieName]=e)}return e(s)}return Promise.reject(i)}),e}export{n as BootstrapError,d as CLIENT_PLATFORM,r as ConfigurationError,h as LOG_LEVELS,o as NetworkError,a as SSRError,s as SessionError,c as SessionManager,c as default,u as fetchInterceptor,l as logger,g as setupAxiosInterceptor,f as useSession};
|
|
1
|
+
import{useState as e,useRef as i,useEffect as t,useCallback as s}from"react";class n extends Error{constructor(e,i,t={}){super(e),this.name="SessionError",this.code=i,this.details=t,Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor),Object.setPrototypeOf(this,n.prototype)}}class r extends n{constructor(e,i){super(e,"CONFIGURATION_ERROR",i),this.name="ConfigurationError"}}class o extends n{constructor(e,i){super(e,"BOOTSTRAP_ERROR",i),this.name="BootstrapError"}}class a extends n{constructor(e,i){super(e,"NETWORK_ERROR",i),this.name="NetworkError"}}class h extends n{constructor(e,i={}){super(e,"SSR_ERROR",i),this.name="SSRError"}}const l={NONE:0,ERROR:1,WARN:2,INFO:3,DEBUG:4};const d=new class{constructor(){this.level=l.WARN}setLevel(e){this.level="string"==typeof e?l[e.toUpperCase()]??l.WARN:e}error(...e){this.level>=l.ERROR&&console.error("[SessionManager]",...e)}warn(...e){this.level>=l.WARN&&console.warn("[SessionManager]",...e)}info(...e){this.level>=l.INFO&&console.info("[SessionManager]",...e)}debug(...e){this.level>=l.DEBUG&&console.debug("[SessionManager]",...e)}},f={bootstrapUrl:"/session/bootstrap",refreshInterval:15e5,tokenExpiry:18e5,maxRetries:3,bootstrapTimeout:3e4,tokenCookieName:"token",invalidSessionError:"INVALID-GW-SESSION"};class c{constructor(){if(c.instance)return c.instance;this.config={...f},this.state={isInitialized:!1,isLoading:!1,lastRefreshTime:null,error:null,errorCode:null,nextRefreshTime:null,tokenExpiry:null,cfSessionId:null,initializationFailed:!1},this.refreshTimerId=null,this.listeners=new Set,this.initializationPromise=null,this._configured=!1,this._boundHandleVisibilityChange=this._handleVisibilityChange.bind(this),this._boundHandlePageShow=this._handlePageShow.bind(this),this._boundHandleOnline=this._handleOnline.bind(this),c.instance=this}configure(e={}){if(!e.bootstrapUrl||"string"!=typeof e.bootstrapUrl||!e.bootstrapUrl.trim())throw new r("bootstrapUrl is required and must be a non-empty string",{bootstrapUrl:e.bootstrapUrl});if(void 0!==e.refreshInterval){if("number"!=typeof e.refreshInterval||!isFinite(e.refreshInterval))throw new r("refreshInterval must be a finite number",{refreshInterval:e.refreshInterval});if(e.refreshInterval<=0)throw new r("refreshInterval must be positive",{refreshInterval:e.refreshInterval})}if(void 0!==e.tokenExpiry){if("number"!=typeof e.tokenExpiry||!isFinite(e.tokenExpiry))throw new r("tokenExpiry must be a finite number",{tokenExpiry:e.tokenExpiry});if(e.tokenExpiry<=0)throw new r("tokenExpiry must be positive",{tokenExpiry:e.tokenExpiry})}if(void 0!==e.maxRetries&&("number"!=typeof e.maxRetries||!Number.isInteger(e.maxRetries)||e.maxRetries<1))throw new r("maxRetries must be a positive integer",{maxRetries:e.maxRetries});this.config={...this.config,...e,credentials:void 0===e.credentials||e.credentials,logLevel:e.logLevel||"WARN"},d.setLevel(this.config.logLevel),this._configured=!0,this.config.refreshInterval&&this.config.tokenExpiry&&this.config.refreshInterval>=this.config.tokenExpiry&&d.warn("refreshInterval should be less than tokenExpiry to prevent race conditions")}async initialize(e=!1){if("undefined"==typeof window)throw new h("Cannot initialize in non-browser environment (SSR)");if(this._configured||d.warn("initialize() called before configure(). Using default config."),this.state.initializationFailed)throw d.warn("Initialization previously failed. Reload page to retry."),new o("Initialization failed after max retries. Reload page to retry.");if(this.initializationPromise)return d.debug("Initialization already in progress, waiting..."),this.initializationPromise;if(this.state.isInitialized&&!e)return d.debug("Session already initialized"),{token:this.getToken(this.config.tokenCookieName)};this.state.isLoading=!0,this.state.error=null,this.notifyListeners(),this.initializationPromise=(async()=>{let e;for(let i=1;i<=this.config.maxRetries;i++)try{const e=this.generateSessionId();d.info(`Calling bootstrap API (attempt ${i}/${this.config.maxRetries}):`,this.config.bootstrapUrl);const t=new AbortController,s=setTimeout(()=>t.abort(),this.config.bootstrapTimeout);let n,r;try{n=await fetch(this.config.bootstrapUrl,{method:"GET",credentials:this.config.credentials?"include":"same-origin",signal:t.signal,headers:{"cf-session-id":e,"x-client-platform":"web",...this.config.headers}})}catch(e){if("AbortError"===e.name)throw new a(`Bootstrap request timed out after ${this.config.bootstrapTimeout}ms`,{timeout:this.config.bootstrapTimeout,url:this.config.bootstrapUrl});throw e}finally{clearTimeout(s)}if(!n.ok)throw new o(`Bootstrap failed: ${n.status} ${n.statusText}`,{status:n.status,statusText:n.statusText,url:this.config.bootstrapUrl});try{r=await n.json()}catch(e){throw new o("Bootstrap response is not valid JSON",{originalError:e.message})}d.info("Bootstrap successful");const h=r.refresh_time?1e3*r.refresh_time:this.config.refreshInterval,l=r.expire_time?1e3*r.expire_time:this.config.tokenExpiry,f=r[this.config.tokenCookieName];return f&&this.setCookie(this.config.tokenCookieName,f,l/1e3),this.setCfSessionId(e,l/1e3),this.state.isInitialized=!0,this.state.isLoading=!1,this.state.lastRefreshTime=Date.now(),this.state.nextRefreshTime=Date.now()+h,this.state.tokenExpiry=l,this.state.error=null,this.startTokenRefresh(h),this.notifyListeners(),r}catch(t){if(e=t instanceof Error?t:new a("Unknown error occurred",{error:t}),d.error(`Bootstrap attempt ${i} failed:`,e.message),i<this.config.maxRetries){const e=Math.min(1e3*Math.pow(2,i-1),5e3);d.info(`Retrying in ${e}ms...`),await new Promise(i=>setTimeout(i,e))}}throw d.error("All bootstrap attempts failed"),this.state.isLoading=!1,this.state.error=e?.message||"Unknown error",this.state.errorCode=e?.code||"UNKNOWN_ERROR",this.state.initializationFailed=!0,this.notifyListeners(),e})();try{return await this.initializationPromise}finally{this.initializationPromise=null}}isRefreshing(){return null!==this.initializationPromise}async waitForRefresh(){return this.initializationPromise?(d.debug("Waiting for ongoing refresh to complete"),this.initializationPromise):Promise.resolve()}_isTokenLikelyExpired(){return!this.state.lastRefreshTime||!this.state.tokenExpiry||Date.now()>=this.state.lastRefreshTime+this.state.tokenExpiry}async ensureReady(){if(!this.state.isInitialized||this._isTokenLikelyExpired())if(this.initializationPromise)try{await this.initializationPromise}catch(e){d.debug("ensureReady: in-flight initialization failed, proceeding with request")}else if(this._configured&&!this.state.initializationFailed)try{await this.initialize(this.state.isInitialized)}catch(e){d.debug("ensureReady: initialization failed, proceeding with request")}}needsRefresh(){return this.state.isInitialized&&this._isTokenLikelyExpired()}setCfSessionId(e,i){this.state.cfSessionId=e;const t=i||this.config.tokenExpiry/1e3;this.setCookie("cf-session-id",e,t)}generateSessionId(){return"undefined"!=typeof crypto&&crypto.randomUUID?crypto.randomUUID():`${Date.now()}-${Math.random().toString(36).substr(2,9)}`}getSessionId(){return this.state.cfSessionId}shouldUseTokenHeader(){return"undefined"!=typeof window&&"http:"===window.location.protocol}_sanitizeCookieName(e){return e.replace(/[^a-zA-Z0-9_-]/g,"")}getCookie(e){if("undefined"==typeof document)return null;const i=this._sanitizeCookieName(e);if(!i)return null;const t=`; ${document.cookie}`.split(`; ${i}=`);if(2===t.length)try{return decodeURIComponent(t.pop().split(";").shift())}catch(e){return d.error("Failed to decode cookie value:",e instanceof Error?e.message:String(e)),null}return null}getToken(e=this.config.tokenCookieName){return this.getCookie(e)}setCookie(e,i,t){if("undefined"==typeof document)return void d.warn("Cannot set cookie in non-browser environment");const s=this._sanitizeCookieName(e);if(!s)return void d.error("Invalid cookie name provided");const n=new Date(Date.now()+1e3*t).toUTCString(),r="undefined"!=typeof window&&"https:"===window.location.protocol,o=r?"; Secure":"",a=encodeURIComponent(i);document.cookie=`${s}=${a}; path=/; expires=${n}; SameSite=Lax${o}`,d.debug(`Cookie set: ${s}, expires in ${t}s${r?" (Secure)":""} [WARNING: Not HttpOnly]`)}startTokenRefresh(e=this.config.refreshInterval){this.stopTokenRefresh(),d.info(`Scheduling token refresh in ${e/1e3} seconds`),this.refreshTimerId=setTimeout(async()=>{d.info("Auto-refreshing token...");try{await this.refreshToken()}catch(i){const t=i instanceof Error?i.message:String(i),s=i?.code||"UNKNOWN_ERROR";d.error("Auto-refresh failed:",t),this.state.error=t,this.state.errorCode=s,this.notifyListeners();const n=Math.min(2*e,6e4);d.info(`Scheduling auto-refresh retry in ${n/1e3}s`),this.startTokenRefresh(n)}},e),this._startVisibilityListener(),this._startOnlineListener()}stopTokenRefresh(){this.refreshTimerId&&(clearTimeout(this.refreshTimerId),this.refreshTimerId=null,d.debug("Token refresh timer stopped")),this._stopVisibilityListener(),this._stopOnlineListener()}_startVisibilityListener(){"undefined"!=typeof document&&(document.addEventListener("visibilitychange",this._boundHandleVisibilityChange),"undefined"!=typeof window&&window.addEventListener("pageshow",this._boundHandlePageShow))}_stopVisibilityListener(){"undefined"!=typeof document&&(document.removeEventListener("visibilitychange",this._boundHandleVisibilityChange),"undefined"!=typeof window&&window.removeEventListener("pageshow",this._boundHandlePageShow))}_refreshIfOverdue(){if(!this.state.isInitialized||this.state.initializationFailed)return!1;if(this.isRefreshing())return!1;const e=Date.now(),{nextRefreshTime:i}=this.state;return!!(i&&e>=i)&&(d.info("Missed scheduled refresh — refreshing token now"),this.refreshToken().catch(e=>{const i=e instanceof Error?e.message:String(e);d.error("Triggered refresh failed:",i)}),!0)}_handleVisibilityChange(){"visible"===document.visibilityState&&this._refreshIfOverdue()}_handlePageShow(e){e.persisted&&this.state.isInitialized&&!this.state.initializationFailed&&(this.isRefreshing()||(d.info("Page restored from bfcache — forcing bootstrap"),this.refreshToken().catch(e=>{const i=e instanceof Error?e.message:String(e);d.error("bfcache-triggered refresh failed:",i)})))}_startOnlineListener(){"undefined"!=typeof window&&window.addEventListener("online",this._boundHandleOnline)}_stopOnlineListener(){"undefined"!=typeof window&&window.removeEventListener("online",this._boundHandleOnline)}_handleOnline(){this.state.initializationFailed&&(d.info("Network restored — retrying session initialization"),this.refreshToken().catch(e=>{const i=e instanceof Error?e.message:String(e);d.error("Online-triggered refresh failed:",i)}))}async refreshToken(){return d.info("Manual token refresh triggered"),this.state.initializationFailed=!1,this.initialize(!0)}getSessionStatus(){return{isInitialized:this.state.isInitialized,isLoading:this.state.isLoading,lastRefreshTime:this.state.lastRefreshTime,nextRefreshTime:this.state.nextRefreshTime,tokenExpiry:this.state.tokenExpiry,error:this.state.error,errorCode:this.state.errorCode,initializationFailed:this.state.initializationFailed,timeUntilRefresh:this.state.nextRefreshTime?Math.max(0,this.state.nextRefreshTime-Date.now()):null}}subscribe(e){if("function"!=typeof e)throw new TypeError("Listener must be a function");return this.listeners.add(e),()=>{this.listeners.delete(e)}}notifyListeners(){const e=this.getSessionStatus();Array.from(this.listeners).forEach(i=>{try{i(e)}catch(e){const i=e instanceof Error?e.message:String(e);d.error("Listener error:",i)}})}destroy(){this.stopTokenRefresh(),this.initializationPromise=null,this.listeners.clear(),this.setCookie("cf-session-id","",-1),this.setCookie(this.config.tokenCookieName,"",-1),this.config={...f},this.state={isInitialized:!1,isLoading:!1,lastRefreshTime:null,error:null,errorCode:null,nextRefreshTime:null,tokenExpiry:null,cfSessionId:null,initializationFailed:!1},this._configured=!1,d.info("Destroyed")}static getInstance(){return c.instance||(c.instance=new c),c.instance}}function u(n={}){const{autoInitialize:r=!0}=n,o=c.getInstance(),[a,h]=e(()=>o.getSessionStatus()),[l,f]=e(()=>{const{nextRefreshTime:e}=o.getSessionStatus();return e?Math.max(0,e-Date.now()):null}),u=i(a.nextRefreshTime);t(()=>o.subscribe(e=>{h(e),u.current=e.nextRefreshTime}),[]),t(()=>{if(!a.nextRefreshTime)return void f(null);f(Math.max(0,a.nextRefreshTime-Date.now()));const e=setInterval(()=>{const e=u.current;f(e?Math.max(0,e-Date.now()):null)},1e3);return()=>clearInterval(e)},[a.nextRefreshTime]),t(()=>{!r||a.isInitialized||a.isLoading||a.initializationFailed||o.initialize().catch(e=>{d.error("Auto-initialization failed:",e)})},[r,a.isInitialized,a.isLoading,a.initializationFailed]);const g=s(async()=>{try{await o.refreshToken()}catch(e){throw d.error("Manual refresh failed:",e),e}},[]),m=s(async()=>{try{await o.initialize()}catch(e){throw d.error("Manual initialization failed:",e),e}},[]);return{isInitialized:a.isInitialized,isLoading:a.isLoading,error:a.error,lastRefreshTime:a.lastRefreshTime,nextRefreshTime:a.nextRefreshTime,timeUntilRefresh:l,initializationFailed:a.initializationFailed,refresh:g,initialize:m}}const g="web";async function m(e,i={}){const t=c.getInstance();await t.ensureReady();const s={...i,credentials:i.credentials||"include",headers:{...i.headers,"cf-session-id":t.getSessionId(),"x-client-platform":g}};if(t.shouldUseTokenHeader()){const e=t.getToken(t.config.tokenCookieName);e&&(s.headers[t.config.tokenCookieName]=e)}let n=await fetch(e,s);if(401===n.status){const i=n.clone();try{if((await i.json()).error_msg===t.config.invalidSessionError){if(t.isRefreshing()?await t.waitForRefresh():await t.refreshToken(),s.headers["cf-session-id"]=t.getSessionId(),t.shouldUseTokenHeader()){const e=t.getToken(t.config.tokenCookieName);e&&(s.headers[t.config.tokenCookieName]=e)}n=await fetch(e,s)}}catch(e){}}return n}function p(e){const i=c.getInstance();return void 0!==e._gwSessionRequestId&&e.interceptors.request.eject(e._gwSessionRequestId),void 0!==e._gwSessionResponseId&&e.interceptors.response.eject(e._gwSessionResponseId),e._gwSessionRequestId=e.interceptors.request.use(async e=>{if(await i.ensureReady(),i.config.credentials&&(e.withCredentials=!0),e.headers["cf-session-id"]=i.getSessionId(),e.headers["x-client-platform"]=g,i.shouldUseTokenHeader()){const t=i.getToken(i.config.tokenCookieName);t&&(e.headers[i.config.tokenCookieName]=t)}return e},e=>Promise.reject(e)),e._gwSessionResponseId=e.interceptors.response.use(e=>e,async t=>{const s=t.config;if(401===t.response?.status&&!s._retry&&t.response?.data?.error_msg===i.config.invalidSessionError){if(s._retry=!0,i.isRefreshing()?await i.waitForRefresh():await i.refreshToken(),s.headers["cf-session-id"]=i.getSessionId(),s.headers["x-client-platform"]=g,i.shouldUseTokenHeader()){const e=i.getToken(i.config.tokenCookieName);e&&(s.headers[i.config.tokenCookieName]=e)}return e(s)}return Promise.reject(t)}),e}export{o as BootstrapError,g as CLIENT_PLATFORM,r as ConfigurationError,l as LOG_LEVELS,a as NetworkError,h as SSRError,n as SessionError,c as SessionManager,c as default,m as fetchInterceptor,d as logger,p as setupAxiosInterceptor,u as useSession};
|
|
2
2
|
//# sourceMappingURL=index.esm.js.map
|
package/dist/index.esm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.esm.js","sources":["../src/errors.js","../src/logger.js","../src/SessionManager.js","../src/hooks/useSession.js","../src/interceptors.js"],"sourcesContent":["/**\n * Custom error classes for SessionManager\n */\n\nexport class SessionError extends Error {\n constructor(message, code, details = {}) {\n super(message);\n this.name = 'SessionError';\n this.code = code;\n this.details = details;\n \n // Maintains proper stack trace for where error was thrown\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor);\n }\n \n // Set the prototype explicitly for proper instanceof checks\n Object.setPrototypeOf(this, SessionError.prototype);\n }\n}\n\nexport class ConfigurationError extends SessionError {\n constructor(message, details) {\n super(message, 'CONFIGURATION_ERROR', details);\n this.name = 'ConfigurationError';\n }\n}\n\nexport class BootstrapError extends SessionError {\n constructor(message, details) {\n super(message, 'BOOTSTRAP_ERROR', details);\n this.name = 'BootstrapError';\n }\n}\n\nexport class NetworkError extends SessionError {\n constructor(message, details) {\n super(message, 'NETWORK_ERROR', details);\n this.name = 'NetworkError';\n }\n}\n\nexport class SSRError extends SessionError {\n constructor(message, details = {}) {\n super(message, 'SSR_ERROR', details);\n this.name = 'SSRError';\n }\n}\n","/**\n * Logger utility with configurable log levels\n */\n\nconst LOG_LEVELS = {\n NONE: 0,\n ERROR: 1,\n WARN: 2,\n INFO: 3,\n DEBUG: 4\n};\n\nclass Logger {\n constructor() {\n this.level = LOG_LEVELS.WARN; // Default to WARN in production\n }\n\n setLevel(level) {\n if (typeof level === 'string') {\n this.level = LOG_LEVELS[level.toUpperCase()] ?? LOG_LEVELS.WARN;\n } else {\n this.level = level;\n }\n }\n\n error(...args) {\n if (this.level >= LOG_LEVELS.ERROR) {\n console.error('[SessionManager]', ...args);\n }\n }\n\n warn(...args) {\n if (this.level >= LOG_LEVELS.WARN) {\n console.warn('[SessionManager]', ...args);\n }\n }\n\n info(...args) {\n if (this.level >= LOG_LEVELS.INFO) {\n console.info('[SessionManager]', ...args);\n }\n }\n\n debug(...args) {\n if (this.level >= LOG_LEVELS.DEBUG) {\n console.debug('[SessionManager]', ...args);\n }\n }\n}\n\nexport const logger = new Logger();\nexport { LOG_LEVELS };\n","import {BootstrapError, ConfigurationError, NetworkError, SSRError} from './errors.js';\nimport {logger} from './logger.js';\nconst CLIENT_PLATFORM = 'web';\n/**\n * SessionManager - Core session token management class\n * Handles session bootstrap, automatic token refresh, and lifecycle management\n */\nclass SessionManager {\n constructor() {\n if (SessionManager.instance) {\n return SessionManager.instance;\n }\n\n this.config = {\n bootstrapUrl: '/session/bootstrap',\n refreshInterval: 25 * 60 * 1000, // 25 minutes in milliseconds\n tokenExpiry: 30 * 60 * 1000, // 30 minutes in milliseconds\n maxRetries: 3,\n tokenCookieName: 'token', // Cookie name to read token from\n };\n\n this.state = {\n isInitialized: false,\n isLoading: false,\n lastRefreshTime: null,\n error: null,\n errorCode: null,\n nextRefreshTime: null,\n tokenExpiry: null,\n cfSessionId: null,\n initializationFailed: false,\n };\n\n this.refreshTimerId = null;\n this.tickTimerId = null;\n this.listeners = new Set();\n this.initializationPromise = null;\n\n SessionManager.instance = this;\n }\n\n /**\n * Configure the session manager\n * @param {Object} config - Configuration options\n * @param {string} config.bootstrapUrl - URL for bootstrap API endpoint\n * @param {number} config.refreshInterval - Interval for token refresh in milliseconds (default: 25 mins)\n * @param {number} config.tokenExpiry - Token expiry time in milliseconds (default: 30 mins)\n * @param {number} config.maxRetries - Maximum retry attempts on failure (default: 3)\n * @param {Object} config.headers - Additional headers for API calls\n * @param {boolean} config.credentials - Include credentials in requests (default: true)\n * @param {string} config.tokenCookieName - Cookie name to read token from (default: 'token')\n */\n configure(config = {}) {\n if (!config.bootstrapUrl || typeof config.bootstrapUrl !== 'string' || !config.bootstrapUrl.trim()) {\n throw new ConfigurationError('bootstrapUrl is required and must be a non-empty string', { bootstrapUrl: config.bootstrapUrl });\n }\n \n if (config.refreshInterval !== undefined) {\n if (typeof config.refreshInterval !== 'number' || !isFinite(config.refreshInterval)) {\n throw new ConfigurationError('refreshInterval must be a finite number', { refreshInterval: config.refreshInterval });\n }\n if (config.refreshInterval <= 0) {\n throw new ConfigurationError('refreshInterval must be positive', { refreshInterval: config.refreshInterval });\n }\n }\n \n if (config.tokenExpiry !== undefined) {\n if (typeof config.tokenExpiry !== 'number' || !isFinite(config.tokenExpiry)) {\n throw new ConfigurationError('tokenExpiry must be a finite number', { tokenExpiry: config.tokenExpiry });\n }\n if (config.tokenExpiry <= 0) {\n throw new ConfigurationError('tokenExpiry must be positive', { tokenExpiry: config.tokenExpiry });\n }\n }\n\n this.config = {\n ...this.config,\n ...config,\n credentials: config.credentials !== undefined ? config.credentials : true,\n logLevel: config.logLevel || 'WARN'\n };\n\n logger.setLevel(this.config.logLevel);\n\n if (this.config.refreshInterval && this.config.tokenExpiry && \n this.config.refreshInterval >= this.config.tokenExpiry) {\n logger.warn('refreshInterval should be less than tokenExpiry to prevent race conditions');\n }\n }\n\n /**\n * Initialize session by calling bootstrap API\n * @param {boolean} skipCookieCheck - Skip session_initialized cookie check (for auto-refresh)\n * @returns {Promise<Object>} Bootstrap response\n */\n async initialize(skipCookieCheck = false) {\n if (typeof window === 'undefined') {\n throw new SSRError('Cannot initialize in non-browser environment (SSR)');\n }\n \n if (this.state.initializationFailed) {\n logger.warn('Initialization previously failed. Reload page to retry.');\n throw new BootstrapError('Initialization failed after max retries. Reload page to retry.');\n }\n \n // Return existing promise if refresh already in progress\n if (this.initializationPromise) {\n logger.debug('Initialization already in progress, waiting...');\n return this.initializationPromise;\n }\n\n // Check if session is already initialized via cookie (only on page load)\n if (!skipCookieCheck) {\n const sessionInitialized = this.getCookie('session_initialized');\n if (sessionInitialized === 'true') {\n logger.info('Session already initialized, skipping bootstrap API call');\n this.state.isInitialized = true;\n this.state.tokenExpiry = this.config.tokenExpiry;\n\n // Restore cf-session-id from cookie\n const storedSessionId = this.getCookie('cf-session-id');\n if (storedSessionId) {\n this.state.cfSessionId = storedSessionId;\n } else {\n this.setCfSessionId(this.generateSessionId());\n }\n\n // Calculate remaining time from stored refresh timestamp\n const refreshAt = parseInt(this.getCookie('session_refresh_at'), 10);\n let remaining;\n if (refreshAt && !isNaN(refreshAt)) {\n remaining = Math.max(0, refreshAt - Date.now());\n } else {\n remaining = this.config.refreshInterval;\n }\n\n this.state.lastRefreshTime = Date.now();\n this.state.nextRefreshTime = Date.now() + remaining;\n\n this.startTokenRefresh(remaining);\n this.notifyListeners();\n return { token: this.getToken(this.config.tokenCookieName) };\n }\n }\n\n this.state.isLoading = true;\n this.state.error = null;\n this.notifyListeners();\n\n this.initializationPromise = (async () => {\n let lastError;\n \n for (let attempt = 1; attempt <= this.config.maxRetries; attempt++) {\n try {\n // Generate new cf-session-id for each bootstrap call\n const newSessionId = this.generateSessionId();\n this.setCfSessionId(newSessionId);\n\n logger.info(`Calling bootstrap API (attempt ${attempt}/${this.config.maxRetries}):`, this.config.bootstrapUrl);\n\n const response = await fetch(this.config.bootstrapUrl, {\n method: 'GET',\n credentials: this.config.credentials ? 'include' : 'same-origin',\n headers: {\n 'Content-Type': 'application/json',\n 'cf-session-id': this.state.cfSessionId,\n 'x-client-platform': CLIENT_PLATFORM,\n ...this.config.headers,\n },\n });\n\n if (!response.ok) {\n throw new BootstrapError(`Bootstrap failed: ${response.status} ${response.statusText}`, {\n status: response.status,\n statusText: response.statusText,\n url: this.config.bootstrapUrl\n });\n }\n\n let data;\n try {\n data = await response.json();\n } catch (jsonError) {\n throw new BootstrapError('Bootstrap response is not valid JSON', { originalError: jsonError.message });\n }\n logger.info('Bootstrap successful');\n\n // Check if server set a cookie\n const setCookieHeader = response.headers.get('set-cookie');\n const serverSetCookie = setCookieHeader && setCookieHeader.includes(this.config.tokenCookieName);\n\n // Use refresh_time from response (in seconds) or fall back to config\n const refreshInterval = data.refresh_time \n ? data.refresh_time * 1000 \n : this.config.refreshInterval;\n\n // Calculate token expiry from response (in seconds) or fall back to config\n const tokenExpiry = data.expire_time\n ? data.expire_time * 1000\n : this.config.tokenExpiry;\n\n if (data.token && !serverSetCookie) {\n logger.debug('Server did not set cookie, setting from SDK');\n this.setCookie(this.config.tokenCookieName, data.token, tokenExpiry / 1000);\n } else if (serverSetCookie) {\n logger.debug('Server set cookie, skipping SDK cookie logic');\n }\n\n // Re-set cf-session-id cookie with actual expire_time from response\n this.setCfSessionId(this.state.cfSessionId, tokenExpiry / 1000);\n\n this.state.isInitialized = true;\n this.state.isLoading = false;\n this.state.lastRefreshTime = Date.now();\n this.state.nextRefreshTime = Date.now() + refreshInterval;\n this.state.tokenExpiry = tokenExpiry;\n this.state.error = null;\n\n // Set session_initialized cookie (expires at refresh interval time)\n this.setCookie('session_initialized', 'true', refreshInterval / 1000);\n\n // Store next refresh timestamp for reload recovery\n this.setCookie('session_refresh_at', String(Date.now() + refreshInterval), refreshInterval / 1000);\n\n // Start automatic refresh timer with dynamic interval\n this.startTokenRefresh(refreshInterval);\n\n this.notifyListeners();\n return data;\n } catch (error) {\n lastError = error instanceof Error ? error : new NetworkError('Unknown error occurred', { error });\n logger.error(`Bootstrap attempt ${attempt} failed:`, lastError.message);\n \n if (attempt < this.config.maxRetries) {\n const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);\n logger.info(`Retrying in ${delay}ms...`);\n await new Promise(resolve => setTimeout(resolve, delay));\n }\n }\n }\n \n logger.error('All bootstrap attempts failed');\n this.state.isLoading = false;\n this.state.error = lastError?.message || 'Unknown error';\n this.state.errorCode = lastError?.code || 'UNKNOWN_ERROR';\n this.state.initializationFailed = true;\n this.notifyListeners();\n throw lastError;\n })();\n\n try {\n return await this.initializationPromise;\n } finally {\n this.initializationPromise = null;\n }\n }\n\n /**\n * Check if token refresh is currently in progress\n * @returns {boolean} True if refresh is in progress\n */\n isRefreshing() {\n return this.initializationPromise !== null;\n }\n\n /**\n * Wait for ongoing refresh to complete\n * @returns {Promise<Object>} Bootstrap response from ongoing refresh\n */\n async waitForRefresh() {\n if (this.initializationPromise) {\n logger.debug('Waiting for ongoing refresh to complete');\n return this.initializationPromise;\n }\n return Promise.resolve();\n }\n\n /**\n * Set cf-session-id in cookie\n * @param {string} sessionId - Session ID\n */\n setCfSessionId(sessionId, maxAge) {\n this.state.cfSessionId = sessionId;\n const cookieMaxAge = maxAge || this.config.tokenExpiry / 1000;\n this.setCookie('cf-session-id', sessionId, cookieMaxAge);\n }\n\n /**\n * Generate unique session ID\n * @returns {string} Generated session ID\n */\n generateSessionId() {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n }\n\n /**\n * Get cf-session-id for requests\n * @returns {string} Current cf-session-id\n */\n getSessionId() {\n return this.state.cfSessionId;\n }\n\n /**\n * Check if token should be added to headers (HTTP or cross-origin)\n * @returns {boolean} True if token should be added to headers\n */\n shouldUseTokenHeader() {\n if (typeof window === 'undefined') return false;\n return window.location.protocol === 'http:';\n }\n\n /**\n * Get cookie value\n * @param {string} name - Cookie name\n * @returns {string|null} Cookie value or null\n */\n getCookie(name) {\n if (typeof document === 'undefined') return null;\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${name}=`);\n if (parts.length === 2) {\n try {\n return decodeURIComponent(parts.pop().split(';').shift());\n } catch (error) {\n logger.error('Failed to decode cookie value:', error instanceof Error ? error.message : String(error));\n return null;\n }\n }\n return null;\n }\n\n /**\n * Get token from cookie\n * @param {string} name - Cookie name (default: configured tokenCookieName)\n * @returns {string|null} Token value or null\n */\n getToken(name = this.config.tokenCookieName) {\n return this.getCookie(name);\n }\n\n /**\n * Set cookie from client-side (WARNING: Not HttpOnly, less secure than server-set cookies)\n * @param {string} name - Cookie name\n * @param {string} value - Cookie value\n * @param {number} maxAge - Max age in seconds\n */\n setCookie(name, value, maxAge) {\n if (typeof document === 'undefined') {\n logger.warn('Cannot set cookie in non-browser environment');\n return;\n }\n \n // Sanitize cookie name to prevent XSS\n const sanitizedName = name.replace(/[^a-zA-Z0-9_-]/g, '');\n if (!sanitizedName) {\n logger.error('Invalid cookie name provided');\n return;\n }\n \n const expires = new Date(Date.now() + maxAge * 1000).toUTCString();\n const isSecure = typeof window !== 'undefined' && window.location.protocol === 'https:';\n const secureFlag = isSecure ? '; Secure' : '';\n const encodedValue = encodeURIComponent(value);\n document.cookie = `${sanitizedName}=${encodedValue}; path=/; expires=${expires}; SameSite=Lax${secureFlag}`;\n logger.debug(`Cookie set: ${sanitizedName}, expires in ${maxAge}s${isSecure ? ' (Secure)' : ''} [WARNING: Not HttpOnly]`);\n }\n\n /**\n * Start automatic token refresh timer\n * @param {number} interval - Refresh interval in milliseconds\n */\n startTokenRefresh(interval = this.config.refreshInterval) {\n this.stopTokenRefresh();\n logger.info(`Scheduling token refresh in ${interval / 1000} seconds`);\n\n this.refreshTimerId = setTimeout(async () => {\n logger.info('Auto-refreshing token...');\n try {\n await this.refreshToken();\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n const errorCode = error?.code || 'UNKNOWN_ERROR';\n logger.error('Auto-refresh failed:', errorMessage);\n this.state.error = errorMessage;\n this.state.errorCode = errorCode;\n this.notifyListeners();\n }\n }, interval);\n\n this.startTickTimer();\n }\n\n /**\n * Stop automatic token refresh timer\n */\n stopTokenRefresh() {\n if (this.refreshTimerId) {\n clearTimeout(this.refreshTimerId);\n this.refreshTimerId = null;\n logger.debug('Token refresh timer stopped');\n }\n this.stopTickTimer();\n }\n\n /**\n * Start periodic tick timer to notify listeners for countdown updates\n */\n startTickTimer() {\n this.stopTickTimer();\n this.tickTimerId = setInterval(() => {\n this.notifyListeners();\n }, 1000);\n }\n\n /**\n * Stop periodic tick timer\n */\n stopTickTimer() {\n if (this.tickTimerId) {\n clearInterval(this.tickTimerId);\n this.tickTimerId = null;\n }\n }\n\n /**\n * Manually refresh the session token\n * @returns {Promise<Object>} Bootstrap response\n */\n async refreshToken() {\n logger.info('Manual token refresh triggered');\n this.state.initializationFailed = false;\n return this.initialize(true);\n }\n\n /**\n * Get current session status\n * @returns {Object} Current session state\n */\n getSessionStatus() {\n return {\n isInitialized: this.state.isInitialized,\n isLoading: this.state.isLoading,\n lastRefreshTime: this.state.lastRefreshTime,\n nextRefreshTime: this.state.nextRefreshTime,\n tokenExpiry: this.state.tokenExpiry,\n error: this.state.error,\n errorCode: this.state.errorCode,\n initializationFailed: this.state.initializationFailed,\n timeUntilRefresh: this.state.nextRefreshTime\n ? Math.max(0, this.state.nextRefreshTime - Date.now())\n : null,\n };\n }\n\n /**\n * Subscribe to session state changes\n * @param {Function} listener - Callback function to be called on state changes\n * @returns {Function} Unsubscribe function\n */\n subscribe(listener) {\n if (typeof listener !== 'function') {\n throw new TypeError('Listener must be a function');\n }\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Notify all listeners of state changes\n */\n notifyListeners() {\n const status = this.getSessionStatus();\n // Use Array.from to create a snapshot, preventing issues if listeners modify the Set\n Array.from(this.listeners).forEach(listener => {\n try {\n listener(status);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n logger.error('Listener error:', errorMessage);\n }\n });\n }\n\n /**\n * Clean up and reset the session manager\n */\n destroy() {\n this.stopTokenRefresh();\n this.listeners.clear();\n this.state = {\n isInitialized: false,\n isLoading: false,\n lastRefreshTime: null,\n error: null,\n errorCode: null,\n nextRefreshTime: null,\n tokenExpiry: null,\n cfSessionId: null,\n initializationFailed: false,\n };\n // Clear cookies\n this.setCookie('cf-session-id', '', -1);\n this.setCookie('session_initialized', '', -1);\n this.setCookie('session_refresh_at', '', -1);\n logger.info('Destroyed');\n }\n\n /**\n * Get the singleton instance\n * @returns {SessionManager} Singleton instance\n */\n static getInstance() {\n if (!SessionManager.instance) {\n SessionManager.instance = new SessionManager();\n }\n return SessionManager.instance;\n }\n}\n\nexport default SessionManager;\n","import { useState, useEffect, useCallback } from 'react';\nimport SessionManager from '../SessionManager.js';\nimport { logger } from '../logger.js';\n\n/**\n * React hook for session management\n * Provides session state and controls for React components\n * \n * @param {Object} options - Configuration options\n * @param {boolean} options.autoInitialize - Automatically initialize on mount (default: true)\n * @returns {Object} Session state and controls\n */\nexport function useSession(options = {}) {\n const { autoInitialize = true } = options;\n\n const sessionManager = SessionManager.getInstance();\n\n const [sessionState, setSessionState] = useState(() =>\n sessionManager.getSessionStatus()\n );\n\n // Subscribe to session state changes\n useEffect(() => {\n const unsubscribe = sessionManager.subscribe((newState) => {\n setSessionState(newState);\n });\n\n return unsubscribe;\n }, []); // sessionManager is a singleton, no need to include in deps\n\n // Auto-initialize if enabled\n useEffect(() => {\n if (autoInitialize && !sessionState.isInitialized && !sessionState.isLoading && !sessionState.initializationFailed) {\n sessionManager.initialize().catch(error => {\n logger.error('Auto-initialization failed:', error);\n });\n }\n }, [autoInitialize, sessionState.isInitialized, sessionState.isLoading, sessionState.initializationFailed]);\n\n // Manual refresh function\n const refresh = useCallback(async () => {\n try {\n await sessionManager.refreshToken();\n } catch (error) {\n logger.error('Manual refresh failed:', error);\n throw error;\n }\n }, []);\n\n // Initialize function (for manual control)\n const initialize = useCallback(async () => {\n try {\n await sessionManager.initialize();\n } catch (error) {\n logger.error('Manual initialization failed:', error);\n throw error;\n }\n }, []);\n\n return {\n // State\n isInitialized: sessionState.isInitialized,\n isLoading: sessionState.isLoading,\n error: sessionState.error,\n lastRefreshTime: sessionState.lastRefreshTime,\n nextRefreshTime: sessionState.nextRefreshTime,\n timeUntilRefresh: sessionState.timeUntilRefresh,\n initializationFailed: sessionState.initializationFailed,\n\n // Actions\n refresh,\n initialize,\n };\n}\n\nexport default useSession;\n","import SessionManager from './SessionManager.js';\nexport const CLIENT_PLATFORM = 'web';\n\n/**\n * Fetch interceptor with automatic token refresh on 401\n * @param {string} url - Request URL\n * @param {Object} options - Fetch options\n * @returns {Promise<Response>} Fetch response\n */\nexport async function fetchInterceptor(url, options = {}) {\n const sessionManager = SessionManager.getInstance();\n \n // Ensure credentials are included to send cookies\n options.credentials = options.credentials || 'include';\n \n // Add headers\n options.headers = {\n ...options.headers,\n 'cf-session-id': sessionManager.getSessionId(),\n 'x-client-platform': CLIENT_PLATFORM,\n };\n \n // Add token to header if HTTP (not HTTPS)\n if (sessionManager.shouldUseTokenHeader()) {\n const token = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (token) {\n options.headers[sessionManager.config.tokenCookieName] = token;\n }\n }\n \n let response = await fetch(url, options);\n \n // Retry once if unauthorized with INVALID_SESSION\n if (response.status === 401) {\n const clonedResponse = response.clone();\n try {\n const data = await clonedResponse.json();\n if (data.error_msg === 'INVALID_SESSION') {\n if (sessionManager.isRefreshing()) {\n await sessionManager.waitForRefresh();\n } else {\n const existingToken = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (!existingToken) {\n await sessionManager.refreshToken();\n }\n }\n options.headers['cf-session-id'] = sessionManager.getSessionId();\n options.headers['x-client-platform'] = CLIENT_PLATFORM;\n if (sessionManager.shouldUseTokenHeader()) {\n const newToken = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (newToken) {\n options.headers[sessionManager.config.tokenCookieName] = newToken;\n }\n }\n response = await fetch(url, options);\n }\n } catch (e) {\n // Not JSON or parsing failed, return original response\n }\n }\n \n return response;\n}\n\n/**\n * Axios interceptor with automatic token refresh on 401 and INVALID_SESSION\n * @param {Object} axiosInstance - Axios instance\n */\nexport function setupAxiosInterceptor(axiosInstance) {\n const sessionManager = SessionManager.getInstance();\n \n // Request interceptor to add cf-session-id and token\n axiosInstance.interceptors.request.use(\n (config) => {\n config.headers['cf-session-id'] = sessionManager.getSessionId();\n config.headers['x-client-platform'] = CLIENT_PLATFORM;\n if (sessionManager.shouldUseTokenHeader()) {\n const token = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (token) {\n config.headers[sessionManager.config.tokenCookieName] = token;\n }\n }\n return config;\n },\n (error) => Promise.reject(error)\n );\n \n // Response interceptor for token refresh\n axiosInstance.interceptors.response.use(\n (response) => response,\n async (error) => {\n const originalRequest = error.config;\n \n // Retry once if unauthorized with INVALID_SESSION\n if (error.response?.status === 401 && !originalRequest._retry) {\n if (error.response?.data?.error_msg === 'INVALID_SESSION') {\n originalRequest._retry = true;\n \n if (sessionManager.isRefreshing()) {\n await sessionManager.waitForRefresh();\n } else {\n const existingToken = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (!existingToken) {\n await sessionManager.refreshToken();\n }\n }\n \n originalRequest.headers['cf-session-id'] = sessionManager.getSessionId();\n originalRequest.headers['x-client-platform'] = CLIENT_PLATFORM;\n if (sessionManager.shouldUseTokenHeader()) {\n const newToken = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (newToken) {\n originalRequest.headers[sessionManager.config.tokenCookieName] = newToken;\n }\n }\n return axiosInstance(originalRequest);\n }\n }\n \n return Promise.reject(error);\n }\n );\n \n return axiosInstance;\n}\n"],"names":["SessionError","Error","constructor","message","code","details","super","this","name","captureStackTrace","Object","setPrototypeOf","prototype","ConfigurationError","BootstrapError","NetworkError","SSRError","LOG_LEVELS","NONE","ERROR","WARN","INFO","DEBUG","logger","level","setLevel","toUpperCase","error","args","console","warn","info","debug","SessionManager","instance","config","bootstrapUrl","refreshInterval","tokenExpiry","maxRetries","tokenCookieName","state","isInitialized","isLoading","lastRefreshTime","errorCode","nextRefreshTime","cfSessionId","initializationFailed","refreshTimerId","tickTimerId","listeners","Set","initializationPromise","configure","trim","undefined","isFinite","credentials","logLevel","initialize","skipCookieCheck","window","getCookie","storedSessionId","setCfSessionId","generateSessionId","refreshAt","parseInt","remaining","isNaN","Math","max","Date","now","startTokenRefresh","notifyListeners","token","getToken","lastError","attempt","newSessionId","response","fetch","method","headers","ok","status","statusText","url","data","json","jsonError","originalError","setCookieHeader","get","serverSetCookie","includes","refresh_time","expire_time","setCookie","String","delay","min","pow","Promise","resolve","setTimeout","isRefreshing","waitForRefresh","sessionId","maxAge","cookieMaxAge","crypto","randomUUID","random","toString","substr","getSessionId","shouldUseTokenHeader","location","protocol","document","parts","cookie","split","length","decodeURIComponent","pop","shift","value","sanitizedName","replace","expires","toUTCString","isSecure","secureFlag","encodedValue","encodeURIComponent","interval","stopTokenRefresh","async","refreshToken","errorMessage","startTickTimer","clearTimeout","stopTickTimer","setInterval","clearInterval","getSessionStatus","timeUntilRefresh","subscribe","listener","TypeError","add","delete","Array","from","forEach","destroy","clear","getInstance","useSession","options","autoInitialize","sessionManager","sessionState","setSessionState","useState","useEffect","newState","catch","refresh","useCallback","CLIENT_PLATFORM","fetchInterceptor","clonedResponse","clone","error_msg","newToken","e","setupAxiosInterceptor","axiosInstance","interceptors","request","use","reject","originalRequest","_retry"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":["../src/errors.js","../src/logger.js","../src/SessionManager.js","../src/hooks/useSession.js","../src/interceptors.js"],"sourcesContent":["/**\n * Custom error classes for SessionManager\n */\n\nexport class SessionError extends Error {\n constructor(message, code, details = {}) {\n super(message);\n this.name = 'SessionError';\n this.code = code;\n this.details = details;\n \n // Maintains proper stack trace for where error was thrown\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor);\n }\n \n // Set the prototype explicitly for proper instanceof checks\n Object.setPrototypeOf(this, SessionError.prototype);\n }\n}\n\nexport class ConfigurationError extends SessionError {\n constructor(message, details) {\n super(message, 'CONFIGURATION_ERROR', details);\n this.name = 'ConfigurationError';\n }\n}\n\nexport class BootstrapError extends SessionError {\n constructor(message, details) {\n super(message, 'BOOTSTRAP_ERROR', details);\n this.name = 'BootstrapError';\n }\n}\n\nexport class NetworkError extends SessionError {\n constructor(message, details) {\n super(message, 'NETWORK_ERROR', details);\n this.name = 'NetworkError';\n }\n}\n\nexport class SSRError extends SessionError {\n constructor(message, details = {}) {\n super(message, 'SSR_ERROR', details);\n this.name = 'SSRError';\n }\n}\n","/**\n * Logger utility with configurable log levels\n */\n\nconst LOG_LEVELS = {\n NONE: 0,\n ERROR: 1,\n WARN: 2,\n INFO: 3,\n DEBUG: 4\n};\n\nclass Logger {\n constructor() {\n this.level = LOG_LEVELS.WARN; // Default to WARN in production\n }\n\n setLevel(level) {\n if (typeof level === 'string') {\n this.level = LOG_LEVELS[level.toUpperCase()] ?? LOG_LEVELS.WARN;\n } else {\n this.level = level;\n }\n }\n\n error(...args) {\n if (this.level >= LOG_LEVELS.ERROR) {\n console.error('[SessionManager]', ...args);\n }\n }\n\n warn(...args) {\n if (this.level >= LOG_LEVELS.WARN) {\n console.warn('[SessionManager]', ...args);\n }\n }\n\n info(...args) {\n if (this.level >= LOG_LEVELS.INFO) {\n console.info('[SessionManager]', ...args);\n }\n }\n\n debug(...args) {\n if (this.level >= LOG_LEVELS.DEBUG) {\n console.debug('[SessionManager]', ...args);\n }\n }\n}\n\nexport const logger = new Logger();\nexport { LOG_LEVELS };\n","import {BootstrapError, ConfigurationError, NetworkError, SSRError} from './errors.js';\nimport {logger} from './logger.js';\nconst CLIENT_PLATFORM = 'web';\n\nconst DEFAULT_CONFIG = {\n bootstrapUrl: '/session/bootstrap',\n refreshInterval: 25 * 60 * 1000, // 25 minutes in milliseconds\n tokenExpiry: 30 * 60 * 1000, // 30 minutes in milliseconds\n maxRetries: 3,\n bootstrapTimeout: 30000, // 30 seconds timeout for bootstrap fetch\n tokenCookieName: 'token', // Must match server's session_header\n invalidSessionError: 'INVALID-GW-SESSION', // Must match server's error response\n};\n\n/**\n * SessionManager - Core session token management class\n * Handles session bootstrap, automatic token refresh, and lifecycle management\n */\nclass SessionManager {\n constructor() {\n if (SessionManager.instance) {\n return SessionManager.instance;\n }\n\n this.config = { ...DEFAULT_CONFIG };\n\n this.state = {\n isInitialized: false,\n isLoading: false,\n lastRefreshTime: null,\n error: null,\n errorCode: null,\n nextRefreshTime: null,\n tokenExpiry: null,\n cfSessionId: null,\n initializationFailed: false,\n };\n\n this.refreshTimerId = null;\n this.listeners = new Set();\n this.initializationPromise = null;\n this._configured = false;\n this._boundHandleVisibilityChange = this._handleVisibilityChange.bind(this);\n this._boundHandlePageShow = this._handlePageShow.bind(this);\n this._boundHandleOnline = this._handleOnline.bind(this);\n\n SessionManager.instance = this;\n }\n\n /**\n * Configure the session manager\n * @param {Object} config - Configuration options\n * @param {string} config.bootstrapUrl - URL for bootstrap API endpoint\n * @param {number} config.refreshInterval - Interval for token refresh in milliseconds (default: 25 mins)\n * @param {number} config.tokenExpiry - Token expiry time in milliseconds (default: 30 mins)\n * @param {number} config.maxRetries - Maximum retry attempts on failure (default: 3)\n * @param {number} config.bootstrapTimeout - Timeout for bootstrap fetch in milliseconds (default: 30s)\n * @param {Object} config.headers - Additional headers for API calls\n * @param {boolean} config.credentials - Include credentials in requests (default: true)\n * @param {string} config.tokenCookieName - Cookie name to read token from (default: 'token')\n */\n configure(config = {}) {\n if (!config.bootstrapUrl || typeof config.bootstrapUrl !== 'string' || !config.bootstrapUrl.trim()) {\n throw new ConfigurationError('bootstrapUrl is required and must be a non-empty string', { bootstrapUrl: config.bootstrapUrl });\n }\n\n if (config.refreshInterval !== undefined) {\n if (typeof config.refreshInterval !== 'number' || !isFinite(config.refreshInterval)) {\n throw new ConfigurationError('refreshInterval must be a finite number', { refreshInterval: config.refreshInterval });\n }\n if (config.refreshInterval <= 0) {\n throw new ConfigurationError('refreshInterval must be positive', { refreshInterval: config.refreshInterval });\n }\n }\n\n if (config.tokenExpiry !== undefined) {\n if (typeof config.tokenExpiry !== 'number' || !isFinite(config.tokenExpiry)) {\n throw new ConfigurationError('tokenExpiry must be a finite number', { tokenExpiry: config.tokenExpiry });\n }\n if (config.tokenExpiry <= 0) {\n throw new ConfigurationError('tokenExpiry must be positive', { tokenExpiry: config.tokenExpiry });\n }\n }\n\n if (config.maxRetries !== undefined) {\n if (typeof config.maxRetries !== 'number' || !Number.isInteger(config.maxRetries) || config.maxRetries < 1) {\n throw new ConfigurationError('maxRetries must be a positive integer', { maxRetries: config.maxRetries });\n }\n }\n\n this.config = {\n ...this.config,\n ...config,\n credentials: config.credentials !== undefined ? config.credentials : true,\n logLevel: config.logLevel || 'WARN'\n };\n\n logger.setLevel(this.config.logLevel);\n this._configured = true;\n\n if (this.config.refreshInterval && this.config.tokenExpiry &&\n this.config.refreshInterval >= this.config.tokenExpiry) {\n logger.warn('refreshInterval should be less than tokenExpiry to prevent race conditions');\n }\n }\n\n /**\n * Initialize session by calling bootstrap API\n * @param {boolean} forceRefresh - Force a new bootstrap call even if already initialized (used by refreshToken)\n * @returns {Promise<Object>} Bootstrap response\n */\n async initialize(forceRefresh = false) {\n if (typeof window === 'undefined') {\n throw new SSRError('Cannot initialize in non-browser environment (SSR)');\n }\n\n if (!this._configured) {\n logger.warn('initialize() called before configure(). Using default config.');\n }\n\n if (this.state.initializationFailed) {\n logger.warn('Initialization previously failed. Reload page to retry.');\n throw new BootstrapError('Initialization failed after max retries. Reload page to retry.');\n }\n\n // Return existing promise if refresh already in progress\n if (this.initializationPromise) {\n logger.debug('Initialization already in progress, waiting...');\n return this.initializationPromise;\n }\n\n // If already initialized in this page session and not a forced refresh, return current state\n if (this.state.isInitialized && !forceRefresh) {\n logger.debug('Session already initialized');\n return { token: this.getToken(this.config.tokenCookieName) };\n }\n\n this.state.isLoading = true;\n this.state.error = null;\n this.notifyListeners();\n\n this.initializationPromise = (async () => {\n let lastError;\n\n for (let attempt = 1; attempt <= this.config.maxRetries; attempt++) {\n try {\n const newSessionId = this.generateSessionId();\n\n logger.info(`Calling bootstrap API (attempt ${attempt}/${this.config.maxRetries}):`, this.config.bootstrapUrl);\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.bootstrapTimeout);\n\n let response;\n try {\n response = await fetch(this.config.bootstrapUrl, {\n method: 'GET',\n credentials: this.config.credentials ? 'include' : 'same-origin',\n signal: controller.signal,\n headers: {\n 'cf-session-id': newSessionId,\n 'x-client-platform': CLIENT_PLATFORM,\n ...this.config.headers,\n },\n });\n } catch (fetchError) {\n if (fetchError.name === 'AbortError') {\n throw new NetworkError(`Bootstrap request timed out after ${this.config.bootstrapTimeout}ms`, {\n timeout: this.config.bootstrapTimeout,\n url: this.config.bootstrapUrl\n });\n }\n throw fetchError;\n } finally {\n clearTimeout(timeoutId);\n }\n\n if (!response.ok) {\n throw new BootstrapError(`Bootstrap failed: ${response.status} ${response.statusText}`, {\n status: response.status,\n statusText: response.statusText,\n url: this.config.bootstrapUrl\n });\n }\n\n let data;\n try {\n data = await response.json();\n } catch (jsonError) {\n throw new BootstrapError('Bootstrap response is not valid JSON', { originalError: jsonError.message });\n }\n logger.info('Bootstrap successful');\n\n // Use refresh_time from response (in seconds) or fall back to config\n const refreshInterval = data.refresh_time\n ? data.refresh_time * 1000\n : this.config.refreshInterval;\n\n // Calculate token expiry from response (in seconds) or fall back to config\n const tokenExpiry = data.expire_time\n ? data.expire_time * 1000\n : this.config.tokenExpiry;\n\n // If server returned token in response body (non-cookie mode),\n // store it as a client-side cookie. Key matches server's session_header.\n const tokenFromResponse = data[this.config.tokenCookieName];\n if (tokenFromResponse) {\n this.setCookie(this.config.tokenCookieName, tokenFromResponse, tokenExpiry / 1000);\n }\n\n // Now safe to update cf-session-id — new token is stored/set\n this.setCfSessionId(newSessionId, tokenExpiry / 1000);\n\n this.state.isInitialized = true;\n this.state.isLoading = false;\n this.state.lastRefreshTime = Date.now();\n this.state.nextRefreshTime = Date.now() + refreshInterval;\n this.state.tokenExpiry = tokenExpiry;\n this.state.error = null;\n\n // Start automatic refresh timer with dynamic interval\n this.startTokenRefresh(refreshInterval);\n\n this.notifyListeners();\n return data;\n } catch (error) {\n lastError = error instanceof Error ? error : new NetworkError('Unknown error occurred', { error });\n logger.error(`Bootstrap attempt ${attempt} failed:`, lastError.message);\n\n if (attempt < this.config.maxRetries) {\n const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);\n logger.info(`Retrying in ${delay}ms...`);\n await new Promise(resolve => setTimeout(resolve, delay));\n }\n }\n }\n\n logger.error('All bootstrap attempts failed');\n this.state.isLoading = false;\n this.state.error = lastError?.message || 'Unknown error';\n this.state.errorCode = lastError?.code || 'UNKNOWN_ERROR';\n this.state.initializationFailed = true;\n this.notifyListeners();\n throw lastError;\n })();\n\n try {\n return await this.initializationPromise;\n } finally {\n this.initializationPromise = null;\n }\n }\n\n /**\n * Check if token refresh is currently in progress\n * @returns {boolean} True if refresh is in progress\n */\n isRefreshing() {\n return this.initializationPromise !== null;\n }\n\n /**\n * Wait for ongoing refresh to complete\n * @returns {Promise<Object>} Bootstrap response from ongoing refresh\n */\n async waitForRefresh() {\n if (this.initializationPromise) {\n logger.debug('Waiting for ongoing refresh to complete');\n return this.initializationPromise;\n }\n return Promise.resolve();\n }\n\n /**\n * Check if the token is likely expired based on in-memory timestamps.\n * After a full browser close + reopen, lastRefreshTime is null, so this returns true.\n * @returns {boolean} True if token appears expired or state is unknown\n */\n _isTokenLikelyExpired() {\n if (!this.state.lastRefreshTime || !this.state.tokenExpiry) return true;\n return Date.now() >= this.state.lastRefreshTime + this.state.tokenExpiry;\n }\n\n /**\n * Ensure the session is ready before making API calls.\n * - If initialized and token not expired, resolves immediately.\n * - If initialization is in progress, waits for it.\n * - If not initialized but configured, triggers initialization and waits.\n * - If not configured or permanently failed, resolves (lets request proceed;\n * the 401 handler in the interceptor will deal with it).\n * @returns {Promise<void>}\n */\n async ensureReady() {\n // Happy path: session is live and token hasn't expired\n if (this.state.isInitialized && !this._isTokenLikelyExpired()) {\n return;\n }\n\n // An initialization / refresh is already in flight — piggyback on it\n if (this.initializationPromise) {\n try {\n await this.initializationPromise;\n } catch (_) {\n // Initialization failed — let the request proceed, 401 handler will deal with it\n logger.debug('ensureReady: in-flight initialization failed, proceeding with request');\n }\n return;\n }\n\n // Not initialized / token expired, no call in flight, but we *can* bootstrap\n if (this._configured && !this.state.initializationFailed) {\n try {\n await this.initialize(this.state.isInitialized); // force refresh if already initialized but token expired\n } catch (_) {\n logger.debug('ensureReady: initialization failed, proceeding with request');\n }\n }\n\n // Not configured or permanently failed — nothing we can do here\n }\n\n /**\n * Check if the session needs a refresh (initialized but token expired).\n * Useful for interceptors to proactively refresh before sending a request.\n * @returns {boolean}\n */\n needsRefresh() {\n return this.state.isInitialized && this._isTokenLikelyExpired();\n }\n\n /**\n * Set cf-session-id in cookie\n * @param {string} sessionId - Session ID\n * @param {number} maxAge - Max age in seconds\n */\n setCfSessionId(sessionId, maxAge) {\n this.state.cfSessionId = sessionId;\n const cookieMaxAge = maxAge || this.config.tokenExpiry / 1000;\n this.setCookie('cf-session-id', sessionId, cookieMaxAge);\n }\n\n /**\n * Generate unique session ID\n * @returns {string} Generated session ID\n */\n generateSessionId() {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n }\n\n /**\n * Get cf-session-id for requests\n * @returns {string} Current cf-session-id\n */\n getSessionId() {\n return this.state.cfSessionId;\n }\n\n /**\n * Check if token should be added to headers (HTTP or cross-origin)\n * @returns {boolean} True if token should be added to headers\n */\n shouldUseTokenHeader() {\n if (typeof window === 'undefined') return false;\n return window.location.protocol === 'http:';\n }\n\n /**\n * Shared cookie name sanitizer used by both getCookie and setCookie\n * @param {string} name - Raw cookie name\n * @returns {string} Sanitized cookie name\n */\n _sanitizeCookieName(name) {\n return name.replace(/[^a-zA-Z0-9_-]/g, '');\n }\n\n /**\n * Get cookie value\n * @param {string} name - Cookie name\n * @returns {string|null} Cookie value or null\n */\n getCookie(name) {\n if (typeof document === 'undefined') return null;\n const sanitizedName = this._sanitizeCookieName(name);\n if (!sanitizedName) return null;\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${sanitizedName}=`);\n if (parts.length === 2) {\n try {\n return decodeURIComponent(parts.pop().split(';').shift());\n } catch (error) {\n logger.error('Failed to decode cookie value:', error instanceof Error ? error.message : String(error));\n return null;\n }\n }\n return null;\n }\n\n /**\n * Get token from cookie\n * @param {string} name - Cookie name (default: configured tokenCookieName)\n * @returns {string|null} Token value or null\n */\n getToken(name = this.config.tokenCookieName) {\n return this.getCookie(name);\n }\n\n /**\n * Set cookie from client-side (WARNING: Not HttpOnly, less secure than server-set cookies)\n * @param {string} name - Cookie name\n * @param {string} value - Cookie value\n * @param {number} maxAge - Max age in seconds\n */\n setCookie(name, value, maxAge) {\n if (typeof document === 'undefined') {\n logger.warn('Cannot set cookie in non-browser environment');\n return;\n }\n\n const sanitizedName = this._sanitizeCookieName(name);\n if (!sanitizedName) {\n logger.error('Invalid cookie name provided');\n return;\n }\n\n const expires = new Date(Date.now() + maxAge * 1000).toUTCString();\n const isSecure = typeof window !== 'undefined' && window.location.protocol === 'https:';\n const secureFlag = isSecure ? '; Secure' : '';\n const encodedValue = encodeURIComponent(value);\n document.cookie = `${sanitizedName}=${encodedValue}; path=/; expires=${expires}; SameSite=Lax${secureFlag}`;\n logger.debug(`Cookie set: ${sanitizedName}, expires in ${maxAge}s${isSecure ? ' (Secure)' : ''} [WARNING: Not HttpOnly]`);\n }\n\n /**\n * Start automatic token refresh timer\n * @param {number} interval - Refresh interval in milliseconds\n */\n startTokenRefresh(interval = this.config.refreshInterval) {\n this.stopTokenRefresh();\n logger.info(`Scheduling token refresh in ${interval / 1000} seconds`);\n\n this.refreshTimerId = setTimeout(async () => {\n logger.info('Auto-refreshing token...');\n try {\n await this.refreshToken();\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n const errorCode = error?.code || 'UNKNOWN_ERROR';\n logger.error('Auto-refresh failed:', errorMessage);\n this.state.error = errorMessage;\n this.state.errorCode = errorCode;\n this.notifyListeners();\n // Schedule retry instead of dying permanently\n const retryDelay = Math.min(interval * 2, 60000);\n logger.info(`Scheduling auto-refresh retry in ${retryDelay / 1000}s`);\n this.startTokenRefresh(retryDelay);\n }\n }, interval);\n\n // Listen for visibility changes (tab wake from sleep / bfcache restore)\n this._startVisibilityListener();\n // Listen for network recovery\n this._startOnlineListener();\n }\n\n /**\n * Stop automatic token refresh timer\n */\n stopTokenRefresh() {\n if (this.refreshTimerId) {\n clearTimeout(this.refreshTimerId);\n this.refreshTimerId = null;\n logger.debug('Token refresh timer stopped');\n }\n this._stopVisibilityListener();\n this._stopOnlineListener();\n }\n\n /**\n * Start listening for page visibility changes and bfcache restore\n */\n _startVisibilityListener() {\n if (typeof document === 'undefined') return;\n document.addEventListener('visibilitychange', this._boundHandleVisibilityChange);\n if (typeof window !== 'undefined') {\n window.addEventListener('pageshow', this._boundHandlePageShow);\n }\n }\n\n /**\n * Stop listening for page visibility changes\n */\n _stopVisibilityListener() {\n if (typeof document === 'undefined') return;\n document.removeEventListener('visibilitychange', this._boundHandleVisibilityChange);\n if (typeof window !== 'undefined') {\n window.removeEventListener('pageshow', this._boundHandlePageShow);\n }\n }\n\n /**\n * Check if token refresh is overdue and trigger refresh if so\n * @returns {boolean} True if refresh was triggered\n */\n _refreshIfOverdue() {\n if (!this.state.isInitialized || this.state.initializationFailed) return false;\n if (this.isRefreshing()) return false;\n\n const now = Date.now();\n const { nextRefreshTime } = this.state;\n\n if (nextRefreshTime && now >= nextRefreshTime) {\n logger.info('Missed scheduled refresh — refreshing token now');\n this.refreshToken().catch(error => {\n const errorMessage = error instanceof Error ? error.message : String(error);\n logger.error('Triggered refresh failed:', errorMessage);\n });\n return true;\n }\n return false;\n }\n\n /**\n * Handle visibility change — refresh if overdue when tab becomes visible\n */\n _handleVisibilityChange() {\n if (document.visibilityState !== 'visible') return;\n this._refreshIfOverdue();\n }\n\n /**\n * Handle pageshow — force bootstrap on bfcache restore (browser close + reopen)\n */\n _handlePageShow(event) {\n if (!event.persisted) return;\n // bfcache restore means browser was closed — always force a fresh bootstrap\n if (!this.state.isInitialized || this.state.initializationFailed) return;\n if (this.isRefreshing()) return;\n logger.info('Page restored from bfcache — forcing bootstrap');\n this.refreshToken().catch(error => {\n const errorMessage = error instanceof Error ? error.message : String(error);\n logger.error('bfcache-triggered refresh failed:', errorMessage);\n });\n }\n\n /**\n * Start listening for network recovery\n */\n _startOnlineListener() {\n if (typeof window === 'undefined') return;\n window.addEventListener('online', this._boundHandleOnline);\n }\n\n /**\n * Stop listening for network recovery\n */\n _stopOnlineListener() {\n if (typeof window === 'undefined') return;\n window.removeEventListener('online', this._boundHandleOnline);\n }\n\n /**\n * Handle online event — retry if initialization had failed due to network\n */\n _handleOnline() {\n if (!this.state.initializationFailed) return;\n logger.info('Network restored — retrying session initialization');\n this.refreshToken().catch(error => {\n const errorMessage = error instanceof Error ? error.message : String(error);\n logger.error('Online-triggered refresh failed:', errorMessage);\n });\n }\n\n /**\n * Manually refresh the session token\n * @returns {Promise<Object>} Bootstrap response\n */\n async refreshToken() {\n logger.info('Manual token refresh triggered');\n this.state.initializationFailed = false;\n return this.initialize(true);\n }\n\n /**\n * Get current session status\n * @returns {Object} Current session state\n */\n getSessionStatus() {\n return {\n isInitialized: this.state.isInitialized,\n isLoading: this.state.isLoading,\n lastRefreshTime: this.state.lastRefreshTime,\n nextRefreshTime: this.state.nextRefreshTime,\n tokenExpiry: this.state.tokenExpiry,\n error: this.state.error,\n errorCode: this.state.errorCode,\n initializationFailed: this.state.initializationFailed,\n timeUntilRefresh: this.state.nextRefreshTime\n ? Math.max(0, this.state.nextRefreshTime - Date.now())\n : null,\n };\n }\n\n /**\n * Subscribe to session state changes\n * @param {Function} listener - Callback function to be called on state changes\n * @returns {Function} Unsubscribe function\n */\n subscribe(listener) {\n if (typeof listener !== 'function') {\n throw new TypeError('Listener must be a function');\n }\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Notify all listeners of state changes\n */\n notifyListeners() {\n const status = this.getSessionStatus();\n Array.from(this.listeners).forEach(listener => {\n try {\n listener(status);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n logger.error('Listener error:', errorMessage);\n }\n });\n }\n\n /**\n * Clean up and reset the session manager\n */\n destroy() {\n this.stopTokenRefresh();\n this.initializationPromise = null;\n this.listeners.clear();\n this.setCookie('cf-session-id', '', -1);\n this.setCookie(this.config.tokenCookieName, '', -1);\n this.config = { ...DEFAULT_CONFIG };\n this.state = {\n isInitialized: false,\n isLoading: false,\n lastRefreshTime: null,\n error: null,\n errorCode: null,\n nextRefreshTime: null,\n tokenExpiry: null,\n cfSessionId: null,\n initializationFailed: false,\n };\n this._configured = false;\n logger.info('Destroyed');\n }\n\n /**\n * Get the singleton instance\n * @returns {SessionManager} Singleton instance\n */\n static getInstance() {\n if (!SessionManager.instance) {\n SessionManager.instance = new SessionManager();\n }\n return SessionManager.instance;\n }\n}\n\nexport default SessionManager;","import { useState, useEffect, useCallback, useRef } from 'react';\nimport SessionManager from '../SessionManager.js';\nimport { logger } from '../logger.js';\n\n/**\n * React hook for session management\n * Provides session state and controls for React components\n *\n * @param {Object} options - Configuration options\n * @param {boolean} options.autoInitialize - Automatically initialize on mount (default: true)\n * @returns {Object} Session state and controls\n */\nexport function useSession(options = {}) {\n const { autoInitialize = true } = options;\n\n const sessionManager = SessionManager.getInstance();\n\n const [sessionState, setSessionState] = useState(() =>\n sessionManager.getSessionStatus()\n );\n\n // FIX #11: Separate countdown state — only this re-renders every second,\n // not the full session state which would cause unnecessary subscriber churn\n const [timeUntilRefresh, setTimeUntilRefresh] = useState(() => {\n const { nextRefreshTime } = sessionManager.getSessionStatus();\n return nextRefreshTime ? Math.max(0, nextRefreshTime - Date.now()) : null;\n });\n\n const nextRefreshTimeRef = useRef(sessionState.nextRefreshTime);\n\n // Subscribe to meaningful session state changes only\n useEffect(() => {\n const unsubscribe = sessionManager.subscribe((newState) => {\n setSessionState(newState);\n nextRefreshTimeRef.current = newState.nextRefreshTime;\n });\n\n return unsubscribe;\n }, []); // sessionManager is a singleton, no need to include in deps\n\n // Local tick timer for countdown display — avoids global tick in SessionManager\n useEffect(() => {\n if (!sessionState.nextRefreshTime) {\n setTimeUntilRefresh(null);\n return;\n }\n // Set immediately on mount / when nextRefreshTime changes\n setTimeUntilRefresh(Math.max(0, sessionState.nextRefreshTime - Date.now()));\n const tickId = setInterval(() => {\n const nrt = nextRefreshTimeRef.current;\n setTimeUntilRefresh(nrt ? Math.max(0, nrt - Date.now()) : null);\n }, 1000);\n return () => clearInterval(tickId);\n }, [sessionState.nextRefreshTime]);\n\n // Auto-initialize if enabled\n useEffect(() => {\n if (autoInitialize && !sessionState.isInitialized && !sessionState.isLoading && !sessionState.initializationFailed) {\n sessionManager.initialize().catch(error => {\n logger.error('Auto-initialization failed:', error);\n });\n }\n }, [autoInitialize, sessionState.isInitialized, sessionState.isLoading, sessionState.initializationFailed]);\n\n // Manual refresh function\n const refresh = useCallback(async () => {\n try {\n await sessionManager.refreshToken();\n } catch (error) {\n logger.error('Manual refresh failed:', error);\n throw error;\n }\n }, []);\n\n // Initialize function (for manual control)\n const initialize = useCallback(async () => {\n try {\n await sessionManager.initialize();\n } catch (error) {\n logger.error('Manual initialization failed:', error);\n throw error;\n }\n }, []);\n\n return {\n // State\n isInitialized: sessionState.isInitialized,\n isLoading: sessionState.isLoading,\n error: sessionState.error,\n lastRefreshTime: sessionState.lastRefreshTime,\n nextRefreshTime: sessionState.nextRefreshTime,\n timeUntilRefresh,\n initializationFailed: sessionState.initializationFailed,\n\n // Actions\n refresh,\n initialize,\n };\n}\n\nexport default useSession;","import SessionManager from './SessionManager.js';\nexport const CLIENT_PLATFORM = 'web';\n\n/**\n * Fetch interceptor with automatic token refresh on 401\n * @param {string} url - Request URL\n * @param {Object} options - Fetch options\n * @returns {Promise<Response>} Fetch response\n */\nexport async function fetchInterceptor(url, options = {}) {\n const sessionManager = SessionManager.getInstance();\n\n // Gate on session readiness: if not initialized (e.g., browser was closed and\n // reopened), this triggers bootstrap and waits; if a refresh is in progress\n // (tab wake, online recovery, concurrent 401), it piggybacks on it.\n await sessionManager.ensureReady();\n\n // FIX #6: Shallow-clone options to avoid mutating the caller's object\n const requestOptions = {\n ...options,\n credentials: options.credentials || 'include',\n headers: {\n ...options.headers,\n 'cf-session-id': sessionManager.getSessionId(),\n 'x-client-platform': CLIENT_PLATFORM,\n },\n };\n\n // Add token to header if HTTP (not HTTPS)\n if (sessionManager.shouldUseTokenHeader()) {\n const token = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (token) {\n requestOptions.headers[sessionManager.config.tokenCookieName] = token;\n }\n }\n\n let response = await fetch(url, requestOptions);\n\n // Retry once if unauthorized with INVALID_SESSION\n if (response.status === 401) {\n const clonedResponse = response.clone();\n try {\n const data = await clonedResponse.json();\n if (data.error_msg === sessionManager.config.invalidSessionError) {\n if (sessionManager.isRefreshing()) {\n await sessionManager.waitForRefresh();\n } else {\n await sessionManager.refreshToken();\n }\n requestOptions.headers['cf-session-id'] = sessionManager.getSessionId();\n if (sessionManager.shouldUseTokenHeader()) {\n const newToken = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (newToken) {\n requestOptions.headers[sessionManager.config.tokenCookieName] = newToken;\n }\n }\n response = await fetch(url, requestOptions);\n }\n } catch (e) {\n // Not JSON or parsing failed, return original response\n }\n }\n\n return response;\n}\n\n/**\n * Axios interceptor with automatic token refresh on 401 and INVALID_SESSION\n * @param {Object} axiosInstance - Axios instance\n * @returns {Object} The same axios instance with interceptors attached\n */\nexport function setupAxiosInterceptor(axiosInstance) {\n const sessionManager = SessionManager.getInstance();\n\n // FIX #7: Eject previous interceptors if this instance was already set up\n if (axiosInstance._gwSessionRequestId !== undefined) {\n axiosInstance.interceptors.request.eject(axiosInstance._gwSessionRequestId);\n }\n if (axiosInstance._gwSessionResponseId !== undefined) {\n axiosInstance.interceptors.response.eject(axiosInstance._gwSessionResponseId);\n }\n\n // Request interceptor to add cf-session-id, token, and credentials\n axiosInstance._gwSessionRequestId = axiosInstance.interceptors.request.use(\n async (config) => {\n // Gate on session readiness before every request\n await sessionManager.ensureReady();\n if (sessionManager.config.credentials) {\n config.withCredentials = true;\n }\n config.headers['cf-session-id'] = sessionManager.getSessionId();\n config.headers['x-client-platform'] = CLIENT_PLATFORM;\n if (sessionManager.shouldUseTokenHeader()) {\n const token = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (token) {\n config.headers[sessionManager.config.tokenCookieName] = token;\n }\n }\n return config;\n },\n (error) => Promise.reject(error)\n );\n\n // Response interceptor for token refresh\n axiosInstance._gwSessionResponseId = axiosInstance.interceptors.response.use(\n (response) => response,\n async (error) => {\n const originalRequest = error.config;\n\n // Retry once if unauthorized with INVALID_SESSION\n if (error.response?.status === 401 && !originalRequest._retry) {\n if (error.response?.data?.error_msg === sessionManager.config.invalidSessionError) {\n originalRequest._retry = true;\n\n if (sessionManager.isRefreshing()) {\n await sessionManager.waitForRefresh();\n } else {\n await sessionManager.refreshToken();\n }\n\n originalRequest.headers['cf-session-id'] = sessionManager.getSessionId();\n originalRequest.headers['x-client-platform'] = CLIENT_PLATFORM;\n if (sessionManager.shouldUseTokenHeader()) {\n const newToken = sessionManager.getToken(sessionManager.config.tokenCookieName);\n if (newToken) {\n originalRequest.headers[sessionManager.config.tokenCookieName] = newToken;\n }\n }\n return axiosInstance(originalRequest);\n }\n }\n\n return Promise.reject(error);\n }\n );\n\n return axiosInstance;\n}"],"names":["SessionError","Error","constructor","message","code","details","super","this","name","captureStackTrace","Object","setPrototypeOf","prototype","ConfigurationError","BootstrapError","NetworkError","SSRError","LOG_LEVELS","NONE","ERROR","WARN","INFO","DEBUG","logger","level","setLevel","toUpperCase","error","args","console","warn","info","debug","DEFAULT_CONFIG","bootstrapUrl","refreshInterval","tokenExpiry","maxRetries","bootstrapTimeout","tokenCookieName","invalidSessionError","SessionManager","instance","config","state","isInitialized","isLoading","lastRefreshTime","errorCode","nextRefreshTime","cfSessionId","initializationFailed","refreshTimerId","listeners","Set","initializationPromise","_configured","_boundHandleVisibilityChange","_handleVisibilityChange","bind","_boundHandlePageShow","_handlePageShow","_boundHandleOnline","_handleOnline","configure","trim","undefined","isFinite","Number","isInteger","credentials","logLevel","initialize","forceRefresh","window","token","getToken","notifyListeners","lastError","attempt","newSessionId","generateSessionId","controller","AbortController","timeoutId","setTimeout","abort","response","data","fetch","method","signal","headers","fetchError","timeout","url","clearTimeout","ok","status","statusText","json","jsonError","originalError","refresh_time","expire_time","tokenFromResponse","setCookie","setCfSessionId","Date","now","startTokenRefresh","delay","Math","min","pow","Promise","resolve","isRefreshing","waitForRefresh","_isTokenLikelyExpired","ensureReady","_","needsRefresh","sessionId","maxAge","cookieMaxAge","crypto","randomUUID","random","toString","substr","getSessionId","shouldUseTokenHeader","location","protocol","_sanitizeCookieName","replace","getCookie","document","sanitizedName","parts","cookie","split","length","decodeURIComponent","pop","shift","String","value","expires","toUTCString","isSecure","secureFlag","encodedValue","encodeURIComponent","interval","stopTokenRefresh","async","refreshToken","errorMessage","retryDelay","_startVisibilityListener","_startOnlineListener","_stopVisibilityListener","_stopOnlineListener","addEventListener","removeEventListener","_refreshIfOverdue","catch","visibilityState","event","persisted","getSessionStatus","timeUntilRefresh","max","subscribe","listener","TypeError","add","delete","Array","from","forEach","destroy","clear","getInstance","useSession","options","autoInitialize","sessionManager","sessionState","setSessionState","useState","setTimeUntilRefresh","nextRefreshTimeRef","useRef","useEffect","newState","current","tickId","setInterval","nrt","clearInterval","refresh","useCallback","CLIENT_PLATFORM","fetchInterceptor","requestOptions","clonedResponse","clone","error_msg","newToken","e","setupAxiosInterceptor","axiosInstance","_gwSessionRequestId","interceptors","request","eject","_gwSessionResponseId","use","withCredentials","reject","originalRequest","_retry"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mapnests/gateway-web-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "Session token management SDK with automatic refresh for React/Next.js applications",
|
|
5
5
|
"main": "dist/index.cjs.js",
|
|
6
6
|
"module": "dist/index.esm.js",
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"react": ">=16.8.0",
|
|
46
|
-
"react-dom": ">=16.8.0"
|
|
46
|
+
"react-dom": ">=16.8.0",
|
|
47
|
+
"axios": ">=0.21.0"
|
|
47
48
|
},
|
|
48
49
|
"peerDependenciesMeta": {
|
|
49
50
|
"axios": {
|
package/src/SessionManager.d.ts
CHANGED
|
@@ -10,10 +10,12 @@ export interface SessionConfig {
|
|
|
10
10
|
refreshInterval?: number;
|
|
11
11
|
tokenExpiry?: number;
|
|
12
12
|
maxRetries?: number;
|
|
13
|
+
bootstrapTimeout?: number;
|
|
13
14
|
headers?: Record<string, string>;
|
|
14
15
|
credentials?: boolean;
|
|
15
16
|
logLevel?: 'NONE' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
|
|
16
17
|
tokenCookieName?: string;
|
|
18
|
+
invalidSessionError?: string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export interface SessionState {
|
|
@@ -82,6 +84,18 @@ export default class SessionManager {
|
|
|
82
84
|
*/
|
|
83
85
|
waitForRefresh(): Promise<BootstrapResponse | void>;
|
|
84
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Ensure the session is ready before making API calls.
|
|
89
|
+
* Waits for in-progress initialization, triggers bootstrap if needed,
|
|
90
|
+
* or resolves immediately if the session is live and token is fresh.
|
|
91
|
+
*/
|
|
92
|
+
ensureReady(): Promise<void>;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if the session needs a refresh (initialized but token expired).
|
|
96
|
+
*/
|
|
97
|
+
needsRefresh(): boolean;
|
|
98
|
+
|
|
85
99
|
/**
|
|
86
100
|
* Clean up and reset the session manager
|
|
87
101
|
*/
|