@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.
- package/README.md +475 -265
- 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
|
-
-
|
|
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 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
|
-
##
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @mapnests/gateway-web-sdk
|
|
20
|
+
```
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
---
|
|
20
23
|
|
|
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
|
|
24
|
+
## Implementation Guide
|
|
25
25
|
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
+
Then create a config helper to read these values:
|
|
35
51
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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';
|
|
73
109
|
|
|
74
|
-
const
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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,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
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
152
|
-
if (error) return <p>Session error
|
|
153
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
211
|
-
|
|
212
|
-
Configure the session manager before your app renders:
|
|
310
|
+
### Step 2 — Config Helper
|
|
213
311
|
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
230
|
-
<React.StrictMode>
|
|
231
|
-
<App />
|
|
232
|
-
</React.StrictMode>
|
|
233
|
-
);
|
|
322
|
+
export { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME };
|
|
234
323
|
```
|
|
235
324
|
|
|
236
|
-
###
|
|
325
|
+
### App Router
|
|
326
|
+
|
|
327
|
+
#### Step 3 — Create a Session Provider
|
|
237
328
|
|
|
238
|
-
|
|
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
|
-
|
|
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:
|
|
344
|
+
bootstrapUrl: `${API_BASE_URL}${BOOTSTRAP_PATH}`,
|
|
345
|
+
tokenCookieName: TOKEN_COOKIE_NAME,
|
|
251
346
|
});
|
|
252
|
-
|
|
253
|
-
|
|
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>
|
|
367
|
+
<body>
|
|
368
|
+
<SessionProvider>{children}</SessionProvider>
|
|
369
|
+
</body>
|
|
259
370
|
</html>
|
|
260
371
|
);
|
|
261
372
|
}
|
|
262
373
|
```
|
|
263
374
|
|
|
264
|
-
####
|
|
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:
|
|
436
|
+
bootstrapUrl: `${API_BASE_URL}${BOOTSTRAP_PATH}`,
|
|
437
|
+
tokenCookieName: TOKEN_COOKIE_NAME,
|
|
275
438
|
});
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
495
|
+
**Returns:**
|
|
299
496
|
|
|
300
497
|
```typescript
|
|
301
498
|
{
|
|
302
|
-
isInitialized: boolean;
|
|
303
|
-
isLoading: boolean;
|
|
304
|
-
error: string | null;
|
|
305
|
-
lastRefreshTime: number
|
|
306
|
-
nextRefreshTime: number
|
|
307
|
-
timeUntilRefresh: number
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
####
|
|
515
|
+
#### `SessionManager.getInstance()`
|
|
318
516
|
|
|
319
|
-
|
|
517
|
+
Returns the singleton instance.
|
|
320
518
|
|
|
321
|
-
|
|
519
|
+
#### `configure(config)`
|
|
322
520
|
|
|
323
521
|
```javascript
|
|
324
522
|
sessionManager.configure({
|
|
325
|
-
bootstrapUrl: '/session/bootstrap',
|
|
326
|
-
tokenCookieName: '
|
|
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
|
-
|
|
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
|
-
|
|
534
|
+
#### `initialize()`
|
|
336
535
|
|
|
337
|
-
Initialize session by calling bootstrap
|
|
536
|
+
Initialize session by calling the bootstrap endpoint.
|
|
338
537
|
|
|
339
538
|
```javascript
|
|
340
539
|
await sessionManager.initialize();
|
|
341
540
|
```
|
|
342
541
|
|
|
343
|
-
|
|
542
|
+
#### `refreshToken()`
|
|
344
543
|
|
|
345
|
-
Manually
|
|
544
|
+
Manually trigger a session token refresh.
|
|
346
545
|
|
|
347
546
|
```javascript
|
|
348
547
|
await sessionManager.refreshToken();
|
|
349
548
|
```
|
|
350
549
|
|
|
351
|
-
|
|
550
|
+
#### `getSessionId()`
|
|
352
551
|
|
|
353
|
-
|
|
552
|
+
Returns the current `cf-session-id`.
|
|
354
553
|
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
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
|
-
|
|
600
|
+
#### `destroy()`
|
|
601
|
+
|
|
602
|
+
Clean up timers, listeners, and cookies. Resets the session manager.
|
|
603
|
+
|
|
604
|
+
### `fetchInterceptor(url, options?)`
|
|
372
605
|
|
|
373
|
-
|
|
606
|
+
Drop-in `fetch` wrapper. Automatically attaches session headers and retries on 401 `INVALID_SESSION`.
|
|
374
607
|
|
|
375
608
|
```javascript
|
|
376
|
-
|
|
609
|
+
import { fetchInterceptor } from '@mapnests/gateway-web-sdk';
|
|
610
|
+
const response = await fetchInterceptor('/api/data');
|
|
377
611
|
```
|
|
378
612
|
|
|
379
|
-
###
|
|
613
|
+
### `setupAxiosInterceptor(axiosInstance)`
|
|
380
614
|
|
|
381
|
-
|
|
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
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
637
|
+
All errors extend `SessionError`, which provides `code` (string) and `details` (object) properties.
|
|
395
638
|
|
|
396
|
-
|
|
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
|
|
400
|
-
import { setupAxiosInterceptor } from 'gateway-web-sdk';
|
|
644
|
+
import { logger, LOG_LEVELS } from '@mapnests/gateway-web-sdk';
|
|
401
645
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}));
|
|
646
|
+
logger.setLevel('DEBUG'); // or logger.setLevel(LOG_LEVELS.DEBUG)
|
|
647
|
+
logger.info('Custom log'); // [SessionManager] Custom log
|
|
405
648
|
```
|
|
406
649
|
|
|
407
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|