diff --git a/src/server/workspace-routes.ts b/src/server/workspace-routes.ts index 82ba5bb..9410d46 100644 --- a/src/server/workspace-routes.ts +++ b/src/server/workspace-routes.ts @@ -38,7 +38,7 @@ export function workspaceRoutes(platform: Platform, voice: VoiceService) { description: z.string().max(500).default(''), }) .strict() - .safeParse(await c.req.json()); + .safeParse(await c.req.json().catch(() => null)); if (!data.success) return c.json( { error: 'Enter a Space name (up to 60 characters).' }, @@ -52,7 +52,7 @@ export function workspaceRoutes(platform: Platform, voice: VoiceService) { app.post('/dots', async (c) => { const data = dotSchema .extend({ spaceId: z.string() }) - .safeParse(await c.req.json()); + .safeParse(await c.req.json().catch(() => null)); if (!data.success) return c.json( { @@ -92,7 +92,7 @@ export function workspaceRoutes(platform: Platform, voice: VoiceService) { ); }); app.put('/dots/:id', async (c) => { - const data = dotSchema.safeParse(await c.req.json()); + const data = dotSchema.safeParse(await c.req.json().catch(() => null)); if (!data.success) return c.json({ error: 'Invalid specialist settings.' }, 400); const current = platform.workspace.dot(c.req.param('id')); @@ -124,7 +124,7 @@ export function workspaceRoutes(platform: Platform, voice: VoiceService) { title: z.string().trim().min(1).max(120).default('A new thought'), }) .strict() - .safeParse(await c.req.json()); + .safeParse(await c.req.json().catch(() => null)); if (!data.success) return c.json({ error: 'Select a Dot and a conversation title.' }, 400); if (platform.setup().missing.length) @@ -144,7 +144,7 @@ export function workspaceRoutes(platform: Platform, voice: VoiceService) { const data = z .object({ threadId: z.string(), sdp: z.string().max(100000) }) .strict() - .safeParse(await c.req.json()); + .safeParse(await c.req.json().catch(() => null)); if (!data.success) return c.json( { error: 'A conversation and audio SDP offer are required.' }, @@ -169,7 +169,7 @@ export function workspaceRoutes(platform: Platform, voice: VoiceService) { transcript: z.string().max(12000).default(''), }) .strict() - .safeParse(await c.req.json()); + .safeParse(await c.req.json().catch(() => null)); if (!data.success) return c.json( { error: 'A bounded compute request and tool call ID are required.' }, @@ -190,7 +190,7 @@ export function workspaceRoutes(platform: Platform, voice: VoiceService) { anchorMessageId: z.string().max(200).optional(), }) .strict() - .safeParse(await c.req.json()); + .safeParse(await c.req.json().catch(() => null)); if (!data.success) return c.json( { error: 'Transcript exceeds the 20,000 character limit.' }, @@ -202,6 +202,8 @@ export function workspaceRoutes(platform: Platform, voice: VoiceService) { app.all('/copilotkit/*', (c) => platform.handle(c.req.raw)); app.onError((error, c) => { const text = error.message; + if (text.startsWith('Space access must include')) + return c.json({ error: text }, 400); const known = /^(Setup|Voice setup|Dot |Space |Specialist |Conversation |Call |This call|End the current|Voice provider|An audio|Intelligence could not)/.test( text, diff --git a/tests/page-routes.test.ts b/tests/page-routes.test.ts index 9b0f8fe..10d637a 100644 --- a/tests/page-routes.test.ts +++ b/tests/page-routes.test.ts @@ -1,4 +1,4 @@ -import { afterEach, expect, it } from 'vitest'; +import { afterEach, expect, it, vi } from 'vitest'; import { Store } from '../src/server/store.js'; import { WorkspaceStore } from '../src/server/workspace.js'; import { Platform } from '../src/server/platform.js'; @@ -221,3 +221,50 @@ it('restores review receipts through the owner API with current thread and Space ws.updateDot(dot.id, { ...dot, spaceId: other.id, spaceIds: [other.id] }); expect((await app.request(`${base}/call`, { headers })).status).toBe(403); }); + +it('answers malformed JSON and invalid Space access with 400 on workspace routes', async () => { + const { ws, app } = fixture(); + const dot = ws.dots()[0]; + for (const path of ['/api/spaces', '/api/dots', '/api/conversations']) { + const response = await app.request(path, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: '{', + }); + expect(response.status).toBe(400); + } + const body = { + name: dot.name, + instructions: dot.instructions, + researchAllowed: true, + memoryAllowed: true, + }; + expect( + ( + await app.request( + '/api/dots', + request({ ...body, spaceId: 'missing-space' }), + ) + ).status, + ).toBe(400); + expect( + ( + await app.request( + `/api/dots/${dot.id}`, + request({ ...body, spaceIds: ['missing-space'] }, 'PUT'), + ) + ).status, + ).toBe(400); +}); + +it('keeps upstream JSON parsing failures on workspace routes as 503', async () => { + const { ws, app } = fixture(); + vi.spyOn(ws, 'createSpace').mockImplementation(() => { + throw new SyntaxError('Unexpected token in upstream response'); + }); + const response = await app.request( + '/api/spaces', + request({ name: 'Valid', description: '' }), + ); + expect(response.status).toBe(503); +});