@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/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@55387.ai/uniauth-client",
3
+ "version": "1.0.0",
4
+ "description": "UniAuth Frontend SDK - Phone, Email, SSO login for browser",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
21
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "lint": "eslint src --ext .ts",
25
+ "clean": "rm -rf dist"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^22.10.2",
29
+ "tsup": "^8.3.5",
30
+ "typescript": "^5.7.2",
31
+ "vitest": "^2.1.9"
32
+ },
33
+ "keywords": [
34
+ "auth",
35
+ "authentication",
36
+ "sms",
37
+ "login",
38
+ "sdk"
39
+ ]
40
+ }
@@ -0,0 +1,476 @@
1
+ /**
2
+ * UniAuth Client SDK Tests
3
+ * UniAuth 客户端 SDK 测试
4
+ */
5
+
6
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
7
+
8
+ // Test the exported utilities from http.ts
9
+ describe('HTTP Utilities', () => {
10
+ describe('PKCE Functions', () => {
11
+ // Mock crypto for Node.js environment
12
+ beforeEach(() => {
13
+ if (typeof globalThis.crypto === 'undefined') {
14
+ // @ts-ignore - Adding mock crypto for testing
15
+ globalThis.crypto = {
16
+ getRandomValues: (arr: Uint8Array) => {
17
+ for (let i = 0; i < arr.length; i++) {
18
+ arr[i] = Math.floor(Math.random() * 256);
19
+ }
20
+ return arr;
21
+ },
22
+ subtle: {
23
+ digest: async (_algorithm: string, data: ArrayBuffer) => {
24
+ // Simple mock - in real scenario would use actual SHA-256
25
+ const view = new Uint8Array(data);
26
+ const hash = new Uint8Array(32);
27
+ for (let i = 0; i < 32; i++) {
28
+ hash[i] = view[i % view.length] ^ (i * 7);
29
+ }
30
+ return hash.buffer;
31
+ },
32
+ },
33
+ };
34
+ }
35
+ });
36
+
37
+ it('should generate a valid code verifier', async () => {
38
+ const { generateCodeVerifier } = await import('./http.js');
39
+ const verifier = generateCodeVerifier();
40
+
41
+ // Should be base64url encoded
42
+ expect(verifier).toMatch(/^[A-Za-z0-9_-]+$/);
43
+ // Should be at least 43 characters (32 bytes base64 encoded)
44
+ expect(verifier.length).toBeGreaterThanOrEqual(43);
45
+ });
46
+
47
+ it('should generate different verifiers each time', async () => {
48
+ const { generateCodeVerifier } = await import('./http.js');
49
+ const verifier1 = generateCodeVerifier();
50
+ const verifier2 = generateCodeVerifier();
51
+
52
+ expect(verifier1).not.toBe(verifier2);
53
+ });
54
+
55
+ it('should generate a valid code challenge from verifier', async () => {
56
+ const { generateCodeVerifier, generateCodeChallenge } =
57
+ await import('./http.js');
58
+ const verifier = generateCodeVerifier();
59
+ const challenge = await generateCodeChallenge(verifier);
60
+
61
+ // Should be base64url encoded
62
+ expect(challenge).toMatch(/^[A-Za-z0-9_-]+$/);
63
+ // Should be 43 characters (32 bytes SHA-256 base64 encoded)
64
+ expect(challenge.length).toBe(43);
65
+ });
66
+
67
+ it('should generate consistent challenge for same verifier', async () => {
68
+ const { generateCodeChallenge } = await import('./http.js');
69
+ const verifier = 'test-verifier-123';
70
+ const challenge1 = await generateCodeChallenge(verifier);
71
+ const challenge2 = await generateCodeChallenge(verifier);
72
+
73
+ expect(challenge1).toBe(challenge2);
74
+ });
75
+ });
76
+
77
+ describe('Storage Functions', () => {
78
+ beforeEach(() => {
79
+ // Mock sessionStorage
80
+ const storage: Record<string, string> = {};
81
+ vi.stubGlobal('sessionStorage', {
82
+ getItem: (key: string) => storage[key] || null,
83
+ setItem: (key: string, value: string) => {
84
+ storage[key] = value;
85
+ },
86
+ removeItem: (key: string) => {
87
+ delete storage[key];
88
+ },
89
+ clear: () => {
90
+ Object.keys(storage).forEach((key) => delete storage[key]);
91
+ },
92
+ });
93
+ });
94
+
95
+ afterEach(() => {
96
+ vi.unstubAllGlobals();
97
+ });
98
+
99
+ it('should store and retrieve code verifier', async () => {
100
+ const { storeCodeVerifier, getAndClearCodeVerifier } =
101
+ await import('./http.js');
102
+
103
+ storeCodeVerifier('test-verifier');
104
+ const retrieved = getAndClearCodeVerifier();
105
+
106
+ expect(retrieved).toBe('test-verifier');
107
+ });
108
+
109
+ it('should clear code verifier after retrieval', async () => {
110
+ const { storeCodeVerifier, getAndClearCodeVerifier } =
111
+ await import('./http.js');
112
+
113
+ storeCodeVerifier('test-verifier');
114
+ getAndClearCodeVerifier();
115
+ const secondRetrieval = getAndClearCodeVerifier();
116
+
117
+ expect(secondRetrieval).toBeNull();
118
+ });
119
+
120
+ it('should support custom storage key', async () => {
121
+ const { storeCodeVerifier, getAndClearCodeVerifier } =
122
+ await import('./http.js');
123
+
124
+ storeCodeVerifier('custom-verifier', 'custom_key');
125
+ const retrieved = getAndClearCodeVerifier('custom_key');
126
+
127
+ expect(retrieved).toBe('custom-verifier');
128
+ });
129
+ });
130
+ });
131
+
132
+ describe('UniAuthClient', () => {
133
+ beforeEach(() => {
134
+ // Mock localStorage
135
+ const storage: Record<string, string> = {};
136
+ vi.stubGlobal('localStorage', {
137
+ getItem: (key: string) => storage[key] || null,
138
+ setItem: (key: string, value: string) => {
139
+ storage[key] = value;
140
+ },
141
+ removeItem: (key: string) => {
142
+ delete storage[key];
143
+ },
144
+ clear: () => {
145
+ Object.keys(storage).forEach((key) => delete storage[key]);
146
+ },
147
+ });
148
+
149
+ // Mock fetch
150
+ vi.stubGlobal('fetch', vi.fn());
151
+ });
152
+
153
+ afterEach(() => {
154
+ vi.unstubAllGlobals();
155
+ vi.resetAllMocks();
156
+ });
157
+
158
+ describe('Configuration', () => {
159
+ it('should create client with default config', async () => {
160
+ const { UniAuthClient } = await import('./index.js');
161
+ const client = new UniAuthClient({
162
+ baseUrl: 'https://auth.example.com',
163
+ });
164
+
165
+ expect(client).toBeDefined();
166
+ });
167
+
168
+ it('should create client with custom storage', async () => {
169
+ const { UniAuthClient } = await import('./index.js');
170
+ const client = new UniAuthClient({
171
+ baseUrl: 'https://auth.example.com',
172
+ storage: 'memory',
173
+ });
174
+
175
+ expect(client).toBeDefined();
176
+ });
177
+ });
178
+
179
+ describe('Authentication State', () => {
180
+ it('should return false for isAuthenticated when no token', async () => {
181
+ const { UniAuthClient } = await import('./index.js');
182
+ const client = new UniAuthClient({
183
+ baseUrl: 'https://auth.example.com',
184
+ storage: 'memory',
185
+ });
186
+
187
+ expect(client.isAuthenticated()).toBe(false);
188
+ });
189
+ });
190
+
191
+ describe('Send Code', () => {
192
+ it('should send verification code successfully', async () => {
193
+ const mockResponse = {
194
+ success: true,
195
+ data: {
196
+ expires_in: 300,
197
+ retry_after: 60,
198
+ },
199
+ };
200
+
201
+ vi.mocked(fetch).mockResolvedValueOnce({
202
+ ok: true,
203
+ json: async () => mockResponse,
204
+ } as Response);
205
+
206
+ const { UniAuthClient } = await import('./index.js');
207
+ const client = new UniAuthClient({
208
+ baseUrl: 'https://auth.example.com',
209
+ appKey: 'test-key',
210
+ enableRetry: false,
211
+ });
212
+
213
+ const result = await client.sendCode('+8613800138000');
214
+
215
+ expect(result.expires_in).toBe(300);
216
+ expect(result.retry_after).toBe(60);
217
+ });
218
+
219
+ it('should throw error when send code fails', async () => {
220
+ const mockResponse = {
221
+ success: false,
222
+ error: {
223
+ code: 'RATE_LIMITED',
224
+ message: 'Too many requests',
225
+ },
226
+ };
227
+
228
+ vi.mocked(fetch).mockResolvedValueOnce({
229
+ ok: true,
230
+ json: async () => mockResponse,
231
+ } as Response);
232
+
233
+ const { UniAuthClient } = await import('./index.js');
234
+ const client = new UniAuthClient({
235
+ baseUrl: 'https://auth.example.com',
236
+ enableRetry: false,
237
+ });
238
+
239
+ await expect(client.sendCode('+8613800138000')).rejects.toThrow(
240
+ 'Too many requests'
241
+ );
242
+ });
243
+ });
244
+
245
+ describe('Login', () => {
246
+ it('should login with phone code successfully', async () => {
247
+ const mockResponse = {
248
+ success: true,
249
+ data: {
250
+ user: {
251
+ id: 'user-123',
252
+ phone: '+8613800138000',
253
+ email: null,
254
+ nickname: 'Test User',
255
+ avatar_url: null,
256
+ },
257
+ access_token: 'access-token-123',
258
+ refresh_token: 'refresh-token-123',
259
+ expires_in: 3600,
260
+ is_new_user: false,
261
+ },
262
+ };
263
+
264
+ vi.mocked(fetch).mockResolvedValueOnce({
265
+ ok: true,
266
+ json: async () => mockResponse,
267
+ } as Response);
268
+
269
+ const { UniAuthClient } = await import('./index.js');
270
+ const client = new UniAuthClient({
271
+ baseUrl: 'https://auth.example.com',
272
+ storage: 'memory',
273
+ enableRetry: false,
274
+ });
275
+
276
+ const result = await client.loginWithCode('+8613800138000', '123456');
277
+
278
+ expect(result.user.id).toBe('user-123');
279
+ expect(result.access_token).toBe('access-token-123');
280
+ expect(client.isAuthenticated()).toBe(true);
281
+ });
282
+
283
+ it('should login with email code successfully', async () => {
284
+ const mockResponse = {
285
+ success: true,
286
+ data: {
287
+ user: {
288
+ id: 'user-456',
289
+ phone: null,
290
+ email: 'test@example.com',
291
+ nickname: 'Email User',
292
+ avatar_url: null,
293
+ },
294
+ access_token: 'email-token-123',
295
+ refresh_token: 'refresh-token-456',
296
+ expires_in: 3600,
297
+ is_new_user: true,
298
+ },
299
+ };
300
+
301
+ vi.mocked(fetch).mockResolvedValueOnce({
302
+ ok: true,
303
+ json: async () => mockResponse,
304
+ } as Response);
305
+
306
+ const { UniAuthClient } = await import('./index.js');
307
+ const client = new UniAuthClient({
308
+ baseUrl: 'https://auth.example.com',
309
+ storage: 'memory',
310
+ enableRetry: false,
311
+ });
312
+
313
+ const result = await client.loginWithEmailCode(
314
+ 'test@example.com',
315
+ '654321'
316
+ );
317
+
318
+ expect(result.user.email).toBe('test@example.com');
319
+ expect(result.is_new_user).toBe(true);
320
+ });
321
+ });
322
+
323
+ describe('OAuth2 Flow', () => {
324
+ it('should generate authorization URL', async () => {
325
+ // Mock crypto for PKCE
326
+ vi.stubGlobal('crypto', {
327
+ getRandomValues: (arr: Uint8Array) => {
328
+ for (let i = 0; i < arr.length; i++) {
329
+ arr[i] = i % 256;
330
+ }
331
+ return arr;
332
+ },
333
+ subtle: {
334
+ digest: async () => new Uint8Array(32).buffer,
335
+ },
336
+ });
337
+ vi.stubGlobal('sessionStorage', {
338
+ getItem: () => null,
339
+ setItem: () => { },
340
+ removeItem: () => { },
341
+ clear: () => { },
342
+ });
343
+
344
+ const { UniAuthClient } = await import('./index.js');
345
+ const client = new UniAuthClient({
346
+ baseUrl: 'https://auth.example.com',
347
+ clientId: 'my-client-id',
348
+ });
349
+
350
+ const url = await client.startOAuth2Flow({
351
+ redirectUri: 'https://myapp.com/callback',
352
+ scope: 'openid profile',
353
+ state: 'random-state',
354
+ });
355
+
356
+ expect(url).toContain('client_id=my-client-id');
357
+ expect(url).toContain('redirect_uri=https%3A%2F%2Fmyapp.com%2Fcallback');
358
+ expect(url).toContain('scope=openid+profile');
359
+ expect(url).toContain('state=random-state');
360
+ expect(url).toContain('response_type=code');
361
+ });
362
+
363
+ it('should throw error when clientId not configured', async () => {
364
+ const { UniAuthClient } = await import('./index.js');
365
+ const client = new UniAuthClient({
366
+ baseUrl: 'https://auth.example.com',
367
+ // No clientId
368
+ });
369
+
370
+ await expect(
371
+ client.startOAuth2Flow({
372
+ redirectUri: 'https://myapp.com/callback',
373
+ })
374
+ ).rejects.toThrow('clientId is required');
375
+ });
376
+ });
377
+
378
+ describe('Logout', () => {
379
+ it('should clear tokens on logout', async () => {
380
+ // Mock login response first
381
+ const mockLoginResponse = {
382
+ success: true,
383
+ data: {
384
+ user: {
385
+ id: 'user-123',
386
+ phone: '+8613800138000',
387
+ email: null,
388
+ nickname: 'Test',
389
+ avatar_url: null,
390
+ },
391
+ access_token: 'access-token',
392
+ refresh_token: 'refresh-token',
393
+ expires_in: 3600,
394
+ is_new_user: false,
395
+ },
396
+ };
397
+
398
+ vi.mocked(fetch)
399
+ .mockResolvedValueOnce({
400
+ ok: true,
401
+ json: async () => mockLoginResponse,
402
+ } as Response)
403
+ .mockResolvedValueOnce({
404
+ ok: true,
405
+ json: async () => ({ success: true }),
406
+ } as Response);
407
+
408
+ const { UniAuthClient } = await import('./index.js');
409
+ const client = new UniAuthClient({
410
+ baseUrl: 'https://auth.example.com',
411
+ storage: 'memory',
412
+ enableRetry: false,
413
+ });
414
+
415
+ // Login first
416
+ await client.loginWithCode('+8613800138000', '123456');
417
+ expect(client.isAuthenticated()).toBe(true);
418
+
419
+ // Then logout
420
+ await client.logout();
421
+
422
+ expect(client.isAuthenticated()).toBe(false);
423
+ });
424
+ });
425
+ });
426
+
427
+ describe('Retry Logic', () => {
428
+ beforeEach(() => {
429
+ vi.stubGlobal('fetch', vi.fn());
430
+ });
431
+
432
+ afterEach(() => {
433
+ vi.unstubAllGlobals();
434
+ vi.resetAllMocks();
435
+ });
436
+
437
+ it('should retry on 503 status', async () => {
438
+ const { fetchWithRetry } = await import('./http.js');
439
+
440
+ // First call returns 503, second succeeds
441
+ vi.mocked(fetch)
442
+ .mockResolvedValueOnce({
443
+ status: 503,
444
+ headers: new Map(),
445
+ } as unknown as Response)
446
+ .mockResolvedValueOnce({
447
+ status: 200,
448
+ ok: true,
449
+ } as Response);
450
+
451
+ const response = await fetchWithRetry('https://api.test.com', {
452
+ maxRetries: 2,
453
+ baseDelay: 10, // Short delay for testing
454
+ });
455
+
456
+ expect(response.status).toBe(200);
457
+ expect(fetch).toHaveBeenCalledTimes(2);
458
+ });
459
+
460
+ it('should not retry on 400 status', async () => {
461
+ const { fetchWithRetry } = await import('./http.js');
462
+
463
+ vi.mocked(fetch).mockResolvedValueOnce({
464
+ status: 400,
465
+ ok: false,
466
+ } as Response);
467
+
468
+ const response = await fetchWithRetry('https://api.test.com', {
469
+ maxRetries: 3,
470
+ baseDelay: 10,
471
+ });
472
+
473
+ expect(response.status).toBe(400);
474
+ expect(fetch).toHaveBeenCalledTimes(1);
475
+ });
476
+ });