@55387.ai/uniauth-client 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/README.md +178 -0
- package/dist/index.cjs +1020 -0
- package/dist/index.d.cts +500 -0
- package/dist/index.d.ts +500 -0
- package/dist/index.js +986 -0
- package/package.json +40 -0
- package/src/client.test.ts +476 -0
- package/src/http.ts +224 -0
- package/src/index.ts +1278 -0
- package/tsconfig.json +19 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1020 @@
|
|
|
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
|
+
AuthErrorCode: () => AuthErrorCode,
|
|
24
|
+
UniAuthClient: () => UniAuthClient,
|
|
25
|
+
UniAuthError: () => UniAuthError,
|
|
26
|
+
default: () => index_default,
|
|
27
|
+
fetchWithRetry: () => fetchWithRetry,
|
|
28
|
+
generateCodeChallenge: () => generateCodeChallenge,
|
|
29
|
+
generateCodeVerifier: () => generateCodeVerifier,
|
|
30
|
+
getAndClearCodeVerifier: () => getAndClearCodeVerifier,
|
|
31
|
+
storeCodeVerifier: () => storeCodeVerifier
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
|
|
35
|
+
// src/http.ts
|
|
36
|
+
var DEFAULT_RETRY_STATUS_CODES = [
|
|
37
|
+
408,
|
|
38
|
+
// Request Timeout
|
|
39
|
+
429,
|
|
40
|
+
// Too Many Requests
|
|
41
|
+
500,
|
|
42
|
+
// Internal Server Error
|
|
43
|
+
502,
|
|
44
|
+
// Bad Gateway
|
|
45
|
+
503,
|
|
46
|
+
// Service Unavailable
|
|
47
|
+
504
|
|
48
|
+
// Gateway Timeout
|
|
49
|
+
];
|
|
50
|
+
async function fetchWithRetry(url, options = {}) {
|
|
51
|
+
const {
|
|
52
|
+
maxRetries = 3,
|
|
53
|
+
baseDelay = 500,
|
|
54
|
+
timeout = 3e4,
|
|
55
|
+
retryStatusCodes = DEFAULT_RETRY_STATUS_CODES,
|
|
56
|
+
...fetchOptions
|
|
57
|
+
} = options;
|
|
58
|
+
let lastError = null;
|
|
59
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
60
|
+
try {
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
63
|
+
const response = await fetch(url, {
|
|
64
|
+
...fetchOptions,
|
|
65
|
+
signal: controller.signal
|
|
66
|
+
});
|
|
67
|
+
clearTimeout(timeoutId);
|
|
68
|
+
if (retryStatusCodes.includes(response.status) && attempt < maxRetries) {
|
|
69
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
70
|
+
const delay = retryAfter ? parseRetryAfter(retryAfter) : calculateBackoffDelay(attempt, baseDelay);
|
|
71
|
+
await sleep(delay);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
return response;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
77
|
+
if (lastError.name === "AbortError" && attempt >= maxRetries) {
|
|
78
|
+
throw new Error(`Request timeout after ${timeout}ms`);
|
|
79
|
+
}
|
|
80
|
+
if (attempt < maxRetries) {
|
|
81
|
+
const delay = calculateBackoffDelay(attempt, baseDelay);
|
|
82
|
+
await sleep(delay);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
throw lastError || new Error("Request failed after all retries");
|
|
87
|
+
}
|
|
88
|
+
function calculateBackoffDelay(attempt, baseDelay) {
|
|
89
|
+
const exponentialDelay = baseDelay * Math.pow(2, attempt);
|
|
90
|
+
const jitter = exponentialDelay * 0.25 * (Math.random() * 2 - 1);
|
|
91
|
+
return Math.min(exponentialDelay + jitter, 3e4);
|
|
92
|
+
}
|
|
93
|
+
function parseRetryAfter(value) {
|
|
94
|
+
const seconds = parseInt(value, 10);
|
|
95
|
+
if (!isNaN(seconds)) {
|
|
96
|
+
return seconds * 1e3;
|
|
97
|
+
}
|
|
98
|
+
const date = new Date(value);
|
|
99
|
+
if (!isNaN(date.getTime())) {
|
|
100
|
+
const delay = date.getTime() - Date.now();
|
|
101
|
+
return Math.max(delay, 0);
|
|
102
|
+
}
|
|
103
|
+
return 1e3;
|
|
104
|
+
}
|
|
105
|
+
function sleep(ms) {
|
|
106
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
107
|
+
}
|
|
108
|
+
function generateCodeVerifier() {
|
|
109
|
+
const array = new Uint8Array(32);
|
|
110
|
+
crypto.getRandomValues(array);
|
|
111
|
+
return base64UrlEncode(array);
|
|
112
|
+
}
|
|
113
|
+
async function generateCodeChallenge(verifier) {
|
|
114
|
+
const encoder = new TextEncoder();
|
|
115
|
+
const data = encoder.encode(verifier);
|
|
116
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
117
|
+
return base64UrlEncode(new Uint8Array(digest));
|
|
118
|
+
}
|
|
119
|
+
function base64UrlEncode(array) {
|
|
120
|
+
let binary = "";
|
|
121
|
+
for (let i = 0; i < array.byteLength; i++) {
|
|
122
|
+
binary += String.fromCharCode(array[i]);
|
|
123
|
+
}
|
|
124
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
125
|
+
}
|
|
126
|
+
function storeCodeVerifier(verifier, storageKey = "uniauth_pkce_verifier") {
|
|
127
|
+
if (typeof sessionStorage !== "undefined") {
|
|
128
|
+
sessionStorage.setItem(storageKey, verifier);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function getAndClearCodeVerifier(storageKey = "uniauth_pkce_verifier") {
|
|
132
|
+
if (typeof sessionStorage !== "undefined") {
|
|
133
|
+
const verifier = sessionStorage.getItem(storageKey);
|
|
134
|
+
sessionStorage.removeItem(storageKey);
|
|
135
|
+
return verifier;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/index.ts
|
|
141
|
+
var AuthErrorCode = {
|
|
142
|
+
// Authentication errors
|
|
143
|
+
SEND_CODE_FAILED: "SEND_CODE_FAILED",
|
|
144
|
+
VERIFY_FAILED: "VERIFY_FAILED",
|
|
145
|
+
LOGIN_FAILED: "LOGIN_FAILED",
|
|
146
|
+
OAUTH_FAILED: "OAUTH_FAILED",
|
|
147
|
+
MFA_REQUIRED: "MFA_REQUIRED",
|
|
148
|
+
MFA_FAILED: "MFA_FAILED",
|
|
149
|
+
REGISTER_FAILED: "REGISTER_FAILED",
|
|
150
|
+
// Token errors
|
|
151
|
+
NOT_AUTHENTICATED: "NOT_AUTHENTICATED",
|
|
152
|
+
TOKEN_EXPIRED: "TOKEN_EXPIRED",
|
|
153
|
+
REFRESH_FAILED: "REFRESH_FAILED",
|
|
154
|
+
// Configuration errors
|
|
155
|
+
CONFIG_ERROR: "CONFIG_ERROR",
|
|
156
|
+
SSO_NOT_CONFIGURED: "SSO_NOT_CONFIGURED",
|
|
157
|
+
INVALID_STATE: "INVALID_STATE",
|
|
158
|
+
// Network errors
|
|
159
|
+
NETWORK_ERROR: "NETWORK_ERROR",
|
|
160
|
+
TIMEOUT: "TIMEOUT",
|
|
161
|
+
INTERNAL_ERROR: "INTERNAL_ERROR"
|
|
162
|
+
};
|
|
163
|
+
var UniAuthError = class _UniAuthError extends Error {
|
|
164
|
+
code;
|
|
165
|
+
statusCode;
|
|
166
|
+
details;
|
|
167
|
+
constructor(code, message, statusCode, details) {
|
|
168
|
+
super(message);
|
|
169
|
+
this.name = "UniAuthError";
|
|
170
|
+
this.code = code;
|
|
171
|
+
this.statusCode = statusCode;
|
|
172
|
+
this.details = details;
|
|
173
|
+
if (Error.captureStackTrace) {
|
|
174
|
+
Error.captureStackTrace(this, _UniAuthError);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
var LocalStorageAdapter = class {
|
|
179
|
+
accessTokenKey = "uniauth_access_token";
|
|
180
|
+
refreshTokenKey = "uniauth_refresh_token";
|
|
181
|
+
getAccessToken() {
|
|
182
|
+
if (typeof localStorage === "undefined") return null;
|
|
183
|
+
return localStorage.getItem(this.accessTokenKey);
|
|
184
|
+
}
|
|
185
|
+
setAccessToken(token) {
|
|
186
|
+
if (typeof localStorage === "undefined") return;
|
|
187
|
+
localStorage.setItem(this.accessTokenKey, token);
|
|
188
|
+
}
|
|
189
|
+
getRefreshToken() {
|
|
190
|
+
if (typeof localStorage === "undefined") return null;
|
|
191
|
+
return localStorage.getItem(this.refreshTokenKey);
|
|
192
|
+
}
|
|
193
|
+
setRefreshToken(token) {
|
|
194
|
+
if (typeof localStorage === "undefined") return;
|
|
195
|
+
localStorage.setItem(this.refreshTokenKey, token);
|
|
196
|
+
}
|
|
197
|
+
clear() {
|
|
198
|
+
if (typeof localStorage === "undefined") return;
|
|
199
|
+
localStorage.removeItem(this.accessTokenKey);
|
|
200
|
+
localStorage.removeItem(this.refreshTokenKey);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
var SessionStorageAdapter = class {
|
|
204
|
+
accessTokenKey = "uniauth_access_token";
|
|
205
|
+
refreshTokenKey = "uniauth_refresh_token";
|
|
206
|
+
getAccessToken() {
|
|
207
|
+
if (typeof sessionStorage === "undefined") return null;
|
|
208
|
+
return sessionStorage.getItem(this.accessTokenKey);
|
|
209
|
+
}
|
|
210
|
+
setAccessToken(token) {
|
|
211
|
+
if (typeof sessionStorage === "undefined") return;
|
|
212
|
+
sessionStorage.setItem(this.accessTokenKey, token);
|
|
213
|
+
}
|
|
214
|
+
getRefreshToken() {
|
|
215
|
+
if (typeof sessionStorage === "undefined") return null;
|
|
216
|
+
return sessionStorage.getItem(this.refreshTokenKey);
|
|
217
|
+
}
|
|
218
|
+
setRefreshToken(token) {
|
|
219
|
+
if (typeof sessionStorage === "undefined") return;
|
|
220
|
+
sessionStorage.setItem(this.refreshTokenKey, token);
|
|
221
|
+
}
|
|
222
|
+
clear() {
|
|
223
|
+
if (typeof sessionStorage === "undefined") return;
|
|
224
|
+
sessionStorage.removeItem(this.accessTokenKey);
|
|
225
|
+
sessionStorage.removeItem(this.refreshTokenKey);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
var MemoryStorageAdapter = class {
|
|
229
|
+
accessToken = null;
|
|
230
|
+
refreshToken = null;
|
|
231
|
+
getAccessToken() {
|
|
232
|
+
return this.accessToken;
|
|
233
|
+
}
|
|
234
|
+
setAccessToken(token) {
|
|
235
|
+
this.accessToken = token;
|
|
236
|
+
}
|
|
237
|
+
getRefreshToken() {
|
|
238
|
+
return this.refreshToken;
|
|
239
|
+
}
|
|
240
|
+
setRefreshToken(token) {
|
|
241
|
+
this.refreshToken = token;
|
|
242
|
+
}
|
|
243
|
+
clear() {
|
|
244
|
+
this.accessToken = null;
|
|
245
|
+
this.refreshToken = null;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
var UniAuthClient = class {
|
|
249
|
+
config;
|
|
250
|
+
storage;
|
|
251
|
+
refreshPromise = null;
|
|
252
|
+
constructor(config) {
|
|
253
|
+
this.config = {
|
|
254
|
+
enableRetry: true,
|
|
255
|
+
timeout: 3e4,
|
|
256
|
+
...config
|
|
257
|
+
};
|
|
258
|
+
switch (config.storage) {
|
|
259
|
+
case "sessionStorage":
|
|
260
|
+
this.storage = new SessionStorageAdapter();
|
|
261
|
+
break;
|
|
262
|
+
case "memory":
|
|
263
|
+
this.storage = new MemoryStorageAdapter();
|
|
264
|
+
break;
|
|
265
|
+
default:
|
|
266
|
+
this.storage = new LocalStorageAdapter();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Send verification code to phone number
|
|
271
|
+
* 发送验证码到手机号
|
|
272
|
+
*/
|
|
273
|
+
async sendCode(phone, type = "login") {
|
|
274
|
+
const response = await this.request("/api/v1/auth/send-code", {
|
|
275
|
+
method: "POST",
|
|
276
|
+
body: JSON.stringify({ phone, type })
|
|
277
|
+
});
|
|
278
|
+
if (!response.success || !response.data) {
|
|
279
|
+
throw this.createError(response.error?.code || "SEND_CODE_FAILED", response.error?.message || "Failed to send code");
|
|
280
|
+
}
|
|
281
|
+
return response.data;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Send verification code to email
|
|
285
|
+
* 发送验证码到邮箱
|
|
286
|
+
*/
|
|
287
|
+
async sendEmailCode(email, type = "login") {
|
|
288
|
+
const response = await this.request("/api/v1/auth/send-code", {
|
|
289
|
+
method: "POST",
|
|
290
|
+
body: JSON.stringify({ email, type })
|
|
291
|
+
});
|
|
292
|
+
if (!response.success || !response.data) {
|
|
293
|
+
throw this.createError(response.error?.code || "SEND_CODE_FAILED", response.error?.message || "Failed to send code");
|
|
294
|
+
}
|
|
295
|
+
return response.data;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Login with phone verification code
|
|
299
|
+
* 使用手机验证码登录
|
|
300
|
+
*/
|
|
301
|
+
async loginWithCode(phone, code) {
|
|
302
|
+
const response = await this.request("/api/v1/auth/phone/verify", {
|
|
303
|
+
method: "POST",
|
|
304
|
+
body: JSON.stringify({ phone, code })
|
|
305
|
+
});
|
|
306
|
+
if (!response.success || !response.data) {
|
|
307
|
+
throw this.createError(response.error?.code || "VERIFY_FAILED", response.error?.message || "Failed to verify code");
|
|
308
|
+
}
|
|
309
|
+
if (!response.data.mfa_required) {
|
|
310
|
+
this.storage.setAccessToken(response.data.access_token);
|
|
311
|
+
this.storage.setRefreshToken(response.data.refresh_token);
|
|
312
|
+
this.notifyAuthStateChange(response.data.user);
|
|
313
|
+
}
|
|
314
|
+
return response.data;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Login with email verification code
|
|
318
|
+
* 使用邮箱验证码登录
|
|
319
|
+
*/
|
|
320
|
+
async loginWithEmailCode(email, code) {
|
|
321
|
+
const response = await this.request("/api/v1/auth/email/verify", {
|
|
322
|
+
method: "POST",
|
|
323
|
+
body: JSON.stringify({ email, code })
|
|
324
|
+
});
|
|
325
|
+
if (!response.success || !response.data) {
|
|
326
|
+
throw this.createError(response.error?.code || "VERIFY_FAILED", response.error?.message || "Failed to verify code");
|
|
327
|
+
}
|
|
328
|
+
if (!response.data.mfa_required) {
|
|
329
|
+
this.storage.setAccessToken(response.data.access_token);
|
|
330
|
+
this.storage.setRefreshToken(response.data.refresh_token);
|
|
331
|
+
this.notifyAuthStateChange(response.data.user);
|
|
332
|
+
}
|
|
333
|
+
return response.data;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Login with email and password
|
|
337
|
+
* 使用邮箱密码登录
|
|
338
|
+
*/
|
|
339
|
+
async loginWithEmail(email, password) {
|
|
340
|
+
const response = await this.request("/api/v1/auth/email/login", {
|
|
341
|
+
method: "POST",
|
|
342
|
+
body: JSON.stringify({ email, password })
|
|
343
|
+
});
|
|
344
|
+
if (!response.success || !response.data) {
|
|
345
|
+
throw this.createError(response.error?.code || "LOGIN_FAILED", response.error?.message || "Failed to login");
|
|
346
|
+
}
|
|
347
|
+
if (!response.data.mfa_required) {
|
|
348
|
+
this.storage.setAccessToken(response.data.access_token);
|
|
349
|
+
this.storage.setRefreshToken(response.data.refresh_token);
|
|
350
|
+
this.notifyAuthStateChange(response.data.user);
|
|
351
|
+
}
|
|
352
|
+
return response.data;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Handle OAuth callback (for social login)
|
|
356
|
+
* 处理 OAuth 回调(社交登录)
|
|
357
|
+
*/
|
|
358
|
+
async handleOAuthCallback(provider, code) {
|
|
359
|
+
const response = await this.request("/api/v1/auth/oauth/callback", {
|
|
360
|
+
method: "POST",
|
|
361
|
+
body: JSON.stringify({ provider, code })
|
|
362
|
+
});
|
|
363
|
+
if (!response.success || !response.data) {
|
|
364
|
+
throw this.createError(response.error?.code || "OAUTH_FAILED", response.error?.message || "OAuth callback failed");
|
|
365
|
+
}
|
|
366
|
+
if (!response.data.mfa_required) {
|
|
367
|
+
this.storage.setAccessToken(response.data.access_token);
|
|
368
|
+
this.storage.setRefreshToken(response.data.refresh_token);
|
|
369
|
+
this.notifyAuthStateChange(response.data.user);
|
|
370
|
+
}
|
|
371
|
+
return response.data;
|
|
372
|
+
}
|
|
373
|
+
// ============================================
|
|
374
|
+
// Email Registration / 邮箱注册
|
|
375
|
+
// ============================================
|
|
376
|
+
/**
|
|
377
|
+
* Register with email and password
|
|
378
|
+
* 使用邮箱密码注册
|
|
379
|
+
*/
|
|
380
|
+
async registerWithEmail(email, password, nickname) {
|
|
381
|
+
const response = await this.request("/api/v1/auth/email/register", {
|
|
382
|
+
method: "POST",
|
|
383
|
+
body: JSON.stringify({ email, password, nickname })
|
|
384
|
+
});
|
|
385
|
+
if (!response.success || !response.data) {
|
|
386
|
+
throw this.createError(response.error?.code || "REGISTER_FAILED", response.error?.message || "Failed to register");
|
|
387
|
+
}
|
|
388
|
+
this.storage.setAccessToken(response.data.access_token);
|
|
389
|
+
this.storage.setRefreshToken(response.data.refresh_token);
|
|
390
|
+
this.notifyAuthStateChange(response.data.user);
|
|
391
|
+
return response.data;
|
|
392
|
+
}
|
|
393
|
+
// ============================================
|
|
394
|
+
// MFA (Multi-Factor Authentication) / 多因素认证
|
|
395
|
+
// ============================================
|
|
396
|
+
/**
|
|
397
|
+
* Verify MFA code to complete login
|
|
398
|
+
* 验证 MFA 验证码完成登录
|
|
399
|
+
*
|
|
400
|
+
* Call this after login returns mfa_required: true
|
|
401
|
+
* 当登录返回 mfa_required: true 时调用此方法
|
|
402
|
+
*
|
|
403
|
+
* @example
|
|
404
|
+
* ```typescript
|
|
405
|
+
* const result = await auth.loginWithCode(phone, code);
|
|
406
|
+
* if (result.mfa_required) {
|
|
407
|
+
* const mfaCode = prompt('Enter MFA code:');
|
|
408
|
+
* const finalResult = await auth.verifyMFA(result.mfa_token!, mfaCode);
|
|
409
|
+
* }
|
|
410
|
+
* ```
|
|
411
|
+
*/
|
|
412
|
+
async verifyMFA(mfaToken, code) {
|
|
413
|
+
const response = await this.request("/api/v1/auth/mfa/verify-login", {
|
|
414
|
+
method: "POST",
|
|
415
|
+
body: JSON.stringify({ mfa_token: mfaToken, code })
|
|
416
|
+
});
|
|
417
|
+
if (!response.success || !response.data) {
|
|
418
|
+
throw this.createError(response.error?.code || "MFA_FAILED", response.error?.message || "MFA verification failed");
|
|
419
|
+
}
|
|
420
|
+
this.storage.setAccessToken(response.data.access_token);
|
|
421
|
+
this.storage.setRefreshToken(response.data.refresh_token);
|
|
422
|
+
this.notifyAuthStateChange(response.data.user);
|
|
423
|
+
return response.data;
|
|
424
|
+
}
|
|
425
|
+
// ============================================
|
|
426
|
+
// Social Login / 社交登录
|
|
427
|
+
// ============================================
|
|
428
|
+
/**
|
|
429
|
+
* Get available OAuth providers
|
|
430
|
+
* 获取可用的 OAuth 提供商列表
|
|
431
|
+
*/
|
|
432
|
+
async getOAuthProviders() {
|
|
433
|
+
const response = await this.request("/api/v1/auth/oauth/providers", {
|
|
434
|
+
method: "GET"
|
|
435
|
+
});
|
|
436
|
+
if (!response.success || !response.data) {
|
|
437
|
+
return [];
|
|
438
|
+
}
|
|
439
|
+
return response.data.providers || [];
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Start social login (redirect to OAuth provider)
|
|
443
|
+
* 开始社交登录(重定向到 OAuth 提供商)
|
|
444
|
+
*
|
|
445
|
+
* @param provider - OAuth provider ID (e.g., 'google', 'github', 'wechat')
|
|
446
|
+
* @param redirectUri - Where to redirect after OAuth (optional, uses default)
|
|
447
|
+
*
|
|
448
|
+
* @example
|
|
449
|
+
* ```typescript
|
|
450
|
+
* // Redirect user to Google login
|
|
451
|
+
* auth.startSocialLogin('google');
|
|
452
|
+
* ```
|
|
453
|
+
*/
|
|
454
|
+
startSocialLogin(provider, redirectUri) {
|
|
455
|
+
const params = new URLSearchParams();
|
|
456
|
+
if (redirectUri) {
|
|
457
|
+
params.set("redirect_uri", redirectUri);
|
|
458
|
+
}
|
|
459
|
+
const query = params.toString();
|
|
460
|
+
const url = `${this.config.baseUrl}/api/v1/auth/oauth/${provider}/authorize${query ? "?" + query : ""}`;
|
|
461
|
+
if (typeof window !== "undefined") {
|
|
462
|
+
window.location.href = url;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// ============================================
|
|
466
|
+
// Auth State Management / 认证状态管理
|
|
467
|
+
// ============================================
|
|
468
|
+
authStateCallbacks = [];
|
|
469
|
+
currentUser = null;
|
|
470
|
+
/**
|
|
471
|
+
* Subscribe to auth state changes
|
|
472
|
+
* 订阅认证状态变更
|
|
473
|
+
*
|
|
474
|
+
* @returns Unsubscribe function
|
|
475
|
+
*
|
|
476
|
+
* @example
|
|
477
|
+
* ```typescript
|
|
478
|
+
* const unsubscribe = auth.onAuthStateChange((user, isAuthenticated) => {
|
|
479
|
+
* if (isAuthenticated) {
|
|
480
|
+
* console.log('User logged in:', user);
|
|
481
|
+
* } else {
|
|
482
|
+
* console.log('User logged out');
|
|
483
|
+
* }
|
|
484
|
+
* });
|
|
485
|
+
*
|
|
486
|
+
* // Later, to unsubscribe:
|
|
487
|
+
* unsubscribe();
|
|
488
|
+
* ```
|
|
489
|
+
*/
|
|
490
|
+
onAuthStateChange(callback) {
|
|
491
|
+
this.authStateCallbacks.push(callback);
|
|
492
|
+
return () => {
|
|
493
|
+
const index = this.authStateCallbacks.indexOf(callback);
|
|
494
|
+
if (index !== -1) {
|
|
495
|
+
this.authStateCallbacks.splice(index, 1);
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Notify all subscribers of auth state change
|
|
501
|
+
* 通知所有订阅者认证状态变更
|
|
502
|
+
*/
|
|
503
|
+
notifyAuthStateChange(user) {
|
|
504
|
+
this.currentUser = user;
|
|
505
|
+
const isAuthenticated = this.isAuthenticated();
|
|
506
|
+
for (const callback of this.authStateCallbacks) {
|
|
507
|
+
try {
|
|
508
|
+
callback(user, isAuthenticated);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
console.error("Auth state callback error:", error);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Get cached current user (sync, may be stale)
|
|
516
|
+
* 获取缓存的当前用户(同步,可能过时)
|
|
517
|
+
*/
|
|
518
|
+
getCachedUser() {
|
|
519
|
+
return this.currentUser;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Get access token synchronously (without refresh check)
|
|
523
|
+
* 同步获取访问令牌(不检查刷新)
|
|
524
|
+
*/
|
|
525
|
+
getAccessTokenSync() {
|
|
526
|
+
return this.storage.getAccessToken();
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Check if current token is valid (not expired)
|
|
530
|
+
* 检查当前令牌是否有效(未过期)
|
|
531
|
+
*/
|
|
532
|
+
isTokenValid() {
|
|
533
|
+
const token = this.storage.getAccessToken();
|
|
534
|
+
if (!token) return false;
|
|
535
|
+
try {
|
|
536
|
+
const payload = JSON.parse(atob(token.split(".")[1]));
|
|
537
|
+
const exp = payload.exp * 1e3;
|
|
538
|
+
return Date.now() < exp;
|
|
539
|
+
} catch {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Get current user info
|
|
545
|
+
* 获取当前用户信息
|
|
546
|
+
*/
|
|
547
|
+
async getCurrentUser() {
|
|
548
|
+
if (!this.isAuthenticated()) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
const response = await this.authenticatedRequest("/api/v1/user/me", {
|
|
553
|
+
method: "GET"
|
|
554
|
+
});
|
|
555
|
+
if (!response.success || !response.data) {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
return response.data;
|
|
559
|
+
} catch {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Update user profile
|
|
565
|
+
* 更新用户资料
|
|
566
|
+
*/
|
|
567
|
+
async updateProfile(updates) {
|
|
568
|
+
const response = await this.authenticatedRequest("/api/v1/user/me", {
|
|
569
|
+
method: "PATCH",
|
|
570
|
+
body: JSON.stringify(updates)
|
|
571
|
+
});
|
|
572
|
+
if (!response.success || !response.data) {
|
|
573
|
+
throw this.createError(response.error?.code || "UPDATE_FAILED", response.error?.message || "Failed to update profile");
|
|
574
|
+
}
|
|
575
|
+
return response.data;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Get access token (auto-refresh if needed)
|
|
579
|
+
* 获取访问令牌(如需要则自动刷新)
|
|
580
|
+
*/
|
|
581
|
+
async getAccessToken() {
|
|
582
|
+
const token = this.storage.getAccessToken();
|
|
583
|
+
if (!token) {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
const payload = JSON.parse(atob(token.split(".")[1]));
|
|
588
|
+
const exp = payload.exp * 1e3;
|
|
589
|
+
if (Date.now() > exp - 5 * 60 * 1e3) {
|
|
590
|
+
await this.refreshTokens();
|
|
591
|
+
return this.storage.getAccessToken();
|
|
592
|
+
}
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
595
|
+
return token;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Check if user is authenticated
|
|
599
|
+
* 检查用户是否已认证
|
|
600
|
+
*/
|
|
601
|
+
isAuthenticated() {
|
|
602
|
+
return !!this.storage.getAccessToken();
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Logout current session
|
|
606
|
+
* 登出当前会话
|
|
607
|
+
*/
|
|
608
|
+
async logout() {
|
|
609
|
+
const refreshToken = this.storage.getRefreshToken();
|
|
610
|
+
try {
|
|
611
|
+
await this.authenticatedRequest("/api/v1/auth/logout", {
|
|
612
|
+
method: "POST",
|
|
613
|
+
body: JSON.stringify({ refresh_token: refreshToken })
|
|
614
|
+
});
|
|
615
|
+
} finally {
|
|
616
|
+
this.storage.clear();
|
|
617
|
+
this.notifyAuthStateChange(null);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Logout from all devices
|
|
622
|
+
* 从所有设备登出
|
|
623
|
+
*/
|
|
624
|
+
async logoutAll() {
|
|
625
|
+
try {
|
|
626
|
+
await this.authenticatedRequest("/api/v1/auth/logout-all", {
|
|
627
|
+
method: "POST"
|
|
628
|
+
});
|
|
629
|
+
} finally {
|
|
630
|
+
this.storage.clear();
|
|
631
|
+
this.notifyAuthStateChange(null);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// ============================================
|
|
635
|
+
// OAuth2 Client Methods (for integrating with other OAuth providers using UniAuth)
|
|
636
|
+
// OAuth2 客户端方法
|
|
637
|
+
// ============================================
|
|
638
|
+
/**
|
|
639
|
+
* Start OAuth2 authorization flow
|
|
640
|
+
* 开始 OAuth2 授权流程
|
|
641
|
+
*/
|
|
642
|
+
async startOAuth2Flow(options) {
|
|
643
|
+
if (!this.config.clientId) {
|
|
644
|
+
throw this.createError("CONFIG_ERROR", "clientId is required for OAuth2 flow");
|
|
645
|
+
}
|
|
646
|
+
const params = new URLSearchParams({
|
|
647
|
+
client_id: this.config.clientId,
|
|
648
|
+
redirect_uri: options.redirectUri,
|
|
649
|
+
response_type: "code"
|
|
650
|
+
});
|
|
651
|
+
if (options.scope) {
|
|
652
|
+
params.set("scope", options.scope);
|
|
653
|
+
}
|
|
654
|
+
if (options.state) {
|
|
655
|
+
params.set("state", options.state);
|
|
656
|
+
}
|
|
657
|
+
if (options.usePKCE) {
|
|
658
|
+
const verifier = generateCodeVerifier();
|
|
659
|
+
const challenge = await generateCodeChallenge(verifier);
|
|
660
|
+
storeCodeVerifier(verifier);
|
|
661
|
+
params.set("code_challenge", challenge);
|
|
662
|
+
params.set("code_challenge_method", "S256");
|
|
663
|
+
}
|
|
664
|
+
return `${this.config.baseUrl}/api/v1/oauth2/authorize?${params.toString()}`;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Exchange authorization code for tokens (OAuth2 client flow)
|
|
668
|
+
* 使用授权码换取令牌
|
|
669
|
+
*/
|
|
670
|
+
async exchangeOAuth2Code(code, redirectUri, clientSecret) {
|
|
671
|
+
if (!this.config.clientId) {
|
|
672
|
+
throw this.createError("CONFIG_ERROR", "clientId is required for OAuth2 flow");
|
|
673
|
+
}
|
|
674
|
+
const body = {
|
|
675
|
+
grant_type: "authorization_code",
|
|
676
|
+
client_id: this.config.clientId,
|
|
677
|
+
code,
|
|
678
|
+
redirect_uri: redirectUri
|
|
679
|
+
};
|
|
680
|
+
if (clientSecret) {
|
|
681
|
+
body.client_secret = clientSecret;
|
|
682
|
+
}
|
|
683
|
+
const codeVerifier = getAndClearCodeVerifier();
|
|
684
|
+
if (codeVerifier) {
|
|
685
|
+
body.code_verifier = codeVerifier;
|
|
686
|
+
}
|
|
687
|
+
const response = await fetchWithRetry(`${this.config.baseUrl}/api/v1/oauth2/token`, {
|
|
688
|
+
method: "POST",
|
|
689
|
+
headers: {
|
|
690
|
+
"Content-Type": "application/json"
|
|
691
|
+
},
|
|
692
|
+
body: JSON.stringify(body),
|
|
693
|
+
maxRetries: this.config.enableRetry ? 3 : 0,
|
|
694
|
+
timeout: this.config.timeout
|
|
695
|
+
});
|
|
696
|
+
const data = await response.json();
|
|
697
|
+
if (data.error) {
|
|
698
|
+
throw this.createError(data.error, data.error_description || "Token exchange failed");
|
|
699
|
+
}
|
|
700
|
+
return data;
|
|
701
|
+
}
|
|
702
|
+
// ============================================
|
|
703
|
+
// SSO Methods (Cross-Domain Single Sign-On)
|
|
704
|
+
// SSO 方法(跨域单点登录)
|
|
705
|
+
// ============================================
|
|
706
|
+
ssoConfig = null;
|
|
707
|
+
/**
|
|
708
|
+
* Configure SSO settings
|
|
709
|
+
* 配置 SSO 设置
|
|
710
|
+
*
|
|
711
|
+
* @example
|
|
712
|
+
* ```typescript
|
|
713
|
+
* auth.configureSso({
|
|
714
|
+
* ssoUrl: 'https://sso.55387.xyz',
|
|
715
|
+
* clientId: 'my-app',
|
|
716
|
+
* redirectUri: 'https://my-app.com/auth/callback',
|
|
717
|
+
* });
|
|
718
|
+
* ```
|
|
719
|
+
*/
|
|
720
|
+
configureSso(config) {
|
|
721
|
+
this.ssoConfig = {
|
|
722
|
+
scope: "openid profile email",
|
|
723
|
+
...config
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Start SSO login flow
|
|
728
|
+
* 开始 SSO 登录流程
|
|
729
|
+
*
|
|
730
|
+
* This will redirect the user to the SSO service.
|
|
731
|
+
* If the user already has an SSO session, they'll be automatically logged in (silent auth).
|
|
732
|
+
*
|
|
733
|
+
* @example
|
|
734
|
+
* ```typescript
|
|
735
|
+
* // Simple usage - redirects to SSO
|
|
736
|
+
* auth.loginWithSSO();
|
|
737
|
+
*
|
|
738
|
+
* // With options
|
|
739
|
+
* auth.loginWithSSO({ usePKCE: true });
|
|
740
|
+
* ```
|
|
741
|
+
*/
|
|
742
|
+
loginWithSSO(options = {}) {
|
|
743
|
+
if (!this.ssoConfig) {
|
|
744
|
+
throw this.createError("SSO_NOT_CONFIGURED", "SSO is not configured. Call configureSso() first.");
|
|
745
|
+
}
|
|
746
|
+
const { usePKCE = true, state } = options;
|
|
747
|
+
const stateValue = state || this.generateRandomState();
|
|
748
|
+
this.storeState(stateValue);
|
|
749
|
+
const params = new URLSearchParams({
|
|
750
|
+
client_id: this.ssoConfig.clientId,
|
|
751
|
+
redirect_uri: this.ssoConfig.redirectUri,
|
|
752
|
+
response_type: "code",
|
|
753
|
+
scope: this.ssoConfig.scope || "openid profile email",
|
|
754
|
+
state: stateValue
|
|
755
|
+
});
|
|
756
|
+
if (usePKCE) {
|
|
757
|
+
const verifier = generateCodeVerifier();
|
|
758
|
+
storeCodeVerifier(verifier);
|
|
759
|
+
generateCodeChallenge(verifier).then((challenge) => {
|
|
760
|
+
params.set("code_challenge", challenge);
|
|
761
|
+
params.set("code_challenge_method", "S256");
|
|
762
|
+
window.location.href = `${this.ssoConfig.ssoUrl}/api/v1/oauth2/authorize?${params.toString()}`;
|
|
763
|
+
});
|
|
764
|
+
} else {
|
|
765
|
+
window.location.href = `${this.ssoConfig.ssoUrl}/api/v1/oauth2/authorize?${params.toString()}`;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Check if current URL is an SSO callback
|
|
770
|
+
* 检查当前 URL 是否是 SSO 回调
|
|
771
|
+
*
|
|
772
|
+
* @example
|
|
773
|
+
* ```typescript
|
|
774
|
+
* if (auth.isSSOCallback()) {
|
|
775
|
+
* await auth.handleSSOCallback();
|
|
776
|
+
* }
|
|
777
|
+
* ```
|
|
778
|
+
*/
|
|
779
|
+
isSSOCallback() {
|
|
780
|
+
if (typeof window === "undefined") return false;
|
|
781
|
+
const params = new URLSearchParams(window.location.search);
|
|
782
|
+
return !!(params.get("code") && params.get("state"));
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Handle SSO callback and exchange code for tokens
|
|
786
|
+
* 处理 SSO 回调并交换授权码获取令牌
|
|
787
|
+
*
|
|
788
|
+
* Call this on your callback page after SSO redirects back.
|
|
789
|
+
*
|
|
790
|
+
* @returns LoginResult or null if callback handling failed
|
|
791
|
+
*
|
|
792
|
+
* @example
|
|
793
|
+
* ```typescript
|
|
794
|
+
* // In your callback page component
|
|
795
|
+
* useEffect(() => {
|
|
796
|
+
* if (auth.isSSOCallback()) {
|
|
797
|
+
* auth.handleSSOCallback()
|
|
798
|
+
* .then(result => {
|
|
799
|
+
* if (result) {
|
|
800
|
+
* navigate('/dashboard');
|
|
801
|
+
* }
|
|
802
|
+
* })
|
|
803
|
+
* .catch(err => console.error('SSO login failed:', err));
|
|
804
|
+
* }
|
|
805
|
+
* }, []);
|
|
806
|
+
* ```
|
|
807
|
+
*/
|
|
808
|
+
async handleSSOCallback() {
|
|
809
|
+
if (!this.ssoConfig) {
|
|
810
|
+
throw this.createError("SSO_NOT_CONFIGURED", "SSO is not configured. Call configureSso() first.");
|
|
811
|
+
}
|
|
812
|
+
if (typeof window === "undefined") {
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
const params = new URLSearchParams(window.location.search);
|
|
816
|
+
const code = params.get("code");
|
|
817
|
+
const state = params.get("state");
|
|
818
|
+
const error = params.get("error");
|
|
819
|
+
const errorDescription = params.get("error_description");
|
|
820
|
+
if (error) {
|
|
821
|
+
throw this.createError(error, errorDescription || "SSO login failed");
|
|
822
|
+
}
|
|
823
|
+
const savedState = this.getAndClearState();
|
|
824
|
+
if (state && savedState && state !== savedState) {
|
|
825
|
+
throw this.createError("INVALID_STATE", "Invalid state parameter. Please try logging in again.");
|
|
826
|
+
}
|
|
827
|
+
if (!code) {
|
|
828
|
+
throw this.createError("NO_CODE", "No authorization code received.");
|
|
829
|
+
}
|
|
830
|
+
const tokenResult = await this.exchangeSSOCode(code, this.ssoConfig.redirectUri);
|
|
831
|
+
this.storage.setAccessToken(tokenResult.access_token);
|
|
832
|
+
if (tokenResult.refresh_token) {
|
|
833
|
+
this.storage.setRefreshToken(tokenResult.refresh_token);
|
|
834
|
+
}
|
|
835
|
+
const user = await this.getCurrentUser();
|
|
836
|
+
if (typeof window !== "undefined" && window.history) {
|
|
837
|
+
const cleanUrl = window.location.pathname;
|
|
838
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
user: user || { id: "", phone: null, email: null, nickname: null, avatar_url: null },
|
|
842
|
+
access_token: tokenResult.access_token,
|
|
843
|
+
refresh_token: tokenResult.refresh_token || "",
|
|
844
|
+
expires_in: tokenResult.expires_in,
|
|
845
|
+
is_new_user: false
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Check if user can be silently authenticated via SSO
|
|
850
|
+
* 检查用户是否可以通过 SSO 静默登录
|
|
851
|
+
*
|
|
852
|
+
* This starts a silent SSO flow using an iframe to check if user has an active SSO session.
|
|
853
|
+
*
|
|
854
|
+
* @returns Promise that resolves to true if silent auth succeeded
|
|
855
|
+
*/
|
|
856
|
+
async checkSSOSession() {
|
|
857
|
+
if (!this.ssoConfig) {
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
if (this.isAuthenticated()) {
|
|
861
|
+
return true;
|
|
862
|
+
}
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
// Helper methods for SSO
|
|
866
|
+
generateRandomState() {
|
|
867
|
+
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
868
|
+
}
|
|
869
|
+
storeState(state) {
|
|
870
|
+
if (typeof localStorage !== "undefined") {
|
|
871
|
+
localStorage.setItem("uniauth_sso_state", state);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
getAndClearState() {
|
|
875
|
+
if (typeof localStorage === "undefined") return null;
|
|
876
|
+
const state = localStorage.getItem("uniauth_sso_state");
|
|
877
|
+
localStorage.removeItem("uniauth_sso_state");
|
|
878
|
+
return state;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Exchange SSO authorization code for tokens
|
|
882
|
+
* This is a private method used internally by handleSSOCallback
|
|
883
|
+
*/
|
|
884
|
+
async exchangeSSOCode(code, redirectUri) {
|
|
885
|
+
const baseUrl = this.ssoConfig?.ssoUrl || this.config.baseUrl;
|
|
886
|
+
const clientId = this.ssoConfig?.clientId || this.config.clientId;
|
|
887
|
+
if (!clientId) {
|
|
888
|
+
throw this.createError("CONFIG_ERROR", "clientId is required for OAuth2 flow");
|
|
889
|
+
}
|
|
890
|
+
const body = {
|
|
891
|
+
grant_type: "authorization_code",
|
|
892
|
+
client_id: clientId,
|
|
893
|
+
code,
|
|
894
|
+
redirect_uri: redirectUri
|
|
895
|
+
};
|
|
896
|
+
const codeVerifier = getAndClearCodeVerifier();
|
|
897
|
+
if (codeVerifier) {
|
|
898
|
+
body.code_verifier = codeVerifier;
|
|
899
|
+
}
|
|
900
|
+
const response = await fetchWithRetry(`${baseUrl}/api/v1/oauth2/token`, {
|
|
901
|
+
method: "POST",
|
|
902
|
+
headers: {
|
|
903
|
+
"Content-Type": "application/json"
|
|
904
|
+
},
|
|
905
|
+
body: JSON.stringify(body),
|
|
906
|
+
maxRetries: this.config.enableRetry ? 3 : 0,
|
|
907
|
+
timeout: this.config.timeout
|
|
908
|
+
});
|
|
909
|
+
const data = await response.json();
|
|
910
|
+
if (data.error) {
|
|
911
|
+
throw this.createError(data.error, data.error_description || "Token exchange failed");
|
|
912
|
+
}
|
|
913
|
+
return data;
|
|
914
|
+
}
|
|
915
|
+
// ============================================
|
|
916
|
+
// Private Methods
|
|
917
|
+
// ============================================
|
|
918
|
+
/**
|
|
919
|
+
* Refresh tokens
|
|
920
|
+
* 刷新令牌
|
|
921
|
+
*/
|
|
922
|
+
async refreshTokens() {
|
|
923
|
+
if (this.refreshPromise) {
|
|
924
|
+
return this.refreshPromise;
|
|
925
|
+
}
|
|
926
|
+
this.refreshPromise = this.doRefreshTokens();
|
|
927
|
+
try {
|
|
928
|
+
return await this.refreshPromise;
|
|
929
|
+
} finally {
|
|
930
|
+
this.refreshPromise = null;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
async doRefreshTokens() {
|
|
934
|
+
const refreshToken = this.storage.getRefreshToken();
|
|
935
|
+
if (!refreshToken) {
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
try {
|
|
939
|
+
const response = await this.request("/api/v1/auth/refresh", {
|
|
940
|
+
method: "POST",
|
|
941
|
+
body: JSON.stringify({ refresh_token: refreshToken })
|
|
942
|
+
});
|
|
943
|
+
if (!response.success || !response.data) {
|
|
944
|
+
this.storage.clear();
|
|
945
|
+
this.config.onAuthError?.({
|
|
946
|
+
code: "REFRESH_FAILED",
|
|
947
|
+
message: response.error?.message || "Failed to refresh token"
|
|
948
|
+
});
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
this.storage.setAccessToken(response.data.access_token);
|
|
952
|
+
this.storage.setRefreshToken(response.data.refresh_token);
|
|
953
|
+
this.config.onTokenRefresh?.(response.data);
|
|
954
|
+
return true;
|
|
955
|
+
} catch (error) {
|
|
956
|
+
this.storage.clear();
|
|
957
|
+
this.config.onAuthError?.({
|
|
958
|
+
code: "REFRESH_ERROR",
|
|
959
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
960
|
+
});
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Make an authenticated request
|
|
966
|
+
* 发起已认证的请求
|
|
967
|
+
*/
|
|
968
|
+
async authenticatedRequest(path, options = {}) {
|
|
969
|
+
const token = await this.getAccessToken();
|
|
970
|
+
if (!token) {
|
|
971
|
+
throw this.createError("NOT_AUTHENTICATED", "Not authenticated");
|
|
972
|
+
}
|
|
973
|
+
return this.request(path, {
|
|
974
|
+
...options,
|
|
975
|
+
headers: {
|
|
976
|
+
...options.headers,
|
|
977
|
+
Authorization: `Bearer ${token}`
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Make a request to the API with retry support
|
|
983
|
+
* 向 API 发起请求(支持重试)
|
|
984
|
+
*/
|
|
985
|
+
async request(path, options = {}) {
|
|
986
|
+
const url = `${this.config.baseUrl}${path}`;
|
|
987
|
+
const fetchOptions = {
|
|
988
|
+
...options,
|
|
989
|
+
headers: {
|
|
990
|
+
"Content-Type": "application/json",
|
|
991
|
+
...this.config.appKey && { "X-App-Key": this.config.appKey },
|
|
992
|
+
...options.headers
|
|
993
|
+
},
|
|
994
|
+
maxRetries: this.config.enableRetry ? 3 : 0,
|
|
995
|
+
timeout: this.config.timeout
|
|
996
|
+
};
|
|
997
|
+
const response = await fetchWithRetry(url, fetchOptions);
|
|
998
|
+
const data = await response.json();
|
|
999
|
+
return data;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Create an error object
|
|
1003
|
+
* 创建错误对象
|
|
1004
|
+
*/
|
|
1005
|
+
createError(code, message, statusCode) {
|
|
1006
|
+
return new UniAuthError(code, message, statusCode);
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
var index_default = UniAuthClient;
|
|
1010
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1011
|
+
0 && (module.exports = {
|
|
1012
|
+
AuthErrorCode,
|
|
1013
|
+
UniAuthClient,
|
|
1014
|
+
UniAuthError,
|
|
1015
|
+
fetchWithRetry,
|
|
1016
|
+
generateCodeChallenge,
|
|
1017
|
+
generateCodeVerifier,
|
|
1018
|
+
getAndClearCodeVerifier,
|
|
1019
|
+
storeCodeVerifier
|
|
1020
|
+
});
|