@lark-apaas/http-client 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 +13 -0
- package/README.md +815 -0
- package/dist/index.d.mts +323 -0
- package/dist/index.d.ts +323 -0
- package/dist/index.js +625 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +591 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +37 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DEFAULT_CLOCK_TOLERANCE_SEC: () => DEFAULT_CLOCK_TOLERANCE_SEC,
|
|
24
|
+
DEFAULT_JWT_EXPIRE_TIME_MS: () => DEFAULT_JWT_EXPIRE_TIME_MS,
|
|
25
|
+
HttpClient: () => HttpClient,
|
|
26
|
+
HttpError: () => HttpError,
|
|
27
|
+
generateJWTToken: () => generateJWTToken,
|
|
28
|
+
parseJWTTokenWithVerify: () => parseJWTTokenWithVerify,
|
|
29
|
+
registerPlatformPlugin: () => registerPlatformPlugin,
|
|
30
|
+
resolvePlatformBaseURL: () => resolvePlatformBaseURL
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
|
|
34
|
+
// src/helper/jwt.ts
|
|
35
|
+
var import_crypto = require("crypto");
|
|
36
|
+
var DEFAULT_JWT_EXPIRE_TIME_MS = 30 * 60 * 1e3;
|
|
37
|
+
var DEFAULT_CLOCK_TOLERANCE_SEC = 60;
|
|
38
|
+
var JWT_HEADER = {
|
|
39
|
+
alg: "HS256",
|
|
40
|
+
typ: "JWT"
|
|
41
|
+
};
|
|
42
|
+
function generateJWTToken(customClaims, config) {
|
|
43
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
44
|
+
const payload = {
|
|
45
|
+
...customClaims,
|
|
46
|
+
iss: customClaims.access_key,
|
|
47
|
+
iat: nowSeconds,
|
|
48
|
+
nbf: nowSeconds,
|
|
49
|
+
exp: nowSeconds + Math.floor(config.expireTimeMs / 1e3),
|
|
50
|
+
jti: (0, import_crypto.randomUUID)()
|
|
51
|
+
};
|
|
52
|
+
const encodedHeader = base64UrlEncode(JSON.stringify(JWT_HEADER));
|
|
53
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
|
54
|
+
const signature = sign(`${encodedHeader}.${encodedPayload}`, config.secretKey);
|
|
55
|
+
return `${encodedHeader}.${encodedPayload}.${signature}`;
|
|
56
|
+
}
|
|
57
|
+
function parseJWTTokenWithVerify(tokenString, config, options) {
|
|
58
|
+
const segments = tokenString.split(".");
|
|
59
|
+
if (segments.length !== 3) {
|
|
60
|
+
throw new Error("invalid JWT token format");
|
|
61
|
+
}
|
|
62
|
+
const [encodedHeader, encodedPayload, signature] = segments;
|
|
63
|
+
const headerJson = JSON.parse(base64UrlDecode(encodedHeader));
|
|
64
|
+
if (headerJson.alg !== "HS256") {
|
|
65
|
+
throw new Error("unsupported JWT alg");
|
|
66
|
+
}
|
|
67
|
+
const expectedSignature = sign(`${encodedHeader}.${encodedPayload}`, config.secretKey);
|
|
68
|
+
const expectedBuffer = Buffer.from(expectedSignature);
|
|
69
|
+
const signatureBuffer = Buffer.from(signature);
|
|
70
|
+
if (expectedBuffer.length !== signatureBuffer.length || !(0, import_crypto.timingSafeEqual)(expectedBuffer, signatureBuffer)) {
|
|
71
|
+
throw new Error("JWT signature verification failed");
|
|
72
|
+
}
|
|
73
|
+
const payload = JSON.parse(base64UrlDecode(encodedPayload));
|
|
74
|
+
if (!options?.skipExpiration) {
|
|
75
|
+
const clockTolerance = options?.clockTolerance ?? DEFAULT_CLOCK_TOLERANCE_SEC;
|
|
76
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
77
|
+
if (payload.exp !== void 0) {
|
|
78
|
+
if (payload.exp + clockTolerance < now) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`JWT token expired at ${new Date(payload.exp * 1e3).toISOString()}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (payload.nbf !== void 0) {
|
|
85
|
+
if (payload.nbf - clockTolerance > now) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`JWT token not yet valid, will be valid at ${new Date(payload.nbf * 1e3).toISOString()}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (payload.iat !== void 0) {
|
|
92
|
+
if (payload.iat - clockTolerance > now) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`JWT token issued in the future at ${new Date(payload.iat * 1e3).toISOString()}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return payload;
|
|
100
|
+
}
|
|
101
|
+
function sign(input, secret) {
|
|
102
|
+
return (0, import_crypto.createHmac)("sha256", secret).update(input).digest("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
103
|
+
}
|
|
104
|
+
function base64UrlEncode(input) {
|
|
105
|
+
return Buffer.from(input).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
106
|
+
}
|
|
107
|
+
function base64UrlDecode(input) {
|
|
108
|
+
const padLength = (4 - (input.length % 4 || 4)) % 4;
|
|
109
|
+
const padded = `${input}${"=".repeat(padLength)}`.replace(/-/g, "+").replace(/_/g, "/");
|
|
110
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/helper/jwt-manager.ts
|
|
114
|
+
var JWTTokenManager = class {
|
|
115
|
+
cache = /* @__PURE__ */ new Map();
|
|
116
|
+
config;
|
|
117
|
+
constructor(config) {
|
|
118
|
+
this.config = {
|
|
119
|
+
refreshBeforeMs: 5 * 60 * 1e3,
|
|
120
|
+
// 默认 5 分钟
|
|
121
|
+
...config
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 获取 JWT token,优先使用缓存
|
|
126
|
+
*
|
|
127
|
+
* 如果缓存的 token 即将过期(距离过期时间小于 refreshBeforeMs),
|
|
128
|
+
* 则会生成新的 token 并更新缓存
|
|
129
|
+
*
|
|
130
|
+
* @param claims - JWT claims
|
|
131
|
+
* @returns JWT token 字符串
|
|
132
|
+
*/
|
|
133
|
+
getToken(claims) {
|
|
134
|
+
const cacheKey = this.getCacheKey(claims);
|
|
135
|
+
const cached = this.cache.get(cacheKey);
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
if (cached && cached.expiresAtMs - now > this.config.refreshBeforeMs) {
|
|
138
|
+
return cached.token;
|
|
139
|
+
}
|
|
140
|
+
const token = generateJWTToken(claims, this.config);
|
|
141
|
+
const expiresAtMs = now + this.config.expireTimeMs;
|
|
142
|
+
this.cache.set(cacheKey, { token, expiresAtMs });
|
|
143
|
+
return token;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 清除所有缓存的 token
|
|
147
|
+
*/
|
|
148
|
+
clearCache() {
|
|
149
|
+
this.cache.clear();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 清除特定 claims 的缓存
|
|
153
|
+
*
|
|
154
|
+
* @param claims - 要清除的 claims
|
|
155
|
+
*/
|
|
156
|
+
clearCacheFor(claims) {
|
|
157
|
+
const cacheKey = this.getCacheKey(claims);
|
|
158
|
+
this.cache.delete(cacheKey);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 生成缓存 key(基于关键的 claims 字段)
|
|
162
|
+
*
|
|
163
|
+
* 使用稳定的字段顺序确保相同 claims 生成相同 key
|
|
164
|
+
*
|
|
165
|
+
* @private
|
|
166
|
+
*/
|
|
167
|
+
getCacheKey(claims) {
|
|
168
|
+
const parts = [
|
|
169
|
+
claims.access_key,
|
|
170
|
+
claims.tenant_id?.toString() || "",
|
|
171
|
+
claims.user_id || "",
|
|
172
|
+
claims.app_id || "",
|
|
173
|
+
claims.app_env || "",
|
|
174
|
+
claims.sandbox_id || ""
|
|
175
|
+
];
|
|
176
|
+
return parts.join(":");
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 获取缓存统计信息(用于监控和调试)
|
|
180
|
+
*
|
|
181
|
+
* @returns 缓存统计信息
|
|
182
|
+
*/
|
|
183
|
+
getCacheStats() {
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
let validCount = 0;
|
|
186
|
+
let expiredCount = 0;
|
|
187
|
+
for (const cached of this.cache.values()) {
|
|
188
|
+
if (cached.expiresAtMs > now) {
|
|
189
|
+
validCount++;
|
|
190
|
+
} else {
|
|
191
|
+
expiredCount++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
total: this.cache.size,
|
|
196
|
+
valid: validCount,
|
|
197
|
+
expired: expiredCount
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// src/plugins/platform-plugin.ts
|
|
203
|
+
var DEFAULT_DOMAIN_ENV = "FORCE_AUTHN_INNERAPI_DOMAIN";
|
|
204
|
+
var DEFAULT_ACCESS_KEY_ENV = "FORCE_AUTHN_ACCESS_KEY";
|
|
205
|
+
var DEFAULT_SECRET_KEY_ENV = "FORCE_AUTHN_SECRET_KEY";
|
|
206
|
+
function resolvePlatformBaseURL(options) {
|
|
207
|
+
if (!options?.enabled) {
|
|
208
|
+
return options?.baseURL;
|
|
209
|
+
}
|
|
210
|
+
if (options.baseURL) {
|
|
211
|
+
return options.baseURL;
|
|
212
|
+
}
|
|
213
|
+
const envName = options.domainEnv || DEFAULT_DOMAIN_ENV;
|
|
214
|
+
const domain = process.env[envName];
|
|
215
|
+
if (!domain) {
|
|
216
|
+
throw new Error(`\u5E73\u53F0\u6A21\u5F0F\u9700\u8981\u57FA\u7840\u57DF\u540D\uFF0C\u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF ${envName}`);
|
|
217
|
+
}
|
|
218
|
+
return domain;
|
|
219
|
+
}
|
|
220
|
+
function registerPlatformPlugin(interceptors, options) {
|
|
221
|
+
if (!options.enabled) {
|
|
222
|
+
return void 0;
|
|
223
|
+
}
|
|
224
|
+
ensureNoAccessKeyOverride("platform.defaultClaims", options.defaultClaims);
|
|
225
|
+
const accessKeyEnv = options.accessKeyEnv || DEFAULT_ACCESS_KEY_ENV;
|
|
226
|
+
const secretKeyEnv = options.secretKeyEnv || DEFAULT_SECRET_KEY_ENV;
|
|
227
|
+
const accessKey = options.accessKey ?? process.env[accessKeyEnv];
|
|
228
|
+
if (!accessKey) {
|
|
229
|
+
throw new Error(`\u5E73\u53F0\u9274\u6743\u9700\u8981 access key\uFF0C\u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF ${accessKeyEnv}`);
|
|
230
|
+
}
|
|
231
|
+
const secretKey = options.secretKey ?? process.env[secretKeyEnv];
|
|
232
|
+
if (!secretKey) {
|
|
233
|
+
throw new Error(`\u5E73\u53F0\u9274\u6743\u9700\u8981 secret key\uFF0C\u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF ${secretKeyEnv}`);
|
|
234
|
+
}
|
|
235
|
+
const expireTimeMs = options.expireTimeMs ?? DEFAULT_JWT_EXPIRE_TIME_MS;
|
|
236
|
+
const tokenManager = new JWTTokenManager({
|
|
237
|
+
accessKey,
|
|
238
|
+
secretKey,
|
|
239
|
+
expireTimeMs,
|
|
240
|
+
refreshBeforeMs: options.refreshBeforeMs
|
|
241
|
+
});
|
|
242
|
+
interceptors.request.use((config) => {
|
|
243
|
+
ensureNoAccessKeyOverride("request.platformAuth.customClaims", config.platformAuth?.customClaims);
|
|
244
|
+
const claims = {
|
|
245
|
+
...options.defaultClaims || {},
|
|
246
|
+
...config.platformAuth?.customClaims || {},
|
|
247
|
+
access_key: accessKey
|
|
248
|
+
};
|
|
249
|
+
const token = tokenManager.getToken(claims);
|
|
250
|
+
const headers = {
|
|
251
|
+
...config.headers,
|
|
252
|
+
"Authorization": `Bearer ${token}`,
|
|
253
|
+
"x-api-key": accessKey
|
|
254
|
+
};
|
|
255
|
+
return {
|
|
256
|
+
...config,
|
|
257
|
+
headers
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
return tokenManager;
|
|
261
|
+
}
|
|
262
|
+
function ensureNoAccessKeyOverride(name, claims) {
|
|
263
|
+
if (claims && Object.prototype.hasOwnProperty.call(claims, "access_key")) {
|
|
264
|
+
throw new Error(`${name} \u4E0D\u5141\u8BB8\u8BBE\u7F6E access_key`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/core/InterceptorManager.ts
|
|
269
|
+
var InterceptorManager = class {
|
|
270
|
+
interceptors = [];
|
|
271
|
+
/**
|
|
272
|
+
* 添加拦截器
|
|
273
|
+
* @returns 拦截器 ID,用于后续移除
|
|
274
|
+
*/
|
|
275
|
+
use(onFulfilled, onRejected) {
|
|
276
|
+
this.interceptors.push({ onFulfilled, onRejected });
|
|
277
|
+
return this.interceptors.length - 1;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* 移除拦截器
|
|
281
|
+
*/
|
|
282
|
+
eject(id) {
|
|
283
|
+
if (this.interceptors[id]) {
|
|
284
|
+
this.interceptors[id] = null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* 清空所有拦截器
|
|
289
|
+
*/
|
|
290
|
+
clear() {
|
|
291
|
+
this.interceptors = [];
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* 遍历所有拦截器
|
|
295
|
+
*/
|
|
296
|
+
forEach(fn) {
|
|
297
|
+
this.interceptors.forEach((interceptor) => {
|
|
298
|
+
if (interceptor !== null) {
|
|
299
|
+
fn(interceptor);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// src/core/utils.ts
|
|
306
|
+
function isPlainObject(value) {
|
|
307
|
+
if (typeof value !== "object" || value === null) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
const proto = Object.getPrototypeOf(value);
|
|
311
|
+
return proto === Object.prototype || proto === null;
|
|
312
|
+
}
|
|
313
|
+
function normalizeHeaders(headers) {
|
|
314
|
+
if (!headers) {
|
|
315
|
+
return {};
|
|
316
|
+
}
|
|
317
|
+
if (headers instanceof Headers) {
|
|
318
|
+
const normalized2 = {};
|
|
319
|
+
headers.forEach((value, key) => {
|
|
320
|
+
normalized2[key] = value;
|
|
321
|
+
});
|
|
322
|
+
return normalized2;
|
|
323
|
+
}
|
|
324
|
+
if (Array.isArray(headers)) {
|
|
325
|
+
return headers.reduce((acc, [key, value]) => {
|
|
326
|
+
acc[key] = value;
|
|
327
|
+
return acc;
|
|
328
|
+
}, {});
|
|
329
|
+
}
|
|
330
|
+
const normalized = {};
|
|
331
|
+
Object.entries(headers).forEach(
|
|
332
|
+
([key, value]) => {
|
|
333
|
+
const normalizedValue = Array.isArray(value) ? value.join(",") : value;
|
|
334
|
+
normalized[key] = normalizedValue;
|
|
335
|
+
}
|
|
336
|
+
);
|
|
337
|
+
return normalized;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/core/types.ts
|
|
341
|
+
var HttpError = class _HttpError extends Error {
|
|
342
|
+
isHttpError = true;
|
|
343
|
+
response;
|
|
344
|
+
config;
|
|
345
|
+
constructor(response, config, message) {
|
|
346
|
+
super(message || `Request failed with status ${response?.status || "unknown"}`);
|
|
347
|
+
this.name = "HttpError";
|
|
348
|
+
this.response = response;
|
|
349
|
+
this.config = sanitizeConfig(config);
|
|
350
|
+
Object.setPrototypeOf(this, _HttpError.prototype);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
function sanitizeConfig(config) {
|
|
354
|
+
const { headers, ...rest } = config;
|
|
355
|
+
return {
|
|
356
|
+
...rest,
|
|
357
|
+
headers: sanitizeHeaders(headers)
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function sanitizeHeaders(headers) {
|
|
361
|
+
if (!headers) {
|
|
362
|
+
return void 0;
|
|
363
|
+
}
|
|
364
|
+
const normalized = normalizeHeaders(headers);
|
|
365
|
+
const sanitized = {};
|
|
366
|
+
const sensitiveHeaders = ["authorization", "x-api-key", "cookie", "x-secret"];
|
|
367
|
+
for (const [key, value] of Object.entries(normalized)) {
|
|
368
|
+
const lowerKey = key.toLowerCase();
|
|
369
|
+
if (sensitiveHeaders.includes(lowerKey)) {
|
|
370
|
+
sanitized[key] = "[REDACTED]";
|
|
371
|
+
} else {
|
|
372
|
+
sanitized[key] = value;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return sanitized;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/core/HttpClient.ts
|
|
379
|
+
var HttpClient = class {
|
|
380
|
+
defaultConfig;
|
|
381
|
+
securityConfig;
|
|
382
|
+
/**
|
|
383
|
+
* 拦截器管理器(API 与 Axios 一致)
|
|
384
|
+
*/
|
|
385
|
+
interceptors = {
|
|
386
|
+
request: new InterceptorManager(),
|
|
387
|
+
response: new InterceptorManager()
|
|
388
|
+
};
|
|
389
|
+
constructor(config) {
|
|
390
|
+
const { platform, security, ...restConfig } = config || {};
|
|
391
|
+
this.defaultConfig = {
|
|
392
|
+
timeout: 5e3,
|
|
393
|
+
...restConfig
|
|
394
|
+
};
|
|
395
|
+
const strictMode = security?.strictMode ?? false;
|
|
396
|
+
this.securityConfig = {
|
|
397
|
+
allowedProtocols: strictMode ? ["http:", "https:"] : null,
|
|
398
|
+
maxResponseSize: strictMode ? 50 * 1024 * 1024 : 0,
|
|
399
|
+
// 50MB
|
|
400
|
+
strictMode,
|
|
401
|
+
...security
|
|
402
|
+
};
|
|
403
|
+
if (platform?.enabled) {
|
|
404
|
+
if (!this.defaultConfig.baseURL) {
|
|
405
|
+
this.defaultConfig.baseURL = resolvePlatformBaseURL(platform);
|
|
406
|
+
}
|
|
407
|
+
registerPlatformPlugin(this.interceptors, platform);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* GET 请求
|
|
412
|
+
*/
|
|
413
|
+
async get(url, config) {
|
|
414
|
+
return this.request({ ...config, url, method: "GET" });
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* POST 请求
|
|
418
|
+
*/
|
|
419
|
+
async post(url, data, config) {
|
|
420
|
+
const headers = { ...config?.headers };
|
|
421
|
+
let body = data;
|
|
422
|
+
if (isPlainObject(data)) {
|
|
423
|
+
body = JSON.stringify(data);
|
|
424
|
+
if (!Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) {
|
|
425
|
+
headers["Content-Type"] = "application/json";
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return this.request({
|
|
429
|
+
...config,
|
|
430
|
+
url,
|
|
431
|
+
method: "POST",
|
|
432
|
+
body,
|
|
433
|
+
headers
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* PUT 请求
|
|
438
|
+
*/
|
|
439
|
+
async put(url, data, config) {
|
|
440
|
+
const headers = { ...config?.headers };
|
|
441
|
+
let body = data;
|
|
442
|
+
if (isPlainObject(data)) {
|
|
443
|
+
body = JSON.stringify(data);
|
|
444
|
+
if (!Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) {
|
|
445
|
+
headers["Content-Type"] = "application/json";
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return this.request({
|
|
449
|
+
...config,
|
|
450
|
+
url,
|
|
451
|
+
method: "PUT",
|
|
452
|
+
body,
|
|
453
|
+
headers
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* DELETE 请求
|
|
458
|
+
*/
|
|
459
|
+
async delete(url, config) {
|
|
460
|
+
return this.request({ ...config, url, method: "DELETE" });
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* PATCH 请求
|
|
464
|
+
*/
|
|
465
|
+
async patch(url, data, config) {
|
|
466
|
+
const headers = { ...config?.headers };
|
|
467
|
+
let body = data;
|
|
468
|
+
if (isPlainObject(data)) {
|
|
469
|
+
body = JSON.stringify(data);
|
|
470
|
+
if (!Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) {
|
|
471
|
+
headers["Content-Type"] = "application/json";
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return this.request({
|
|
475
|
+
...config,
|
|
476
|
+
url,
|
|
477
|
+
method: "PATCH",
|
|
478
|
+
body,
|
|
479
|
+
headers
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* 核心请求方法(fetch 风格)
|
|
484
|
+
*/
|
|
485
|
+
async request(config) {
|
|
486
|
+
const baseHeaders = normalizeHeaders(this.defaultConfig.headers);
|
|
487
|
+
const overrideHeaders = normalizeHeaders(config.headers);
|
|
488
|
+
const mergedKeyMap = {};
|
|
489
|
+
for (const key in baseHeaders) {
|
|
490
|
+
mergedKeyMap[key.toLowerCase()] = { key, value: baseHeaders[key] };
|
|
491
|
+
}
|
|
492
|
+
for (const key in overrideHeaders) {
|
|
493
|
+
mergedKeyMap[key.toLowerCase()] = { key, value: overrideHeaders[key] };
|
|
494
|
+
}
|
|
495
|
+
const finalHeaders = {};
|
|
496
|
+
for (const lowerKey in mergedKeyMap) {
|
|
497
|
+
const { key, value } = mergedKeyMap[lowerKey];
|
|
498
|
+
finalHeaders[key] = value;
|
|
499
|
+
}
|
|
500
|
+
let mergedConfig = {
|
|
501
|
+
...this.defaultConfig,
|
|
502
|
+
...config,
|
|
503
|
+
headers: finalHeaders
|
|
504
|
+
};
|
|
505
|
+
try {
|
|
506
|
+
mergedConfig = await this.runRequestInterceptors(mergedConfig);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
return Promise.reject(error);
|
|
509
|
+
}
|
|
510
|
+
mergedConfig.headers = normalizeHeaders(mergedConfig.headers);
|
|
511
|
+
const fullUrl = this.buildUrl(mergedConfig.url, mergedConfig.params);
|
|
512
|
+
const timeout = mergedConfig.timeout || this.defaultConfig.timeout || 5e3;
|
|
513
|
+
const controller = new AbortController();
|
|
514
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
515
|
+
const externalSignal = mergedConfig.signal;
|
|
516
|
+
if (externalSignal) {
|
|
517
|
+
if (externalSignal.aborted) {
|
|
518
|
+
clearTimeout(timeoutId);
|
|
519
|
+
controller.abort();
|
|
520
|
+
} else {
|
|
521
|
+
externalSignal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const { platformAuth, params, timeout: _timeout, baseURL, url, ...fetchOptions } = mergedConfig;
|
|
525
|
+
try {
|
|
526
|
+
const response = await fetch(fullUrl, {
|
|
527
|
+
...fetchOptions,
|
|
528
|
+
signal: controller.signal
|
|
529
|
+
// 传递我们自己的 controller signal
|
|
530
|
+
});
|
|
531
|
+
clearTimeout(timeoutId);
|
|
532
|
+
if (this.securityConfig.maxResponseSize > 0) {
|
|
533
|
+
const contentLength = response.headers.get("content-length");
|
|
534
|
+
if (contentLength) {
|
|
535
|
+
const size = parseInt(contentLength, 10);
|
|
536
|
+
if (size > this.securityConfig.maxResponseSize) {
|
|
537
|
+
throw new Error(
|
|
538
|
+
`Response size ${size} bytes exceeds limit of ${this.securityConfig.maxResponseSize} bytes`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (!response.ok) {
|
|
544
|
+
const error = new HttpError(response, mergedConfig);
|
|
545
|
+
return this.runResponseInterceptors(Promise.reject(error));
|
|
546
|
+
}
|
|
547
|
+
return this.runResponseInterceptors(Promise.resolve(response));
|
|
548
|
+
} catch (error) {
|
|
549
|
+
clearTimeout(timeoutId);
|
|
550
|
+
const httpError = error instanceof HttpError ? error : new HttpError(
|
|
551
|
+
void 0,
|
|
552
|
+
mergedConfig,
|
|
553
|
+
error.name === "AbortError" ? "Request aborted" : error.message
|
|
554
|
+
);
|
|
555
|
+
return this.runResponseInterceptors(Promise.reject(httpError));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* 执行请求拦截器链
|
|
560
|
+
*/
|
|
561
|
+
runRequestInterceptors(config) {
|
|
562
|
+
let promise = Promise.resolve(config);
|
|
563
|
+
const interceptors = [];
|
|
564
|
+
this.interceptors.request.forEach((interceptor) => {
|
|
565
|
+
interceptors.push(interceptor);
|
|
566
|
+
});
|
|
567
|
+
for (const interceptor of interceptors) {
|
|
568
|
+
promise = promise.then(
|
|
569
|
+
interceptor.onFulfilled,
|
|
570
|
+
interceptor.onRejected
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
return promise;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* 执行响应拦截器链
|
|
577
|
+
*/
|
|
578
|
+
async runResponseInterceptors(promise) {
|
|
579
|
+
let currentPromise = promise;
|
|
580
|
+
const interceptors = [];
|
|
581
|
+
this.interceptors.response.forEach((interceptor) => {
|
|
582
|
+
interceptors.push(interceptor);
|
|
583
|
+
});
|
|
584
|
+
for (const interceptor of interceptors) {
|
|
585
|
+
currentPromise = currentPromise.then(
|
|
586
|
+
interceptor.onFulfilled,
|
|
587
|
+
interceptor.onRejected
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
return currentPromise;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* 构建完整 URL(包含 baseURL 和 query params)
|
|
594
|
+
*/
|
|
595
|
+
buildUrl(url, params) {
|
|
596
|
+
const fullUrl = new URL(url, this.defaultConfig.baseURL);
|
|
597
|
+
if (this.securityConfig.allowedProtocols) {
|
|
598
|
+
if (!this.securityConfig.allowedProtocols.includes(fullUrl.protocol)) {
|
|
599
|
+
throw new Error(
|
|
600
|
+
`Protocol ${fullUrl.protocol} is not allowed. Allowed protocols: ${this.securityConfig.allowedProtocols.join(", ")}`
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (params) {
|
|
605
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
606
|
+
if (value !== void 0 && value !== null) {
|
|
607
|
+
fullUrl.searchParams.set(key, String(value));
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
return fullUrl.href;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
615
|
+
0 && (module.exports = {
|
|
616
|
+
DEFAULT_CLOCK_TOLERANCE_SEC,
|
|
617
|
+
DEFAULT_JWT_EXPIRE_TIME_MS,
|
|
618
|
+
HttpClient,
|
|
619
|
+
HttpError,
|
|
620
|
+
generateJWTToken,
|
|
621
|
+
parseJWTTokenWithVerify,
|
|
622
|
+
registerPlatformPlugin,
|
|
623
|
+
resolvePlatformBaseURL
|
|
624
|
+
});
|
|
625
|
+
//# sourceMappingURL=index.js.map
|