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