@objectstack/plugin-auth 4.0.3 → 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 +345 -19928
  3. package/dist/index.d.ts +345 -19928
  4. package/dist/index.js +411 -857
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +415 -837
  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 -325
  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 -758
  16. package/src/auth-manager.ts +0 -338
  17. package/src/auth-plugin.test.ts +0 -443
  18. package/src/auth-plugin.ts +0 -292
  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,443 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { describe, it, expect, vi, beforeEach } from 'vitest';
4
- import { AuthPlugin } from './auth-plugin';
5
- import type { PluginContext } from '@objectstack/core';
6
-
7
- describe('AuthPlugin', () => {
8
- let mockContext: PluginContext;
9
- let authPlugin: AuthPlugin;
10
-
11
- /** Shared hook capture utilities for tests that need kernel:ready simulation */
12
- const createHookCapture = () => {
13
- const handlers = new Map<string, Array<(...args: any[]) => Promise<void>>>();
14
- const hookFn = vi.fn((name: string, handler: (...args: any[]) => Promise<void>) => {
15
- if (!handlers.has(name)) handlers.set(name, []);
16
- handlers.get(name)!.push(handler);
17
- });
18
- const trigger = async (name: string) => {
19
- for (const h of handlers.get(name) || []) await h();
20
- };
21
- return { handlers, hookFn, trigger };
22
- };
23
-
24
- beforeEach(() => {
25
- mockContext = {
26
- registerService: vi.fn(),
27
- getService: vi.fn((name: string) => {
28
- if (name === 'manifest') return { register: vi.fn() };
29
- if (name === 'data') return undefined;
30
- return undefined;
31
- }),
32
- getServices: vi.fn(() => new Map()),
33
- hook: vi.fn(),
34
- trigger: vi.fn(),
35
- logger: {
36
- info: vi.fn(),
37
- error: vi.fn(),
38
- warn: vi.fn(),
39
- debug: vi.fn(),
40
- },
41
- getKernel: vi.fn(),
42
- };
43
- });
44
-
45
- describe('Plugin Metadata', () => {
46
- it('should have correct plugin metadata', () => {
47
- authPlugin = new AuthPlugin({
48
- secret: 'test-secret',
49
- });
50
-
51
- expect(authPlugin.name).toBe('com.objectstack.auth');
52
- expect(authPlugin.type).toBe('standard');
53
- expect(authPlugin.version).toBe('1.0.0');
54
- expect(authPlugin.dependencies).toEqual(['com.objectstack.engine.objectql']);
55
- });
56
- });
57
-
58
- describe('Initialization', () => {
59
- it('should throw error if secret is not provided', async () => {
60
- authPlugin = new AuthPlugin({});
61
-
62
- await expect(authPlugin.init(mockContext)).rejects.toThrow(
63
- 'AuthPlugin: secret is required'
64
- );
65
- });
66
-
67
- it('should initialize successfully with required config', async () => {
68
- authPlugin = new AuthPlugin({
69
- secret: 'test-secret-at-least-32-chars-long',
70
- baseUrl: 'http://localhost:3000',
71
- });
72
-
73
- await authPlugin.init(mockContext);
74
-
75
- expect(mockContext.logger.info).toHaveBeenCalledWith('Initializing Auth Plugin...');
76
- expect(mockContext.registerService).toHaveBeenCalledWith('auth', expect.anything());
77
- expect(mockContext.logger.info).toHaveBeenCalledWith('Auth Plugin initialized successfully');
78
- });
79
-
80
- it('should configure OAuth providers', async () => {
81
- authPlugin = new AuthPlugin({
82
- secret: 'test-secret-at-least-32-chars-long',
83
- baseUrl: 'http://localhost:3000',
84
- providers: [
85
- {
86
- id: 'google',
87
- clientId: 'google-client-id',
88
- clientSecret: 'google-client-secret',
89
- scope: ['email', 'profile'],
90
- },
91
- ],
92
- });
93
-
94
- await authPlugin.init(mockContext);
95
-
96
- expect(mockContext.registerService).toHaveBeenCalled();
97
- });
98
-
99
- it('should configure plugins', async () => {
100
- authPlugin = new AuthPlugin({
101
- secret: 'test-secret-at-least-32-chars-long',
102
- baseUrl: 'http://localhost:3000',
103
- plugins: {
104
- organization: true,
105
- twoFactor: true,
106
- passkeys: true,
107
- magicLink: true,
108
- },
109
- });
110
-
111
- await authPlugin.init(mockContext);
112
-
113
- expect(mockContext.registerService).toHaveBeenCalled();
114
- });
115
- });
116
-
117
- describe('Start Phase', () => {
118
- let hookCapture: ReturnType<typeof createHookCapture>;
119
-
120
- beforeEach(async () => {
121
- hookCapture = createHookCapture();
122
- authPlugin = new AuthPlugin({
123
- secret: 'test-secret-at-least-32-chars-long',
124
- baseUrl: 'http://localhost:3000',
125
- });
126
- // Capture hook registrations so we can trigger them in tests
127
- mockContext.hook = hookCapture.hookFn;
128
- await authPlugin.init(mockContext);
129
- });
130
-
131
- it('should register a kernel:ready hook for route registration', async () => {
132
- await authPlugin.start(mockContext);
133
-
134
- expect(mockContext.hook).toHaveBeenCalledWith('kernel:ready', expect.any(Function));
135
- });
136
-
137
- it('should register routes with HTTP server on kernel:ready', async () => {
138
- const mockRawApp = {
139
- all: vi.fn(),
140
- };
141
-
142
- const mockHttpServer = {
143
- post: vi.fn(),
144
- get: vi.fn(),
145
- put: vi.fn(),
146
- delete: vi.fn(),
147
- patch: vi.fn(),
148
- use: vi.fn(),
149
- getRawApp: vi.fn(() => mockRawApp),
150
- };
151
-
152
- mockContext.getService = vi.fn((name: string) => {
153
- if (name === 'http-server') return mockHttpServer;
154
- throw new Error(`Service not found: ${name}`);
155
- });
156
-
157
- await authPlugin.start(mockContext);
158
-
159
- // Routes should NOT be registered yet (deferred to kernel:ready)
160
- expect(mockRawApp.all).not.toHaveBeenCalled();
161
-
162
- // Simulate kernel:ready
163
- await hookCapture.trigger('kernel:ready');
164
-
165
- expect(mockContext.getService).toHaveBeenCalledWith('http-server');
166
- expect(mockHttpServer.getRawApp).toHaveBeenCalled();
167
- expect(mockRawApp.all).toHaveBeenCalledWith('/api/v1/auth/*', expect.any(Function));
168
- expect(mockContext.logger.info).toHaveBeenCalledWith(
169
- expect.stringContaining('Auth routes registered')
170
- );
171
- });
172
-
173
- it('should log via ctx.logger when better-auth returns a 500 response', async () => {
174
- const mockRawApp = {
175
- all: vi.fn(),
176
- };
177
-
178
- const mockHttpServer = {
179
- post: vi.fn(),
180
- get: vi.fn(),
181
- put: vi.fn(),
182
- delete: vi.fn(),
183
- patch: vi.fn(),
184
- use: vi.fn(),
185
- getRawApp: vi.fn(() => mockRawApp),
186
- };
187
-
188
- mockContext.getService = vi.fn((name: string) => {
189
- if (name === 'http-server') return mockHttpServer;
190
- throw new Error(`Service not found: ${name}`);
191
- });
192
-
193
- await authPlugin.start(mockContext);
194
- await hookCapture.trigger('kernel:ready');
195
-
196
- // Extract the registered route handler
197
- const routeHandler = mockRawApp.all.mock.calls[0][1];
198
-
199
- // Create a mock Hono context with a request that will trigger a 500 response
200
- const errorResponse = new Response(
201
- JSON.stringify({ error: 'Database connection failed' }),
202
- { status: 500, headers: { 'Content-Type': 'application/json' } }
203
- );
204
-
205
- // Mock the authManager's handleRequest to return a 500 response
206
- // We access the private authManager through the registered service
207
- const registeredAuthManager = (mockContext.registerService as any).mock.calls[0][1];
208
- vi.spyOn(registeredAuthManager, 'handleRequest').mockResolvedValue(errorResponse);
209
-
210
- const mockHonoCtx = {
211
- req: {
212
- raw: new Request('http://localhost:3000/api/v1/auth/sign-up/email', {
213
- method: 'POST',
214
- body: JSON.stringify({ email: 'a@b.com', password: 'pass' }),
215
- headers: { 'Content-Type': 'application/json' },
216
- }),
217
- },
218
- };
219
-
220
- const result = await routeHandler(mockHonoCtx);
221
-
222
- expect(result.status).toBe(500);
223
- expect(mockContext.logger.error).toHaveBeenCalledWith(
224
- '[AuthPlugin] better-auth returned server error',
225
- expect.any(Error)
226
- );
227
- });
228
-
229
- it('should skip route registration when disabled', async () => {
230
- authPlugin = new AuthPlugin({
231
- secret: 'test-secret-at-least-32-chars-long',
232
- baseUrl: 'http://localhost:3000',
233
- registerRoutes: false,
234
- });
235
-
236
- await authPlugin.init(mockContext);
237
- await authPlugin.start(mockContext);
238
-
239
- // Should not register kernel:ready hook for routes
240
- expect(mockContext.hook).not.toHaveBeenCalledWith('kernel:ready', expect.any(Function));
241
- });
242
-
243
- it('should gracefully skip routes when http-server is not available', async () => {
244
- mockContext.getService = vi.fn(() => null);
245
-
246
- await authPlugin.start(mockContext);
247
- await hookCapture.trigger('kernel:ready');
248
-
249
- expect(mockContext.getService).toHaveBeenCalledWith('http-server');
250
- expect(mockContext.logger.warn).toHaveBeenCalledWith(
251
- expect.stringContaining('No HTTP server available')
252
- );
253
- // Should NOT throw — auth service is still registered from init()
254
- });
255
-
256
- it('should gracefully handle http-server getService throwing', async () => {
257
- mockContext.getService = vi.fn(() => {
258
- throw new Error('Service not found: http-server');
259
- });
260
-
261
- await authPlugin.start(mockContext);
262
- await hookCapture.trigger('kernel:ready');
263
-
264
- expect(mockContext.logger.warn).toHaveBeenCalledWith(
265
- expect.stringContaining('No HTTP server available')
266
- );
267
- // Auth service should still be registered from init()
268
- expect(mockContext.registerService).toHaveBeenCalledWith('auth', expect.anything());
269
- // Should NOT throw
270
- });
271
-
272
- it('should auto-detect baseUrl from http-server port when port differs', async () => {
273
- const mockRawApp = { all: vi.fn() };
274
- const mockHttpServer = {
275
- post: vi.fn(), get: vi.fn(), put: vi.fn(), delete: vi.fn(),
276
- patch: vi.fn(), use: vi.fn(),
277
- getRawApp: vi.fn(() => mockRawApp),
278
- getPort: vi.fn(() => 3002),
279
- };
280
-
281
- mockContext.getService = vi.fn((name: string) => {
282
- if (name === 'http-server') return mockHttpServer;
283
- throw new Error(`Service not found: ${name}`);
284
- });
285
-
286
- // AuthPlugin configured with default port 3000, but server will be on 3002
287
- const registeredAuthManager = (mockContext.registerService as any).mock.calls[0][1];
288
- const setRuntimeSpy = vi.spyOn(registeredAuthManager, 'setRuntimeBaseUrl');
289
-
290
- await authPlugin.start(mockContext);
291
- await hookCapture.trigger('kernel:ready');
292
-
293
- expect(setRuntimeSpy).toHaveBeenCalledWith('http://localhost:3002');
294
- expect(mockContext.logger.info).toHaveBeenCalledWith(
295
- expect.stringContaining('Auth baseUrl auto-updated to http://localhost:3002'),
296
- );
297
- });
298
-
299
- it('should NOT update baseUrl when port matches configured value', async () => {
300
- const localHookCapture = createHookCapture();
301
- const localPlugin = new AuthPlugin({
302
- secret: 'test-secret-at-least-32-chars-long',
303
- baseUrl: 'http://localhost:3000',
304
- });
305
- mockContext.hook = localHookCapture.hookFn;
306
- (mockContext.registerService as any).mockClear();
307
- await localPlugin.init(mockContext);
308
-
309
- const mockRawApp = { all: vi.fn() };
310
- const mockHttpServer = {
311
- post: vi.fn(), get: vi.fn(), put: vi.fn(), delete: vi.fn(),
312
- patch: vi.fn(), use: vi.fn(),
313
- getRawApp: vi.fn(() => mockRawApp),
314
- getPort: vi.fn(() => 3000),
315
- };
316
-
317
- mockContext.getService = vi.fn((name: string) => {
318
- if (name === 'http-server') return mockHttpServer;
319
- throw new Error(`Service not found: ${name}`);
320
- });
321
-
322
- const registeredAuthManager = (mockContext.registerService as any).mock.calls[0][1];
323
- const setRuntimeSpy = vi.spyOn(registeredAuthManager, 'setRuntimeBaseUrl');
324
-
325
- await localPlugin.start(mockContext);
326
- await localHookCapture.trigger('kernel:ready');
327
-
328
- expect(setRuntimeSpy).not.toHaveBeenCalled();
329
- });
330
-
331
- it('should auto-detect baseUrl when no baseUrl configured (uses default fallback)', async () => {
332
- const localHookCapture = createHookCapture();
333
- // No baseUrl — defaults to http://localhost:3000 internally
334
- const localPlugin = new AuthPlugin({
335
- secret: 'test-secret-at-least-32-chars-long',
336
- });
337
- mockContext.hook = localHookCapture.hookFn;
338
- (mockContext.registerService as any).mockClear();
339
- await localPlugin.init(mockContext);
340
-
341
- const mockRawApp = { all: vi.fn() };
342
- const mockHttpServer = {
343
- post: vi.fn(), get: vi.fn(), put: vi.fn(), delete: vi.fn(),
344
- patch: vi.fn(), use: vi.fn(),
345
- getRawApp: vi.fn(() => mockRawApp),
346
- getPort: vi.fn(() => 3002),
347
- };
348
-
349
- mockContext.getService = vi.fn((name: string) => {
350
- if (name === 'http-server') return mockHttpServer;
351
- throw new Error(`Service not found: ${name}`);
352
- });
353
-
354
- const registeredAuthManager = (mockContext.registerService as any).mock.calls[0][1];
355
- const setRuntimeSpy = vi.spyOn(registeredAuthManager, 'setRuntimeBaseUrl');
356
-
357
- await localPlugin.start(mockContext);
358
- await localHookCapture.trigger('kernel:ready');
359
-
360
- expect(setRuntimeSpy).toHaveBeenCalledWith('http://localhost:3002');
361
- });
362
-
363
- it('should throw error if auth not initialized', async () => {
364
- const uninitializedPlugin = new AuthPlugin({
365
- secret: 'test-secret',
366
- });
367
-
368
- await expect(uninitializedPlugin.start(mockContext)).rejects.toThrow(
369
- 'Auth manager not initialized'
370
- );
371
- });
372
- });
373
-
374
- describe('Destroy Phase', () => {
375
- it('should cleanup resources', async () => {
376
- authPlugin = new AuthPlugin({
377
- secret: 'test-secret-at-least-32-chars-long',
378
- });
379
-
380
- await authPlugin.init(mockContext);
381
- await authPlugin.destroy();
382
-
383
- // Should not throw
384
- expect(true).toBe(true);
385
- });
386
- });
387
-
388
- describe('Configuration Options', () => {
389
- it('should use custom base path', async () => {
390
- const { hookFn, trigger } = createHookCapture();
391
- mockContext.hook = hookFn;
392
-
393
- authPlugin = new AuthPlugin({
394
- secret: 'test-secret-at-least-32-chars-long',
395
- baseUrl: 'http://localhost:3000',
396
- basePath: '/custom/auth',
397
- });
398
-
399
- await authPlugin.init(mockContext);
400
-
401
- const mockRawApp = {
402
- all: vi.fn(),
403
- };
404
-
405
- const mockHttpServer = {
406
- post: vi.fn(),
407
- get: vi.fn(),
408
- put: vi.fn(),
409
- delete: vi.fn(),
410
- patch: vi.fn(),
411
- use: vi.fn(),
412
- getRawApp: vi.fn(() => mockRawApp),
413
- };
414
-
415
- mockContext.getService = vi.fn(() => mockHttpServer);
416
-
417
- await authPlugin.start(mockContext);
418
-
419
- // Trigger kernel:ready to actually register routes
420
- await trigger('kernel:ready');
421
-
422
- expect(mockRawApp.all).toHaveBeenCalledWith(
423
- '/custom/auth/*',
424
- expect.any(Function)
425
- );
426
- });
427
-
428
- it('should configure session options', async () => {
429
- authPlugin = new AuthPlugin({
430
- secret: 'test-secret-at-least-32-chars-long',
431
- baseUrl: 'http://localhost:3000',
432
- session: {
433
- expiresIn: 60 * 60 * 24 * 30, // 30 days
434
- updateAge: 60 * 60 * 24, // 1 day
435
- },
436
- });
437
-
438
- await authPlugin.init(mockContext);
439
-
440
- expect(mockContext.registerService).toHaveBeenCalled();
441
- });
442
- });
443
- });