@mapnests/gateway-web-sdk 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +475 -265
  2. package/package.json +3 -2
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 runtime dependencies (`react` and `react-dom` as peer dependencies)
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
17
+
18
+ ```bash
19
+ npm install @mapnests/gateway-web-sdk
20
+ ```
18
21
 
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.
22
+ ---
20
23
 
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
24
+ ## Implementation Guide
25
25
 
26
- ## Installation
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
27
 
28
- ```bash
29
- npm install gateway-web-sdk
28
+ > **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.
29
+
30
+ ---
31
+
32
+ ### Common Setup (Vite / CRA / React Apps)
33
+
34
+ #### Step 1 — Environment Variables
35
+
36
+ Create a `.env` file in your project root with the following variables:
37
+
38
+ ```env
39
+ VITE_API_BASE_URL=https://your-gateway.example.com
40
+ VITE_BOOTSTRAP_PATH=/api/session/bootstrap
41
+ VITE_TOKEN_COOKIE_NAME=token
30
42
  ```
31
43
 
32
- ## Quick Start
44
+ | Variable | Description |
45
+ |----------|-------------|
46
+ | `VITE_API_BASE_URL` | Base URL of your API gateway |
47
+ | `VITE_BOOTSTRAP_PATH` | Path to the session bootstrap endpoint |
48
+ | `VITE_TOKEN_COOKIE_NAME` | Name of the token cookie set by your server |
33
49
 
34
- ### Simple Implementation (Copy-Paste)
50
+ Then create a config helper to read these values:
35
51
 
36
- This setup covers bootstrap on app start and three API patterns:
37
- - Users → Fetch interceptor
38
- - Products Axios interceptor
39
- - Orders Manual implementation
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';
73
109
 
74
- const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://your-gateway.example.com';
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>;
75
138
 
76
- // Users via Fetch interceptor
77
- export const getUser = () => fetchInterceptor(`${API_BASE_URL}/api/user`);
139
+ return <pre>{JSON.stringify(user, null, 2)}</pre>;
140
+ }
141
+ ```
142
+
143
+ ---
144
+
145
+ ### Approach B — Axios Interceptor
146
+
147
+ Best if you already use Axios. Wraps an Axios instance with automatic session headers and 401 retry.
148
+
149
+ > Requires `axios` as a dependency: `npm install axios`
150
+
151
+ #### Step 3 — Create the Axios instance and API layer
78
152
 
79
- // Products via Axios interceptor
80
- export const axiosInstance = setupAxiosInterceptor(
81
- axios.create({ baseURL: API_BASE_URL, withCredentials: true })
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,175 +242,203 @@ 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
- })();
281
+
282
+ getUser()
283
+ .then(res => res.json())
284
+ .then(setUser)
285
+ .catch(err => console.error('Failed to fetch user:', err));
149
286
  }, [isInitialized]);
150
287
 
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>;
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>;
154
291
 
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
- );
292
+ return <pre>{JSON.stringify(user, null, 2)}</pre>;
165
293
  }
166
294
  ```
167
295
 
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
-
177
- ### React Application
178
-
179
- ```jsx
180
- import React from 'react';
181
- import { useSession } from 'gateway-web-sdk';
296
+ ---
182
297
 
183
- function App() {
184
- const { isInitialized, isLoading, error, timeUntilRefresh, refresh } = useSession();
185
-
186
- if (isLoading) {
187
- return <div>Loading session...</div>;
188
- }
298
+ ## Next.js Integration
189
299
 
190
- if (error) {
191
- return <div>Error: {error}</div>;
192
- }
193
-
194
- if (!isInitialized) {
195
- return <div>Session not initialized</div>;
196
- }
300
+ > **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.
197
301
 
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
- );
205
- }
302
+ ### Step 1 — Environment Variables
206
303
 
207
- export default App;
304
+ ```env
305
+ NEXT_PUBLIC_API_BASE_URL=https://your-gateway.example.com
306
+ NEXT_PUBLIC_BOOTSTRAP_PATH=/api/session/bootstrap
307
+ NEXT_PUBLIC_TOKEN_COOKIE_NAME=token
208
308
  ```
209
309
 
210
- ### Configuration
211
-
212
- Configure the session manager before your app renders:
310
+ ### Step 2 — Config Helper
213
311
 
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';
312
+ ```js
313
+ // src/config.js
314
+ const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;
315
+ const BOOTSTRAP_PATH = process.env.NEXT_PUBLIC_BOOTSTRAP_PATH;
316
+ const TOKEN_COOKIE_NAME = process.env.NEXT_PUBLIC_TOKEN_COOKIE_NAME;
219
317
 
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
- });
318
+ if (!API_BASE_URL) throw new Error('NEXT_PUBLIC_API_BASE_URL is not defined');
319
+ if (!BOOTSTRAP_PATH) throw new Error('NEXT_PUBLIC_BOOTSTRAP_PATH is not defined');
320
+ if (!TOKEN_COOKIE_NAME) throw new Error('NEXT_PUBLIC_TOKEN_COOKIE_NAME is not defined');
228
321
 
229
- ReactDOM.createRoot(document.getElementById('root')).render(
230
- <React.StrictMode>
231
- <App />
232
- </React.StrictMode>
233
- );
322
+ export { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME };
234
323
  ```
235
324
 
236
- ### Next.js Application
325
+ ### App Router
326
+
327
+ #### Step 3 — Create a Session Provider
237
328
 
238
- #### Using App Router (`app/layout.js`)
329
+ Create a client component that initializes the session. This keeps the root layout as a Server Component, preserving the benefits of React Server Components.
239
330
 
240
331
  ```jsx
332
+ // app/providers/SessionProvider.jsx
241
333
  'use client';
242
334
 
243
335
  import { useEffect } from 'react';
244
- import { SessionManager } from 'gateway-web-sdk';
336
+ import { SessionManager } from '@mapnests/gateway-web-sdk';
337
+ import { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME } from '@/src/config';
245
338
 
246
- export default function RootLayout({ children }) {
339
+ const sessionManager = SessionManager.getInstance();
340
+
341
+ export default function SessionProvider({ children }) {
247
342
  useEffect(() => {
248
- const sessionManager = SessionManager.getInstance();
249
343
  sessionManager.configure({
250
- bootstrapUrl: '/api/session/bootstrap',
344
+ bootstrapUrl: `${API_BASE_URL}${BOOTSTRAP_PATH}`,
345
+ tokenCookieName: TOKEN_COOKIE_NAME,
251
346
  });
252
-
253
- sessionManager.initialize();
347
+ sessionManager.initialize().catch(err =>
348
+ console.error('Failed to initialize session:', err)
349
+ );
254
350
  }, []);
255
351
 
352
+ return children;
353
+ }
354
+ ```
355
+
356
+ #### Step 4 — Add the Provider to the Root Layout
357
+
358
+ The root layout stays as a Server Component — do **not** add `'use client'` here.
359
+
360
+ ```jsx
361
+ // app/layout.js
362
+ import SessionProvider from './providers/SessionProvider';
363
+
364
+ export default function RootLayout({ children }) {
256
365
  return (
257
366
  <html lang="en">
258
- <body>{children}</body>
367
+ <body>
368
+ <SessionProvider>{children}</SessionProvider>
369
+ </body>
259
370
  </html>
260
371
  );
261
372
  }
262
373
  ```
263
374
 
264
- #### Using Pages Router (`pages/_app.js`)
375
+ #### Step 5 Create an API Layer and Use in a Page
376
+
377
+ 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`.
378
+
379
+ For example, using Approach A (Fetch Interceptor):
380
+
381
+ ```js
382
+ // src/api.js
383
+ import { fetchInterceptor } from '@mapnests/gateway-web-sdk';
384
+ import { API_BASE_URL } from './config';
385
+
386
+ export const getUser = () =>
387
+ fetchInterceptor(`${API_BASE_URL}/api/user`);
388
+ ```
389
+
390
+ Then use it in a page component. Page components that use hooks must be client components.
265
391
 
266
392
  ```jsx
393
+ // app/dashboard/page.jsx
394
+ 'use client';
395
+
396
+ import { useEffect, useState } from 'react';
397
+ import { useSession } from '@mapnests/gateway-web-sdk';
398
+ import { getUser } from '@/src/api';
399
+
400
+ export default function DashboardPage() {
401
+ const { isInitialized, isLoading, error } = useSession();
402
+ const [user, setUser] = useState(null);
403
+
404
+ useEffect(() => {
405
+ if (!isInitialized) return;
406
+
407
+ getUser()
408
+ .then(res => res.json())
409
+ .then(setUser)
410
+ .catch(err => console.error('Failed to fetch user:', err));
411
+ }, [isInitialized]);
412
+
413
+ if (isLoading) return <p>Loading session...</p>;
414
+ if (error) return <p>Session error: {error}</p>;
415
+ if (!user) return <p>Loading data...</p>;
416
+
417
+ return <pre>{JSON.stringify(user, null, 2)}</pre>;
418
+ }
419
+ ```
420
+
421
+ ### Pages Router
422
+
423
+ #### Step 3 — Initialize in `_app.js`
424
+
425
+ ```jsx
426
+ // pages/_app.js
267
427
  import { useEffect } from 'react';
268
- import { SessionManager } from 'gateway-web-sdk';
428
+ import { SessionManager } from '@mapnests/gateway-web-sdk';
429
+ import { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME } from '../src/config';
430
+
431
+ const sessionManager = SessionManager.getInstance();
269
432
 
270
433
  function MyApp({ Component, pageProps }) {
271
434
  useEffect(() => {
272
- const sessionManager = SessionManager.getInstance();
273
435
  sessionManager.configure({
274
- bootstrapUrl: '/api/session/bootstrap',
436
+ bootstrapUrl: `${API_BASE_URL}${BOOTSTRAP_PATH}`,
437
+ tokenCookieName: TOKEN_COOKIE_NAME,
275
438
  });
276
-
277
- sessionManager.initialize();
439
+ sessionManager.initialize().catch(err =>
440
+ console.error('Failed to initialize session:', err)
441
+ );
278
442
  }, []);
279
443
 
280
444
  return <Component {...pageProps} />;
@@ -283,30 +447,64 @@ function MyApp({ Component, pageProps }) {
283
447
  export default MyApp;
284
448
  ```
285
449
 
450
+ #### Step 4 — Create an API Layer and Use in a Page
451
+
452
+ Same as the App Router — create an `src/api.js` using any of **Approach A / B / C**, then use it in your page:
453
+
454
+ ```jsx
455
+ // pages/dashboard.jsx
456
+ import { useEffect, useState } from 'react';
457
+ import { useSession } from '@mapnests/gateway-web-sdk';
458
+ import { getUser } from '../src/api';
459
+
460
+ export default function Dashboard() {
461
+ const { isInitialized, isLoading, error } = useSession();
462
+ const [user, setUser] = useState(null);
463
+
464
+ useEffect(() => {
465
+ if (!isInitialized) return;
466
+
467
+ getUser()
468
+ .then(res => res.json())
469
+ .then(setUser)
470
+ .catch(err => console.error('Failed to fetch user:', err));
471
+ }, [isInitialized]);
472
+
473
+ if (isLoading) return <p>Loading session...</p>;
474
+ if (error) return <p>Session error: {error}</p>;
475
+ if (!user) return <p>Loading data...</p>;
476
+
477
+ return <pre>{JSON.stringify(user, null, 2)}</pre>;
478
+ }
479
+ ```
480
+
481
+ ---
482
+
286
483
  ## API Reference
287
484
 
288
- ### `useSession(options)`
485
+ ### `useSession(options?)`
289
486
 
290
487
  React hook for session management.
291
488
 
292
- #### Parameters
489
+ **Parameters:**
293
490
 
294
491
  | Parameter | Type | Default | Description |
295
492
  |-----------|------|---------|-------------|
296
493
  | `options.autoInitialize` | boolean | `true` | Automatically initialize session on mount |
297
494
 
298
- #### Return Value
495
+ **Returns:**
299
496
 
300
497
  ```typescript
301
498
  {
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
499
+ isInitialized: boolean;
500
+ isLoading: boolean;
501
+ error: string | null;
502
+ lastRefreshTime: number | null;
503
+ nextRefreshTime: number | null;
504
+ timeUntilRefresh: number | null;
505
+ initializationFailed: boolean;
506
+ refresh: () => Promise<void>;
507
+ initialize: () => Promise<void>;
310
508
  }
311
509
  ```
312
510
 
@@ -314,175 +512,187 @@ React hook for session management.
314
512
 
315
513
  Core session management class (Singleton).
316
514
 
317
- #### Methods
515
+ #### `SessionManager.getInstance()`
318
516
 
319
- ##### `configure(config)`
517
+ Returns the singleton instance.
320
518
 
321
- Configure the session manager.
519
+ #### `configure(config)`
322
520
 
323
521
  ```javascript
324
522
  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
523
+ bootstrapUrl: '/session/bootstrap', // Required: Bootstrap API endpoint
524
+ tokenCookieName: 'token', // Optional: Token cookie name (default: 'token')
525
+ maxRetries: 3, // Optional: Max retry attempts (default: 3)
526
+ headers: {}, // Optional: Additional headers for bootstrap calls
329
527
  credentials: true, // Optional: Include credentials (default: true)
528
+ logLevel: 'WARN', // Optional: 'NONE' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG'
330
529
  });
331
530
  ```
332
531
 
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.
532
+ > `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
533
 
335
- ##### `initialize()`
534
+ #### `initialize()`
336
535
 
337
- Initialize session by calling bootstrap API.
536
+ Initialize session by calling the bootstrap endpoint.
338
537
 
339
538
  ```javascript
340
539
  await sessionManager.initialize();
341
540
  ```
342
541
 
343
- ##### `refreshToken()`
542
+ #### `refreshToken()`
344
543
 
345
- Manually refresh the session token.
544
+ Manually trigger a session token refresh.
346
545
 
347
546
  ```javascript
348
547
  await sessionManager.refreshToken();
349
548
  ```
350
549
 
351
- ##### `getSessionStatus()`
550
+ #### `getSessionId()`
352
551
 
353
- Get current session status.
552
+ Returns the current `cf-session-id`.
354
553
 
355
- ```javascript
356
- const status = sessionManager.getSessionStatus();
554
+ #### `getToken(name?)`
555
+
556
+ Returns the token value from the named cookie, or `null`.
557
+
558
+ #### `shouldUseTokenHeader()`
559
+
560
+ Returns `true` if the token should be sent as a request header (i.e. when the app is served over HTTP, not HTTPS).
561
+
562
+ #### `isRefreshing()`
563
+
564
+ Returns `true` if a refresh/initialize is currently in progress.
565
+
566
+ #### `waitForRefresh()`
567
+
568
+ Returns a promise that resolves when the in-progress refresh completes.
569
+
570
+ #### `getSessionStatus()`
571
+
572
+ Returns the current session state object.
573
+
574
+ ```typescript
575
+ {
576
+ isInitialized: boolean;
577
+ isLoading: boolean;
578
+ lastRefreshTime: number | null;
579
+ nextRefreshTime: number | null;
580
+ tokenExpiry: number | null;
581
+ error: string | null;
582
+ errorCode: string | null;
583
+ initializationFailed: boolean;
584
+ timeUntilRefresh: number | null;
585
+ }
357
586
  ```
358
587
 
359
- ##### `subscribe(listener)`
588
+ > **Note:** `getSessionStatus()` returns `tokenExpiry` and `errorCode` which are not exposed by the `useSession` hook. Use this method directly if you need those fields.
589
+
590
+ #### `subscribe(listener)`
360
591
 
361
- Subscribe to session state changes.
592
+ Subscribe to session state changes. Returns an unsubscribe function.
362
593
 
363
594
  ```javascript
364
595
  const unsubscribe = sessionManager.subscribe((state) => {
365
596
  console.log('Session state:', state);
366
597
  });
367
-
368
- // Later: unsubscribe()
369
598
  ```
370
599
 
371
- ##### `destroy()`
600
+ #### `destroy()`
601
+
602
+ Clean up timers, listeners, and cookies. Resets the session manager.
603
+
604
+ ### `fetchInterceptor(url, options?)`
372
605
 
373
- Clean up and reset the session manager.
606
+ Drop-in `fetch` wrapper. Automatically attaches session headers and retries on 401 `INVALID_SESSION`.
374
607
 
375
608
  ```javascript
376
- sessionManager.destroy();
609
+ import { fetchInterceptor } from '@mapnests/gateway-web-sdk';
610
+ const response = await fetchInterceptor('/api/data');
377
611
  ```
378
612
 
379
- ### Interceptors (Optional)
613
+ ### `setupAxiosInterceptor(axiosInstance)`
380
614
 
381
- #### `fetchInterceptor(url, options)`
382
-
383
- Fetch wrapper with automatic token refresh on 401/403.
615
+ Attaches request/response interceptors to an Axios instance. Returns the same instance.
384
616
 
385
617
  ```javascript
386
- import { fetchInterceptor } from 'gateway-web-sdk';
618
+ import axios from 'axios';
619
+ import { setupAxiosInterceptor } from '@mapnests/gateway-web-sdk';
620
+ const api = setupAxiosInterceptor(axios.create({ baseURL: '/api' }));
621
+ ```
387
622
 
388
- const response = await fetchInterceptor('/api/data', {
389
- method: 'GET',
390
- headers: { 'Content-Type': 'application/json' }
391
- });
623
+ ### Error Classes
624
+
625
+ The SDK exports custom error classes for typed error handling:
626
+
627
+ ```javascript
628
+ import {
629
+ SessionError, // Base error class (code, details)
630
+ ConfigurationError, // Invalid configuration (code: 'CONFIGURATION_ERROR')
631
+ BootstrapError, // Bootstrap API failure (code: 'BOOTSTRAP_ERROR')
632
+ NetworkError, // Network-level failure (code: 'NETWORK_ERROR')
633
+ SSRError, // Called in non-browser environment (code: 'SSR_ERROR')
634
+ } from '@mapnests/gateway-web-sdk';
392
635
  ```
393
636
 
394
- #### `setupAxiosInterceptor(axiosInstance)`
637
+ All errors extend `SessionError`, which provides `code` (string) and `details` (object) properties.
395
638
 
396
- Configure Axios instance with automatic token refresh.
639
+ ### `logger` and `LOG_LEVELS`
640
+
641
+ The SDK's internal logger is exported for advanced use (e.g. setting log level independently of `configure()`):
397
642
 
398
643
  ```javascript
399
- import axios from 'axios';
400
- import { setupAxiosInterceptor } from 'gateway-web-sdk';
644
+ import { logger, LOG_LEVELS } from '@mapnests/gateway-web-sdk';
401
645
 
402
- const api = setupAxiosInterceptor(axios.create({
403
- baseURL: 'https://api.example.com'
404
- }));
646
+ logger.setLevel('DEBUG'); // or logger.setLevel(LOG_LEVELS.DEBUG)
647
+ logger.info('Custom log'); // [SessionManager] Custom log
405
648
  ```
406
649
 
407
- ## Client-Side Best Practices
650
+ Available levels: `NONE` (0), `ERROR` (1), `WARN` (2, default), `INFO` (3), `DEBUG` (4).
651
+
652
+ ---
653
+
654
+ ## Security Notice
655
+
656
+ 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.
657
+
658
+ **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.
408
659
 
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.
660
+ ---
661
+
662
+ ## Best Practices
663
+
664
+ 1. **Single Instance** Call `configure()` once at app startup and reuse the singleton.
665
+ 2. **Token Timing** The server controls refresh and expiry timing via the bootstrap response.
666
+ 3. **Error Handling** — Handle errors gracefully and provide user feedback for limited mode.
667
+ 4. **HTTPS** — Always use HTTPS in production environments.
668
+ 5. **CORS/Credentials** — If cross-origin, ensure your server CORS allows credentials.
669
+ 6. **Initialization Order** — Always call `configure()` then `initialize()` before making API calls.
670
+
671
+ ---
415
672
 
416
673
  ## Troubleshooting
417
674
 
418
675
  ### Cookies not being set
419
-
420
676
  - Verify your API returns `Set-Cookie` header with `HttpOnly` flag
421
677
  - Check CORS configuration allows credentials
422
678
  - Ensure `credentials: true` in configuration
423
679
 
424
680
  ### Automatic refresh not working
425
-
426
681
  - Check browser console for errors
427
682
  - Verify server is sending `refresh_time` in bootstrap response
428
683
  - Ensure timer isn't being cleared prematurely
429
684
 
430
685
  ### Multiple initializations
431
-
432
686
  - The SDK uses singleton pattern, but ensure you're not calling `initialize()` multiple times
433
687
  - Use `autoInitialize: false` in `useSession()` if you want manual control
434
688
 
435
689
  ### Next.js SSR errors
436
-
437
- - The SDK automatically detects SSR environments and prevents initialization
690
+ - The SDK detects SSR environments and prevents initialization
438
691
  - Always wrap initialization in `useEffect` or client components (`'use client'`)
439
692
  - Do not call `initialize()` during server-side rendering
440
693
 
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
694
+ ---
475
695
 
476
696
  ## License
477
697
 
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)
698
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mapnests/gateway-web-sdk",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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": {