@objectstack/plugin-auth 4.0.4 → 4.0.5

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.
Files changed (40) hide show
  1. package/README.md +4 -1
  2. package/dist/index.d.mts +332 -19942
  3. package/dist/index.d.ts +332 -19942
  4. package/dist/index.js +351 -882
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +355 -862
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +35 -12
  9. package/.turbo/turbo-build.log +0 -78
  10. package/ARCHITECTURE.md +0 -176
  11. package/CHANGELOG.md +0 -333
  12. package/IMPLEMENTATION_SUMMARY.md +0 -192
  13. package/examples/basic-usage.ts +0 -107
  14. package/objectstack.config.ts +0 -24
  15. package/src/auth-manager.test.ts +0 -883
  16. package/src/auth-manager.ts +0 -419
  17. package/src/auth-plugin.test.ts +0 -446
  18. package/src/auth-plugin.ts +0 -314
  19. package/src/auth-schema-config.ts +0 -339
  20. package/src/index.ts +0 -16
  21. package/src/objectql-adapter.test.ts +0 -281
  22. package/src/objectql-adapter.ts +0 -279
  23. package/src/objects/auth-account.object.ts +0 -7
  24. package/src/objects/auth-session.object.ts +0 -7
  25. package/src/objects/auth-user.object.ts +0 -7
  26. package/src/objects/auth-verification.object.ts +0 -7
  27. package/src/objects/index.ts +0 -40
  28. package/src/objects/sys-account.object.ts +0 -111
  29. package/src/objects/sys-api-key.object.ts +0 -104
  30. package/src/objects/sys-invitation.object.ts +0 -93
  31. package/src/objects/sys-member.object.ts +0 -68
  32. package/src/objects/sys-organization.object.ts +0 -82
  33. package/src/objects/sys-session.object.ts +0 -84
  34. package/src/objects/sys-team-member.object.ts +0 -61
  35. package/src/objects/sys-team.object.ts +0 -69
  36. package/src/objects/sys-two-factor.object.ts +0 -73
  37. package/src/objects/sys-user-preference.object.ts +0 -82
  38. package/src/objects/sys-user.object.ts +0 -91
  39. package/src/objects/sys-verification.object.ts +0 -75
  40. package/tsconfig.json +0 -18
@@ -1,883 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
4
- import { AuthManager } from './auth-manager';
5
-
6
- // Mock better-auth so we can control the handler behaviour
7
- vi.mock('better-auth', () => ({
8
- betterAuth: vi.fn(() => ({
9
- handler: vi.fn(),
10
- api: {},
11
- })),
12
- }));
13
-
14
- // Mock plugin imports — we only need to verify they are called with the
15
- // correct schema options; the actual plugin logic is tested by better-auth.
16
- vi.mock('better-auth/plugins/organization', () => ({
17
- organization: vi.fn((opts: any) => ({ id: 'organization', _opts: opts })),
18
- }));
19
-
20
- vi.mock('better-auth/plugins/two-factor', () => ({
21
- twoFactor: vi.fn((opts: any) => ({ id: 'two-factor', _opts: opts })),
22
- }));
23
-
24
- vi.mock('better-auth/plugins/magic-link', () => ({
25
- magicLink: vi.fn((_opts?: any) => ({ id: 'magic-link' })),
26
- }));
27
-
28
- import { betterAuth } from 'better-auth';
29
-
30
- describe('AuthManager', () => {
31
- let consoleSpy: ReturnType<typeof vi.spyOn>;
32
-
33
- beforeEach(() => {
34
- vi.clearAllMocks();
35
- consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
36
- });
37
-
38
- afterEach(() => {
39
- consoleSpy.mockRestore();
40
- });
41
-
42
- describe('handleRequest – error response logging', () => {
43
- it('should log when better-auth returns a 500 response', async () => {
44
- const errorResponse = new Response(
45
- JSON.stringify({ error: 'Internal database error' }),
46
- { status: 500, headers: { 'Content-Type': 'application/json' } },
47
- );
48
-
49
- const mockHandler = vi.fn().mockResolvedValue(errorResponse);
50
- (betterAuth as any).mockReturnValue({ handler: mockHandler, api: {} });
51
-
52
- const manager = new AuthManager({
53
- secret: 'test-secret-at-least-32-chars-long',
54
- baseUrl: 'http://localhost:3000',
55
- });
56
-
57
- const request = new Request('http://localhost:3000/sign-up/email', {
58
- method: 'POST',
59
- body: JSON.stringify({ email: 'a@b.com', password: 'pass' }),
60
- headers: { 'Content-Type': 'application/json' },
61
- });
62
-
63
- const response = await manager.handleRequest(request);
64
-
65
- expect(response.status).toBe(500);
66
- expect(consoleSpy).toHaveBeenCalledWith(
67
- '[AuthManager] better-auth returned error:',
68
- 500,
69
- expect.stringContaining('Internal database error'),
70
- );
71
- });
72
-
73
- it('should NOT log for successful (2xx) responses', async () => {
74
- const okResponse = new Response(JSON.stringify({ user: {} }), {
75
- status: 200,
76
- });
77
-
78
- const mockHandler = vi.fn().mockResolvedValue(okResponse);
79
- (betterAuth as any).mockReturnValue({ handler: mockHandler, api: {} });
80
-
81
- const manager = new AuthManager({
82
- secret: 'test-secret-at-least-32-chars-long',
83
- baseUrl: 'http://localhost:3000',
84
- });
85
-
86
- const request = new Request('http://localhost:3000/sign-in/email', {
87
- method: 'POST',
88
- body: JSON.stringify({ email: 'a@b.com', password: 'pass' }),
89
- headers: { 'Content-Type': 'application/json' },
90
- });
91
-
92
- const response = await manager.handleRequest(request);
93
-
94
- expect(response.status).toBe(200);
95
- expect(consoleSpy).not.toHaveBeenCalled();
96
- });
97
-
98
- it('should NOT log for 4xx responses', async () => {
99
- const badRequestResponse = new Response(
100
- JSON.stringify({ error: 'Bad request' }),
101
- { status: 400 },
102
- );
103
-
104
- const mockHandler = vi.fn().mockResolvedValue(badRequestResponse);
105
- (betterAuth as any).mockReturnValue({ handler: mockHandler, api: {} });
106
-
107
- const manager = new AuthManager({
108
- secret: 'test-secret-at-least-32-chars-long',
109
- baseUrl: 'http://localhost:3000',
110
- });
111
-
112
- const request = new Request('http://localhost:3000/sign-in/email', {
113
- method: 'POST',
114
- });
115
-
116
- const response = await manager.handleRequest(request);
117
-
118
- expect(response.status).toBe(400);
119
- expect(consoleSpy).not.toHaveBeenCalled();
120
- });
121
- });
122
-
123
- describe('createDatabaseConfig – adapter wrapping', () => {
124
- it('should pass a function (AdapterFactory) to betterAuth when dataEngine is provided', () => {
125
- const mockDataEngine = {
126
- insert: vi.fn(),
127
- findOne: vi.fn(),
128
- find: vi.fn(),
129
- count: vi.fn(),
130
- update: vi.fn(),
131
- delete: vi.fn(),
132
- };
133
-
134
- new AuthManager({
135
- secret: 'test-secret-at-least-32-chars-long',
136
- baseUrl: 'http://localhost:3000',
137
- dataEngine: mockDataEngine as any,
138
- });
139
-
140
- // Trigger lazy initialization by calling getAuthInstance()
141
- // betterAuth should have been called with a database value that is a function
142
- // We need to trigger the lazy init first
143
- });
144
-
145
- it('should provide a factory function as database config', () => {
146
- const mockDataEngine = {
147
- insert: vi.fn().mockResolvedValue({ id: '1' }),
148
- findOne: vi.fn().mockResolvedValue({ id: '1' }),
149
- find: vi.fn().mockResolvedValue([]),
150
- count: vi.fn().mockResolvedValue(0),
151
- update: vi.fn().mockResolvedValue({ id: '1' }),
152
- delete: vi.fn().mockResolvedValue(undefined),
153
- };
154
-
155
- let capturedConfig: any;
156
- (betterAuth as any).mockImplementation((config: any) => {
157
- capturedConfig = config;
158
- return { handler: vi.fn(), api: {} };
159
- });
160
-
161
- const manager = new AuthManager({
162
- secret: 'test-secret-at-least-32-chars-long',
163
- baseUrl: 'http://localhost:3000',
164
- dataEngine: mockDataEngine as any,
165
- });
166
-
167
- // Trigger lazy initialisation
168
- manager.getAuthInstance();
169
-
170
- // The database config should be a function (AdapterFactory)
171
- expect(typeof capturedConfig.database).toBe('function');
172
- });
173
-
174
- it('should include modelName and fields mapping for user, session, account, verification', () => {
175
- const mockDataEngine = {
176
- insert: vi.fn().mockResolvedValue({ id: '1' }),
177
- findOne: vi.fn().mockResolvedValue({ id: '1' }),
178
- find: vi.fn().mockResolvedValue([]),
179
- count: vi.fn().mockResolvedValue(0),
180
- update: vi.fn().mockResolvedValue({ id: '1' }),
181
- delete: vi.fn().mockResolvedValue(undefined),
182
- };
183
-
184
- let capturedConfig: any;
185
- (betterAuth as any).mockImplementation((config: any) => {
186
- capturedConfig = config;
187
- return { handler: vi.fn(), api: {} };
188
- });
189
-
190
- const manager = new AuthManager({
191
- secret: 'test-secret-at-least-32-chars-long',
192
- baseUrl: 'http://localhost:3000',
193
- dataEngine: mockDataEngine as any,
194
- });
195
-
196
- manager.getAuthInstance();
197
-
198
- // Verify user model config
199
- expect(capturedConfig.user).toBeDefined();
200
- expect(capturedConfig.user.modelName).toBe('sys_user');
201
- expect(capturedConfig.user.fields).toEqual(expect.objectContaining({
202
- emailVerified: 'email_verified',
203
- createdAt: 'created_at',
204
- updatedAt: 'updated_at',
205
- }));
206
-
207
- // Verify session model config (merged with session timing config)
208
- expect(capturedConfig.session).toBeDefined();
209
- expect(capturedConfig.session.modelName).toBe('sys_session');
210
- expect(capturedConfig.session.fields).toEqual(expect.objectContaining({
211
- userId: 'user_id',
212
- expiresAt: 'expires_at',
213
- ipAddress: 'ip_address',
214
- userAgent: 'user_agent',
215
- }));
216
-
217
- // Verify account model config
218
- expect(capturedConfig.account).toBeDefined();
219
- expect(capturedConfig.account.modelName).toBe('sys_account');
220
- expect(capturedConfig.account.fields).toEqual(expect.objectContaining({
221
- userId: 'user_id',
222
- providerId: 'provider_id',
223
- accountId: 'account_id',
224
- accessToken: 'access_token',
225
- refreshToken: 'refresh_token',
226
- idToken: 'id_token',
227
- accessTokenExpiresAt: 'access_token_expires_at',
228
- refreshTokenExpiresAt: 'refresh_token_expires_at',
229
- }));
230
-
231
- // Verify verification model config
232
- expect(capturedConfig.verification).toBeDefined();
233
- expect(capturedConfig.verification.modelName).toBe('sys_verification');
234
- expect(capturedConfig.verification.fields).toEqual(expect.objectContaining({
235
- expiresAt: 'expires_at',
236
- createdAt: 'created_at',
237
- updatedAt: 'updated_at',
238
- }));
239
- });
240
-
241
- it('should return undefined (in-memory fallback) when no dataEngine is provided', () => {
242
- let capturedConfig: any;
243
- (betterAuth as any).mockImplementation((config: any) => {
244
- capturedConfig = config;
245
- return { handler: vi.fn(), api: {} };
246
- });
247
-
248
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
249
-
250
- const manager = new AuthManager({
251
- secret: 'test-secret-at-least-32-chars-long',
252
- baseUrl: 'http://localhost:3000',
253
- });
254
-
255
- manager.getAuthInstance();
256
-
257
- expect(capturedConfig.database).toBeUndefined();
258
- warnSpy.mockRestore();
259
- });
260
- });
261
-
262
- describe('basePath configuration', () => {
263
- it('should default basePath to /api/v1/auth when not specified', () => {
264
- let capturedConfig: any;
265
- (betterAuth as any).mockImplementation((config: any) => {
266
- capturedConfig = config;
267
- return { handler: vi.fn(), api: {} };
268
- });
269
-
270
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
271
- const manager = new AuthManager({
272
- secret: 'test-secret-at-least-32-chars-long',
273
- baseUrl: 'http://localhost:3000',
274
- });
275
- manager.getAuthInstance();
276
- warnSpy.mockRestore();
277
-
278
- expect(capturedConfig.basePath).toBe('/api/v1/auth');
279
- });
280
-
281
- it('should use custom basePath when provided', () => {
282
- let capturedConfig: any;
283
- (betterAuth as any).mockImplementation((config: any) => {
284
- capturedConfig = config;
285
- return { handler: vi.fn(), api: {} };
286
- });
287
-
288
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
289
- const manager = new AuthManager({
290
- secret: 'test-secret-at-least-32-chars-long',
291
- baseUrl: 'http://localhost:3000',
292
- basePath: '/custom/auth',
293
- });
294
- manager.getAuthInstance();
295
- warnSpy.mockRestore();
296
-
297
- expect(capturedConfig.basePath).toBe('/custom/auth');
298
- });
299
- });
300
-
301
- describe('plugin registration', () => {
302
- it('should not include any plugins when no plugin config is provided', () => {
303
- let capturedConfig: any;
304
- (betterAuth as any).mockImplementation((config: any) => {
305
- capturedConfig = config;
306
- return { handler: vi.fn(), api: {} };
307
- });
308
-
309
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
310
- const manager = new AuthManager({
311
- secret: 'test-secret-at-least-32-chars-long',
312
- baseUrl: 'http://localhost:3000',
313
- });
314
- manager.getAuthInstance();
315
- warnSpy.mockRestore();
316
-
317
- expect(capturedConfig.plugins).toEqual([]);
318
- });
319
-
320
- it('should register organization plugin with schema mapping when enabled', () => {
321
- let capturedConfig: any;
322
- (betterAuth as any).mockImplementation((config: any) => {
323
- capturedConfig = config;
324
- return { handler: vi.fn(), api: {} };
325
- });
326
-
327
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
328
- const manager = new AuthManager({
329
- secret: 'test-secret-at-least-32-chars-long',
330
- baseUrl: 'http://localhost:3000',
331
- plugins: { organization: true },
332
- });
333
- manager.getAuthInstance();
334
- warnSpy.mockRestore();
335
-
336
- const orgPlugin = capturedConfig.plugins.find((p: any) => p.id === 'organization');
337
- expect(orgPlugin).toBeDefined();
338
- // Verify schema was passed to organization() call
339
- expect(orgPlugin._opts.schema.organization.modelName).toBe('sys_organization');
340
- expect(orgPlugin._opts.schema.member.modelName).toBe('sys_member');
341
- expect(orgPlugin._opts.schema.invitation.modelName).toBe('sys_invitation');
342
- expect(orgPlugin._opts.schema.team.modelName).toBe('sys_team');
343
- expect(orgPlugin._opts.schema.teamMember.modelName).toBe('sys_team_member');
344
- expect(orgPlugin._opts.schema.session.fields.activeOrganizationId).toBe('active_organization_id');
345
- });
346
-
347
- it('should register twoFactor plugin with schema mapping when enabled', () => {
348
- let capturedConfig: any;
349
- (betterAuth as any).mockImplementation((config: any) => {
350
- capturedConfig = config;
351
- return { handler: vi.fn(), api: {} };
352
- });
353
-
354
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
355
- const manager = new AuthManager({
356
- secret: 'test-secret-at-least-32-chars-long',
357
- baseUrl: 'http://localhost:3000',
358
- plugins: { twoFactor: true },
359
- });
360
- manager.getAuthInstance();
361
- warnSpy.mockRestore();
362
-
363
- const tfPlugin = capturedConfig.plugins.find((p: any) => p.id === 'two-factor');
364
- expect(tfPlugin).toBeDefined();
365
- expect(tfPlugin._opts.schema.twoFactor.modelName).toBe('sys_two_factor');
366
- expect(tfPlugin._opts.schema.twoFactor.fields.backupCodes).toBe('backup_codes');
367
- expect(tfPlugin._opts.schema.twoFactor.fields.userId).toBe('user_id');
368
- expect(tfPlugin._opts.schema.user.fields.twoFactorEnabled).toBe('two_factor_enabled');
369
- });
370
-
371
- it('should register magicLink plugin when enabled', () => {
372
- let capturedConfig: any;
373
- (betterAuth as any).mockImplementation((config: any) => {
374
- capturedConfig = config;
375
- return { handler: vi.fn(), api: {} };
376
- });
377
-
378
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
379
- const manager = new AuthManager({
380
- secret: 'test-secret-at-least-32-chars-long',
381
- baseUrl: 'http://localhost:3000',
382
- plugins: { magicLink: true },
383
- });
384
- manager.getAuthInstance();
385
- warnSpy.mockRestore();
386
-
387
- const mlPlugin = capturedConfig.plugins.find((p: any) => p.id === 'magic-link');
388
- expect(mlPlugin).toBeDefined();
389
- });
390
-
391
- it('should register multiple plugins when multiple flags are enabled', () => {
392
- let capturedConfig: any;
393
- (betterAuth as any).mockImplementation((config: any) => {
394
- capturedConfig = config;
395
- return { handler: vi.fn(), api: {} };
396
- });
397
-
398
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
399
- const manager = new AuthManager({
400
- secret: 'test-secret-at-least-32-chars-long',
401
- baseUrl: 'http://localhost:3000',
402
- plugins: { organization: true, twoFactor: true, magicLink: true },
403
- });
404
- manager.getAuthInstance();
405
- warnSpy.mockRestore();
406
-
407
- expect(capturedConfig.plugins).toHaveLength(3);
408
- expect(capturedConfig.plugins.map((p: any) => p.id).sort()).toEqual(
409
- ['magic-link', 'organization', 'two-factor'],
410
- );
411
- });
412
- });
413
-
414
- describe('trustedOrigins passthrough', () => {
415
- it('should forward trustedOrigins to betterAuth when provided', () => {
416
- let capturedConfig: any;
417
- (betterAuth as any).mockImplementation((config: any) => {
418
- capturedConfig = config;
419
- return { handler: vi.fn(), api: {} };
420
- });
421
-
422
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
423
- const manager = new AuthManager({
424
- secret: 'test-secret-at-least-32-chars-long',
425
- baseUrl: 'http://localhost:3000',
426
- trustedOrigins: ['https://*.objectos.app', 'http://localhost:*'],
427
- });
428
- manager.getAuthInstance();
429
- warnSpy.mockRestore();
430
-
431
- expect(capturedConfig.trustedOrigins).toEqual([
432
- 'https://*.objectos.app',
433
- 'http://localhost:*',
434
- ]);
435
- });
436
-
437
- it('should default to localhost wildcard when trustedOrigins not provided', () => {
438
- let capturedConfig: any;
439
- (betterAuth as any).mockImplementation((config: any) => {
440
- capturedConfig = config;
441
- return { handler: vi.fn(), api: {} };
442
- });
443
-
444
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
445
- const manager = new AuthManager({
446
- secret: 'test-secret-at-least-32-chars-long',
447
- baseUrl: 'http://localhost:3000',
448
- });
449
- manager.getAuthInstance();
450
- warnSpy.mockRestore();
451
-
452
- expect(capturedConfig.trustedOrigins).toEqual(['http://localhost:*']);
453
- });
454
-
455
- it('should default to localhost wildcard when trustedOrigins array is empty', () => {
456
- let capturedConfig: any;
457
- (betterAuth as any).mockImplementation((config: any) => {
458
- capturedConfig = config;
459
- return { handler: vi.fn(), api: {} };
460
- });
461
-
462
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
463
- const manager = new AuthManager({
464
- secret: 'test-secret-at-least-32-chars-long',
465
- baseUrl: 'http://localhost:3000',
466
- trustedOrigins: [],
467
- });
468
- manager.getAuthInstance();
469
- warnSpy.mockRestore();
470
-
471
- expect(capturedConfig.trustedOrigins).toEqual(['http://localhost:*']);
472
- });
473
- });
474
-
475
- describe('setRuntimeBaseUrl', () => {
476
- it('should update baseURL before auth instance is created', () => {
477
- let capturedConfig: any;
478
- (betterAuth as any).mockImplementation((config: any) => {
479
- capturedConfig = config;
480
- return { handler: vi.fn(), api: {} };
481
- });
482
-
483
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
484
- const manager = new AuthManager({
485
- secret: 'test-secret-at-least-32-chars-long',
486
- baseUrl: 'http://localhost:3000',
487
- });
488
-
489
- manager.setRuntimeBaseUrl('http://localhost:3002');
490
- manager.getAuthInstance();
491
- warnSpy.mockRestore();
492
-
493
- expect(capturedConfig.baseURL).toBe('http://localhost:3002');
494
- });
495
-
496
- it('should be a no-op and warn when called after auth instance is created', () => {
497
- let capturedConfig: any;
498
- (betterAuth as any).mockImplementation((config: any) => {
499
- capturedConfig = config;
500
- return { handler: vi.fn(), api: {} };
501
- });
502
-
503
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
504
- const manager = new AuthManager({
505
- secret: 'test-secret-at-least-32-chars-long',
506
- baseUrl: 'http://localhost:3000',
507
- });
508
-
509
- // Force auth instance creation
510
- manager.getAuthInstance();
511
- expect(capturedConfig.baseURL).toBe('http://localhost:3000');
512
-
513
- // Now try to change — should warn and not affect the already-created instance
514
- manager.setRuntimeBaseUrl('http://localhost:4000');
515
-
516
- expect(warnSpy).toHaveBeenCalledWith(
517
- expect.stringContaining('setRuntimeBaseUrl() called after the auth instance was already created'),
518
- );
519
- warnSpy.mockRestore();
520
- });
521
-
522
- it('should override the default fallback (localhost:3000) when no baseUrl was configured', () => {
523
- let capturedConfig: any;
524
- (betterAuth as any).mockImplementation((config: any) => {
525
- capturedConfig = config;
526
- return { handler: vi.fn(), api: {} };
527
- });
528
-
529
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
530
- const manager = new AuthManager({
531
- secret: 'test-secret-at-least-32-chars-long',
532
- });
533
-
534
- manager.setRuntimeBaseUrl('http://localhost:3002');
535
- manager.getAuthInstance();
536
- warnSpy.mockRestore();
537
-
538
- expect(capturedConfig.baseURL).toBe('http://localhost:3002');
539
- });
540
- });
541
-
542
- describe('socialProviders passthrough', () => {
543
- it('should forward socialProviders to betterAuth when provided', () => {
544
- let capturedConfig: any;
545
- (betterAuth as any).mockImplementation((config: any) => {
546
- capturedConfig = config;
547
- return { handler: vi.fn(), api: {} };
548
- });
549
-
550
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
551
- const manager = new AuthManager({
552
- secret: 'test-secret-at-least-32-chars-long',
553
- baseUrl: 'http://localhost:3000',
554
- socialProviders: {
555
- google: { clientId: 'gid', clientSecret: 'gsecret' },
556
- github: { clientId: 'ghid', clientSecret: 'ghsecret' },
557
- },
558
- });
559
- manager.getAuthInstance();
560
- warnSpy.mockRestore();
561
-
562
- expect(capturedConfig.socialProviders).toEqual({
563
- google: { clientId: 'gid', clientSecret: 'gsecret' },
564
- github: { clientId: 'ghid', clientSecret: 'ghsecret' },
565
- });
566
- });
567
-
568
- it('should NOT include socialProviders when not provided', () => {
569
- let capturedConfig: any;
570
- (betterAuth as any).mockImplementation((config: any) => {
571
- capturedConfig = config;
572
- return { handler: vi.fn(), api: {} };
573
- });
574
-
575
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
576
- const manager = new AuthManager({
577
- secret: 'test-secret-at-least-32-chars-long',
578
- baseUrl: 'http://localhost:3000',
579
- });
580
- manager.getAuthInstance();
581
- warnSpy.mockRestore();
582
-
583
- expect(capturedConfig).not.toHaveProperty('socialProviders');
584
- });
585
- });
586
-
587
- describe('emailAndPassword passthrough', () => {
588
- it('should default emailAndPassword to enabled: true', () => {
589
- let capturedConfig: any;
590
- (betterAuth as any).mockImplementation((config: any) => {
591
- capturedConfig = config;
592
- return { handler: vi.fn(), api: {} };
593
- });
594
-
595
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
596
- const manager = new AuthManager({
597
- secret: 'test-secret-at-least-32-chars-long',
598
- baseUrl: 'http://localhost:3000',
599
- });
600
- manager.getAuthInstance();
601
- warnSpy.mockRestore();
602
-
603
- expect(capturedConfig.emailAndPassword.enabled).toBe(true);
604
- });
605
-
606
- it('should forward extended emailAndPassword options', () => {
607
- let capturedConfig: any;
608
- (betterAuth as any).mockImplementation((config: any) => {
609
- capturedConfig = config;
610
- return { handler: vi.fn(), api: {} };
611
- });
612
-
613
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
614
- const manager = new AuthManager({
615
- secret: 'test-secret-at-least-32-chars-long',
616
- baseUrl: 'http://localhost:3000',
617
- emailAndPassword: {
618
- enabled: true,
619
- minPasswordLength: 12,
620
- maxPasswordLength: 64,
621
- requireEmailVerification: true,
622
- autoSignIn: false,
623
- revokeSessionsOnPasswordReset: true,
624
- },
625
- });
626
- manager.getAuthInstance();
627
- warnSpy.mockRestore();
628
-
629
- expect(capturedConfig.emailAndPassword).toEqual({
630
- enabled: true,
631
- minPasswordLength: 12,
632
- maxPasswordLength: 64,
633
- requireEmailVerification: true,
634
- autoSignIn: false,
635
- revokeSessionsOnPasswordReset: true,
636
- });
637
- });
638
- });
639
-
640
- describe('emailVerification passthrough', () => {
641
- it('should forward emailVerification when provided', () => {
642
- let capturedConfig: any;
643
- (betterAuth as any).mockImplementation((config: any) => {
644
- capturedConfig = config;
645
- return { handler: vi.fn(), api: {} };
646
- });
647
-
648
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
649
- const manager = new AuthManager({
650
- secret: 'test-secret-at-least-32-chars-long',
651
- baseUrl: 'http://localhost:3000',
652
- emailVerification: {
653
- sendOnSignUp: true,
654
- expiresIn: 1800,
655
- },
656
- });
657
- manager.getAuthInstance();
658
- warnSpy.mockRestore();
659
-
660
- expect(capturedConfig.emailVerification).toEqual({
661
- sendOnSignUp: true,
662
- expiresIn: 1800,
663
- });
664
- });
665
-
666
- it('should NOT include emailVerification when not provided', () => {
667
- let capturedConfig: any;
668
- (betterAuth as any).mockImplementation((config: any) => {
669
- capturedConfig = config;
670
- return { handler: vi.fn(), api: {} };
671
- });
672
-
673
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
674
- const manager = new AuthManager({
675
- secret: 'test-secret-at-least-32-chars-long',
676
- baseUrl: 'http://localhost:3000',
677
- });
678
- manager.getAuthInstance();
679
- warnSpy.mockRestore();
680
-
681
- expect(capturedConfig).not.toHaveProperty('emailVerification');
682
- });
683
- });
684
-
685
- describe('advanced options passthrough', () => {
686
- it('should forward crossSubDomainCookies when provided', () => {
687
- let capturedConfig: any;
688
- (betterAuth as any).mockImplementation((config: any) => {
689
- capturedConfig = config;
690
- return { handler: vi.fn(), api: {} };
691
- });
692
-
693
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
694
- const manager = new AuthManager({
695
- secret: 'test-secret-at-least-32-chars-long',
696
- baseUrl: 'http://localhost:3000',
697
- advanced: {
698
- crossSubDomainCookies: {
699
- enabled: true,
700
- domain: '.objectos.app',
701
- },
702
- useSecureCookies: true,
703
- },
704
- });
705
- manager.getAuthInstance();
706
- warnSpy.mockRestore();
707
-
708
- expect(capturedConfig.advanced).toEqual({
709
- crossSubDomainCookies: {
710
- enabled: true,
711
- domain: '.objectos.app',
712
- },
713
- useSecureCookies: true,
714
- });
715
- });
716
-
717
- it('should forward cookiePrefix and disableCSRFCheck', () => {
718
- let capturedConfig: any;
719
- (betterAuth as any).mockImplementation((config: any) => {
720
- capturedConfig = config;
721
- return { handler: vi.fn(), api: {} };
722
- });
723
-
724
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
725
- const manager = new AuthManager({
726
- secret: 'test-secret-at-least-32-chars-long',
727
- baseUrl: 'http://localhost:3000',
728
- advanced: {
729
- disableCSRFCheck: true,
730
- cookiePrefix: 'objectos',
731
- },
732
- });
733
- manager.getAuthInstance();
734
- warnSpy.mockRestore();
735
-
736
- expect(capturedConfig.advanced.disableCSRFCheck).toBe(true);
737
- expect(capturedConfig.advanced.cookiePrefix).toBe('objectos');
738
- });
739
-
740
- it('should NOT include advanced when not provided', () => {
741
- let capturedConfig: any;
742
- (betterAuth as any).mockImplementation((config: any) => {
743
- capturedConfig = config;
744
- return { handler: vi.fn(), api: {} };
745
- });
746
-
747
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
748
- const manager = new AuthManager({
749
- secret: 'test-secret-at-least-32-chars-long',
750
- baseUrl: 'http://localhost:3000',
751
- });
752
- manager.getAuthInstance();
753
- warnSpy.mockRestore();
754
-
755
- expect(capturedConfig).not.toHaveProperty('advanced');
756
- });
757
- });
758
-
759
- describe('getPublicConfig', () => {
760
- it('should return safe public configuration', () => {
761
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
762
- const manager = new AuthManager({
763
- secret: 'test-secret-at-least-32-chars-long',
764
- baseUrl: 'http://localhost:3000',
765
- socialProviders: {
766
- google: {
767
- clientId: 'google-client-id',
768
- clientSecret: 'google-client-secret',
769
- enabled: true,
770
- },
771
- github: {
772
- clientId: 'github-client-id',
773
- clientSecret: 'github-client-secret',
774
- },
775
- },
776
- emailAndPassword: {
777
- enabled: true,
778
- disableSignUp: false,
779
- requireEmailVerification: true,
780
- },
781
- plugins: {
782
- twoFactor: true,
783
- organization: true,
784
- },
785
- });
786
- warnSpy.mockRestore();
787
-
788
- const config = manager.getPublicConfig();
789
-
790
- // Should include social providers without secrets
791
- expect(config.socialProviders).toHaveLength(2);
792
- expect(config.socialProviders[0]).toEqual({
793
- id: 'google',
794
- name: 'Google',
795
- enabled: true,
796
- });
797
- expect(config.socialProviders[1]).toEqual({
798
- id: 'github',
799
- name: 'GitHub',
800
- enabled: true,
801
- });
802
-
803
- // Should NOT include sensitive data
804
- expect(config).not.toHaveProperty('secret');
805
- expect(config.socialProviders[0]).not.toHaveProperty('clientSecret');
806
- expect(config.socialProviders[0]).not.toHaveProperty('clientId');
807
-
808
- // Should include email/password config
809
- expect(config.emailPassword).toEqual({
810
- enabled: true,
811
- disableSignUp: false,
812
- requireEmailVerification: true,
813
- });
814
-
815
- // Should include features
816
- expect(config.features).toEqual({
817
- twoFactor: true,
818
- passkeys: false,
819
- magicLink: false,
820
- organization: true,
821
- });
822
- });
823
-
824
- it('should filter out disabled providers', () => {
825
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
826
- const manager = new AuthManager({
827
- secret: 'test-secret-at-least-32-chars-long',
828
- socialProviders: {
829
- google: {
830
- clientId: 'google-client-id',
831
- clientSecret: 'google-client-secret',
832
- enabled: true,
833
- },
834
- github: {
835
- clientId: 'github-client-id',
836
- clientSecret: 'github-client-secret',
837
- enabled: false,
838
- },
839
- },
840
- });
841
- warnSpy.mockRestore();
842
-
843
- const config = manager.getPublicConfig();
844
-
845
- expect(config.socialProviders).toHaveLength(1);
846
- expect(config.socialProviders[0].id).toBe('google');
847
- });
848
-
849
- it('should default email/password to enabled', () => {
850
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
851
- const manager = new AuthManager({
852
- secret: 'test-secret-at-least-32-chars-long',
853
- });
854
- warnSpy.mockRestore();
855
-
856
- const config = manager.getPublicConfig();
857
-
858
- expect(config.emailPassword.enabled).toBe(true);
859
- });
860
-
861
- it('should handle unknown provider names', () => {
862
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
863
- const manager = new AuthManager({
864
- secret: 'test-secret-at-least-32-chars-long',
865
- socialProviders: {
866
- customProvider: {
867
- clientId: 'custom-client-id',
868
- clientSecret: 'custom-client-secret',
869
- },
870
- },
871
- });
872
- warnSpy.mockRestore();
873
-
874
- const config = manager.getPublicConfig();
875
-
876
- expect(config.socialProviders[0]).toEqual({
877
- id: 'customProvider',
878
- name: 'CustomProvider',
879
- enabled: true,
880
- });
881
- });
882
- });
883
- });