Skip to content

Commit 67b305d

Browse files
authored
Merge pull request #591 from objectstack-ai/copilot/update-adapters-with-authplugin
2 parents 4f54fbe + af97090 commit 67b305d

6 files changed

Lines changed: 301 additions & 1 deletion

File tree

packages/adapters/hono/src/hono.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,77 @@ describe('createHonoApp', () => {
9494
});
9595
});
9696

97+
describe('Auth via AuthPlugin service', () => {
98+
it('uses kernel.getService("auth") when available', async () => {
99+
const mockHandleRequest = vi.fn().mockResolvedValue(
100+
new Response(JSON.stringify({ user: { id: '1' } }), {
101+
status: 200,
102+
headers: { 'Content-Type': 'application/json' },
103+
}),
104+
);
105+
const kernelWithAuth = {
106+
...mockKernel,
107+
getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }),
108+
};
109+
const app = createHonoApp({ kernel: kernelWithAuth });
110+
const res = await app.request('/api/auth/sign-in/email', {
111+
method: 'POST',
112+
headers: { 'Content-Type': 'application/json' },
113+
body: JSON.stringify({ email: 'a@b.com', password: 'pass' }),
114+
});
115+
expect(res.status).toBe(200);
116+
const json = await res.json();
117+
expect(json.user.id).toBe('1');
118+
expect(kernelWithAuth.getService).toHaveBeenCalledWith('auth');
119+
expect(mockHandleRequest).toHaveBeenCalledWith(expect.any(Request));
120+
expect(mockDispatcher.handleAuth).not.toHaveBeenCalled();
121+
});
122+
123+
it('falls back to dispatcher.handleAuth when auth service is not available', async () => {
124+
const kernelWithoutAuth = {
125+
...mockKernel,
126+
getService: vi.fn().mockReturnValue(null),
127+
};
128+
const app = createHonoApp({ kernel: kernelWithoutAuth });
129+
const res = await app.request('/api/auth/login', { method: 'POST' });
130+
expect(res.status).toBe(200);
131+
expect(mockDispatcher.handleAuth).toHaveBeenCalled();
132+
});
133+
134+
it('forwards GET requests to auth service', async () => {
135+
const mockHandleRequest = vi.fn().mockResolvedValue(
136+
new Response(JSON.stringify({ session: { token: 'abc' } }), {
137+
status: 200,
138+
headers: { 'Content-Type': 'application/json' },
139+
}),
140+
);
141+
const kernelWithAuth = {
142+
...mockKernel,
143+
getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }),
144+
};
145+
const app = createHonoApp({ kernel: kernelWithAuth });
146+
const res = await app.request('/api/auth/get-session', { method: 'GET' });
147+
expect(res.status).toBe(200);
148+
const json = await res.json();
149+
expect(json.session.token).toBe('abc');
150+
expect(mockHandleRequest).toHaveBeenCalled();
151+
});
152+
153+
it('returns error when auth service throws', async () => {
154+
const mockHandleRequest = vi.fn().mockRejectedValue(new Error('Auth failed'));
155+
const kernelWithAuth = {
156+
...mockKernel,
157+
getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }),
158+
};
159+
const app = createHonoApp({ kernel: kernelWithAuth });
160+
const res = await app.request('/api/auth/sign-in/email', { method: 'POST' });
161+
expect(res.status).toBe(500);
162+
const json = await res.json();
163+
expect(json.success).toBe(false);
164+
expect(json.error.message).toBe('Auth failed');
165+
});
166+
});
167+
97168
describe('GraphQL Endpoint', () => {
98169
it('POST /api/graphql calls handleGraphQL', async () => {
99170
const app = createHonoApp({ kernel: mockKernel });

packages/adapters/hono/src/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,17 @@ export function createHonoApp(options: ObjectStackHonoOptions) {
6666
// --- 1. Auth ---
6767
app.all(`${prefix}/auth/*`, async (c) => {
6868
try {
69-
// subpath from /api/auth/login -> login
69+
// Try AuthPlugin service first (preferred path)
70+
const authService = typeof options.kernel.getService === 'function'
71+
? options.kernel.getService('auth')
72+
: null;
73+
74+
if (authService && typeof authService.handleRequest === 'function') {
75+
const response = await authService.handleRequest(c.req.raw);
76+
return response;
77+
}
78+
79+
// Fallback to legacy dispatcher
7080
const path = c.req.path.substring(c.req.path.indexOf('/auth/') + 6);
7181
const body = await c.req.parseBody().catch(() => ({}));
7282

packages/adapters/nestjs/src/index.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,42 @@ export class ObjectStackController {
106106
@All('auth/*')
107107
async auth(@Req() req: any, @Res() res: any, @Body() body: any) {
108108
try {
109+
// Try AuthPlugin service first (preferred path)
110+
const kernel = this.service.getKernel();
111+
const authService = typeof kernel.getService === 'function'
112+
? kernel.getService('auth')
113+
: null;
114+
115+
if (authService && typeof authService.handleRequest === 'function') {
116+
// Construct a Web standard Request from the Express/Fastify request
117+
const protocol = req.protocol || 'http';
118+
const host = req.get?.('host') || req.headers?.host || 'localhost';
119+
const url = `${protocol}://${host}${req.originalUrl || req.url}`;
120+
const headers = new Headers();
121+
if (req.headers) {
122+
Object.entries(req.headers).forEach(([k, v]) => {
123+
if (typeof v === 'string') headers.set(k, v);
124+
else if (Array.isArray(v)) headers.set(k, v.join(', '));
125+
});
126+
}
127+
const init: RequestInit = { method: req.method, headers };
128+
if (req.method !== 'GET' && req.method !== 'HEAD' && body) {
129+
init.body = JSON.stringify(body);
130+
if (!headers.has('content-type')) {
131+
headers.set('content-type', 'application/json');
132+
}
133+
}
134+
const webRequest = new Request(url, init);
135+
const response = await authService.handleRequest(webRequest);
136+
137+
// Convert Web Response to Express/Fastify response
138+
res.status(response.status);
139+
response.headers.forEach((v: string, k: string) => res.setHeader(k, v));
140+
const text = await response.text();
141+
return res.send(text);
142+
}
143+
144+
// Fallback to legacy dispatcher
109145
const path = req.params[0] || req.url.split('/auth/')[1]?.split('?')[0] || '';
110146
const result = await this.service.dispatcher.handleAuth(path, req.method, body, { request: req, response: res });
111147
return this.normalizeResponse(result, res);

packages/adapters/nestjs/src/nestjs.test.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,109 @@ describe('ObjectStackController', () => {
190190
});
191191
});
192192

193+
describe('auth() via AuthPlugin service', () => {
194+
it('uses kernel.getService("auth") when available', async () => {
195+
const mockHandleRequest = vi.fn().mockResolvedValue(
196+
new Response(JSON.stringify({ user: { id: '1' } }), {
197+
status: 200,
198+
headers: new Headers({ 'Content-Type': 'application/json' }),
199+
}),
200+
);
201+
const kernelWithAuth = {
202+
...createMockKernel(),
203+
getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }),
204+
};
205+
const svc = new ObjectStackService(kernelWithAuth);
206+
const ctrl = new ObjectStackController(svc);
207+
const r = createMockRes();
208+
const req = {
209+
params: { 0: 'sign-in/email' },
210+
url: '/api/auth/sign-in/email',
211+
method: 'POST',
212+
protocol: 'http',
213+
get: (key: string) => key === 'host' ? 'localhost' : undefined,
214+
headers: { 'content-type': 'application/json' },
215+
originalUrl: '/api/auth/sign-in/email',
216+
};
217+
218+
await ctrl.auth(req, r, { email: 'a@b.com', password: 'pass' });
219+
220+
expect(kernelWithAuth.getService).toHaveBeenCalledWith('auth');
221+
expect(mockHandleRequest).toHaveBeenCalledWith(expect.any(Request));
222+
expect(r._status).toBe(200);
223+
});
224+
225+
it('falls back to dispatcher when auth service is not available', async () => {
226+
const kernelWithoutAuth = {
227+
...createMockKernel(),
228+
getService: vi.fn().mockReturnValue(null),
229+
};
230+
const svc = new ObjectStackService(kernelWithoutAuth);
231+
const ctrl = new ObjectStackController(svc);
232+
const r = createMockRes();
233+
const req = { params: { 0: 'login' }, url: '/api/auth/login', method: 'POST' };
234+
235+
await ctrl.auth(req, r, { email: 'a@b.com' });
236+
237+
expect(svc.dispatcher.handleAuth).toHaveBeenCalled();
238+
});
239+
240+
it('forwards GET requests to auth service', async () => {
241+
const mockHandleRequest = vi.fn().mockResolvedValue(
242+
new Response(JSON.stringify({ session: { token: 'abc' } }), {
243+
status: 200,
244+
headers: new Headers({ 'Content-Type': 'application/json' }),
245+
}),
246+
);
247+
const kernelWithAuth = {
248+
...createMockKernel(),
249+
getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }),
250+
};
251+
const svc = new ObjectStackService(kernelWithAuth);
252+
const ctrl = new ObjectStackController(svc);
253+
const r = createMockRes();
254+
const req = {
255+
params: { 0: 'get-session' },
256+
url: '/api/auth/get-session',
257+
method: 'GET',
258+
protocol: 'http',
259+
get: (key: string) => key === 'host' ? 'localhost' : undefined,
260+
headers: {},
261+
originalUrl: '/api/auth/get-session',
262+
};
263+
264+
await ctrl.auth(req, r, {});
265+
266+
expect(mockHandleRequest).toHaveBeenCalled();
267+
expect(r._status).toBe(200);
268+
});
269+
270+
it('returns error when auth service throws', async () => {
271+
const mockHandleRequest = vi.fn().mockRejectedValue(new Error('Auth failed'));
272+
const kernelWithAuth = {
273+
...createMockKernel(),
274+
getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }),
275+
};
276+
const svc = new ObjectStackService(kernelWithAuth);
277+
const ctrl = new ObjectStackController(svc);
278+
const r = createMockRes();
279+
const req = {
280+
params: { 0: 'sign-in/email' },
281+
url: '/api/auth/sign-in/email',
282+
method: 'POST',
283+
protocol: 'http',
284+
get: (key: string) => key === 'host' ? 'localhost' : undefined,
285+
headers: {},
286+
originalUrl: '/api/auth/sign-in/email',
287+
};
288+
289+
await ctrl.auth(req, r, {});
290+
291+
expect(r._status).toBe(500);
292+
expect(r._body.success).toBe(false);
293+
});
294+
});
295+
193296
describe('metadata()', () => {
194297
it('dispatches to handleMetadata with extracted path', async () => {
195298
const req = { params: { 0: '' }, url: '/api/meta/objects', method: 'GET' };

packages/adapters/nextjs/src/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,21 @@ export function createRouteHandler(options: NextAdapterOptions) {
6161

6262
// --- 1. Auth ---
6363
if (segments[0] === 'auth') {
64+
// Try AuthPlugin service first (preferred path)
65+
const authService = typeof options.kernel.getService === 'function'
66+
? options.kernel.getService('auth')
67+
: null;
68+
69+
if (authService && typeof authService.handleRequest === 'function') {
70+
const response = await authService.handleRequest(req);
71+
// Convert Web Response to NextResponse
72+
const body = await response.text();
73+
const headers: Record<string, string> = {};
74+
response.headers.forEach((v: string, k: string) => { headers[k] = v; });
75+
return new NextResponse(body, { status: response.status, headers });
76+
}
77+
78+
// Fallback to legacy dispatcher
6479
const subPath = segments.slice(1).join('/');
6580
const body = method === 'POST' ? await req.json().catch(() => ({})) : {};
6681
const result = await dispatcher.handleAuth(subPath, method, body, { request: req });

packages/adapters/nextjs/src/nextjs.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,71 @@ describe('createRouteHandler', () => {
150150
});
151151
});
152152

153+
describe('Auth via AuthPlugin service', () => {
154+
it('uses kernel.getService("auth") when available', async () => {
155+
const mockHandleRequest = vi.fn().mockResolvedValue(
156+
new Response(JSON.stringify({ user: { id: '1' } }), {
157+
status: 200,
158+
headers: { 'Content-Type': 'application/json' },
159+
}),
160+
);
161+
const kernelWithAuth = {
162+
...mockKernel,
163+
getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }),
164+
};
165+
const handler = createRouteHandler({ kernel: kernelWithAuth });
166+
const req = makeReq('http://localhost/api/auth/sign-in/email', 'POST', { email: 'a@b.com', password: 'pass' });
167+
const res = await handler(req, { params: { objectstack: ['auth', 'sign-in', 'email'] } });
168+
expect(res.status).toBe(200);
169+
expect(kernelWithAuth.getService).toHaveBeenCalledWith('auth');
170+
expect(mockHandleRequest).toHaveBeenCalled();
171+
expect(mockDispatcher.handleAuth).not.toHaveBeenCalled();
172+
});
173+
174+
it('falls back to dispatcher.handleAuth when auth service is not available', async () => {
175+
const kernelWithoutAuth = {
176+
...mockKernel,
177+
getService: vi.fn().mockReturnValue(null),
178+
};
179+
const handler = createRouteHandler({ kernel: kernelWithoutAuth });
180+
const req = makeReq('http://localhost/api/auth/login', 'POST', { email: 'a@b.com' });
181+
const res = await handler(req, { params: { objectstack: ['auth', 'login'] } });
182+
expect(res.status).toBe(200);
183+
expect(mockDispatcher.handleAuth).toHaveBeenCalled();
184+
});
185+
186+
it('forwards GET requests to auth service', async () => {
187+
const mockHandleRequest = vi.fn().mockResolvedValue(
188+
new Response(JSON.stringify({ session: { token: 'abc' } }), {
189+
status: 200,
190+
headers: { 'Content-Type': 'application/json' },
191+
}),
192+
);
193+
const kernelWithAuth = {
194+
...mockKernel,
195+
getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }),
196+
};
197+
const handler = createRouteHandler({ kernel: kernelWithAuth });
198+
const req = makeReq('http://localhost/api/auth/get-session', 'GET');
199+
const res = await handler(req, { params: { objectstack: ['auth', 'get-session'] } });
200+
expect(res.status).toBe(200);
201+
expect(mockHandleRequest).toHaveBeenCalled();
202+
});
203+
204+
it('returns error when auth service throws', async () => {
205+
const mockHandleRequest = vi.fn().mockRejectedValue(new Error('Auth failed'));
206+
const kernelWithAuth = {
207+
...mockKernel,
208+
getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }),
209+
};
210+
const handler = createRouteHandler({ kernel: kernelWithAuth });
211+
const req = makeReq('http://localhost/api/auth/sign-in/email', 'POST', { email: 'a@b.com' });
212+
const res = await handler(req, { params: { objectstack: ['auth', 'sign-in', 'email'] } });
213+
expect(res.status).toBe(500);
214+
expect(res.body.success).toBe(false);
215+
});
216+
});
217+
153218
describe('GraphQL Endpoint', () => {
154219
it('POST graphql calls handleGraphQL', async () => {
155220
const handler = createRouteHandler({ kernel: mockKernel });

0 commit comments

Comments
 (0)