@55387.ai/uniauth-client 1.1.0 → 1.1.2

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 CHANGED
@@ -270,11 +270,71 @@ var UniAuthClient = class {
270
270
  * Send verification code to phone number
271
271
  * 发送验证码到手机号
272
272
  *
273
- * @param phone - Phone number
273
+ * @param phone - Phone number in E.164 format (e.g., +8613800138000)
274
274
  * @param type - Purpose of the verification code
275
275
  * @param captchaToken - Captcha verification token from slider captcha
276
276
  */
277
277
  async sendCode(phone, type = "login", captchaToken) {
278
+ if (!phone || typeof phone !== "string") {
279
+ throw this.createError("INVALID_PHONE", "Phone number is required / \u8BF7\u8F93\u5165\u624B\u673A\u53F7");
280
+ }
281
+ const countryValidation = {
282
+ // China: +86 followed by 11 digits starting with 1
283
+ "+86": {
284
+ regex: /^\+861[3-9]\d{9}$/,
285
+ example: "+8613800138000",
286
+ name: "\u4E2D\u56FD"
287
+ },
288
+ // USA/Canada: +1 followed by 10 digits (NPA-NXX-XXXX)
289
+ "+1": {
290
+ regex: /^\+1[2-9]\d{2}[2-9]\d{6}$/,
291
+ example: "+14155552671",
292
+ name: "USA/Canada"
293
+ },
294
+ // Australia: +61 followed by 9 digits starting with 4 (mobile)
295
+ "+61": {
296
+ regex: /^\+614\d{8}$/,
297
+ example: "+61412345678",
298
+ name: "Australia"
299
+ },
300
+ // UK: +44 followed by 10 digits starting with 7 (mobile)
301
+ "+44": {
302
+ regex: /^\+447\d{9}$/,
303
+ example: "+447911123456",
304
+ name: "UK"
305
+ },
306
+ // Japan: +81 followed by 10-11 digits
307
+ "+81": {
308
+ regex: /^\+81[789]0\d{8}$/,
309
+ example: "+818012345678",
310
+ name: "Japan"
311
+ }
312
+ };
313
+ let countryCode = "";
314
+ let validation = null;
315
+ for (const code of Object.keys(countryValidation).sort((a, b) => b.length - a.length)) {
316
+ if (phone.startsWith(code)) {
317
+ countryCode = code;
318
+ validation = countryValidation[code];
319
+ break;
320
+ }
321
+ }
322
+ if (validation) {
323
+ if (!validation.regex.test(phone)) {
324
+ throw this.createError(
325
+ "INVALID_PHONE_FORMAT",
326
+ `Invalid ${validation.name} phone number format. Example: ${validation.example} / ${validation.name}\u624B\u673A\u53F7\u683C\u5F0F\u9519\u8BEF\uFF0C\u793A\u4F8B\uFF1A${validation.example}`
327
+ );
328
+ }
329
+ } else {
330
+ const e164Regex = /^\+[1-9]\d{6,14}$/;
331
+ if (!e164Regex.test(phone)) {
332
+ throw this.createError(
333
+ "INVALID_PHONE_FORMAT",
334
+ "Phone number must be in E.164 format (e.g., +8613800138000) / \u624B\u673A\u53F7\u683C\u5F0F\u9519\u8BEF\uFF0C\u8BF7\u4F7F\u7528\u56FD\u9645\u683C\u5F0F\uFF08\u5982 +8613800138000\uFF09"
335
+ );
336
+ }
337
+ }
278
338
  const response = await this.request("/api/v1/auth/phone/send-code", {
279
339
  method: "POST",
280
340
  body: JSON.stringify({
@@ -367,10 +427,10 @@ var UniAuthClient = class {
367
427
  * Handle OAuth callback (for social login)
368
428
  * 处理 OAuth 回调(社交登录)
369
429
  */
370
- async handleOAuthCallback(provider, code) {
371
- const response = await this.request("/api/v1/auth/oauth/callback", {
430
+ async handleOAuthCallback(provider, code, redirectUri) {
431
+ const response = await this.request(`/api/v1/auth/oauth/${provider}/callback`, {
372
432
  method: "POST",
373
- body: JSON.stringify({ provider, code })
433
+ body: JSON.stringify({ code, redirect_uri: redirectUri })
374
434
  });
375
435
  if (!response.success || !response.data) {
376
436
  throw this.createError(response.error?.code || "OAUTH_FAILED", response.error?.message || "OAuth callback failed");
@@ -963,6 +1023,7 @@ var UniAuthClient = class {
963
1023
  this.storage.setAccessToken(response.data.access_token);
964
1024
  this.storage.setRefreshToken(response.data.refresh_token);
965
1025
  this.config.onTokenRefresh?.(response.data);
1026
+ this.notifyAuthStateChange(this.currentUser);
966
1027
  return true;
967
1028
  } catch (error) {
968
1029
  this.storage.clear();
package/dist/index.d.cts CHANGED
@@ -88,6 +88,8 @@ interface UniAuthConfig {
88
88
  appKey?: string;
89
89
  /** OAuth2 Client ID (for OAuth flows) */
90
90
  clientId?: string;
91
+ /** OAuth2 Client Secret (for trusted SPA clients like internal admin consoles) */
92
+ clientSecret?: string;
91
93
  /** Storage type for tokens */
92
94
  storage?: 'localStorage' | 'sessionStorage' | 'memory';
93
95
  /** Callback when tokens are refreshed */
@@ -238,7 +240,7 @@ declare class UniAuthClient {
238
240
  * Send verification code to phone number
239
241
  * 发送验证码到手机号
240
242
  *
241
- * @param phone - Phone number
243
+ * @param phone - Phone number in E.164 format (e.g., +8613800138000)
242
244
  * @param type - Purpose of the verification code
243
245
  * @param captchaToken - Captcha verification token from slider captcha
244
246
  */
@@ -271,7 +273,7 @@ declare class UniAuthClient {
271
273
  * Handle OAuth callback (for social login)
272
274
  * 处理 OAuth 回调(社交登录)
273
275
  */
274
- handleOAuthCallback(provider: string, code: string): Promise<LoginResult>;
276
+ handleOAuthCallback(provider: string, code: string, redirectUri?: string): Promise<LoginResult>;
275
277
  /**
276
278
  * Register with email and password
277
279
  * 使用邮箱密码注册
package/dist/index.d.ts CHANGED
@@ -88,6 +88,8 @@ interface UniAuthConfig {
88
88
  appKey?: string;
89
89
  /** OAuth2 Client ID (for OAuth flows) */
90
90
  clientId?: string;
91
+ /** OAuth2 Client Secret (for trusted SPA clients like internal admin consoles) */
92
+ clientSecret?: string;
91
93
  /** Storage type for tokens */
92
94
  storage?: 'localStorage' | 'sessionStorage' | 'memory';
93
95
  /** Callback when tokens are refreshed */
@@ -238,7 +240,7 @@ declare class UniAuthClient {
238
240
  * Send verification code to phone number
239
241
  * 发送验证码到手机号
240
242
  *
241
- * @param phone - Phone number
243
+ * @param phone - Phone number in E.164 format (e.g., +8613800138000)
242
244
  * @param type - Purpose of the verification code
243
245
  * @param captchaToken - Captcha verification token from slider captcha
244
246
  */
@@ -271,7 +273,7 @@ declare class UniAuthClient {
271
273
  * Handle OAuth callback (for social login)
272
274
  * 处理 OAuth 回调(社交登录)
273
275
  */
274
- handleOAuthCallback(provider: string, code: string): Promise<LoginResult>;
276
+ handleOAuthCallback(provider: string, code: string, redirectUri?: string): Promise<LoginResult>;
275
277
  /**
276
278
  * Register with email and password
277
279
  * 使用邮箱密码注册
package/dist/index.js CHANGED
@@ -236,11 +236,71 @@ var UniAuthClient = class {
236
236
  * Send verification code to phone number
237
237
  * 发送验证码到手机号
238
238
  *
239
- * @param phone - Phone number
239
+ * @param phone - Phone number in E.164 format (e.g., +8613800138000)
240
240
  * @param type - Purpose of the verification code
241
241
  * @param captchaToken - Captcha verification token from slider captcha
242
242
  */
243
243
  async sendCode(phone, type = "login", captchaToken) {
244
+ if (!phone || typeof phone !== "string") {
245
+ throw this.createError("INVALID_PHONE", "Phone number is required / \u8BF7\u8F93\u5165\u624B\u673A\u53F7");
246
+ }
247
+ const countryValidation = {
248
+ // China: +86 followed by 11 digits starting with 1
249
+ "+86": {
250
+ regex: /^\+861[3-9]\d{9}$/,
251
+ example: "+8613800138000",
252
+ name: "\u4E2D\u56FD"
253
+ },
254
+ // USA/Canada: +1 followed by 10 digits (NPA-NXX-XXXX)
255
+ "+1": {
256
+ regex: /^\+1[2-9]\d{2}[2-9]\d{6}$/,
257
+ example: "+14155552671",
258
+ name: "USA/Canada"
259
+ },
260
+ // Australia: +61 followed by 9 digits starting with 4 (mobile)
261
+ "+61": {
262
+ regex: /^\+614\d{8}$/,
263
+ example: "+61412345678",
264
+ name: "Australia"
265
+ },
266
+ // UK: +44 followed by 10 digits starting with 7 (mobile)
267
+ "+44": {
268
+ regex: /^\+447\d{9}$/,
269
+ example: "+447911123456",
270
+ name: "UK"
271
+ },
272
+ // Japan: +81 followed by 10-11 digits
273
+ "+81": {
274
+ regex: /^\+81[789]0\d{8}$/,
275
+ example: "+818012345678",
276
+ name: "Japan"
277
+ }
278
+ };
279
+ let countryCode = "";
280
+ let validation = null;
281
+ for (const code of Object.keys(countryValidation).sort((a, b) => b.length - a.length)) {
282
+ if (phone.startsWith(code)) {
283
+ countryCode = code;
284
+ validation = countryValidation[code];
285
+ break;
286
+ }
287
+ }
288
+ if (validation) {
289
+ if (!validation.regex.test(phone)) {
290
+ throw this.createError(
291
+ "INVALID_PHONE_FORMAT",
292
+ `Invalid ${validation.name} phone number format. Example: ${validation.example} / ${validation.name}\u624B\u673A\u53F7\u683C\u5F0F\u9519\u8BEF\uFF0C\u793A\u4F8B\uFF1A${validation.example}`
293
+ );
294
+ }
295
+ } else {
296
+ const e164Regex = /^\+[1-9]\d{6,14}$/;
297
+ if (!e164Regex.test(phone)) {
298
+ throw this.createError(
299
+ "INVALID_PHONE_FORMAT",
300
+ "Phone number must be in E.164 format (e.g., +8613800138000) / \u624B\u673A\u53F7\u683C\u5F0F\u9519\u8BEF\uFF0C\u8BF7\u4F7F\u7528\u56FD\u9645\u683C\u5F0F\uFF08\u5982 +8613800138000\uFF09"
301
+ );
302
+ }
303
+ }
244
304
  const response = await this.request("/api/v1/auth/phone/send-code", {
245
305
  method: "POST",
246
306
  body: JSON.stringify({
@@ -333,10 +393,10 @@ var UniAuthClient = class {
333
393
  * Handle OAuth callback (for social login)
334
394
  * 处理 OAuth 回调(社交登录)
335
395
  */
336
- async handleOAuthCallback(provider, code) {
337
- const response = await this.request("/api/v1/auth/oauth/callback", {
396
+ async handleOAuthCallback(provider, code, redirectUri) {
397
+ const response = await this.request(`/api/v1/auth/oauth/${provider}/callback`, {
338
398
  method: "POST",
339
- body: JSON.stringify({ provider, code })
399
+ body: JSON.stringify({ code, redirect_uri: redirectUri })
340
400
  });
341
401
  if (!response.success || !response.data) {
342
402
  throw this.createError(response.error?.code || "OAUTH_FAILED", response.error?.message || "OAuth callback failed");
@@ -929,6 +989,7 @@ var UniAuthClient = class {
929
989
  this.storage.setAccessToken(response.data.access_token);
930
990
  this.storage.setRefreshToken(response.data.refresh_token);
931
991
  this.config.onTokenRefresh?.(response.data);
992
+ this.notifyAuthStateChange(this.currentUser);
932
993
  return true;
933
994
  } catch (error) {
934
995
  this.storage.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@55387.ai/uniauth-client",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "UniAuth Frontend SDK - Phone, Email, SSO login for browser",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/index.ts CHANGED
@@ -42,6 +42,8 @@ export interface UniAuthConfig {
42
42
  appKey?: string;
43
43
  /** OAuth2 Client ID (for OAuth flows) */
44
44
  clientId?: string;
45
+ /** OAuth2 Client Secret (for trusted SPA clients like internal admin consoles) */
46
+ clientSecret?: string;
45
47
  /** Storage type for tokens */
46
48
  storage?: 'localStorage' | 'sessionStorage' | 'memory';
47
49
  /** Callback when tokens are refreshed */
@@ -345,7 +347,7 @@ export class UniAuthClient {
345
347
  * Send verification code to phone number
346
348
  * 发送验证码到手机号
347
349
  *
348
- * @param phone - Phone number
350
+ * @param phone - Phone number in E.164 format (e.g., +8613800138000)
349
351
  * @param type - Purpose of the verification code
350
352
  * @param captchaToken - Captcha verification token from slider captcha
351
353
  */
@@ -354,6 +356,76 @@ export class UniAuthClient {
354
356
  type: 'login' | 'register' | 'reset' = 'login',
355
357
  captchaToken?: string
356
358
  ): Promise<SendCodeResult> {
359
+ // Validate phone number format (E.164: +countrycode + number)
360
+ if (!phone || typeof phone !== 'string') {
361
+ throw this.createError('INVALID_PHONE', 'Phone number is required / 请输入手机号');
362
+ }
363
+
364
+ // Country-specific validation rules
365
+ const countryValidation: Record<string, { regex: RegExp; example: string; name: string }> = {
366
+ // China: +86 followed by 11 digits starting with 1
367
+ '+86': {
368
+ regex: /^\+861[3-9]\d{9}$/,
369
+ example: '+8613800138000',
370
+ name: '中国'
371
+ },
372
+ // USA/Canada: +1 followed by 10 digits (NPA-NXX-XXXX)
373
+ '+1': {
374
+ regex: /^\+1[2-9]\d{2}[2-9]\d{6}$/,
375
+ example: '+14155552671',
376
+ name: 'USA/Canada'
377
+ },
378
+ // Australia: +61 followed by 9 digits starting with 4 (mobile)
379
+ '+61': {
380
+ regex: /^\+614\d{8}$/,
381
+ example: '+61412345678',
382
+ name: 'Australia'
383
+ },
384
+ // UK: +44 followed by 10 digits starting with 7 (mobile)
385
+ '+44': {
386
+ regex: /^\+447\d{9}$/,
387
+ example: '+447911123456',
388
+ name: 'UK'
389
+ },
390
+ // Japan: +81 followed by 10-11 digits
391
+ '+81': {
392
+ regex: /^\+81[789]0\d{8}$/,
393
+ example: '+818012345678',
394
+ name: 'Japan'
395
+ },
396
+ };
397
+
398
+ // Extract country code (try +1, +86, +61, etc.)
399
+ let countryCode = '';
400
+ let validation = null;
401
+
402
+ for (const code of Object.keys(countryValidation).sort((a, b) => b.length - a.length)) {
403
+ if (phone.startsWith(code)) {
404
+ countryCode = code;
405
+ validation = countryValidation[code];
406
+ break;
407
+ }
408
+ }
409
+
410
+ if (validation) {
411
+ // Country-specific validation
412
+ if (!validation.regex.test(phone)) {
413
+ throw this.createError(
414
+ 'INVALID_PHONE_FORMAT',
415
+ `Invalid ${validation.name} phone number format. Example: ${validation.example} / ${validation.name}手机号格式错误,示例:${validation.example}`
416
+ );
417
+ }
418
+ } else {
419
+ // Fallback: Generic E.164 validation for other countries
420
+ const e164Regex = /^\+[1-9]\d{6,14}$/;
421
+ if (!e164Regex.test(phone)) {
422
+ throw this.createError(
423
+ 'INVALID_PHONE_FORMAT',
424
+ 'Phone number must be in E.164 format (e.g., +8613800138000) / 手机号格式错误,请使用国际格式(如 +8613800138000)'
425
+ );
426
+ }
427
+ }
428
+
357
429
  const response = await this.request<SendCodeResult>('/api/v1/auth/phone/send-code', {
358
430
  method: 'POST',
359
431
  body: JSON.stringify({
@@ -471,10 +543,10 @@ export class UniAuthClient {
471
543
  * Handle OAuth callback (for social login)
472
544
  * 处理 OAuth 回调(社交登录)
473
545
  */
474
- async handleOAuthCallback(provider: string, code: string): Promise<LoginResult> {
475
- const response = await this.request<LoginResult>('/api/v1/auth/oauth/callback', {
546
+ async handleOAuthCallback(provider: string, code: string, redirectUri?: string): Promise<LoginResult> {
547
+ const response = await this.request<LoginResult>(`/api/v1/auth/oauth/${provider}/callback`, {
476
548
  method: 'POST',
477
- body: JSON.stringify({ provider, code }),
549
+ body: JSON.stringify({ code, redirect_uri: redirectUri }),
478
550
  });
479
551
 
480
552
  if (!response.success || !response.data) {
@@ -1207,6 +1279,7 @@ export class UniAuthClient {
1207
1279
  this.storage.setRefreshToken(response.data.refresh_token);
1208
1280
 
1209
1281
  this.config.onTokenRefresh?.(response.data);
1282
+ this.notifyAuthStateChange(this.currentUser);
1210
1283
 
1211
1284
  return true;
1212
1285
  } catch (error) {
@@ -0,0 +1,228 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { UniAuthClient, UniAuthError } from '../src/index';
3
+
4
+ describe('UniAuthClient - Phone Validation', () => {
5
+ let client: UniAuthClient;
6
+
7
+ beforeEach(() => {
8
+ client = new UniAuthClient({
9
+ baseUrl: 'https://test.example.com',
10
+ clientId: 'test-client',
11
+ });
12
+ });
13
+
14
+ describe('sendCode phone validation', () => {
15
+ it('should reject empty phone number', async () => {
16
+ await expect(client.sendCode('')).rejects.toThrow(UniAuthError);
17
+ await expect(client.sendCode('')).rejects.toMatchObject({
18
+ code: 'INVALID_PHONE',
19
+ });
20
+ });
21
+
22
+ it('should reject phone number without country code', async () => {
23
+ await expect(client.sendCode('13800138000')).rejects.toMatchObject({
24
+ code: 'INVALID_PHONE_FORMAT',
25
+ });
26
+ });
27
+
28
+ it('should reject phone number without + prefix', async () => {
29
+ await expect(client.sendCode('8613800138000')).rejects.toMatchObject({
30
+ code: 'INVALID_PHONE_FORMAT',
31
+ });
32
+ });
33
+
34
+ // China-specific validation
35
+ it('should reject invalid China phone number (wrong length)', async () => {
36
+ await expect(client.sendCode('+861380013800')).rejects.toMatchObject({
37
+ code: 'INVALID_PHONE_FORMAT',
38
+ });
39
+ });
40
+
41
+ it('should reject invalid China phone number (wrong prefix)', async () => {
42
+ await expect(client.sendCode('+8612800138000')).rejects.toMatchObject({
43
+ code: 'INVALID_PHONE_FORMAT',
44
+ });
45
+ });
46
+
47
+ // USA-specific validation
48
+ it('should reject invalid USA phone number (wrong format)', async () => {
49
+ await expect(client.sendCode('+11234567890')).rejects.toMatchObject({
50
+ code: 'INVALID_PHONE_FORMAT',
51
+ });
52
+ });
53
+
54
+ // Australia-specific validation
55
+ it('should reject invalid Australia phone number (wrong prefix)', async () => {
56
+ await expect(client.sendCode('+61312345678')).rejects.toMatchObject({
57
+ code: 'INVALID_PHONE_FORMAT',
58
+ });
59
+ });
60
+
61
+ it('should accept valid country-specific phone numbers', async () => {
62
+ // Mock the fetch to avoid actual API calls
63
+ const mockFetch = vi.fn().mockResolvedValue({
64
+ ok: true,
65
+ json: () => Promise.resolve({
66
+ success: true,
67
+ data: { expires_in: 300, retry_after: 60 },
68
+ }),
69
+ });
70
+ global.fetch = mockFetch;
71
+
72
+ // Valid phone numbers for each country
73
+ const validNumbers = [
74
+ '+8613800138000', // China
75
+ '+8619912345678', // China (new prefix)
76
+ '+14155552671', // USA
77
+ '+12025551234', // USA
78
+ '+61412345678', // Australia
79
+ '+447911123456', // UK
80
+ '+818012345678', // Japan
81
+ ];
82
+
83
+ for (const phone of validNumbers) {
84
+ mockFetch.mockClear();
85
+ await expect(client.sendCode(phone)).resolves.toBeDefined();
86
+ expect(mockFetch).toHaveBeenCalledWith(
87
+ expect.stringContaining('/api/v1/auth/phone/send-code'),
88
+ expect.objectContaining({
89
+ method: 'POST',
90
+ body: expect.stringContaining(phone),
91
+ })
92
+ );
93
+ }
94
+ });
95
+ });
96
+ });
97
+
98
+ describe('UniAuthClient - Email Validation', () => {
99
+ let client: UniAuthClient;
100
+
101
+ beforeEach(() => {
102
+ client = new UniAuthClient({
103
+ baseUrl: 'https://test.example.com',
104
+ clientId: 'test-client',
105
+ });
106
+ });
107
+
108
+ describe('sendEmailCode', () => {
109
+ it('should accept valid email and make API call', async () => {
110
+ const mockFetch = vi.fn().mockResolvedValue({
111
+ ok: true,
112
+ json: () => Promise.resolve({
113
+ success: true,
114
+ data: { expires_in: 300, retry_after: 60 },
115
+ }),
116
+ });
117
+ global.fetch = mockFetch;
118
+
119
+ await expect(client.sendEmailCode('test@example.com')).resolves.toBeDefined();
120
+ expect(mockFetch).toHaveBeenCalledWith(
121
+ expect.stringContaining('/api/v1/auth/email/send-code'),
122
+ expect.objectContaining({
123
+ method: 'POST',
124
+ body: expect.stringContaining('test@example.com'),
125
+ })
126
+ );
127
+ });
128
+ });
129
+ });
130
+
131
+ describe('UniAuthClient - OAuth Callback', () => {
132
+ let client: UniAuthClient;
133
+
134
+ beforeEach(() => {
135
+ // Mock localStorage
136
+ const localStorageMock = {
137
+ getItem: vi.fn(),
138
+ setItem: vi.fn(),
139
+ removeItem: vi.fn(),
140
+ clear: vi.fn(),
141
+ };
142
+ Object.defineProperty(global, 'localStorage', {
143
+ value: localStorageMock,
144
+ writable: true
145
+ });
146
+
147
+ client = new UniAuthClient({
148
+ baseUrl: 'https://test.example.com',
149
+ clientId: 'test-client',
150
+ });
151
+ });
152
+
153
+ it('should handle OAuth callback with correct URL and parameters', async () => {
154
+ const mockFetch = vi.fn().mockResolvedValue({
155
+ ok: true,
156
+ json: () => Promise.resolve({
157
+ success: true,
158
+ data: {
159
+ user: { id: '123' },
160
+ access_token: 'access_token',
161
+ refresh_token: 'refresh_token',
162
+ },
163
+ }),
164
+ });
165
+ global.fetch = mockFetch;
166
+
167
+ const provider = 'google';
168
+ const code = 'auth_code';
169
+ const redirectUri = 'https://app.com/callback';
170
+
171
+ await client.handleOAuthCallback(provider, code, redirectUri);
172
+
173
+ expect(mockFetch).toHaveBeenCalledWith(
174
+ expect.stringContaining(`/api/v1/auth/oauth/${provider}/callback`),
175
+ expect.objectContaining({
176
+ method: 'POST',
177
+ body: expect.stringContaining('"redirect_uri":"https://app.com/callback"'),
178
+ })
179
+ );
180
+
181
+ // Also verify code is present
182
+ expect(mockFetch).toHaveBeenCalledWith(
183
+ expect.anything(),
184
+ expect.objectContaining({
185
+ body: expect.stringContaining('"code":"auth_code"'),
186
+ })
187
+ );
188
+ });
189
+ });
190
+
191
+ describe('UniAuthClient - Error Handling', () => {
192
+ let client: UniAuthClient;
193
+
194
+ beforeEach(() => {
195
+ client = new UniAuthClient({
196
+ baseUrl: 'https://test.example.com',
197
+ clientId: 'test-client',
198
+ });
199
+ });
200
+
201
+ it('should create UniAuthError with correct properties', async () => {
202
+ try {
203
+ await client.sendCode('');
204
+ } catch (error) {
205
+ expect(error).toBeInstanceOf(UniAuthError);
206
+ expect((error as UniAuthError).code).toBe('INVALID_PHONE');
207
+ expect((error as UniAuthError).message).toContain('请输入手机号');
208
+ }
209
+ });
210
+
211
+ it('should handle API errors correctly', async () => {
212
+ const mockFetch = vi.fn().mockResolvedValue({
213
+ ok: true,
214
+ json: () => Promise.resolve({
215
+ success: false,
216
+ error: {
217
+ code: 'DAILY_LIMIT_EXCEEDED',
218
+ message: '今日发送次数已达上限',
219
+ },
220
+ }),
221
+ });
222
+ global.fetch = mockFetch;
223
+
224
+ await expect(client.sendCode('+8613800138000')).rejects.toMatchObject({
225
+ code: 'DAILY_LIMIT_EXCEEDED',
226
+ });
227
+ });
228
+ });
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['tests/**/*.test.ts'],
8
+ coverage: {
9
+ provider: 'v8',
10
+ reporter: ['text', 'json', 'html'],
11
+ include: ['src/**/*.ts'],
12
+ },
13
+ },
14
+ });