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
26 changes: 25 additions & 1 deletion packages/runtime/src/dispatcher-plugin.routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ import { createDispatcherPlugin } from './dispatcher-plugin.js';

function makeFakeServer() {
const routes: string[] = [];
const rec = (verb: string) => (path: string, _handler: unknown) => {
const handlers: Record<string, (req: any, res: any) => any> = {};
const rec = (verb: string) => (path: string, handler: any) => {
routes.push(`${verb} ${path}`);
handlers[`${verb} ${path}`] = handler;
};
return {
routes,
handlers,
server: {
get: rec('GET'),
post: rec('POST'),
Expand Down Expand Up @@ -72,4 +75,25 @@ describe('createDispatcherPlugin — HTTP route registration', () => {
expect(routes).toContain('POST /v2/mcp');
expect(routes).toContain('POST /v2/keys');
});

// cloud#152: discovery reflects mutable runtime config (e.g. routes.mcp toggles
// with OS_MCP_SERVER_ENABLED). It must be served Cache-Control: no-store so an
// edge/CDN never serves a stale payload after the config changes.
it('serves both discovery routes with Cache-Control: no-store', async () => {
const { server, handlers } = makeFakeServer();
const plugin = createDispatcherPlugin({ prefix: '/api/v1', securityHeaders: false });
await plugin.start?.(makeCtx(server));

for (const route of ['GET /.well-known/objectstack', 'GET /api/v1/discovery']) {
const handler = handlers[route];
expect(handler, `${route} should be registered`).toBeTypeOf('function');
const headers: Record<string, string> = {};
const res: any = {
header: (k: string, v: string) => { headers[k] = v; },
json: () => {},
};
await handler({}, res);
expect(headers['Cache-Control'], `${route} Cache-Control`).toBe('no-store');
}
});
});
11 changes: 11 additions & 0 deletions packages/runtime/src/dispatcher-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,14 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
res.header(k, v);
}
}
// Discovery reflects MUTABLE runtime config (which routes/services
// are live — e.g. `mcp` only when OS_MCP_SERVER_ENABLED=true). It
// must never be cached by an edge/CDN, or a config change (enable
// MCP) leaves clients reading a stale payload that still says the
// route is absent — the Integrations UI then shows "MCP not
// enabled" against a live server (cloud#152). The body is computed
// fresh per request; the only staleness is the HTTP cache layer.
res.header('Cache-Control', 'no-store');
res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
});

Expand All @@ -462,6 +470,9 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
res.header(k, v);
}
}
// See the .well-known handler above: discovery must not be cached
// (mutable runtime config; cloud#152 stale `routes.mcp`).
res.header('Cache-Control', 'no-store');
res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
});

Expand Down
Loading