@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/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
+ });