fzkit 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fengzai6
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,370 @@
1
+ # fzkit / HTTP Client
2
+
3
+ 基于 `axios` 的 HTTP 客户端工厂。帮你统一处理:
4
+
5
+ - access token 注入
6
+ - 401 / 业务失效时的自动刷新
7
+ - 并发刷新合并与冷却
8
+ - 通用重试(网络错误 / 5xx)
9
+ - in-flight GET 请求合并
10
+ - 业务响应与错误钩子
11
+
12
+ > 当前版本 `0.1.0`,API 仍可能调整,适合早期接入与验证。
13
+
14
+ ## 安装
15
+
16
+ ```bash
17
+ # npm
18
+ npm install fzkit axios
19
+
20
+ # yarn
21
+ yarn add fzkit axios
22
+
23
+ # pnpm
24
+ pnpm add fzkit axios
25
+ ```
26
+
27
+ `axios` 是 peer dependency,需要由业务项目自行安装(`^1.7.0`)。
28
+
29
+ ## 快速开始
30
+
31
+ ```ts
32
+ import axios from "axios";
33
+ import { createHttpClient } from "fzkit";
34
+
35
+ const http = createHttpClient({
36
+ axiosConfig: {
37
+ baseURL: "/api",
38
+ timeout: 15_000,
39
+ },
40
+ getAccessToken: () => localStorage.getItem("access_token"),
41
+ refreshAccessToken: async () => {
42
+ const refreshToken = localStorage.getItem("refresh_token");
43
+ if (!refreshToken) {
44
+ throw new Error("refreshToken missing");
45
+ }
46
+
47
+ const { data } = await axios.post("/auth/refresh", { refreshToken });
48
+ if (!data?.accessToken) {
49
+ throw new Error("refresh response missing accessToken");
50
+ }
51
+
52
+ localStorage.setItem("access_token", data.accessToken);
53
+ if (data.refreshToken) {
54
+ localStorage.setItem("refresh_token", data.refreshToken);
55
+ }
56
+
57
+ return data.accessToken;
58
+ },
59
+ onAuthFailure: async () => {
60
+ localStorage.removeItem("access_token");
61
+ localStorage.removeItem("refresh_token");
62
+ // 跳转登录页
63
+ },
64
+ });
65
+
66
+ const profile = await http.get("/account/profile");
67
+ console.log(profile.data);
68
+ ```
69
+
70
+ 返回值是标准 `AxiosInstance`,可继续使用 `http.get/post/request` 等 axios API。
71
+
72
+ ## 设计原则
73
+
74
+ 1. **token 存储完全交给业务侧**
75
+ 库不内置 storage,只通过 `getAccessToken` / `refreshAccessToken` 读写。
76
+ `refreshAccessToken` 返回新 token / `expiresAt` 时,业务必须写回 `getAccessToken` 使用的数据源。
77
+
78
+ 2. **尽量保留 axios 语义**
79
+ 成功返回 `AxiosResponse`;失败默认透传 `AxiosError`。
80
+
81
+ 3. **鉴权失败与瞬时失败分离**
82
+ 只有 refresh 鉴权失败、空 token、或登录过期才会走 `onAuthFailure`。
83
+ 网络错误 / 5xx 默认不登出。
84
+
85
+ ## Public API
86
+
87
+ ```ts
88
+ import {
89
+ createHttpClient,
90
+ TokenRefreshManager,
91
+ type HttpClientOptions,
92
+ type AccessTokenResult,
93
+ type AccessTokenDetail,
94
+ type RetryPolicy,
95
+ type DedupePolicy,
96
+ type RequestDedupePolicy,
97
+ type BusinessResponseResult,
98
+ type ErrorContext,
99
+ type ErrorMessages,
100
+ } from "fzkit/http-client";
101
+ ```
102
+
103
+ | 导出 | 说明 |
104
+ | --- | --- |
105
+ | `createHttpClient(options)` | 创建带鉴权/刷新/重试/合并能力的 axios 实例 |
106
+ | `TokenRefreshManager` | 可选,多客户端共享同一套刷新与冷却状态 |
107
+ | 相关类型 | 配置与回调类型定义 |
108
+
109
+ ## 配置项
110
+
111
+ ### 最小可用
112
+
113
+ | 字段 | 必填 | 说明 |
114
+ | --- | --- | --- |
115
+ | `axiosConfig` | 是 | 透传给 `axios.create` |
116
+ | `getAccessToken` | 是 | 读取当前 access token |
117
+ | `refreshAccessToken` | 否 | 自定义刷新逻辑;不传则 401 直接走 `onAuthFailure` |
118
+ | `onAuthFailure` | 否 | 登录失效收尾(清 token、跳登录) |
119
+ | `skipRefreshUrls` | 否 | 不触发 refresh 的路径(exact / prefix) |
120
+
121
+ ### Token 与 Headers
122
+
123
+ | 字段 | 默认 | 说明 |
124
+ | --- | --- | --- |
125
+ | `accessTokenHeaderName` | `"Authorization"` | token 注入 header 名 |
126
+ | `accessTokenPrefix` | `"Bearer"` | token 前缀 |
127
+ | `headersProvider` | - | 每次请求动态注入 headers(可 async) |
128
+ | `refreshBufferMs` | `0` | 提前刷新窗口;`getAccessToken` 返回 `AccessTokenDetail` 时生效 |
129
+
130
+ 说明:
131
+
132
+ - `getAccessToken` 返回空值 / 空字符串 / `{ token: null|undefined }` 时,不注入鉴权 header
133
+ - 调用方显式传入鉴权 header(含空字符串)时,工厂不覆盖
134
+ - `headersProvider` 返回的 headers 会覆盖同名默认注入项
135
+ - 鉴权刷新后的重试请求中,新 token 优先,不会被 `headersProvider` 的旧 Authorization 覆盖
136
+
137
+ ```ts
138
+ const http = createHttpClient({
139
+ axiosConfig: { baseURL: "/api" },
140
+ getAccessToken: () => ({
141
+ token: localStorage.getItem("access_token") ?? "",
142
+ expiresAt: localStorage.getItem("expires_at"),
143
+ }),
144
+ refreshBufferMs: 60_000,
145
+ headersProvider: () => ({
146
+ "x-request-id": crypto.randomUUID(),
147
+ }),
148
+ });
149
+ ```
150
+
151
+ ### 刷新控制
152
+
153
+ | 字段 | 默认 | 说明 |
154
+ | --- | --- | --- |
155
+ | `refreshAccessToken` | - | 刷新逻辑,返回类型与 `getAccessToken` 一致 |
156
+ | `shouldRefreshByResponse` | `() => false` | 通过业务响应判断是否需要刷新 |
157
+ | `refreshCooldownMs` | `15000` | 刷新成功后的冷却期,冷却内 401 跳过刷新、直接用新 token 重试 |
158
+ | `refreshManager` | 内部实例 | 多客户端共享刷新状态 |
159
+ | `unauthorizedStatusCode` | `401` | 触发刷新流程的 HTTP 状态码 |
160
+ | `refreshFailureCodes` | `[]` | 仅用于 refresh 失败判定的业务 code |
161
+ | `isRefreshFailure` | 内置默认 | 判断 refresh 是否已到需要登出的程度 |
162
+
163
+ 默认 `isRefreshFailure`:
164
+
165
+ - 非 `AxiosError` → 不视为鉴权失败
166
+ - 无 `response`(网络错误)或 `status >= 500` → 不视为鉴权失败
167
+ - `status === unauthorizedStatusCode` 或命中 `refreshFailureCodes` → 鉴权失败
168
+ - `refreshAccessToken` 返回空 token → 按鉴权失败处理(不进冷却、不重试原请求)
169
+
170
+ ```ts
171
+ import { createHttpClient, TokenRefreshManager } from "fzkit/http-client";
172
+
173
+ const sharedManager = new TokenRefreshManager(15_000);
174
+
175
+ const http1 = createHttpClient({
176
+ axiosConfig: { baseURL: "/api1" },
177
+ getAccessToken: () => tokenStore.accessToken,
178
+ refreshAccessToken: () => tokenStore.refresh(),
179
+ refreshManager: sharedManager,
180
+ onAuthFailure: () => tokenStore.logout(),
181
+ });
182
+
183
+ const http2 = createHttpClient({
184
+ axiosConfig: { baseURL: "/api2" },
185
+ getAccessToken: () => tokenStore.accessToken,
186
+ refreshAccessToken: () => tokenStore.refresh(),
187
+ refreshManager: sharedManager,
188
+ onAuthFailure: () => tokenStore.logout(),
189
+ });
190
+ ```
191
+
192
+ ### 重试与请求合并
193
+
194
+ ```ts
195
+ const http = createHttpClient({
196
+ axiosConfig: { baseURL: "/api" },
197
+ getAccessToken: async () => "",
198
+ retryPolicy: {
199
+ maxRetries: 2,
200
+ // shouldRetry / retryDelay 可自定义
201
+ },
202
+ dedupePolicy: {
203
+ enabled: true,
204
+ // generateKey 可自定义
205
+ },
206
+ });
207
+ ```
208
+
209
+ | 字段 | 默认 | 说明 |
210
+ | --- | --- | --- |
211
+ | `retryPolicy.maxRetries` | `0` | 最大重试次数(不含首次) |
212
+ | `retryPolicy.shouldRetry` | 网络错误或 5xx(取消不重试) | 是否重试 |
213
+ | `retryPolicy.retryDelay` | 指数退避,上限 30s | 重试延迟;非有限/负数按 0ms;回调抛错停止重试并走 `onError` |
214
+ | `dedupePolicy.enabled` | `false` | 是否启用 in-flight GET 合并 |
215
+ | `dedupePolicy.generateKey` | `method:baseURL:url:stableParams:headersProvider` | 合并 key |
216
+
217
+ 补充:
218
+
219
+ - 鉴权失败(`unauthorizedStatusCode`)优先于通用 `retryPolicy`
220
+ - dedupe 仅对 GET 生效
221
+ - 请求级可用 `{ dedupePolicy: { enabled: false } }` 覆盖客户端级开关
222
+ - 请求级只能覆盖 `enabled`,不能改 `generateKey`
223
+ - dedupe 在 adapter 层生效,覆盖 `http(url)` / `http.get` / `http.request` 等入口
224
+ - 内部 refresh / retry 在失败后 pending 已清理,可正常重放,不会自死锁
225
+
226
+ #### dedupe 语义边界
227
+
228
+ - **合并粒度**:仅 in-flight;请求结束后相同 key 会重新发起
229
+ - **默认 key**:`method + baseURL + url + stableParams + headersProvider 快照`
230
+ - 只纳入 `headersProvider()` 返回值,不纳入调用方 `config.headers` / 工厂注入的 token
231
+ - 未配置 `headersProvider` 时该段为空
232
+ - **取消**:
233
+ - 每个消费者的 `signal` / `cancelToken` 只取消自己
234
+ - 使用引用计数;全部消费者都离开后,才 abort 底层 shared 请求
235
+ - 已取消的请求不会创建 shared,避免孤儿请求
236
+ - **超时**:
237
+ - 每个消费者按自己的 `timeout` 做本地超时
238
+ - shared 物理超时取当前参与者中的 `max(timeout)`
239
+ - 任一参与者 `timeout === 0`(显式关闭)时,shared 不强制超时
240
+ - 客户端默认 `timeout: 15_000`,未单独配置时会按该值参与 max 计算
241
+ - **响应隔离**:
242
+ - 成功响应深拷贝 `data`
243
+ - 失败时 `error.response.data` 也会深拷贝隔离
244
+ - `onBusinessResponse` 返回完整响应替换时,也会深拷贝 `data`
245
+ - 共享 refresh 失败时,每个 waiter 拿到独立错误实例
246
+ - `headers/status/request` **不保证**引用共享(axios 管线可能重建);仅保证 `data` 隔离
247
+ - **params 序列化**:默认 key 稳定支持 plain object / array / `URLSearchParams` / `Date` / `Map` / `Set`;循环引用会抛出可控错误
248
+ - **网络侧 config**:底层请求以首个创建 shared 的 leader config 为准(除 cancel/timeout 外)
249
+
250
+ ### 业务钩子
251
+
252
+ | 字段 | 说明 |
253
+ | --- | --- |
254
+ | `onBusinessResponse` | 成功响应拦截:`void` 继续,`Error` / throw 失败,完整 `AxiosResponse` 替换响应 |
255
+ | `onError` | 请求失败 / 刷新失败钩子,可替换错误;`return Error` 与 `throw` 都会进入该钩子 |
256
+ | `errorMessages` | 覆盖内部英文默认文案(`refreshTokenExpired` / `loginExpired`) |
257
+
258
+ ```ts
259
+ const http = createHttpClient({
260
+ axiosConfig: { baseURL: "/api" },
261
+ getAccessToken: () => localStorage.getItem("access_token"),
262
+ onBusinessResponse: (response) => {
263
+ const data = response.data as { code?: number; message?: string };
264
+ if (data.code !== 0) {
265
+ return new Error(data.message ?? "business error");
266
+ }
267
+ },
268
+ onError: (error, context) => {
269
+ console.error(`[${context.type}]`, error.message);
270
+ },
271
+ errorMessages: {
272
+ refreshTokenExpired: "登录已过期,请重新登录",
273
+ loginExpired: "登录已失效,请重新登录",
274
+ },
275
+ });
276
+ ```
277
+
278
+ ## 默认请求流程
279
+
280
+ 1. `axios.create(axiosConfig)`
281
+ 2. 请求拦截:注入 token、合并 `headersProvider`
282
+ 3. 成功响应:
283
+ - `shouldRefreshByResponse` 判断是否刷新
284
+ - `onBusinessResponse` 处理业务响应
285
+ - 返回原始 `AxiosResponse`
286
+ 4. 失败响应:
287
+ - 命中 `unauthorizedStatusCode` 且启用刷新 → refresh 后重放
288
+ - 命中 `unauthorizedStatusCode` 且未启用刷新 → `onAuthFailure`
289
+ - 其他错误再走 `retryPolicy`
290
+ 5. refresh 失败:
291
+ - `isRefreshFailure === true` → `onAuthFailure`
292
+ - 否则透传原始错误,并触发 `onError({ type: "refresh" })`
293
+
294
+ ## FAQ
295
+
296
+ ### 1. 和直接用 axios 有什么区别?
297
+
298
+ `createHttpClient` 仍然返回 axios 实例,只是预装了鉴权、刷新、重试、合并等拦截逻辑。你继续用 `get/post/request`,不必换请求写法。
299
+
300
+ ### 2. refresh 返回 400 会自动登出吗?
301
+
302
+ 默认不会。只有:
303
+
304
+ - refresh 返回 `unauthorizedStatusCode`(默认 401)
305
+ - 或命中 `refreshFailureCodes`
306
+ - 或 `refreshAccessToken` 返回空 token
307
+ - 或你自定义的 `isRefreshFailure` 返回 `true`
308
+
309
+ 才会走 `onAuthFailure`。
310
+
311
+ 如果你们后端用业务 code 表示 refresh token 失效,推荐:
312
+
313
+ ```ts
314
+ refreshFailureCodes: [1001002],
315
+ ```
316
+
317
+ ### 3. 为什么网络错误 / 5xx 不登出?
318
+
319
+ 因为这通常不代表 refresh token 失效。默认策略是“瞬时失败可重试,鉴权失败才登出”。
320
+
321
+ ### 4. `skipRefreshUrls` 怎么匹配?
322
+
323
+ 只做 exact / prefix 路径匹配,不是子串 includes:
324
+
325
+ - `/auth` 匹配 `/auth`、`/auth/login`
326
+ - 不匹配 `/user/auth-history`、`/authorization`、`/api/auth/login`
327
+
328
+ ### 5. 多个 http 客户端如何共享一次刷新?
329
+
330
+ 创建共享的 `TokenRefreshManager`,通过 `refreshManager` 注入给多个 `createHttpClient` 实例。
331
+
332
+ 共享 manager 表示同一鉴权域:
333
+
334
+ - 一次合并 refresh 事务的鉴权失败只会触发一次 `onAuthFailure`
335
+ - 使用发起该次 refresh 的 client 回调
336
+ - 建议所有共享 client 使用同一 token 数据源与等价 logout 行为
337
+ - 外部 manager 的冷却由构造参数决定;client 的 `refreshCooldownMs` 不会回写已有 manager
338
+
339
+ ### 6. 请求级如何临时关闭 dedupe?
340
+
341
+ ```ts
342
+ await http.get("/profile", {
343
+ dedupePolicy: { enabled: false },
344
+ });
345
+ ```
346
+
347
+ ### 7. 不需要自动刷新怎么办?
348
+
349
+ 不传 `refreshAccessToken` 即可。此时遇到 `unauthorizedStatusCode` 会直接触发 `onAuthFailure`。
350
+
351
+ ### 8. 某些接口完全不想走鉴权/刷新?
352
+
353
+ 建议单独使用普通 `axios` 实例,而不是复用 `createHttpClient` 产物。
354
+
355
+ ### 9. 类型上为什么会看到 `dedupePolicy`?
356
+
357
+ 当前通过 `declare module "axios"` 扩展了请求配置,方便请求级覆盖。
358
+ 该字段只对 `createHttpClient` 创建的实例有运行时效果。
359
+
360
+ ## 测试
361
+
362
+ ```bash
363
+ yarn workspace fzkit test
364
+ ```
365
+
366
+ 测试分层与覆盖说明见 [`tests/README.md`](./tests/README.md)。
367
+
368
+ ## License
369
+
370
+ MIT
@@ -0,0 +1,265 @@
1
+ import { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
2
+ //#region src/http-client/types/token.d.ts
3
+ /**
4
+ * token 详细信息,用于主动刷新判断。
5
+ */
6
+ interface AccessTokenDetail {
7
+ token: string;
8
+ expiresAt: Date | string | number;
9
+ }
10
+ /**
11
+ * getAccessToken 的返回值类型,兼容旧版纯字符串返回。
12
+ */
13
+ type AccessTokenResult = string | AccessTokenDetail | null;
14
+ //#endregion
15
+ //#region src/http-client/token-refresh-manager.d.ts
16
+ /** @internal 冷却期内跳过刷新时的哨兵值,不对业务侧开放。 */
17
+ declare const REFRESH_SKIPPED: unique symbol;
18
+ declare class TokenRefreshManager {
19
+ private refreshingPromise;
20
+ private lastRefreshTime;
21
+ private readonly cooldownMs;
22
+ constructor(cooldownMs?: number);
23
+ /** @internal 供 createHttpClient 内部调用。 */
24
+ runRefresh(task: () => Promise<AccessTokenResult>): Promise<AccessTokenResult | typeof REFRESH_SKIPPED>;
25
+ }
26
+ //#endregion
27
+ //#region src/http-client/types/common.d.ts
28
+ /**
29
+ * onBusinessResponse 的返回值类型。
30
+ * - void:继续正常流程(表示成功)
31
+ * - Error:抛出错误(表示业务失败)
32
+ * - AxiosResponse:用完整响应形态(status/data/headers/config/statusText)替换原响应,不会二次触发 onBusinessResponse
33
+ */
34
+ type BusinessResponseResult = void | Error | AxiosResponse;
35
+ /**
36
+ * 错误上下文,传递给 onError 钩子。
37
+ */
38
+ interface ErrorContext {
39
+ /**
40
+ * 错误来源类型。
41
+ */
42
+ type: 'request' | 'refresh';
43
+ }
44
+ /**
45
+ * 自定义错误消息。
46
+ */
47
+ interface ErrorMessages {
48
+ refreshTokenExpired?: string;
49
+ loginExpired?: string;
50
+ }
51
+ //#endregion
52
+ //#region src/http-client/types/http-client-options.d.ts
53
+ /**
54
+ * 请求合并配置。
55
+ */
56
+ interface DedupePolicy {
57
+ /** 是否启用请求合并。默认 false。仅合并 GET 请求。 */
58
+ enabled?: boolean;
59
+ /**
60
+ * 自定义合并 key 生成器。
61
+ * 默认:`method:baseURL:url:stableParams:headersProviderSnapshot`
62
+ *
63
+ * 默认实现仅纳入 `headersProvider()` 返回值快照(`config.__dedupeProviderHeaders`),
64
+ * 不纳入调用方 `config.headers` 或工厂注入的 token。
65
+ * params 稳定序列化支持 plain object / array / URLSearchParams / Date / Map / Set;
66
+ * 循环引用会抛错,避免误合并。
67
+ */
68
+ generateKey?: (config: AxiosRequestConfig) => string;
69
+ }
70
+ /**
71
+ * 请求级合并配置。
72
+ * 仅允许覆盖 enabled;generateKey 只能在客户端级配置。
73
+ */
74
+ interface RequestDedupePolicy {
75
+ /** 是否启用请求合并。覆盖客户端级 enabled。 */
76
+ enabled?: boolean;
77
+ }
78
+ declare module 'axios' {
79
+ interface AxiosRequestConfig {
80
+ /**
81
+ * 请求级合并策略。
82
+ * 仅可覆盖 enabled,不能修改 generateKey。
83
+ * 仅 createHttpClient 创建的实例生效。
84
+ */
85
+ dedupePolicy?: RequestDedupePolicy;
86
+ }
87
+ }
88
+ /**
89
+ * 重试策略配置。
90
+ */
91
+ interface RetryPolicy {
92
+ /** 最大重试次数(不含首次请求)。默认 0(不重试)。 */
93
+ maxRetries?: number;
94
+ /**
95
+ * 判断是否需要重试。
96
+ * 默认:网络错误(无 response)或 5xx 状态码时重试;用户取消不重试。
97
+ */
98
+ shouldRetry?: (error: unknown, retryCount: number) => boolean;
99
+ /**
100
+ * 计算重试延迟(毫秒)。
101
+ * 默认:指数退避,公式 `1000 * 2^retryCount`,上限 30 秒。
102
+ * 返回非有限数(NaN/Infinity)或负数时,按 0ms 处理。
103
+ * shouldRetry / retryDelay 抛错会停止重试,并进入 onError({ type: "request" })。
104
+ */
105
+ retryDelay?: (retryCount: number) => number;
106
+ }
107
+ /**
108
+ * 创建 HTTP 客户端时可传入的配置。
109
+ *
110
+ * @typeParam T - getAccessToken / refreshAccessToken 的统一返回类型,
111
+ * 默认为 `AccessTokenResult`。当 T 为 `AccessTokenDetail` 时启用主动刷新。
112
+ */
113
+ interface HttpClientOptions<T extends AccessTokenResult = AccessTokenResult> {
114
+ /**
115
+ * 透传给 axios.create 的初始化配置。
116
+ */
117
+ axiosConfig: AxiosRequestConfig;
118
+ /**
119
+ * 获取当前 access token。
120
+ *
121
+ * 返回 `string` 时仅注入 token,不做主动刷新判断。
122
+ * 返回 `AccessTokenDetail` 时,会在 token 即将过期前主动触发刷新。
123
+ */
124
+ getAccessToken: () => T | Promise<T>;
125
+ /**
126
+ * access token 注入到请求头时使用的字段名。
127
+ * 默认 `Authorization`。
128
+ */
129
+ accessTokenHeaderName?: string;
130
+ /**
131
+ * access token 的前缀。
132
+ * 默认 `Bearer`。
133
+ */
134
+ accessTokenPrefix?: string;
135
+ /**
136
+ * 刷新失败判定的业务 code 列表。
137
+ *
138
+ * 仅用于 `defaultIsRefreshFailure`:当 refresh 请求响应体存在 `data.code`
139
+ * 且命中该列表时,视为刷新鉴权失败。
140
+ *
141
+ * 不会影响普通业务请求的鉴权识别;更通用的入口是 `isRefreshFailure`。
142
+ * 默认假设响应体形状为 `{ code?: number }`。
143
+ */
144
+ refreshFailureCodes?: number[];
145
+ /**
146
+ * 通用重试策略。
147
+ * 默认不重试(maxRetries: 0)。
148
+ */
149
+ retryPolicy?: RetryPolicy;
150
+ /**
151
+ * 请求合并策略。
152
+ * 默认关闭(enabled: false)。
153
+ */
154
+ dedupePolicy?: DedupePolicy;
155
+ /**
156
+ * 运行时动态 headers 提供者。
157
+ *
158
+ * 每次请求时调用,返回的 headers 会合并到请求中。
159
+ * 支持同步或异步返回。
160
+ *
161
+ * 典型场景:
162
+ * - 请求追踪:`{ 'x-trace-id': crypto.randomUUID() }`
163
+ * - 业务标识:从 store 读取 `{ 'x-tenant-id': tenantId }`
164
+ *
165
+ * 注意:
166
+ * - 首次请求时,返回的 headers 会覆盖默认注入的 headers(如 Authorization)
167
+ * - 鉴权刷新后的重试请求中,新 token 优先,不会被 headersProvider 的旧 Authorization 覆盖
168
+ */
169
+ headersProvider?: () => Record<string, string> | Promise<Record<string, string>>;
170
+ /**
171
+ * 触发刷新流程的 HTTP 状态码。
172
+ * 默认 `401`。
173
+ */
174
+ unauthorizedStatusCode?: number;
175
+ /**
176
+ * 自定义错误消息,覆盖内部默认值。
177
+ */
178
+ errorMessages?: ErrorMessages;
179
+ /**
180
+ * 判断刷新 token 请求本身是否已经失败到需要退出登录。
181
+ *
182
+ * 默认行为:
183
+ * - 非 AxiosError(编程错误 / 业务自定义 Error)不视为刷新鉴权失败
184
+ * - AxiosError 无 response(网络错误)或状态码 >= 500 时不视为刷新失败
185
+ * - 状态码 === unauthorizedStatusCode 时视为刷新失败
186
+ * - 响应 data.code 在 refreshFailureCodes 列表中时视为刷新鉴权失败
187
+ *
188
+ * 若业务需要“refresh 抛 Error 即登出”,请自定义该函数。
189
+ */
190
+ isRefreshFailure?: (error: unknown) => boolean;
191
+ /**
192
+ * 自定义刷新逻辑,返回类型必须与 getAccessToken 一致。
193
+ *
194
+ * 库不缓存 token。若返回 AccessTokenDetail,业务必须把新 token / expiresAt
195
+ * 写回 getAccessToken 使用的数据源,后续请求才会读到新过期时间。
196
+ */
197
+ refreshAccessToken?: () => T | Promise<T>;
198
+ /**
199
+ * 不触发 refresh token 流程的请求路径列表。
200
+ *
201
+ * 仅 exact / prefix 匹配,不是任意子串 includes,也不做中间段滑动匹配。
202
+ * 例如配置 `/auth` 会匹配 `/auth`、`/auth/login`,
203
+ * 但不会匹配 `/user/auth-history`、`/authorization`、`/api/auth/login`。
204
+ */
205
+ skipRefreshUrls?: string[];
206
+ /**
207
+ * 提前刷新的毫秒数,默认 0(不提前刷新)。
208
+ * 设置为大于 0 的值时,会在 token 即将过期前主动触发刷新。
209
+ */
210
+ refreshBufferMs?: number;
211
+ /**
212
+ * 刷新 token 后的冷却期(毫秒)。
213
+ * 在冷却期内收到的 401 请求会跳过刷新,直接使用新 token 重试。
214
+ * 用于处理刷新完成后,旧请求陆续返回 401 的并发场景。
215
+ * 默认 15000(15 秒)。
216
+ */
217
+ refreshCooldownMs?: number;
218
+ /**
219
+ * 外部传入的 TokenRefreshManager 实例。
220
+ * 用于多个客户端共享同一鉴权域的 refresh / 冷却状态。
221
+ * 不传则自动创建独立实例。
222
+ *
223
+ * 共享 manager 时:
224
+ * - 一次合并 refresh 事务的鉴权失败只会触发一次 onAuthFailure
225
+ * - 使用发起该次 refresh 的 client 回调
226
+ * - 建议共享 client 使用同一 token 数据源与等价 logout 行为
227
+ * - 外部 manager 的 cooldown 由构造参数决定;client 的 refreshCooldownMs 不会回写已有 manager
228
+ */
229
+ refreshManager?: TokenRefreshManager;
230
+ /**
231
+ * 通过业务响应判断是否需要刷新 token。
232
+ */
233
+ shouldRefreshByResponse?: (response: AxiosResponse<unknown>) => boolean;
234
+ /**
235
+ * 登录失效后的统一收尾回调。
236
+ */
237
+ onAuthFailure?: (error?: unknown) => void | Promise<void>;
238
+ /**
239
+ * 业务响应拦截器。
240
+ * - 返回 void:继续正常流程(表示成功)
241
+ * - 返回 Error:抛出错误(表示业务失败)
242
+ * - throw Error / 非 Error:与返回 Error 一样,统一进入 onError({ type: "request" })
243
+ * - 返回完整 AxiosResponse 形态:用新响应替换原响应,不会二次触发 onBusinessResponse
244
+ * - 可以是 async
245
+ */
246
+ onBusinessResponse?: (response: AxiosResponse<unknown>) => BusinessResponseResult | Promise<BusinessResponseResult>;
247
+ /**
248
+ * 全局错误钩子。
249
+ * - 返回 Error:用新错误替换原错误
250
+ * - 返回 void:继续抛出原错误
251
+ * - 可以是 async
252
+ *
253
+ * error 类型为 AxiosError | Error:
254
+ * - 请求失败时为 AxiosError,可通过 axios.isAxiosError(error) 收敛访问 response/config
255
+ * - 刷新失败时可能为 AxiosError 或业务侧抛出的任意 Error
256
+ * - 登录过期等内部构造的错误为普通 Error
257
+ */
258
+ onError?: (error: AxiosError | Error, context: ErrorContext) => AxiosError | Error | void | Promise<AxiosError | Error | void>;
259
+ }
260
+ //#endregion
261
+ //#region src/http-client/create-http-client.d.ts
262
+ declare const createHttpClient: <T extends AccessTokenResult = AccessTokenResult>(options: HttpClientOptions<T>) => AxiosInstance;
263
+ //#endregion
264
+ export { RetryPolicy as a, ErrorMessages as c, AccessTokenResult as d, RequestDedupePolicy as i, TokenRefreshManager as l, DedupePolicy as n, BusinessResponseResult as o, HttpClientOptions as r, ErrorContext as s, createHttpClient as t, AccessTokenDetail as u };
265
+ //# sourceMappingURL=create-http-client-BoM9-_Ty.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-http-client-BoM9-_Ty.d.cts","names":[],"sources":["../src/http-client/types/token.ts","../src/http-client/token-refresh-manager.ts","../src/http-client/types/common.ts","../src/http-client/types/http-client-options.ts","../src/http-client/create-http-client.ts"],"mappings":";;;;;UAGiB;EACf;EACA,WAAW;;;;;KAMD,6BAA6B;;;;cCR5B;cAEA;UACH;UACA;mBACS;cAEL;;EAKN,WACJ,YAAY,QAAQ,qBACnB,QAAQ,2BAA2B;;;;;;;;;;KCe5B,gCAAgC,QAAQ;;;;UAKnC;;;;EAIf;;;;;UAMe;EACf;EACA;;;;;;;UCzCe;;EAEf;;;;;;;;;;EAWA,eAAe,QAAQ;;;;;;UAOR;;EAEf;;;YAMU;;;;;;IAMR,eAAe;;;;;;UAOF;;EAEf;;;;;EAMA,eAAe,gBAAgB;;;;;;;EAQ/B,cAAc;;;;;;;;UASC,kBAAkB,UAAU,oBAAoB;;;;EAI/D,aAAa;;;;;;;EAUb,sBAAsB,IAAI,QAAQ;;;;;EAMlC;;;;;EAMA;;;;;;;;;;EAaA;;;;;EAMA,cAAc;;;;;EAMd,eAAe;;;;;;;;;;;;;;;EAgBf,wBAAwB,yBAAyB,QAAQ;;;;;EAMzD;;;;EAKA,gBAAgB;;;;;;;;;;;;EAahB,oBAAoB;;;;;;;EAUpB,2BAA2B,IAAI,QAAQ;;;;;;;;EASvC;;;;;EAMA;;;;;;;EAQA;;;;;;;;;;;;EAaA,iBAAiB;;;;EAKjB,2BAA2B,UAAU;;;;EAOrC,iBAAiB,2BAA2B;;;;;;;;;EAU5C,sBACE,UAAU,2BACP,yBAAyB,QAAQ;;;;;;;;;;;;EAatC,WACE,OAAO,aAAa,OACpB,SAAS,iBACN,aAAa,eAAe,QAAQ,aAAa;;;;cC7O3C,mBAAoB,UAAU,oBAAoB,mBAC7D,SAAS,kBAAkB,OAC1B"}