Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/adapters/express/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export function createExpressRouter(options: ExpressAdapterOptions): Router {
const subPath = '/' + (req.params as any).path;
const method = req.method;
const body = (method === 'POST' || method === 'PUT' || method === 'PATCH') ? req.body : undefined;
const result = await dispatcher.dispatch(method, subPath, body, req.query, { request: req, response: res });
const result = await dispatcher.dispatch(method, subPath, body, req.query, { request: req, response: res }, prefix);
return sendResult(result, res);
} catch (err: any) {
return errorResponse(err, res);
Expand Down
6 changes: 5 additions & 1 deletion packages/adapters/fastify/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ export async function objectStackPlugin(fastify: FastifyInstance, options: Fasti
return reply.send({ data: await dispatcher.getDiscoveryInfo(prefix) });
});

fastify.get(`${prefix}/discovery`, async (_request: FastifyRequest, reply: FastifyReply) => {
return reply.send({ data: await dispatcher.getDiscoveryInfo(prefix) });
});

// --- .well-known ---
fastify.get('/.well-known/objectstack', async (_request: FastifyRequest, reply: FastifyReply) => {
return reply.redirect(prefix);
Expand Down Expand Up @@ -172,7 +176,7 @@ export async function objectStackPlugin(fastify: FastifyInstance, options: Fasti
const subPath = urlPath.substring(prefix.length);
const method = request.method;
const body = (method === 'POST' || method === 'PUT' || method === 'PATCH') ? request.body : undefined;
const result = await dispatcher.dispatch(method, subPath, body, request.query, { request: request.raw });
const result = await dispatcher.dispatch(method, subPath, body, request.query, { request: request.raw }, prefix);
return sendResult(result, reply);
Comment on lines 176 to 180
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The adapter now passes a 6th prefix argument to dispatcher.dispatch(...), but the existing Fastify adapter unit tests still assert the old 5-argument call signature (see packages/adapters/fastify/src/fastify.test.ts). This will cause the test suite to fail unless the expectations are updated to include the prefix (e.g. '/api' / custom prefix) and ideally add coverage for the new GET ${prefix}/discovery route.

Copilot uses AI. Check for mistakes.
} catch (err: any) {
return errorResponse(err, reply);
Expand Down
45 changes: 45 additions & 0 deletions packages/adapters/hono/src/hono.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ describe('createHonoApp', () => {
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api');
});

it('GET /api/discovery returns discovery info with correct prefix', async () => {
const res = await app.request('/api/discovery');
expect(res.status).toBe(200);
const json = await res.json();
expect(json.data).toBeDefined();
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api');
});

it('uses custom prefix for discovery', async () => {
const customApp = createHonoApp({ kernel: mockKernel, prefix: '/v2' });
const res = await customApp.request('/v2');
Expand All @@ -105,6 +113,15 @@ describe('createHonoApp', () => {
expect(json.data).toBeDefined();
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/v2');
});

it('uses custom prefix for /discovery route', async () => {
const customApp = createHonoApp({ kernel: mockKernel, prefix: '/v2' });
const res = await customApp.request('/v2/discovery');
expect(res.status).toBe(200);
const json = await res.json();
expect(json.data).toBeDefined();
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/v2');
});
});

describe('.well-known Endpoint', () => {
Expand Down Expand Up @@ -277,6 +294,7 @@ describe('createHonoApp', () => {
undefined,
expect.any(Object),
expect.objectContaining({ request: expect.anything() }),
'/api',
);
});

Expand All @@ -294,6 +312,7 @@ describe('createHonoApp', () => {
body,
expect.any(Object),
expect.objectContaining({ request: expect.anything() }),
'/api',
);
});

Expand All @@ -306,6 +325,7 @@ describe('createHonoApp', () => {
undefined,
expect.any(Object),
expect.objectContaining({ request: expect.anything() }),
'/api',
);
});

Expand All @@ -318,6 +338,7 @@ describe('createHonoApp', () => {
undefined,
expect.objectContaining({ package: 'com.acme.crm' }),
expect.objectContaining({ request: expect.anything() }),
'/api',
);
});

Expand All @@ -330,6 +351,7 @@ describe('createHonoApp', () => {
undefined,
expect.any(Object),
expect.objectContaining({ request: expect.anything() }),
'/api',
);
});

Expand All @@ -347,6 +369,7 @@ describe('createHonoApp', () => {
body,
expect.any(Object),
expect.objectContaining({ request: expect.anything() }),
'/api',
);
});

Expand All @@ -364,6 +387,7 @@ describe('createHonoApp', () => {
body,
expect.any(Object),
expect.objectContaining({ request: expect.anything() }),
'/api',
);
});

Expand All @@ -384,6 +408,7 @@ describe('createHonoApp', () => {
undefined,
expect.any(Object),
expect.objectContaining({ request: expect.anything() }),
'/api',
);
});

Expand All @@ -396,6 +421,7 @@ describe('createHonoApp', () => {
undefined,
expect.any(Object),
expect.objectContaining({ request: expect.anything() }),
'/api',
);
});

Expand All @@ -413,6 +439,7 @@ describe('createHonoApp', () => {
body,
expect.any(Object),
expect.objectContaining({ request: expect.anything() }),
'/api',
);
});

Expand All @@ -425,6 +452,7 @@ describe('createHonoApp', () => {
undefined,
expect.objectContaining({ status: 'active' }),
expect.objectContaining({ request: expect.anything() }),
'/api',
);
});

Expand All @@ -446,6 +474,7 @@ describe('createHonoApp', () => {
undefined,
expect.any(Object),
expect.objectContaining({ request: expect.anything() }),
'/api',
);
});

Expand All @@ -458,6 +487,7 @@ describe('createHonoApp', () => {
undefined,
expect.any(Object),
expect.objectContaining({ request: expect.anything() }),
'/api',
);
});

Expand All @@ -470,6 +500,7 @@ describe('createHonoApp', () => {
undefined,
expect.any(Object),
expect.objectContaining({ request: expect.anything() }),
'/api',
);
});

Expand All @@ -482,6 +513,7 @@ describe('createHonoApp', () => {
undefined,
expect.any(Object),
expect.objectContaining({ request: expect.anything() }),
'/api',
);
});
});
Expand Down Expand Up @@ -571,6 +603,7 @@ describe('createHonoApp', () => {
undefined,
expect.any(Object),
expect.objectContaining({ request: expect.anything() }),
'/api/v1',
);
});

Expand All @@ -585,6 +618,7 @@ describe('createHonoApp', () => {
undefined,
expect.any(Object),
expect.objectContaining({ request: expect.anything() }),
'/api/v1',
);
});

Expand All @@ -598,6 +632,16 @@ describe('createHonoApp', () => {
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api/v1');
});

it('routes /api/v1/discovery through outer→inner delegation with correct prefix', async () => {
const outerApp = createVercelApp();

const res = await outerApp.request('/api/v1/discovery');
expect(res.status).toBe(200);
const json = await res.json();
expect(json.data).toBeDefined();
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api/v1');
});

it('routes /api/v1/data/account through outer→inner delegation', async () => {
const outerApp = createVercelApp();

Expand All @@ -609,6 +653,7 @@ describe('createHonoApp', () => {
undefined,
expect.any(Object),
expect.objectContaining({ request: expect.anything() }),
'/api/v1',
);
});

Expand Down
6 changes: 5 additions & 1 deletion packages/adapters/hono/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
});

app.get(`${prefix}/discovery`, async (c) => {
return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
});

// --- .well-known ---
app.get('/.well-known/objectstack', (c) => {
return c.redirect(prefix);
Expand Down Expand Up @@ -202,7 +206,7 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
const url = new URL(c.req.url);
url.searchParams.forEach((val, key) => { queryParams[key] = val; });

const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw });
const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw }, prefix);
return toResponse(c, result);
} catch (err: any) {
return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);
Expand Down
6 changes: 5 additions & 1 deletion packages/adapters/nextjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ export function createRouteHandler(options: NextAdapterOptions) {
return NextResponse.json({ data: await dispatcher.getDiscoveryInfo(options.prefix || '/api') });
}

if (segments.length === 1 && segments[0] === 'discovery' && method === 'GET') {
return NextResponse.json({ data: await dispatcher.getDiscoveryInfo(options.prefix || '/api') });
}

try {
const rawRequest = req;

Expand Down Expand Up @@ -135,7 +139,7 @@ export function createRouteHandler(options: NextAdapterOptions) {
const queryParams: Record<string, any> = {};
url.searchParams.forEach((val, key) => queryParams[key] = val);

const result = await dispatcher.dispatch(method, path, body, queryParams, { request: rawRequest });
const result = await dispatcher.dispatch(method, path, body, queryParams, { request: rawRequest }, options.prefix || '/api');
return toResponse(result);

} catch (err: any) {
Expand Down
9 changes: 8 additions & 1 deletion packages/adapters/nuxt/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ export function createH3Router(options: NuxtAdapterOptions): Router {
}),
);

router.get(
`${prefix}/discovery`,
defineEventHandler(async () => {
return { data: await dispatcher.getDiscoveryInfo(prefix) };
}),
);

// --- .well-known ---
router.get(
'/.well-known/objectstack',
Expand Down Expand Up @@ -210,7 +217,7 @@ export function createH3Router(options: NuxtAdapterOptions): Router {
? await readBody(event)
: undefined;
const query = getQuery(event);
const result = await dispatcher.dispatch(method, subPath, body, query, { request: event.node.req });
const result = await dispatcher.dispatch(method, subPath, body, query, { request: event.node.req }, prefix);
return toResponse(event, result);
} catch (err: any) {
return errorJson(event, err.message || 'Internal Server Error', err.statusCode || 500);
Expand Down
9 changes: 8 additions & 1 deletion packages/adapters/sveltekit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ export function createRequestHandler(options: SvelteKitAdapterOptions) {
});
}

if (segments.length === 1 && segments[0] === 'discovery' && method === 'GET') {
return new Response(JSON.stringify({ data: await dispatcher.getDiscoveryInfo(prefix) }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}

try {
// --- Auth (needs auth service integration) ---
if (segments[0] === 'auth') {
Expand Down Expand Up @@ -176,7 +183,7 @@ export function createRequestHandler(options: SvelteKitAdapterOptions) {
const queryParams: Record<string, any> = {};
url.searchParams.forEach((val, key) => { queryParams[key] = val; });

const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request });
const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request }, prefix);
return toResponse(result);
Comment on lines 183 to 187
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SvelteKit adapter now passes a 6th prefix argument to dispatcher.dispatch(...), but packages/adapters/sveltekit/src/sveltekit.test.ts still asserts the old 5-argument signature. Update the test expectations to include the prefix, and consider adding a dedicated test for GET ${prefix}/discovery (added above) since that’s the primary regression being fixed.

Copilot uses AI. Check for mistakes.
} catch (err: any) {
return errorJson(err.message || 'Internal Server Error', err.statusCode || 500);
Expand Down
20 changes: 19 additions & 1 deletion packages/plugins/plugin-msw/src/msw-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,23 @@ export class MSWPlugin implements Plugin {
})
);

// Explicit /discovery endpoint — must be registered before catch-all
// so dispatch() is not called with an empty prefix.
this.handlers.push(
http.get(`*${baseUrl}`, async () => {
if (this.dispatcher) {
return HttpResponse.json({ data: await this.dispatcher.getDiscoveryInfo(baseUrl) });
}
return HttpResponse.json({ data: { version: 'v1', url: baseUrl } });
}),
http.get(`*${baseUrl}/discovery`, async () => {
if (this.dispatcher) {
return HttpResponse.json({ data: await this.dispatcher.getDiscoveryInfo(baseUrl) });
}
return HttpResponse.json({ data: { version: 'v1', url: baseUrl } });
})
Comment on lines +234 to +245
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback discovery payload returned when this.dispatcher is unavailable ({ version: 'v1', url: baseUrl }) does not match the shape returned by HttpDispatcher.getDiscoveryInfo() or even the more complete fallback used by the /.well-known/objectstack handler above. This can break clients that rely on fields like routes/endpoints/features even in MSW fallback mode. Consider reusing the same fallback object as the well-known handler (or a minimal stub that still includes routes and features keys with sensible defaults) for consistency across discovery endpoints.

Copilot uses AI. Check for mistakes.
);

if (this.dispatcher) {
const dispatcher = this.dispatcher;

Expand Down Expand Up @@ -271,7 +288,8 @@ export class MSWPlugin implements Plugin {
path,
body,
query,
{ request }
{ request },
baseUrl
);

if (result.handled) {
Expand Down
5 changes: 2 additions & 3 deletions packages/runtime/src/http-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1317,15 +1317,14 @@ export class HttpDispatcher {
* Main Dispatcher Entry Point
* Routes the request to the appropriate handler based on path and precedence
*/
async dispatch(method: string, path: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
async dispatch(method: string, path: string, body: any, query: any, context: HttpProtocolContext, prefix?: string): Promise<HttpDispatcherResult> {
const cleanPath = path.replace(/\/$/, ''); // Remove trailing slash if present, but strict on clean paths

// 0. Discovery Endpoint (GET /discovery or GET /)
// Standard route: /discovery (protocol-compliant)
// Legacy route: / (empty path, for backward compatibility — MSW strips base URL)
if ((cleanPath === '/discovery' || cleanPath === '') && method === 'GET') {
// We use '' as prefix since we are internal dispatcher
const info = await this.getDiscoveryInfo('');
const info = await this.getDiscoveryInfo(prefix ?? '');
return {
handled: true,
response: this.success(info)
Expand Down
Loading