axios-stream-auth-refresh 1.0.0
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/LICENSE.md +22 -0
- package/README.md +361 -0
- package/dist/index.cjs.js +2 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.es.js +60 -0
- package/dist/index.es.js.map +1 -0
- package/package.json +87 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
The MIT License (MIT)
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2025 Pourya Alipanah
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# axios-stream-auth-refresh
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/axios-stream-auth-refresh)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
|
|
9
|
+
### Smart token refresh interceptor for Axios using RxJS streams
|
|
10
|
+
|
|
11
|
+
Prevents duplicate refresh token requests when multiple API calls fail simultaneously with 401 errors.
|
|
12
|
+
|
|
13
|
+
> **Note:** This library is inspired by [axios-auth-refresh](https://github.com/Flyrell/axios-auth-refresh) but redesigned to use RxJS streams for better handling of parallel requests and reactive state management.
|
|
14
|
+
|
|
15
|
+
[Installation](#installation) • [Quick Start](#quick-start) • [API Reference](#api-reference) • [Examples](#examples) • [Acknowledgments](#acknowledgments)
|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## The Problem
|
|
22
|
+
|
|
23
|
+
When your access token expires, multiple API requests might fail at the same time. Without proper handling:
|
|
24
|
+
|
|
25
|
+
- Each failed request triggers a separate token refresh call
|
|
26
|
+
- Race conditions occur between refresh requests
|
|
27
|
+
- Server gets bombarded with unnecessary refresh token calls
|
|
28
|
+
- Complex state management to track refresh status
|
|
29
|
+
|
|
30
|
+
## The Solution
|
|
31
|
+
|
|
32
|
+
`axios-stream-auth-refresh` uses **RxJS BehaviorSubject** to intelligently queue failed requests and refresh the token only once:
|
|
33
|
+
|
|
34
|
+
- **Single refresh call** for multiple simultaneous failures
|
|
35
|
+
- **Automatic request retry** after successful token refresh
|
|
36
|
+
- **Queue management** using reactive streams
|
|
37
|
+
- **Type-safe** with full TypeScript support
|
|
38
|
+
- **Lightweight** with minimal dependencies
|
|
39
|
+
- **Configurable** status codes and behaviors
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install axios-stream-auth-refresh rxjs axios
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or with other package managers:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
yarn add axios-stream-auth-refresh rxjs axios
|
|
53
|
+
pnpm add axios-stream-auth-refresh rxjs axios
|
|
54
|
+
bun add axios-stream-auth-refresh rxjs axios
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Peer Dependencies
|
|
58
|
+
|
|
59
|
+
- `axios` >= 1.0.0
|
|
60
|
+
- `rxjs` >= 7.0.0
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import axios from 'axios'
|
|
68
|
+
import { createStreamRefreshInterceptor } from 'axios-stream-auth-refresh'
|
|
69
|
+
|
|
70
|
+
const api = axios.create({
|
|
71
|
+
baseURL: 'https://api.example.com',
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
createStreamRefreshInterceptor(api, async (failedRequest) => {
|
|
75
|
+
const { data } = await axios.post('/auth/refresh', {
|
|
76
|
+
refreshToken: localStorage.getItem('refreshToken'),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
localStorage.setItem('accessToken', data.accessToken)
|
|
80
|
+
failedRequest.headers.Authorization = `Bearer ${data.accessToken}`
|
|
81
|
+
|
|
82
|
+
return true
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
api.interceptors.request.use((config) => {
|
|
86
|
+
const token = localStorage.getItem('accessToken')
|
|
87
|
+
if (token) {
|
|
88
|
+
config.headers.Authorization = `Bearer ${token}`
|
|
89
|
+
}
|
|
90
|
+
return config
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## How It Works
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
Multiple requests fail (401) ──┐
|
|
100
|
+
│
|
|
101
|
+
├──► First request triggers refresh
|
|
102
|
+
│ │
|
|
103
|
+
Other requests are queued ──────┤ │
|
|
104
|
+
│ ▼
|
|
105
|
+
│ Refresh token API call
|
|
106
|
+
│ │
|
|
107
|
+
│ ▼
|
|
108
|
+
│ Token updated
|
|
109
|
+
│ │
|
|
110
|
+
└────┴──► All queued requests retry
|
|
111
|
+
with new token
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## API Reference
|
|
117
|
+
|
|
118
|
+
### `createStreamRefreshInterceptor(instance, refreshAuthCall, options?)`
|
|
119
|
+
|
|
120
|
+
Sets up the refresh token interceptor on an Axios instance.
|
|
121
|
+
|
|
122
|
+
#### Parameters
|
|
123
|
+
|
|
124
|
+
| Parameter | Type | Required | Description |
|
|
125
|
+
| ----------------- | -------------------------------------------------- | -------- | --------------------------------------------------------------------- |
|
|
126
|
+
| `instance` | `AxiosInstance` | ✅ | The Axios instance to attach the interceptor to |
|
|
127
|
+
| `refreshAuthCall` | `(config: AxiosRequestConfig) => Promise<boolean>` | ✅ | Async function that refreshes the token and returns `true` on success |
|
|
128
|
+
| `options` | `AxiosStreamRefreshOptions` | ❌ | Configuration options |
|
|
129
|
+
|
|
130
|
+
#### Options
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
interface AxiosStreamRefreshOptions {
|
|
134
|
+
statusCodes?: number[]
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Default: `{ statusCodes: [401] }`
|
|
139
|
+
|
|
140
|
+
#### Request Config Extensions
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
interface AxiosRequestConfig {
|
|
144
|
+
skipAuthRefresh?: boolean
|
|
145
|
+
retry?: boolean
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Examples
|
|
152
|
+
|
|
153
|
+
### Basic Usage with JWT
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import axios from 'axios'
|
|
157
|
+
import { createStreamRefreshInterceptor } from 'axios-stream-auth-refresh'
|
|
158
|
+
import type { AxiosRequestConfig } from 'axios'
|
|
159
|
+
|
|
160
|
+
const api = axios.create({
|
|
161
|
+
baseURL: 'https://api.example.com',
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const refreshAuthLogic = async (
|
|
165
|
+
originalConfig: AxiosRequestConfig
|
|
166
|
+
): Promise<boolean> => {
|
|
167
|
+
try {
|
|
168
|
+
const refreshToken = localStorage.getItem('refreshToken')
|
|
169
|
+
const response = await axios.post('https://api.example.com/auth/refresh', {
|
|
170
|
+
refreshToken,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const newToken = response?.data?.accessToken
|
|
174
|
+
|
|
175
|
+
if (!newToken) {
|
|
176
|
+
throw new Error('No access token received')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
localStorage.setItem('accessToken', data.accessToken)
|
|
180
|
+
localStorage.setItem('refreshToken', data.refreshToken)
|
|
181
|
+
|
|
182
|
+
originalConfig.headers = originalConfig.headers || {}
|
|
183
|
+
originalConfig.headers.Authorization = `Bearer ${newToken}`
|
|
184
|
+
|
|
185
|
+
return true
|
|
186
|
+
} catch (error) {
|
|
187
|
+
localStorage.clear()
|
|
188
|
+
window.location.href = '/login'
|
|
189
|
+
throw error
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
createStreamRefreshInterceptor(api, refreshAuthLogic)
|
|
194
|
+
|
|
195
|
+
api.interceptors.request.use((config) => {
|
|
196
|
+
const token = localStorage.getItem('accessToken')
|
|
197
|
+
if (token) {
|
|
198
|
+
config.headers.Authorization = `Bearer ${token}`
|
|
199
|
+
}
|
|
200
|
+
return config
|
|
201
|
+
})
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Custom Status Codes
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
createStreamRefreshInterceptor(
|
|
208
|
+
api,
|
|
209
|
+
async (config) => {
|
|
210
|
+
// Refresh logic
|
|
211
|
+
return true
|
|
212
|
+
},
|
|
213
|
+
{ statusCodes: [401, 403] }
|
|
214
|
+
)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Skip Refresh for Specific Requests
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
axios.post('/auth/refresh', data, {
|
|
221
|
+
skipAuthRefresh: true,
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
axios.post('/auth/login', credentials, {
|
|
225
|
+
skipAuthRefresh: true,
|
|
226
|
+
})
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### With React Context
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
import { createContext, useContext, useEffect } from 'react'
|
|
233
|
+
import axios from 'axios'
|
|
234
|
+
import { createStreamRefreshInterceptor } from 'axios-stream-auth-refresh'
|
|
235
|
+
|
|
236
|
+
const api = axios.create({
|
|
237
|
+
baseURL: process.env.REACT_APP_API_URL
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const ApiContext = createContext(api)
|
|
241
|
+
|
|
242
|
+
export function ApiProvider({ children }) {
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
createStreamRefreshInterceptor(api, async (config) => {
|
|
245
|
+
const refreshToken = localStorage.getItem('refreshToken')
|
|
246
|
+
|
|
247
|
+
if (!refreshToken) {
|
|
248
|
+
window.location.href = '/login'
|
|
249
|
+
throw new Error('No refresh token')
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const { data } = await axios.post('/auth/refresh', {
|
|
254
|
+
refreshToken
|
|
255
|
+
}, { skipAuthRefresh: true })
|
|
256
|
+
|
|
257
|
+
localStorage.setItem('accessToken', data.accessToken)
|
|
258
|
+
config.headers.Authorization = `Bearer ${data.accessToken}`
|
|
259
|
+
|
|
260
|
+
return true
|
|
261
|
+
} catch (error) {
|
|
262
|
+
localStorage.clear()
|
|
263
|
+
window.location.href = '/login'
|
|
264
|
+
throw error
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
api.interceptors.request.use((config) => {
|
|
269
|
+
const token = localStorage.getItem('accessToken')
|
|
270
|
+
if (token) {
|
|
271
|
+
originalConfig.headers = originalConfig.headers || {};
|
|
272
|
+
config.headers.Authorization = `Bearer ${token}`
|
|
273
|
+
}
|
|
274
|
+
return config
|
|
275
|
+
})
|
|
276
|
+
}, [])
|
|
277
|
+
|
|
278
|
+
return <ApiContext.Provider value={api}>{children}</ApiContext.Provider>
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export const useApi = () => useContext(ApiContext)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### With Next.js
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import axios from 'axios'
|
|
288
|
+
import { createStreamRefreshInterceptor } from 'axios-stream-auth-refresh'
|
|
289
|
+
|
|
290
|
+
export const api = axios.create({
|
|
291
|
+
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
createStreamRefreshInterceptor(api, async (config) => {
|
|
295
|
+
const response = await fetch('/api/auth/refresh', {
|
|
296
|
+
method: 'POST',
|
|
297
|
+
credentials: 'include',
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
if (!response.ok) {
|
|
301
|
+
throw new Error('Refresh failed')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const { accessToken } = await response.json()
|
|
305
|
+
config.headers.Authorization = `Bearer ${accessToken}`
|
|
306
|
+
|
|
307
|
+
return true
|
|
308
|
+
})
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Testing
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
npm test
|
|
317
|
+
npm run test:ui
|
|
318
|
+
npm run test:coverage
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Contributing
|
|
324
|
+
|
|
325
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
326
|
+
|
|
327
|
+
1. Fork the repository
|
|
328
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
329
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
330
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
331
|
+
5. Open a Pull Request
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## License
|
|
336
|
+
|
|
337
|
+
MIT © [Pourya Alipanah](https://github.com/Pourya-Alipanah)
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Acknowledgments
|
|
342
|
+
|
|
343
|
+
This library is inspired by [axios-auth-refresh](https://github.com/Flyrell/axios-auth-refresh) by [@Flyrell](https://github.com/Flyrell). While axios-auth-refresh provides excellent token refresh capabilities, this library takes a different approach using **RxJS streams** for:
|
|
344
|
+
|
|
345
|
+
- Better handling of parallel requests through BehaviorSubject
|
|
346
|
+
- Reactive state management for token refresh status
|
|
347
|
+
- More predictable queuing behavior with RxJS operators
|
|
348
|
+
|
|
349
|
+
Special thanks to the axios-auth-refresh project and its contributors for pioneering this pattern in the Axios ecosystem.
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
<div align="center">
|
|
354
|
+
|
|
355
|
+
**Made with ❤️ and RxJS**
|
|
356
|
+
|
|
357
|
+
[Report Bug](https://github.com/Pourya-Alipanah/axios-stream-auth-refresh/issues) - [Request Feature](https://github.com/Pourya-Alipanah/axios-stream-auth-refresh/issues)
|
|
358
|
+
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
---
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const f=require("rxjs");function i(e,u){var t,s;if(!e||!(e!=null&&e.config)||!(e!=null&&e.response))return!1;const{config:n}=e;return!u.statusCodes||!u.statusCodes.length||n!=null&&n.retry||(n.retry=!0,(t=e.config)!=null&&t.skipAuthRefresh)||e.response&&e.request.status===0||!e.response||!((s=u.statusCodes)!=null&&s.includes(e.response.status))?!1:(e.response||(e.response={config:e.config,data:void 0,status:0,statusText:"",headers:{}}),!0)}function a(e,u,n,t){return t.isRefreshing||(t.isRefreshing=!0,t.refreshTokenSubject.next(null),f.from(u(n)).subscribe({next:s=>{t.isRefreshing=!1,t.refreshTokenSubject.next(s)},error:s=>{t.isRefreshing=!1,t.refreshTokenSubject.error(s)}})),t.refreshTokenSubject.pipe(f.filter(s=>s!=null),f.take(1),f.switchMap(()=>f.from(e.request(n).then(s=>s.data))))}function o(e,u,n={statusCodes:[401]}){if(typeof u!="function")throw new Error("axios-stream-refresh requires `refreshAuthCall` to be a function that returns a promise.");const t={isRefreshing:!1,refreshTokenSubject:new f.BehaviorSubject(null)};e.interceptors.response.use(s=>s,async s=>{if(!i(s,n))return Promise.reject(s);const r=a(e,u,s.config,t);return f.firstValueFrom(r)})}exports.createStreamRefreshInterceptor=o;
|
|
2
|
+
//# sourceMappingURL=index.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":["../src/utils.ts","../src/index.ts"],"sourcesContent":["import { filter, from, switchMap, take } from 'rxjs'\r\nimport type { Observable } from 'rxjs'\r\nimport type {\r\n AxiosError,\r\n AxiosInstance,\r\n AxiosRequestConfig,\r\n InternalAxiosRequestConfig,\r\n} from 'axios'\r\nimport type {\r\n AxiosStreamRefreshCache,\r\n AxiosStreamRefreshOptions,\r\n} from './model'\r\n\r\n/**\r\n * Returns TRUE: when error.response.status is contained in options.statusCodes\r\n * Returns FALSE: when error or error.response doesn't exist or options.statusCodes doesn't include response status\r\n *\r\n * @return {boolean}\r\n */\r\nexport function shouldInterceptError(\r\n error: AxiosError,\r\n options: AxiosStreamRefreshOptions\r\n): boolean {\r\n if (!error || !error?.config || !error?.response) {\r\n return false\r\n }\r\n const { config } = error\r\n\r\n if (!options.statusCodes || !options.statusCodes.length) {\r\n return false\r\n }\r\n\r\n // avoid infinite loop\r\n if (config?.retry) {\r\n return false\r\n }\r\n config.retry = true\r\n\r\n if (error.config?.skipAuthRefresh) {\r\n return false\r\n }\r\n\r\n if (\r\n (error.response && error.request.status === 0) ||\r\n !error.response ||\r\n !options.statusCodes?.includes(error.response.status)\r\n ) {\r\n return false\r\n }\r\n\r\n // Copy config to response if there's a network error, so config can be modified and used in the retry\r\n if (!error.response) {\r\n error.response = {\r\n config: error.config as InternalAxiosRequestConfig,\r\n data: undefined,\r\n status: 0,\r\n statusText: '',\r\n headers: {},\r\n }\r\n }\r\n\r\n return true\r\n}\r\n\r\nexport function enqueueRequestAfterRefresh<T = unknown>(\r\n instance: AxiosInstance,\r\n refreshAuthCall: (originalConfig: AxiosRequestConfig) => Promise<boolean>,\r\n originalConfig: AxiosRequestConfig,\r\n cache: AxiosStreamRefreshCache\r\n): Observable<T> {\r\n if (!cache.isRefreshing) {\r\n cache.isRefreshing = true\r\n cache.refreshTokenSubject.next(null)\r\n\r\n from(refreshAuthCall(originalConfig)).subscribe({\r\n next: (newToken) => {\r\n cache.isRefreshing = false\r\n cache.refreshTokenSubject.next(newToken)\r\n },\r\n error: (err) => {\r\n cache.isRefreshing = false\r\n cache.refreshTokenSubject.error(err)\r\n },\r\n })\r\n }\r\n\r\n return cache.refreshTokenSubject.pipe(\r\n filter((token): token is boolean => token != null),\r\n take(1),\r\n switchMap(() => {\r\n return from(\r\n instance.request<T>(originalConfig).then((response) => response.data)\r\n )\r\n })\r\n )\r\n}\r\n","import { BehaviorSubject, firstValueFrom } from 'rxjs'\r\nimport { enqueueRequestAfterRefresh, shouldInterceptError } from './utils'\r\nimport type { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios'\r\nimport type {\r\n AxiosStreamRefreshCache,\r\n AxiosStreamRefreshOptions,\r\n} from './model'\r\n\r\nexport function createStreamRefreshInterceptor(\r\n instance: AxiosInstance,\r\n refreshAuthCall: (originalConfig: AxiosRequestConfig) => Promise<boolean>,\r\n options: AxiosStreamRefreshOptions = {\r\n statusCodes: [401],\r\n }\r\n) {\r\n if (typeof refreshAuthCall !== 'function') {\r\n throw new Error(\r\n 'axios-stream-refresh requires `refreshAuthCall` to be a function that returns a promise.'\r\n )\r\n }\r\n\r\n const cache: AxiosStreamRefreshCache = {\r\n isRefreshing: false,\r\n refreshTokenSubject: new BehaviorSubject<boolean | null>(null),\r\n }\r\n\r\n instance.interceptors.response.use(\r\n (response) => response,\r\n async (error: AxiosError) => {\r\n if (!shouldInterceptError(error, options)) {\r\n return Promise.reject(error)\r\n }\r\n\r\n // we can confirm that error.config are defined here because of shouldInterceptError check\r\n const result$ = enqueueRequestAfterRefresh(\r\n instance,\r\n refreshAuthCall,\r\n error.config!,\r\n cache\r\n )\r\n return firstValueFrom(result$)\r\n }\r\n )\r\n}\r\n"],"names":["shouldInterceptError","error","options","config","_a","_b","enqueueRequestAfterRefresh","instance","refreshAuthCall","originalConfig","cache","from","newToken","err","filter","token","take","switchMap","response","createStreamRefreshInterceptor","BehaviorSubject","result$","firstValueFrom"],"mappings":"wGAmBO,SAASA,EACdC,EACAC,EACS,SACT,GAAI,CAACD,GAAS,EAACA,GAAA,MAAAA,EAAO,SAAU,EAACA,GAAA,MAAAA,EAAO,UACtC,MAAO,GAET,KAAM,CAAE,OAAAE,GAAWF,EAgBnB,MAdI,CAACC,EAAQ,aAAe,CAACA,EAAQ,YAAY,QAK7CC,GAAA,MAAAA,EAAQ,QAGZA,EAAO,MAAQ,IAEXC,EAAAH,EAAM,SAAN,MAAAG,EAAc,kBAKfH,EAAM,UAAYA,EAAM,QAAQ,SAAW,GAC5C,CAACA,EAAM,UACP,GAACI,EAAAH,EAAQ,cAAR,MAAAG,EAAqB,SAASJ,EAAM,SAAS,SAEvC,IAIJA,EAAM,WACTA,EAAM,SAAW,CACf,OAAQA,EAAM,OACd,KAAM,OACN,OAAQ,EACR,WAAY,GACZ,QAAS,CAAA,CAAC,GAIP,GACT,CAEO,SAASK,EACdC,EACAC,EACAC,EACAC,EACe,CACf,OAAKA,EAAM,eACTA,EAAM,aAAe,GACrBA,EAAM,oBAAoB,KAAK,IAAI,EAEnCC,EAAAA,KAAKH,EAAgBC,CAAc,CAAC,EAAE,UAAU,CAC9C,KAAOG,GAAa,CAClBF,EAAM,aAAe,GACrBA,EAAM,oBAAoB,KAAKE,CAAQ,CACzC,EACA,MAAQC,GAAQ,CACdH,EAAM,aAAe,GACrBA,EAAM,oBAAoB,MAAMG,CAAG,CACrC,CAAA,CACD,GAGIH,EAAM,oBAAoB,KAC/BI,EAAAA,OAAQC,GAA4BA,GAAS,IAAI,EACjDC,EAAAA,KAAK,CAAC,EACNC,EAAAA,UAAU,IACDN,EAAAA,KACLJ,EAAS,QAAWE,CAAc,EAAE,KAAMS,GAAaA,EAAS,IAAI,CAAA,CAEvE,CAAA,CAEL,CCvFO,SAASC,EACdZ,EACAC,EACAN,EAAqC,CACnC,YAAa,CAAC,GAAG,CACnB,EACA,CACA,GAAI,OAAOM,GAAoB,WAC7B,MAAM,IAAI,MACR,0FAAA,EAIJ,MAAME,EAAiC,CACrC,aAAc,GACd,oBAAqB,IAAIU,EAAAA,gBAAgC,IAAI,CAAA,EAG/Db,EAAS,aAAa,SAAS,IAC5BW,GAAaA,EACd,MAAOjB,GAAsB,CAC3B,GAAI,CAACD,EAAqBC,EAAOC,CAAO,EACtC,OAAO,QAAQ,OAAOD,CAAK,EAI7B,MAAMoB,EAAUf,EACdC,EACAC,EACAP,EAAM,OACNS,CAAA,EAEF,OAAOY,EAAAA,eAAeD,CAAO,CAC/B,CAAA,CAEJ"}
|
package/dist/index.es.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { from as f, filter as r, take as a, switchMap as o, BehaviorSubject as l, firstValueFrom as h } from "rxjs";
|
|
2
|
+
function p(e, u) {
|
|
3
|
+
var t, s;
|
|
4
|
+
if (!e || !(e != null && e.config) || !(e != null && e.response))
|
|
5
|
+
return !1;
|
|
6
|
+
const { config: n } = e;
|
|
7
|
+
return !u.statusCodes || !u.statusCodes.length || n != null && n.retry || (n.retry = !0, (t = e.config) != null && t.skipAuthRefresh) || e.response && e.request.status === 0 || !e.response || !((s = u.statusCodes) != null && s.includes(e.response.status)) ? !1 : (e.response || (e.response = {
|
|
8
|
+
config: e.config,
|
|
9
|
+
data: void 0,
|
|
10
|
+
status: 0,
|
|
11
|
+
statusText: "",
|
|
12
|
+
headers: {}
|
|
13
|
+
}), !0);
|
|
14
|
+
}
|
|
15
|
+
function c(e, u, n, t) {
|
|
16
|
+
return t.isRefreshing || (t.isRefreshing = !0, t.refreshTokenSubject.next(null), f(u(n)).subscribe({
|
|
17
|
+
next: (s) => {
|
|
18
|
+
t.isRefreshing = !1, t.refreshTokenSubject.next(s);
|
|
19
|
+
},
|
|
20
|
+
error: (s) => {
|
|
21
|
+
t.isRefreshing = !1, t.refreshTokenSubject.error(s);
|
|
22
|
+
}
|
|
23
|
+
})), t.refreshTokenSubject.pipe(
|
|
24
|
+
r((s) => s != null),
|
|
25
|
+
a(1),
|
|
26
|
+
o(() => f(
|
|
27
|
+
e.request(n).then((s) => s.data)
|
|
28
|
+
))
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
function b(e, u, n = {
|
|
32
|
+
statusCodes: [401]
|
|
33
|
+
}) {
|
|
34
|
+
if (typeof u != "function")
|
|
35
|
+
throw new Error(
|
|
36
|
+
"axios-stream-refresh requires `refreshAuthCall` to be a function that returns a promise."
|
|
37
|
+
);
|
|
38
|
+
const t = {
|
|
39
|
+
isRefreshing: !1,
|
|
40
|
+
refreshTokenSubject: new l(null)
|
|
41
|
+
};
|
|
42
|
+
e.interceptors.response.use(
|
|
43
|
+
(s) => s,
|
|
44
|
+
async (s) => {
|
|
45
|
+
if (!p(s, n))
|
|
46
|
+
return Promise.reject(s);
|
|
47
|
+
const i = c(
|
|
48
|
+
e,
|
|
49
|
+
u,
|
|
50
|
+
s.config,
|
|
51
|
+
t
|
|
52
|
+
);
|
|
53
|
+
return h(i);
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
b as createStreamRefreshInterceptor
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=index.es.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.es.js","sources":["../src/utils.ts","../src/index.ts"],"sourcesContent":["import { filter, from, switchMap, take } from 'rxjs'\r\nimport type { Observable } from 'rxjs'\r\nimport type {\r\n AxiosError,\r\n AxiosInstance,\r\n AxiosRequestConfig,\r\n InternalAxiosRequestConfig,\r\n} from 'axios'\r\nimport type {\r\n AxiosStreamRefreshCache,\r\n AxiosStreamRefreshOptions,\r\n} from './model'\r\n\r\n/**\r\n * Returns TRUE: when error.response.status is contained in options.statusCodes\r\n * Returns FALSE: when error or error.response doesn't exist or options.statusCodes doesn't include response status\r\n *\r\n * @return {boolean}\r\n */\r\nexport function shouldInterceptError(\r\n error: AxiosError,\r\n options: AxiosStreamRefreshOptions\r\n): boolean {\r\n if (!error || !error?.config || !error?.response) {\r\n return false\r\n }\r\n const { config } = error\r\n\r\n if (!options.statusCodes || !options.statusCodes.length) {\r\n return false\r\n }\r\n\r\n // avoid infinite loop\r\n if (config?.retry) {\r\n return false\r\n }\r\n config.retry = true\r\n\r\n if (error.config?.skipAuthRefresh) {\r\n return false\r\n }\r\n\r\n if (\r\n (error.response && error.request.status === 0) ||\r\n !error.response ||\r\n !options.statusCodes?.includes(error.response.status)\r\n ) {\r\n return false\r\n }\r\n\r\n // Copy config to response if there's a network error, so config can be modified and used in the retry\r\n if (!error.response) {\r\n error.response = {\r\n config: error.config as InternalAxiosRequestConfig,\r\n data: undefined,\r\n status: 0,\r\n statusText: '',\r\n headers: {},\r\n }\r\n }\r\n\r\n return true\r\n}\r\n\r\nexport function enqueueRequestAfterRefresh<T = unknown>(\r\n instance: AxiosInstance,\r\n refreshAuthCall: (originalConfig: AxiosRequestConfig) => Promise<boolean>,\r\n originalConfig: AxiosRequestConfig,\r\n cache: AxiosStreamRefreshCache\r\n): Observable<T> {\r\n if (!cache.isRefreshing) {\r\n cache.isRefreshing = true\r\n cache.refreshTokenSubject.next(null)\r\n\r\n from(refreshAuthCall(originalConfig)).subscribe({\r\n next: (newToken) => {\r\n cache.isRefreshing = false\r\n cache.refreshTokenSubject.next(newToken)\r\n },\r\n error: (err) => {\r\n cache.isRefreshing = false\r\n cache.refreshTokenSubject.error(err)\r\n },\r\n })\r\n }\r\n\r\n return cache.refreshTokenSubject.pipe(\r\n filter((token): token is boolean => token != null),\r\n take(1),\r\n switchMap(() => {\r\n return from(\r\n instance.request<T>(originalConfig).then((response) => response.data)\r\n )\r\n })\r\n )\r\n}\r\n","import { BehaviorSubject, firstValueFrom } from 'rxjs'\r\nimport { enqueueRequestAfterRefresh, shouldInterceptError } from './utils'\r\nimport type { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios'\r\nimport type {\r\n AxiosStreamRefreshCache,\r\n AxiosStreamRefreshOptions,\r\n} from './model'\r\n\r\nexport function createStreamRefreshInterceptor(\r\n instance: AxiosInstance,\r\n refreshAuthCall: (originalConfig: AxiosRequestConfig) => Promise<boolean>,\r\n options: AxiosStreamRefreshOptions = {\r\n statusCodes: [401],\r\n }\r\n) {\r\n if (typeof refreshAuthCall !== 'function') {\r\n throw new Error(\r\n 'axios-stream-refresh requires `refreshAuthCall` to be a function that returns a promise.'\r\n )\r\n }\r\n\r\n const cache: AxiosStreamRefreshCache = {\r\n isRefreshing: false,\r\n refreshTokenSubject: new BehaviorSubject<boolean | null>(null),\r\n }\r\n\r\n instance.interceptors.response.use(\r\n (response) => response,\r\n async (error: AxiosError) => {\r\n if (!shouldInterceptError(error, options)) {\r\n return Promise.reject(error)\r\n }\r\n\r\n // we can confirm that error.config are defined here because of shouldInterceptError check\r\n const result$ = enqueueRequestAfterRefresh(\r\n instance,\r\n refreshAuthCall,\r\n error.config!,\r\n cache\r\n )\r\n return firstValueFrom(result$)\r\n }\r\n )\r\n}\r\n"],"names":["shouldInterceptError","error","options","config","_a","_b","enqueueRequestAfterRefresh","instance","refreshAuthCall","originalConfig","cache","from","newToken","err","filter","token","take","switchMap","response","createStreamRefreshInterceptor","BehaviorSubject","result$","firstValueFrom"],"mappings":";AAmBO,SAASA,EACdC,GACAC,GACS;;AACT,MAAI,CAACD,KAAS,EAACA,KAAA,QAAAA,EAAO,WAAU,EAACA,KAAA,QAAAA,EAAO;AACtC,WAAO;AAET,QAAM,EAAE,QAAAE,MAAWF;AAgBnB,SAdI,CAACC,EAAQ,eAAe,CAACA,EAAQ,YAAY,UAK7CC,KAAA,QAAAA,EAAQ,UAGZA,EAAO,QAAQ,KAEXC,IAAAH,EAAM,WAAN,QAAAG,EAAc,oBAKfH,EAAM,YAAYA,EAAM,QAAQ,WAAW,KAC5C,CAACA,EAAM,YACP,GAACI,IAAAH,EAAQ,gBAAR,QAAAG,EAAqB,SAASJ,EAAM,SAAS,WAEvC,MAIJA,EAAM,aACTA,EAAM,WAAW;AAAA,IACf,QAAQA,EAAM;AAAA,IACd,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,SAAS,CAAA;AAAA,EAAC,IAIP;AACT;AAEO,SAASK,EACdC,GACAC,GACAC,GACAC,GACe;AACf,SAAKA,EAAM,iBACTA,EAAM,eAAe,IACrBA,EAAM,oBAAoB,KAAK,IAAI,GAEnCC,EAAKH,EAAgBC,CAAc,CAAC,EAAE,UAAU;AAAA,IAC9C,MAAM,CAACG,MAAa;AAClB,MAAAF,EAAM,eAAe,IACrBA,EAAM,oBAAoB,KAAKE,CAAQ;AAAA,IACzC;AAAA,IACA,OAAO,CAACC,MAAQ;AACd,MAAAH,EAAM,eAAe,IACrBA,EAAM,oBAAoB,MAAMG,CAAG;AAAA,IACrC;AAAA,EAAA,CACD,IAGIH,EAAM,oBAAoB;AAAA,IAC/BI,EAAO,CAACC,MAA4BA,KAAS,IAAI;AAAA,IACjDC,EAAK,CAAC;AAAA,IACNC,EAAU,MACDN;AAAA,MACLJ,EAAS,QAAWE,CAAc,EAAE,KAAK,CAACS,MAAaA,EAAS,IAAI;AAAA,IAAA,CAEvE;AAAA,EAAA;AAEL;ACvFO,SAASC,EACdZ,GACAC,GACAN,IAAqC;AAAA,EACnC,aAAa,CAAC,GAAG;AACnB,GACA;AACA,MAAI,OAAOM,KAAoB;AAC7B,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAIJ,QAAME,IAAiC;AAAA,IACrC,cAAc;AAAA,IACd,qBAAqB,IAAIU,EAAgC,IAAI;AAAA,EAAA;AAG/D,EAAAb,EAAS,aAAa,SAAS;AAAA,IAC7B,CAACW,MAAaA;AAAA,IACd,OAAOjB,MAAsB;AAC3B,UAAI,CAACD,EAAqBC,GAAOC,CAAO;AACtC,eAAO,QAAQ,OAAOD,CAAK;AAI7B,YAAMoB,IAAUf;AAAA,QACdC;AAAA,QACAC;AAAA,QACAP,EAAM;AAAA,QACNS;AAAA,MAAA;AAEF,aAAOY,EAAeD,CAAO;AAAA,IAC/B;AAAA,EAAA;AAEJ;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "axios-stream-auth-refresh",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Parallel refresh token handler using RxJS and Axios",
|
|
5
|
+
"author": "Pourya Alipanah <pourya.alipanah2@gmail.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"rxjs",
|
|
9
|
+
"axios",
|
|
10
|
+
"refresh-token",
|
|
11
|
+
"parallel",
|
|
12
|
+
"token-refresh",
|
|
13
|
+
"authentication",
|
|
14
|
+
"interceptor"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/Pourya-Alipanah/axios-stream-auth-refresh.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/Pourya-Alipanah/axios-stream-auth-refresh#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/Pourya-Alipanah/axios-stream-auth-refresh/issues"
|
|
23
|
+
},
|
|
24
|
+
"main": "./dist/index.cjs.js",
|
|
25
|
+
"module": "./dist/index.es.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"import": "./dist/index.es.js",
|
|
30
|
+
"require": "./dist/index.cjs.js",
|
|
31
|
+
"types": "./dist/index.d.ts"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"dev": "vite",
|
|
41
|
+
"build": "tsc && vite build",
|
|
42
|
+
"test": "vitest",
|
|
43
|
+
"test:ui": "vitest --ui",
|
|
44
|
+
"test:run": "vitest run",
|
|
45
|
+
"test:coverage": "vitest run --coverage",
|
|
46
|
+
"lint": "eslint src",
|
|
47
|
+
"lint:fix": "eslint src --fix",
|
|
48
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
49
|
+
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"prepare": "husky",
|
|
52
|
+
"prepublishOnly": "npm run typecheck && npm run lint && npm run test:run && npm run build"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"axios": "^1.0.0",
|
|
56
|
+
"rxjs": "^7.0.0"
|
|
57
|
+
},
|
|
58
|
+
"lint-staged": {
|
|
59
|
+
"*.ts": [
|
|
60
|
+
"prettier --write",
|
|
61
|
+
"eslint --fix"
|
|
62
|
+
],
|
|
63
|
+
"*.{json,md}": [
|
|
64
|
+
"prettier --write"
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@types/node": "^22.19.3",
|
|
69
|
+
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
|
70
|
+
"@typescript-eslint/parser": "^8.50.1",
|
|
71
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
72
|
+
"@vitest/ui": "^4.0.16",
|
|
73
|
+
"axios": "^1.7.9",
|
|
74
|
+
"eslint": "^9.39.2",
|
|
75
|
+
"eslint-config-prettier": "^10.1.8",
|
|
76
|
+
"husky": "^9.1.7",
|
|
77
|
+
"lint-staged": "^16.2.7",
|
|
78
|
+
"prettier": "^3.7.4",
|
|
79
|
+
"rxjs": "^7.8.1",
|
|
80
|
+
"typescript": "^5.7.0",
|
|
81
|
+
"vite": "^6.0.0",
|
|
82
|
+
"vitest": "^4.0.16"
|
|
83
|
+
},
|
|
84
|
+
"engines": {
|
|
85
|
+
"node": ">=18.0.0"
|
|
86
|
+
}
|
|
87
|
+
}
|