@mapnests/gateway-web-sdk 1.0.4 → 1.0.6

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 CHANGED
@@ -4,59 +4,86 @@ A lightweight, production-ready session token management SDK for React and Next.
4
4
 
5
5
  ## Features
6
6
 
7
- - 🔄 **Automatic Token Refresh**: Configurable background refresh (default: 25 minutes)
8
- - 🔒 **HttpOnly Cookie Support**: Secure token storage via server-set cookies
9
- - ⚛️ **React Integration**: Simple React hook for seamless integration
10
- - 🎯 **Singleton Pattern**: Single session instance across your entire app
11
- - 📦 **Zero Dependencies**: Lightweight with only React as a peer dependency
12
- - 🚀 **Next.js Compatible**: Works with both React and Next.js applications (SSR-safe)
13
- - ⏱️ **Configurable Intervals**: Customize refresh and expiry times
14
- - 🔔 **State Subscriptions**: React to session state changes
15
- - 📘 **TypeScript Support**: Full TypeScript definitions included
7
+ - Automatic token refresh with configurable background intervals
8
+ - HttpOnly cookie support for secure token storage
9
+ - React hook (`useSession`) for seamless integration
10
+ - Singleton pattern ensuring a single session instance across your app
11
+ - Zero dependencies (only React as a peer dependency)
12
+ - Next.js compatible (SSR-safe)
13
+ - Built-in Fetch and Axios interceptors for automatic 401 handling
14
+ - Full TypeScript definitions included
16
15
 
17
- ## Security Notice
16
+ ## Installation
18
17
 
19
- ⚠️ **Important**: This SDK prioritizes **server-set HttpOnly cookies** for maximum security. The SDK includes a fallback mechanism to set cookies client-side, but this is **less secure** as these cookies cannot be HttpOnly and are accessible to JavaScript.
18
+ ```bash
19
+ npm install @mapnests/gateway-web-sdk
20
+ ```
20
21
 
21
- **Recommended Setup:**
22
- - Always set cookies from your server using the `Set-Cookie` header with `HttpOnly` flag
23
- - The SDK will automatically detect and skip client-side cookie setting when server cookies are present
24
- - Client-side cookies should only be used for development or specific use cases where server-side setting is not possible
22
+ ---
25
23
 
26
- ## Installation
24
+ ## Implementation Guide
27
25
 
28
- ```bash
29
- npm install gateway-web-sdk
26
+ 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
+
30
+ ### Common Setup (All Approaches)
31
+
32
+ #### Step 1 — Environment Variables
33
+
34
+ Create a `.env` file in your project root with the following variables:
35
+
36
+ ```env
37
+ VITE_API_BASE_URL=https://your-gateway.example.com
38
+ VITE_BOOTSTRAP_PATH=/api/session/bootstrap
39
+ VITE_TOKEN_COOKIE_NAME=token
30
40
  ```
31
41
 
32
- ## Quick Start
42
+ | Variable | Description |
43
+ |----------|-------------|
44
+ | `VITE_API_BASE_URL` | Base URL of your API gateway |
45
+ | `VITE_BOOTSTRAP_PATH` | Path to the session bootstrap endpoint |
46
+ | `VITE_TOKEN_COOKIE_NAME` | Name of the token cookie set by your server |
33
47
 
34
- ### Simple Implementation (Copy-Paste)
48
+ > If you are using Next.js, prefix with `NEXT_PUBLIC_` instead of `VITE_`.
35
49
 
36
- This setup covers bootstrap on app start and three API patterns:
37
- - Users → Fetch interceptor
38
- - Products → Axios interceptor
39
- - Orders → Manual implementation
50
+ Then create a config helper to read these values:
51
+
52
+ ```js
53
+ // src/config.js
54
+ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
55
+ const BOOTSTRAP_PATH = import.meta.env.VITE_BOOTSTRAP_PATH;
56
+ const TOKEN_COOKIE_NAME = import.meta.env.VITE_TOKEN_COOKIE_NAME;
57
+
58
+ if (!API_BASE_URL) throw new Error('VITE_API_BASE_URL is not defined');
59
+ if (!BOOTSTRAP_PATH) throw new Error('VITE_BOOTSTRAP_PATH is not defined');
60
+ if (!TOKEN_COOKIE_NAME) throw new Error('VITE_TOKEN_COOKIE_NAME is not defined');
61
+
62
+ export { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME };
63
+ ```
64
+
65
+ #### Step 2 — Initialize the Session Manager
66
+
67
+ Configure and initialize the SDK once at your app's entry point:
40
68
 
41
- 1) Configure and initialize once on app start
42
69
  ```jsx
70
+ // src/main.jsx
43
71
  import React from 'react';
44
72
  import ReactDOM from 'react-dom/client';
45
- import { SessionManager } from 'gateway-web-sdk';
73
+ import { SessionManager } from '@mapnests/gateway-web-sdk';
46
74
  import { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME } from './config.js';
47
75
  import App from './App';
48
76
 
49
77
  const sessionManager = SessionManager.getInstance();
50
- try {
51
- sessionManager.configure({
52
- bootstrapUrl: `${API_BASE_URL}${BOOTSTRAP_PATH}`,
53
- tokenCookieName: TOKEN_COOKIE_NAME,
54
- });
55
- } catch (error) {
56
- console.error('Failed to configure session manager:', error);
57
- }
58
78
 
59
- sessionManager.initialize().catch(err => console.error('Failed to initialize session:', err));
79
+ sessionManager.configure({
80
+ bootstrapUrl: `${API_BASE_URL}${BOOTSTRAP_PATH}`,
81
+ tokenCookieName: TOKEN_COOKIE_NAME,
82
+ });
83
+
84
+ sessionManager.initialize().catch(err =>
85
+ console.error('Failed to initialize session:', err)
86
+ );
60
87
 
61
88
  ReactDOM.createRoot(document.getElementById('root')).render(
62
89
  <React.StrictMode>
@@ -65,39 +92,148 @@ ReactDOM.createRoot(document.getElementById('root')).render(
65
92
  );
66
93
  ```
67
94
 
68
- 2) API layer with three patterns
95
+ Now proceed with one of the three approaches below.
96
+
97
+ ---
98
+
99
+ ### Approach A — Fetch Interceptor
100
+
101
+ The simplest option. Drop-in replacement for `fetch` that automatically handles session headers and 401 retry.
102
+
103
+ #### Step 3 — Create the API layer
104
+
69
105
  ```js
70
- // src/api/index.js
71
- import axios from 'axios';
72
- import { fetchInterceptor, setupAxiosInterceptor, SessionManager } from 'gateway-web-sdk';
106
+ // src/api.js
107
+ import { fetchInterceptor } from '@mapnests/gateway-web-sdk';
108
+ import { API_BASE_URL } from './config.js';
109
+
110
+ export const getUser = () =>
111
+ fetchInterceptor(`${API_BASE_URL}/api/user`);
112
+ ```
113
+
114
+ #### Step 4 — Use in a component
115
+
116
+ ```jsx
117
+ // src/Dashboard.jsx
118
+ import { useEffect, useState } from 'react';
119
+ import { useSession } from '@mapnests/gateway-web-sdk';
120
+ import { getUser } from './api.js';
121
+
122
+ export default function Dashboard() {
123
+ const { isInitialized, isLoading, error } = useSession();
124
+ const [user, setUser] = useState(null);
125
+
126
+ useEffect(() => {
127
+ if (!isInitialized) return;
128
+
129
+ getUser()
130
+ .then(res => res.json())
131
+ .then(setUser)
132
+ .catch(err => console.error('Failed to fetch user:', err));
133
+ }, [isInitialized]);
134
+
135
+ if (isLoading) return <p>Loading session...</p>;
136
+ if (error) return <p>Session error: {error}</p>;
137
+ if (!user) return <p>Loading data...</p>;
138
+
139
+ return <pre>{JSON.stringify(user, null, 2)}</pre>;
140
+ }
141
+ ```
142
+
143
+ ---
73
144
 
74
- const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://your-gateway.example.com';
145
+ ### Approach B Axios Interceptor
75
146
 
76
- // Users via Fetch interceptor
77
- export const getUser = () => fetchInterceptor(`${API_BASE_URL}/api/user`);
147
+ Best if you already use Axios. Wraps an Axios instance with automatic session headers and 401 retry.
78
148
 
79
- // Products via Axios interceptor
80
- export const axiosInstance = setupAxiosInterceptor(
81
- axios.create({ baseURL: API_BASE_URL, withCredentials: true })
149
+ > Requires `axios` as a dependency: `npm install axios`
150
+
151
+ #### Step 3 Create the Axios instance and API layer
152
+
153
+ ```js
154
+ // src/api.js
155
+ import axios from 'axios';
156
+ import { setupAxiosInterceptor } from '@mapnests/gateway-web-sdk';
157
+ import { API_BASE_URL } from './config.js';
158
+
159
+ const api = setupAxiosInterceptor(
160
+ axios.create({
161
+ baseURL: API_BASE_URL,
162
+ withCredentials: true,
163
+ })
82
164
  );
83
- export const getProducts = () => axiosInstance.get('/api/products');
84
165
 
85
- // Orders via Manual implementation
166
+ export const getUser = () => api.get('/api/user');
167
+ ```
168
+
169
+ #### Step 4 — Use in a component
170
+
171
+ ```jsx
172
+ // src/Dashboard.jsx
173
+ import { useEffect, useState } from 'react';
174
+ import { useSession } from '@mapnests/gateway-web-sdk';
175
+ import { getUser } from './api.js';
176
+
177
+ export default function Dashboard() {
178
+ const { isInitialized, isLoading, error } = useSession();
179
+ const [user, setUser] = useState(null);
180
+
181
+ useEffect(() => {
182
+ if (!isInitialized) return;
183
+
184
+ getUser()
185
+ .then(res => setUser(res.data))
186
+ .catch(err => console.error('Failed to fetch user:', err));
187
+ }, [isInitialized]);
188
+
189
+ if (isLoading) return <p>Loading session...</p>;
190
+ if (error) return <p>Session error: {error}</p>;
191
+ if (!user) return <p>Loading data...</p>;
192
+
193
+ return <pre>{JSON.stringify(user, null, 2)}</pre>;
194
+ }
195
+ ```
196
+
197
+ ---
198
+
199
+ ### Approach C — Manual Implementation
200
+
201
+ Full control over request construction and 401 handling. Use this when you need custom logic or don't want to use the built-in interceptors.
202
+
203
+ #### Step 3 — Create a manual fetch wrapper
204
+
205
+ ```js
206
+ // src/api.js
207
+ import { SessionManager } from '@mapnests/gateway-web-sdk';
208
+ import { API_BASE_URL } from './config.js';
209
+
86
210
  const sm = SessionManager.getInstance();
87
- async function manual(url, init = {}) {
88
- const opts = { ...init, headers: { ...(init.headers || {}) }, credentials: 'include' };
211
+
212
+ async function request(url, init = {}) {
213
+ const opts = {
214
+ ...init,
215
+ credentials: 'include',
216
+ headers: { ...(init.headers || {}) },
217
+ };
218
+
219
+ // Attach session headers
89
220
  opts.headers['cf-session-id'] = sm.getSessionId();
90
221
  opts.headers['x-client-platform'] = 'web';
222
+
91
223
  if (sm.shouldUseTokenHeader()) {
92
- const t = sm.getToken(sm.config.tokenCookieName);
93
- if (t) opts.headers[sm.config.tokenCookieName] = t;
224
+ const token = sm.getToken(sm.config.tokenCookieName);
225
+ if (token) opts.headers[sm.config.tokenCookieName] = token;
94
226
  }
227
+
95
228
  let res = await fetch(url, opts);
229
+
230
+ // Handle 401 INVALID_SESSION
96
231
  if (res.status === 401) {
97
232
  const cloned = res.clone();
98
233
  try {
99
- const data = await cloned.json();
100
- if (data.error_msg === 'INVALID_SESSION') {
234
+ const body = await cloned.json();
235
+ if (body.error_msg === 'INVALID_SESSION') {
236
+ // Wait for any in-progress refresh, or trigger a new one
101
237
  if (sm.isRefreshing()) {
102
238
  await sm.waitForRefresh();
103
239
  } else {
@@ -106,150 +242,76 @@ async function manual(url, init = {}) {
106
242
  await sm.refreshToken();
107
243
  }
108
244
  }
245
+
246
+ // Update headers with refreshed session
109
247
  opts.headers['cf-session-id'] = sm.getSessionId();
110
- opts.headers['x-client-platform'] = 'web';
111
248
  if (sm.shouldUseTokenHeader()) {
112
- const nt = sm.getToken(sm.config.tokenCookieName);
113
- if (nt) opts.headers[sm.config.tokenCookieName] = nt;
249
+ const newToken = sm.getToken(sm.config.tokenCookieName);
250
+ if (newToken) opts.headers[sm.config.tokenCookieName] = newToken;
114
251
  }
252
+
253
+ // Retry the request
115
254
  res = await fetch(url, opts);
116
255
  }
117
256
  } catch {
118
- // Not JSON or parsing failed
257
+ // Response was not JSON return the original response
119
258
  }
120
259
  }
260
+
121
261
  return res;
122
262
  }
123
- export const getOrders = () => manual(`${API_BASE_URL}/api/orders`);
263
+
264
+ export const getUser = () => request(`${API_BASE_URL}/api/user`);
124
265
  ```
125
266
 
126
- 3) Usage in a component
267
+ #### Step 4 — Use in a component
268
+
127
269
  ```jsx
270
+ // src/Dashboard.jsx
128
271
  import { useEffect, useState } from 'react';
129
- import { useSession } from 'gateway-web-sdk';
130
- import { getUser, getProducts, getOrders } from './api/index.js';
272
+ import { useSession } from '@mapnests/gateway-web-sdk';
273
+ import { getUser } from './api.js';
131
274
 
132
275
  export default function Dashboard() {
133
276
  const { isInitialized, isLoading, error } = useSession();
134
- const [data, setData] = useState(null);
277
+ const [user, setUser] = useState(null);
135
278
 
136
279
  useEffect(() => {
137
280
  if (!isInitialized) return;
138
- (async () => {
139
- try {
140
- const [u, p, o] = await Promise.all([getUser(), getProducts(), getOrders()]);
141
- if (!u.ok) throw new Error('User failed');
142
- if (!o.ok) throw new Error('Orders failed');
143
- setData({ user: await u.json(), products: p.data, orders: await o.json() });
144
- } catch (e) {
145
- console.error(e);
146
- setData(null);
147
- }
148
- })();
149
- }, [isInitialized]);
150
-
151
- if (isLoading) return <p>Loading session…</p>;
152
- if (error) return <p>Session error, limited mode.</p>;
153
- if (!data) return <p>Loading data…</p>;
154
-
155
- return (
156
- <div>
157
- <h3>User</h3>
158
- <pre>{JSON.stringify(data.user, null, 2)}</pre>
159
- <h3>Products</h3>
160
- <pre>{JSON.stringify(data.products, null, 2)}</pre>
161
- <h3>Orders</h3>
162
- <pre>{JSON.stringify(data.orders, null, 2)}</pre>
163
- </div>
164
- );
165
- }
166
- ```
167
-
168
- Notes
169
- - Install axios if you use the Axios pattern: `npm i axios`
170
- - Ensure your gateway exposes:
171
- - GET /api/session/bootstrap
172
- - GET /api/user
173
- - GET /api/products
174
- - GET /api/orders
175
- - The SDK automatically sends cookies; token header fallback is added only when necessary.
176
281
 
177
- ### React Application
178
-
179
- ```jsx
180
- import React from 'react';
181
- import { useSession } from 'gateway-web-sdk';
182
-
183
- function App() {
184
- const { isInitialized, isLoading, error, timeUntilRefresh, refresh } = useSession();
185
-
186
- if (isLoading) {
187
- return <div>Loading session...</div>;
188
- }
189
-
190
- if (error) {
191
- return <div>Error: {error}</div>;
192
- }
282
+ getUser()
283
+ .then(res => res.json())
284
+ .then(setUser)
285
+ .catch(err => console.error('Failed to fetch user:', err));
286
+ }, [isInitialized]);
193
287
 
194
- if (!isInitialized) {
195
- return <div>Session not initialized</div>;
196
- }
288
+ if (isLoading) return <p>Loading session...</p>;
289
+ if (error) return <p>Session error: {error}</p>;
290
+ if (!user) return <p>Loading data...</p>;
197
291
 
198
- return (
199
- <div>
200
- <h1>Session Active</h1>
201
- <p>Next refresh in: {Math.floor(timeUntilRefresh / 1000)} seconds</p>
202
- <button onClick={refresh}>Refresh Now</button>
203
- </div>
204
- );
292
+ return <pre>{JSON.stringify(user, null, 2)}</pre>;
205
293
  }
206
-
207
- export default App;
208
294
  ```
209
295
 
210
- ### Configuration
296
+ ---
211
297
 
212
- Configure the session manager before your app renders:
298
+ ## Next.js Integration
213
299
 
214
- ```jsx
215
- import React from 'react';
216
- import ReactDOM from 'react-dom/client';
217
- import { SessionManager } from 'gateway-web-sdk';
218
- import App from './App';
219
-
220
- // Configure session manager
221
- const sessionManager = SessionManager.getInstance();
222
- sessionManager.configure({
223
- bootstrapUrl: 'https://your-api.com/session/bootstrap',
224
- headers: {
225
- 'X-Custom-Header': 'value',
226
- },
227
- });
228
-
229
- ReactDOM.createRoot(document.getElementById('root')).render(
230
- <React.StrictMode>
231
- <App />
232
- </React.StrictMode>
233
- );
234
- ```
235
-
236
- ### Next.js Application
237
-
238
- #### Using App Router (`app/layout.js`)
300
+ ### App Router (`app/layout.js`)
239
301
 
240
302
  ```jsx
241
303
  'use client';
242
304
 
243
305
  import { useEffect } from 'react';
244
- import { SessionManager } from 'gateway-web-sdk';
306
+ import { SessionManager } from '@mapnests/gateway-web-sdk';
245
307
 
246
308
  export default function RootLayout({ children }) {
247
309
  useEffect(() => {
248
310
  const sessionManager = SessionManager.getInstance();
249
311
  sessionManager.configure({
250
- bootstrapUrl: '/api/session/bootstrap',
312
+ bootstrapUrl: `${process.env.NEXT_PUBLIC_API_BASE_URL}${process.env.NEXT_PUBLIC_BOOTSTRAP_PATH}`,
313
+ tokenCookieName: process.env.NEXT_PUBLIC_TOKEN_COOKIE_NAME,
251
314
  });
252
-
253
315
  sessionManager.initialize();
254
316
  }, []);
255
317
 
@@ -261,19 +323,19 @@ export default function RootLayout({ children }) {
261
323
  }
262
324
  ```
263
325
 
264
- #### Using Pages Router (`pages/_app.js`)
326
+ ### Pages Router (`pages/_app.js`)
265
327
 
266
328
  ```jsx
267
329
  import { useEffect } from 'react';
268
- import { SessionManager } from 'gateway-web-sdk';
330
+ import { SessionManager } from '@mapnests/gateway-web-sdk';
269
331
 
270
332
  function MyApp({ Component, pageProps }) {
271
333
  useEffect(() => {
272
334
  const sessionManager = SessionManager.getInstance();
273
335
  sessionManager.configure({
274
- bootstrapUrl: '/api/session/bootstrap',
336
+ bootstrapUrl: `${process.env.NEXT_PUBLIC_API_BASE_URL}${process.env.NEXT_PUBLIC_BOOTSTRAP_PATH}`,
337
+ tokenCookieName: process.env.NEXT_PUBLIC_TOKEN_COOKIE_NAME,
275
338
  });
276
-
277
339
  sessionManager.initialize();
278
340
  }, []);
279
341
 
@@ -283,30 +345,32 @@ function MyApp({ Component, pageProps }) {
283
345
  export default MyApp;
284
346
  ```
285
347
 
348
+ ---
349
+
286
350
  ## API Reference
287
351
 
288
- ### `useSession(options)`
352
+ ### `useSession(options?)`
289
353
 
290
354
  React hook for session management.
291
355
 
292
- #### Parameters
356
+ **Parameters:**
293
357
 
294
358
  | Parameter | Type | Default | Description |
295
359
  |-----------|------|---------|-------------|
296
360
  | `options.autoInitialize` | boolean | `true` | Automatically initialize session on mount |
297
361
 
298
- #### Return Value
362
+ **Returns:**
299
363
 
300
364
  ```typescript
301
365
  {
302
- isInitialized: boolean; // Whether session is initialized
303
- isLoading: boolean; // Whether bootstrap is in progress
304
- error: string | null; // Error message if any
305
- lastRefreshTime: number; // Timestamp of last refresh
306
- nextRefreshTime: number; // Timestamp of next scheduled refresh
307
- timeUntilRefresh: number; // Milliseconds until next refresh
308
- refresh: () => Promise<void>; // Manual refresh function
309
- initialize: () => Promise<void>; // Manual initialize function
366
+ isInitialized: boolean;
367
+ isLoading: boolean;
368
+ error: string | null;
369
+ lastRefreshTime: number;
370
+ nextRefreshTime: number;
371
+ timeUntilRefresh: number;
372
+ refresh: () => Promise<void>;
373
+ initialize: () => Promise<void>;
310
374
  }
311
375
  ```
312
376
 
@@ -314,175 +378,142 @@ React hook for session management.
314
378
 
315
379
  Core session management class (Singleton).
316
380
 
317
- #### Methods
381
+ #### `SessionManager.getInstance()`
318
382
 
319
- ##### `configure(config)`
383
+ Returns the singleton instance.
320
384
 
321
- Configure the session manager.
385
+ #### `configure(config)`
322
386
 
323
387
  ```javascript
324
388
  sessionManager.configure({
325
- bootstrapUrl: '/session/bootstrap', // Required: Bootstrap API endpoint
326
- tokenCookieName: 'stoken', // Optional: Custom token cookie name
327
- maxRetries: 3, // Optional: Max retry attempts
328
- headers: {}, // Optional: Additional headers
389
+ bootstrapUrl: '/session/bootstrap', // Required: Bootstrap API endpoint
390
+ tokenCookieName: 'token', // Optional: Token cookie name (default: 'token')
391
+ maxRetries: 3, // Optional: Max retry attempts (default: 3)
392
+ headers: {}, // Optional: Additional headers for bootstrap calls
329
393
  credentials: true, // Optional: Include credentials (default: true)
394
+ logLevel: 'WARN', // Optional: 'NONE' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG'
330
395
  });
331
396
  ```
332
397
 
333
- Note: `refreshInterval` and `tokenExpiry` are automatically set by the server's bootstrap response (`refresh_time` and `expire` fields). You don't need to configure them manually.
398
+ > `refreshInterval` and `tokenExpiry` are automatically set by the server's bootstrap response (`refresh_time` and `expire_time` fields). You don't need to configure them manually.
334
399
 
335
- ##### `initialize()`
400
+ #### `initialize()`
336
401
 
337
- Initialize session by calling bootstrap API.
402
+ Initialize session by calling the bootstrap endpoint.
338
403
 
339
404
  ```javascript
340
405
  await sessionManager.initialize();
341
406
  ```
342
407
 
343
- ##### `refreshToken()`
408
+ #### `refreshToken()`
344
409
 
345
- Manually refresh the session token.
410
+ Manually trigger a session token refresh.
346
411
 
347
412
  ```javascript
348
413
  await sessionManager.refreshToken();
349
414
  ```
350
415
 
351
- ##### `getSessionStatus()`
416
+ #### `getSessionId()`
352
417
 
353
- Get current session status.
418
+ Returns the current `cf-session-id`.
354
419
 
355
- ```javascript
356
- const status = sessionManager.getSessionStatus();
357
- ```
420
+ #### `getToken(name?)`
421
+
422
+ Returns the token value from the named cookie, or `null`.
423
+
424
+ #### `shouldUseTokenHeader()`
425
+
426
+ Returns `true` if the token should be sent as a request header (manual token mode or HTTP protocol).
427
+
428
+ #### `isRefreshing()`
429
+
430
+ Returns `true` if a refresh/initialize is currently in progress.
431
+
432
+ #### `waitForRefresh()`
433
+
434
+ Returns a promise that resolves when the in-progress refresh completes.
435
+
436
+ #### `getSessionStatus()`
437
+
438
+ Returns the current session state object.
358
439
 
359
- ##### `subscribe(listener)`
440
+ #### `subscribe(listener)`
360
441
 
361
- Subscribe to session state changes.
442
+ Subscribe to session state changes. Returns an unsubscribe function.
362
443
 
363
444
  ```javascript
364
445
  const unsubscribe = sessionManager.subscribe((state) => {
365
446
  console.log('Session state:', state);
366
447
  });
367
-
368
- // Later: unsubscribe()
369
448
  ```
370
449
 
371
- ##### `destroy()`
450
+ #### `destroy()`
451
+
452
+ Clean up timers, listeners, and cookies. Resets the session manager.
453
+
454
+ ### `fetchInterceptor(url, options?)`
372
455
 
373
- Clean up and reset the session manager.
456
+ Drop-in `fetch` wrapper. Automatically attaches session headers and retries on 401 `INVALID_SESSION`.
374
457
 
375
458
  ```javascript
376
- sessionManager.destroy();
459
+ import { fetchInterceptor } from '@mapnests/gateway-web-sdk';
460
+ const response = await fetchInterceptor('/api/data');
377
461
  ```
378
462
 
379
- ### Interceptors (Optional)
463
+ ### `setupAxiosInterceptor(axiosInstance)`
380
464
 
381
- #### `fetchInterceptor(url, options)`
382
-
383
- Fetch wrapper with automatic token refresh on 401/403.
465
+ Attaches request/response interceptors to an Axios instance. Returns the same instance.
384
466
 
385
467
  ```javascript
386
- import { fetchInterceptor } from 'gateway-web-sdk';
387
-
388
- const response = await fetchInterceptor('/api/data', {
389
- method: 'GET',
390
- headers: { 'Content-Type': 'application/json' }
391
- });
468
+ import axios from 'axios';
469
+ import { setupAxiosInterceptor } from '@mapnests/gateway-web-sdk';
470
+ const api = setupAxiosInterceptor(axios.create({ baseURL: '/api' }));
392
471
  ```
393
472
 
394
- #### `setupAxiosInterceptor(axiosInstance)`
473
+ ---
395
474
 
396
- Configure Axios instance with automatic token refresh.
475
+ ## Security Notice
397
476
 
398
- ```javascript
399
- import axios from 'axios';
400
- import { setupAxiosInterceptor } from 'gateway-web-sdk';
477
+ This SDK prioritizes **server-set HttpOnly cookies** for maximum security. The SDK includes a fallback to set cookies client-side, but these cannot be HttpOnly and are accessible to JavaScript.
401
478
 
402
- const api = setupAxiosInterceptor(axios.create({
403
- baseURL: 'https://api.example.com'
404
- }));
405
- ```
479
+ **Recommended:** Always set cookies from your server using the `Set-Cookie` header with `HttpOnly` flag. The SDK will detect server cookies and skip client-side cookie setting automatically.
480
+
481
+ ---
482
+
483
+ ## Best Practices
406
484
 
407
- ## Client-Side Best Practices
485
+ 1. **Single Instance** — Call `configure()` once at app startup and reuse the singleton.
486
+ 2. **Token Timing** — The server controls refresh and expiry timing via the bootstrap response.
487
+ 3. **Error Handling** — Handle errors gracefully and provide user feedback for limited mode.
488
+ 4. **HTTPS** — Always use HTTPS in production environments.
489
+ 5. **CORS/Credentials** — If cross-origin, ensure your server CORS allows credentials.
490
+ 6. **Initialization Order** — Always call `configure()` then `initialize()` before making API calls.
408
491
 
409
- 1. Single Instance: Call configure() once at app startup and reuse the singleton.
410
- 2. Token Timing: The server controls refresh and expiry timing via bootstrap response.
411
- 3. Error Handling: Handle errors and provide user feedback for limited mode.
412
- 4. HTTPS: Use secure, production-grade origins (https) in production environments.
413
- 5. CORS/Credentials: If cross-origin, ensure credentials are enabled in server CORS (SDK defaults to credentials: true).
414
- 6. Initialization: Initialize after configure() and before issuing business API calls.
492
+ ---
415
493
 
416
494
  ## Troubleshooting
417
495
 
418
496
  ### Cookies not being set
419
-
420
497
  - Verify your API returns `Set-Cookie` header with `HttpOnly` flag
421
498
  - Check CORS configuration allows credentials
422
499
  - Ensure `credentials: true` in configuration
423
500
 
424
501
  ### Automatic refresh not working
425
-
426
502
  - Check browser console for errors
427
503
  - Verify server is sending `refresh_time` in bootstrap response
428
504
  - Ensure timer isn't being cleared prematurely
429
505
 
430
506
  ### Multiple initializations
431
-
432
507
  - The SDK uses singleton pattern, but ensure you're not calling `initialize()` multiple times
433
508
  - Use `autoInitialize: false` in `useSession()` if you want manual control
434
509
 
435
510
  ### Next.js SSR errors
436
-
437
- - The SDK automatically detects SSR environments and prevents initialization
511
+ - The SDK detects SSR environments and prevents initialization
438
512
  - Always wrap initialization in `useEffect` or client components (`'use client'`)
439
513
  - Do not call `initialize()` during server-side rendering
440
514
 
441
- ## TypeScript Support
442
-
443
- Full TypeScript definitions are included:
444
-
445
- ```typescript
446
- import { SessionManager, useSession } from 'gateway-web-sdk';
447
- import type { SessionConfig, SessionState, UseSessionOptions } from 'gateway-web-sdk';
448
-
449
- const config: SessionConfig = {
450
- bootstrapUrl: '/api/session',
451
- };
452
-
453
- const manager = SessionManager.getInstance();
454
- manager.configure(config);
455
- ```
456
-
457
- ## Changelog
458
-
459
- ### [1.0.0] - 2024-01-12
460
-
461
- #### Added
462
- - Initial production release
463
- - TypeScript definitions for full type safety
464
- - Build process with Rollup (CJS + ESM outputs)
465
- - SSR detection for Next.js compatibility
466
- - Configuration validation
467
- - Secure session ID generation using crypto.randomUUID()
468
- - URL encoding for cookie values
469
-
470
- #### Security
471
- - Added warnings for client-side cookie limitations
472
- - Improved session ID generation with Web Crypto API
473
- - Added SSR environment checks to prevent runtime errors
474
- - URL-encoded cookie values to prevent injection
515
+ ---
475
516
 
476
517
  ## License
477
518
 
478
- MIT
479
-
480
- ## Contributing
481
-
482
- Contributions are welcome! Please open an issue or submit a pull request.
483
-
484
- ## Support
485
-
486
- For issues and questions:
487
- - GitHub Issues: [Report a bug](https://github.com/yourusername/gateway-web-sdk/issues)
488
- - Documentation: [Full API Reference](#api-reference)
519
+ MIT