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 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
+ [![npm version](https://img.shields.io/npm/v/axios-stream-auth-refresh.svg)](https://www.npmjs.com/package/axios-stream-auth-refresh)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue.svg)](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"}
@@ -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
+ }