@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 +304 -273
- 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 +1 -1
- package/src/SessionManager.d.ts +26 -10
- package/src/hooks/useSession.d.ts +9 -2
- package/src/index.d.ts +2 -2
- package/src/interceptors.d.ts +2 -0
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
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
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
|
-
##
|
|
16
|
+
## Installation
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
```bash
|
|
19
|
+
npm install @mapnests/gateway-web-sdk
|
|
20
|
+
```
|
|
20
21
|
|
|
21
|
-
|
|
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
|
-
##
|
|
24
|
+
## Implementation Guide
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
+
> If you are using Next.js, prefix with `NEXT_PUBLIC_` instead of `VITE_`.
|
|
35
49
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
71
|
-
import
|
|
72
|
-
import {
|
|
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
|
-
|
|
145
|
+
### Approach B — Axios Interceptor
|
|
75
146
|
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
|
93
|
-
if (
|
|
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
|
|
100
|
-
if (
|
|
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
|
|
113
|
-
if (
|
|
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
|
-
//
|
|
257
|
+
// Response was not JSON — return the original response
|
|
119
258
|
}
|
|
120
259
|
}
|
|
260
|
+
|
|
121
261
|
return res;
|
|
122
262
|
}
|
|
123
|
-
|
|
263
|
+
|
|
264
|
+
export const getUser = () => request(`${API_BASE_URL}/api/user`);
|
|
124
265
|
```
|
|
125
266
|
|
|
126
|
-
|
|
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
|
|
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 [
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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 (
|
|
195
|
-
|
|
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
|
-
|
|
296
|
+
---
|
|
211
297
|
|
|
212
|
-
|
|
298
|
+
## Next.js Integration
|
|
213
299
|
|
|
214
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
362
|
+
**Returns:**
|
|
299
363
|
|
|
300
364
|
```typescript
|
|
301
365
|
{
|
|
302
|
-
isInitialized: boolean;
|
|
303
|
-
isLoading: boolean;
|
|
304
|
-
error: string | null;
|
|
305
|
-
lastRefreshTime: number;
|
|
306
|
-
nextRefreshTime: number;
|
|
307
|
-
timeUntilRefresh: number;
|
|
308
|
-
refresh: () => Promise<void>;
|
|
309
|
-
initialize: () => Promise<void>;
|
|
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
|
-
####
|
|
381
|
+
#### `SessionManager.getInstance()`
|
|
318
382
|
|
|
319
|
-
|
|
383
|
+
Returns the singleton instance.
|
|
320
384
|
|
|
321
|
-
|
|
385
|
+
#### `configure(config)`
|
|
322
386
|
|
|
323
387
|
```javascript
|
|
324
388
|
sessionManager.configure({
|
|
325
|
-
bootstrapUrl: '/session/bootstrap',
|
|
326
|
-
tokenCookieName: '
|
|
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
|
-
|
|
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
|
-
|
|
400
|
+
#### `initialize()`
|
|
336
401
|
|
|
337
|
-
Initialize session by calling bootstrap
|
|
402
|
+
Initialize session by calling the bootstrap endpoint.
|
|
338
403
|
|
|
339
404
|
```javascript
|
|
340
405
|
await sessionManager.initialize();
|
|
341
406
|
```
|
|
342
407
|
|
|
343
|
-
|
|
408
|
+
#### `refreshToken()`
|
|
344
409
|
|
|
345
|
-
Manually
|
|
410
|
+
Manually trigger a session token refresh.
|
|
346
411
|
|
|
347
412
|
```javascript
|
|
348
413
|
await sessionManager.refreshToken();
|
|
349
414
|
```
|
|
350
415
|
|
|
351
|
-
|
|
416
|
+
#### `getSessionId()`
|
|
352
417
|
|
|
353
|
-
|
|
418
|
+
Returns the current `cf-session-id`.
|
|
354
419
|
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
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
|
-
|
|
450
|
+
#### `destroy()`
|
|
451
|
+
|
|
452
|
+
Clean up timers, listeners, and cookies. Resets the session manager.
|
|
453
|
+
|
|
454
|
+
### `fetchInterceptor(url, options?)`
|
|
372
455
|
|
|
373
|
-
|
|
456
|
+
Drop-in `fetch` wrapper. Automatically attaches session headers and retries on 401 `INVALID_SESSION`.
|
|
374
457
|
|
|
375
458
|
```javascript
|
|
376
|
-
|
|
459
|
+
import { fetchInterceptor } from '@mapnests/gateway-web-sdk';
|
|
460
|
+
const response = await fetchInterceptor('/api/data');
|
|
377
461
|
```
|
|
378
462
|
|
|
379
|
-
###
|
|
463
|
+
### `setupAxiosInterceptor(axiosInstance)`
|
|
380
464
|
|
|
381
|
-
|
|
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
|
|
387
|
-
|
|
388
|
-
const
|
|
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
|
-
|
|
473
|
+
---
|
|
395
474
|
|
|
396
|
-
|
|
475
|
+
## Security Notice
|
|
397
476
|
|
|
398
|
-
|
|
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
|
-
|
|
403
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|