Skip to content
Open
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
16 changes: 9 additions & 7 deletions src/server/workspace-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).' },
Expand All @@ -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(
{
Expand Down Expand Up @@ -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'));
Expand Down Expand Up @@ -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)
Expand All @@ -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.' },
Expand All @@ -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.' },
Expand All @@ -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.' },
Expand All @@ -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,
Expand Down
49 changes: 48 additions & 1 deletion tests/page-routes.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
});