diff --git a/packages/adapters/hono/src/hono.test.ts b/packages/adapters/hono/src/hono.test.ts index 48ac35124..1a0f52b4d 100644 --- a/packages/adapters/hono/src/hono.test.ts +++ b/packages/adapters/hono/src/hono.test.ts @@ -94,6 +94,77 @@ describe('createHonoApp', () => { }); }); + describe('Auth via AuthPlugin service', () => { + it('uses kernel.getService("auth") when available', async () => { + const mockHandleRequest = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ user: { id: '1' } }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + const kernelWithAuth = { + ...mockKernel, + getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }), + }; + const app = createHonoApp({ kernel: kernelWithAuth }); + const res = await app.request('/api/auth/sign-in/email', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: 'a@b.com', password: 'pass' }), + }); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.user.id).toBe('1'); + expect(kernelWithAuth.getService).toHaveBeenCalledWith('auth'); + expect(mockHandleRequest).toHaveBeenCalledWith(expect.any(Request)); + expect(mockDispatcher.handleAuth).not.toHaveBeenCalled(); + }); + + it('falls back to dispatcher.handleAuth when auth service is not available', async () => { + const kernelWithoutAuth = { + ...mockKernel, + getService: vi.fn().mockReturnValue(null), + }; + const app = createHonoApp({ kernel: kernelWithoutAuth }); + const res = await app.request('/api/auth/login', { method: 'POST' }); + expect(res.status).toBe(200); + expect(mockDispatcher.handleAuth).toHaveBeenCalled(); + }); + + it('forwards GET requests to auth service', async () => { + const mockHandleRequest = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ session: { token: 'abc' } }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + const kernelWithAuth = { + ...mockKernel, + getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }), + }; + const app = createHonoApp({ kernel: kernelWithAuth }); + const res = await app.request('/api/auth/get-session', { method: 'GET' }); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.session.token).toBe('abc'); + expect(mockHandleRequest).toHaveBeenCalled(); + }); + + it('returns error when auth service throws', async () => { + const mockHandleRequest = vi.fn().mockRejectedValue(new Error('Auth failed')); + const kernelWithAuth = { + ...mockKernel, + getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }), + }; + const app = createHonoApp({ kernel: kernelWithAuth }); + const res = await app.request('/api/auth/sign-in/email', { method: 'POST' }); + expect(res.status).toBe(500); + const json = await res.json(); + expect(json.success).toBe(false); + expect(json.error.message).toBe('Auth failed'); + }); + }); + describe('GraphQL Endpoint', () => { it('POST /api/graphql calls handleGraphQL', async () => { const app = createHonoApp({ kernel: mockKernel }); diff --git a/packages/adapters/hono/src/index.ts b/packages/adapters/hono/src/index.ts index 813d87b9f..58178e676 100644 --- a/packages/adapters/hono/src/index.ts +++ b/packages/adapters/hono/src/index.ts @@ -66,7 +66,17 @@ export function createHonoApp(options: ObjectStackHonoOptions) { // --- 1. Auth --- app.all(`${prefix}/auth/*`, async (c) => { try { - // subpath from /api/auth/login -> login + // Try AuthPlugin service first (preferred path) + const authService = typeof options.kernel.getService === 'function' + ? options.kernel.getService('auth') + : null; + + if (authService && typeof authService.handleRequest === 'function') { + const response = await authService.handleRequest(c.req.raw); + return response; + } + + // Fallback to legacy dispatcher const path = c.req.path.substring(c.req.path.indexOf('/auth/') + 6); const body = await c.req.parseBody().catch(() => ({})); diff --git a/packages/adapters/nestjs/src/index.ts b/packages/adapters/nestjs/src/index.ts index 2ca295f96..de741e6a9 100644 --- a/packages/adapters/nestjs/src/index.ts +++ b/packages/adapters/nestjs/src/index.ts @@ -106,6 +106,42 @@ export class ObjectStackController { @All('auth/*') async auth(@Req() req: any, @Res() res: any, @Body() body: any) { try { + // Try AuthPlugin service first (preferred path) + const kernel = this.service.getKernel(); + const authService = typeof kernel.getService === 'function' + ? kernel.getService('auth') + : null; + + if (authService && typeof authService.handleRequest === 'function') { + // Construct a Web standard Request from the Express/Fastify request + const protocol = req.protocol || 'http'; + const host = req.get?.('host') || req.headers?.host || 'localhost'; + const url = `${protocol}://${host}${req.originalUrl || req.url}`; + const headers = new Headers(); + if (req.headers) { + Object.entries(req.headers).forEach(([k, v]) => { + if (typeof v === 'string') headers.set(k, v); + else if (Array.isArray(v)) headers.set(k, v.join(', ')); + }); + } + const init: RequestInit = { method: req.method, headers }; + if (req.method !== 'GET' && req.method !== 'HEAD' && body) { + init.body = JSON.stringify(body); + if (!headers.has('content-type')) { + headers.set('content-type', 'application/json'); + } + } + const webRequest = new Request(url, init); + const response = await authService.handleRequest(webRequest); + + // Convert Web Response to Express/Fastify response + res.status(response.status); + response.headers.forEach((v: string, k: string) => res.setHeader(k, v)); + const text = await response.text(); + return res.send(text); + } + + // Fallback to legacy dispatcher const path = req.params[0] || req.url.split('/auth/')[1]?.split('?')[0] || ''; const result = await this.service.dispatcher.handleAuth(path, req.method, body, { request: req, response: res }); return this.normalizeResponse(result, res); diff --git a/packages/adapters/nestjs/src/nestjs.test.ts b/packages/adapters/nestjs/src/nestjs.test.ts index 63ca6c9ec..a83a3dec9 100644 --- a/packages/adapters/nestjs/src/nestjs.test.ts +++ b/packages/adapters/nestjs/src/nestjs.test.ts @@ -190,6 +190,109 @@ describe('ObjectStackController', () => { }); }); + describe('auth() via AuthPlugin service', () => { + it('uses kernel.getService("auth") when available', async () => { + const mockHandleRequest = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ user: { id: '1' } }), { + status: 200, + headers: new Headers({ 'Content-Type': 'application/json' }), + }), + ); + const kernelWithAuth = { + ...createMockKernel(), + getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }), + }; + const svc = new ObjectStackService(kernelWithAuth); + const ctrl = new ObjectStackController(svc); + const r = createMockRes(); + const req = { + params: { 0: 'sign-in/email' }, + url: '/api/auth/sign-in/email', + method: 'POST', + protocol: 'http', + get: (key: string) => key === 'host' ? 'localhost' : undefined, + headers: { 'content-type': 'application/json' }, + originalUrl: '/api/auth/sign-in/email', + }; + + await ctrl.auth(req, r, { email: 'a@b.com', password: 'pass' }); + + expect(kernelWithAuth.getService).toHaveBeenCalledWith('auth'); + expect(mockHandleRequest).toHaveBeenCalledWith(expect.any(Request)); + expect(r._status).toBe(200); + }); + + it('falls back to dispatcher when auth service is not available', async () => { + const kernelWithoutAuth = { + ...createMockKernel(), + getService: vi.fn().mockReturnValue(null), + }; + const svc = new ObjectStackService(kernelWithoutAuth); + const ctrl = new ObjectStackController(svc); + const r = createMockRes(); + const req = { params: { 0: 'login' }, url: '/api/auth/login', method: 'POST' }; + + await ctrl.auth(req, r, { email: 'a@b.com' }); + + expect(svc.dispatcher.handleAuth).toHaveBeenCalled(); + }); + + it('forwards GET requests to auth service', async () => { + const mockHandleRequest = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ session: { token: 'abc' } }), { + status: 200, + headers: new Headers({ 'Content-Type': 'application/json' }), + }), + ); + const kernelWithAuth = { + ...createMockKernel(), + getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }), + }; + const svc = new ObjectStackService(kernelWithAuth); + const ctrl = new ObjectStackController(svc); + const r = createMockRes(); + const req = { + params: { 0: 'get-session' }, + url: '/api/auth/get-session', + method: 'GET', + protocol: 'http', + get: (key: string) => key === 'host' ? 'localhost' : undefined, + headers: {}, + originalUrl: '/api/auth/get-session', + }; + + await ctrl.auth(req, r, {}); + + expect(mockHandleRequest).toHaveBeenCalled(); + expect(r._status).toBe(200); + }); + + it('returns error when auth service throws', async () => { + const mockHandleRequest = vi.fn().mockRejectedValue(new Error('Auth failed')); + const kernelWithAuth = { + ...createMockKernel(), + getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }), + }; + const svc = new ObjectStackService(kernelWithAuth); + const ctrl = new ObjectStackController(svc); + const r = createMockRes(); + const req = { + params: { 0: 'sign-in/email' }, + url: '/api/auth/sign-in/email', + method: 'POST', + protocol: 'http', + get: (key: string) => key === 'host' ? 'localhost' : undefined, + headers: {}, + originalUrl: '/api/auth/sign-in/email', + }; + + await ctrl.auth(req, r, {}); + + expect(r._status).toBe(500); + expect(r._body.success).toBe(false); + }); + }); + describe('metadata()', () => { it('dispatches to handleMetadata with extracted path', async () => { const req = { params: { 0: '' }, url: '/api/meta/objects', method: 'GET' }; diff --git a/packages/adapters/nextjs/src/index.ts b/packages/adapters/nextjs/src/index.ts index f11aa74c0..efb5e47c6 100644 --- a/packages/adapters/nextjs/src/index.ts +++ b/packages/adapters/nextjs/src/index.ts @@ -61,6 +61,21 @@ export function createRouteHandler(options: NextAdapterOptions) { // --- 1. Auth --- if (segments[0] === 'auth') { + // Try AuthPlugin service first (preferred path) + const authService = typeof options.kernel.getService === 'function' + ? options.kernel.getService('auth') + : null; + + if (authService && typeof authService.handleRequest === 'function') { + const response = await authService.handleRequest(req); + // Convert Web Response to NextResponse + const body = await response.text(); + const headers: Record = {}; + response.headers.forEach((v: string, k: string) => { headers[k] = v; }); + return new NextResponse(body, { status: response.status, headers }); + } + + // Fallback to legacy dispatcher const subPath = segments.slice(1).join('/'); const body = method === 'POST' ? await req.json().catch(() => ({})) : {}; const result = await dispatcher.handleAuth(subPath, method, body, { request: req }); diff --git a/packages/adapters/nextjs/src/nextjs.test.ts b/packages/adapters/nextjs/src/nextjs.test.ts index b1c300993..d1ce2a3ef 100644 --- a/packages/adapters/nextjs/src/nextjs.test.ts +++ b/packages/adapters/nextjs/src/nextjs.test.ts @@ -150,6 +150,71 @@ describe('createRouteHandler', () => { }); }); + describe('Auth via AuthPlugin service', () => { + it('uses kernel.getService("auth") when available', async () => { + const mockHandleRequest = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ user: { id: '1' } }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + const kernelWithAuth = { + ...mockKernel, + getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }), + }; + const handler = createRouteHandler({ kernel: kernelWithAuth }); + const req = makeReq('http://localhost/api/auth/sign-in/email', 'POST', { email: 'a@b.com', password: 'pass' }); + const res = await handler(req, { params: { objectstack: ['auth', 'sign-in', 'email'] } }); + expect(res.status).toBe(200); + expect(kernelWithAuth.getService).toHaveBeenCalledWith('auth'); + expect(mockHandleRequest).toHaveBeenCalled(); + expect(mockDispatcher.handleAuth).not.toHaveBeenCalled(); + }); + + it('falls back to dispatcher.handleAuth when auth service is not available', async () => { + const kernelWithoutAuth = { + ...mockKernel, + getService: vi.fn().mockReturnValue(null), + }; + const handler = createRouteHandler({ kernel: kernelWithoutAuth }); + const req = makeReq('http://localhost/api/auth/login', 'POST', { email: 'a@b.com' }); + const res = await handler(req, { params: { objectstack: ['auth', 'login'] } }); + expect(res.status).toBe(200); + expect(mockDispatcher.handleAuth).toHaveBeenCalled(); + }); + + it('forwards GET requests to auth service', async () => { + const mockHandleRequest = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ session: { token: 'abc' } }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + const kernelWithAuth = { + ...mockKernel, + getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }), + }; + const handler = createRouteHandler({ kernel: kernelWithAuth }); + const req = makeReq('http://localhost/api/auth/get-session', 'GET'); + const res = await handler(req, { params: { objectstack: ['auth', 'get-session'] } }); + expect(res.status).toBe(200); + expect(mockHandleRequest).toHaveBeenCalled(); + }); + + it('returns error when auth service throws', async () => { + const mockHandleRequest = vi.fn().mockRejectedValue(new Error('Auth failed')); + const kernelWithAuth = { + ...mockKernel, + getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }), + }; + const handler = createRouteHandler({ kernel: kernelWithAuth }); + const req = makeReq('http://localhost/api/auth/sign-in/email', 'POST', { email: 'a@b.com' }); + const res = await handler(req, { params: { objectstack: ['auth', 'sign-in', 'email'] } }); + expect(res.status).toBe(500); + expect(res.body.success).toBe(false); + }); + }); + describe('GraphQL Endpoint', () => { it('POST graphql calls handleGraphQL', async () => { const handler = createRouteHandler({ kernel: mockKernel });