@promptbook/cli 0.112.0-134 → 0.112.0-135
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/agents-server/src/app/admin/chat-feedback/page.tsx +2 -4
- package/apps/agents-server/src/app/admin/chat-history/page.tsx +2 -4
- package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +28 -9
- package/apps/agents-server/src/app/api/chat/route.ts +29 -7
- package/apps/agents-server/src/app/api/chat-feedback/export/route.ts +2 -2
- package/apps/agents-server/src/app/api/chat-feedback/route.ts +3 -3
- package/apps/agents-server/src/app/api/chat-history/[id]/route.ts +2 -2
- package/apps/agents-server/src/app/api/chat-history/export/route.ts +2 -2
- package/apps/agents-server/src/app/api/chat-history/route.ts +3 -3
- package/apps/agents-server/src/app/api/chat-streaming/route.ts +29 -0
- package/apps/agents-server/src/app/api/elevenlabs/tts/route.ts +60 -2
- package/apps/agents-server/src/app/api/images/[filename]/route.ts +102 -0
- package/apps/agents-server/src/app/api/openai/v1/audio/transcriptions/route.ts +111 -2
- package/apps/agents-server/src/app/api/page-preview/check/route.ts +18 -0
- package/apps/agents-server/src/app/api/page-preview/screenshot/route.ts +15 -8
- package/apps/agents-server/src/app/api/team-agent-profile/route.ts +20 -0
- package/apps/agents-server/src/components/AgentContextMenu/useAgentContextMenuItems.ts +27 -19
- package/apps/agents-server/src/constants/defaultAgentAvatarVisual.ts +1 -1
- package/apps/agents-server/src/database/migrations/2026-06-2700-default-agent-avatar-visual-octopus3d4.sql +16 -0
- package/apps/agents-server/src/utils/agentOwnership.ts +54 -5
- package/apps/agents-server/src/utils/findAgentForCallerWriteAccess.ts +36 -0
- package/apps/agents-server/src/utils/paidApiRequestGuard.ts +241 -0
- package/apps/agents-server/src/utils/userChat/createRunUserChatJobExecutionContext.ts +36 -8
- package/apps/agents-server/src/utils/userChat/persistUserChatJobProgressCard.ts +11 -2
- package/apps/agents-server/src/utils/userChat/resolveUserChatProgressToolHighlights.ts +100 -0
- package/apps/agents-server/src/utils/userChat/runUserChatJob.ts +4 -3
- package/apps/agents-server/src/utils/userChat/userChatProgressCard.ts +272 -27
- package/apps/agents-server/src/utils/validateApiKey.ts +7 -3
- package/esm/index.es.js +795 -39
- package/esm/index.es.js.map +1 -1
- package/esm/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
- package/esm/src/avatars/visuals/octopus3d4AvatarVisual.d.ts +7 -0
- package/esm/src/utils/validators/url/isValidAgentUrl.d.ts +5 -0
- package/package.json +1 -1
- package/src/avatars/types/AvatarVisualDefinition.ts +1 -0
- package/src/avatars/visuals/avatarVisualRegistry.ts +2 -0
- package/src/avatars/visuals/octopus3d4AvatarVisual.ts +1295 -0
- package/src/other/templates/getTemplatesPipelineCollection.ts +772 -942
- package/src/utils/agents/resolveAgentAvatarImageUrl.ts +1 -1
- package/src/utils/validators/url/isValidAgentUrl.ts +5 -7
- package/src/version.ts +1 -1
- package/src/versions.txt +1 -1
- package/umd/index.umd.js +795 -39
- package/umd/index.umd.js.map +1 -1
- package/umd/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
- package/umd/src/avatars/visuals/octopus3d4AvatarVisual.d.ts +7 -0
- package/umd/src/utils/validators/url/isValidAgentUrl.d.ts +5 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ForbiddenPage } from '../../../components/ForbiddenPage/ForbiddenPage';
|
|
2
|
-
import {
|
|
2
|
+
import { isUserAdmin } from '../../../utils/isUserAdmin';
|
|
3
3
|
import { ChatFeedbackClient } from './ChatFeedbackClient';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -15,9 +15,7 @@ type AdminChatFeedbackPageProps = {
|
|
|
15
15
|
* Handles admin chat feedback page.
|
|
16
16
|
*/
|
|
17
17
|
export default async function AdminChatFeedbackPage({ searchParams }: AdminChatFeedbackPageProps) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (!currentUser) {
|
|
18
|
+
if (!(await isUserAdmin())) {
|
|
21
19
|
return <ForbiddenPage />;
|
|
22
20
|
}
|
|
23
21
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ForbiddenPage } from '../../../components/ForbiddenPage/ForbiddenPage';
|
|
2
|
-
import {
|
|
2
|
+
import { isUserAdmin } from '../../../utils/isUserAdmin';
|
|
3
3
|
import { ChatHistoryClient } from './ChatHistoryClient';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -15,9 +15,7 @@ type AdminChatHistoryPageProps = {
|
|
|
15
15
|
* Handles admin chat history page.
|
|
16
16
|
*/
|
|
17
17
|
export default async function AdminChatHistoryPage({ searchParams }: AdminChatHistoryPageProps) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (!currentUser) {
|
|
18
|
+
if (!(await isUserAdmin())) {
|
|
21
19
|
return <ForbiddenPage />;
|
|
22
20
|
}
|
|
23
21
|
|
|
@@ -7,12 +7,23 @@ import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentColle
|
|
|
7
7
|
import { $provideAgentReferenceResolver } from '@/src/utils/agentReferenceResolver/$provideAgentReferenceResolver';
|
|
8
8
|
import { renameAgentSource } from '@/src/utils/renameAgentSource';
|
|
9
9
|
import { isAgentVisibility, type AgentVisibility } from '@/src/utils/agentVisibility';
|
|
10
|
-
import { TODO_any } from '@promptbook-local/types';
|
|
10
|
+
import { TODO_any, type string_book } from '@promptbook-local/types';
|
|
11
11
|
import { NextResponse } from 'next/server';
|
|
12
|
-
import {
|
|
12
|
+
import { findAgentForCallerWriteAccess } from '@/src/utils/findAgentForCallerWriteAccess';
|
|
13
13
|
import { getCurrentUser } from '@/src/utils/getCurrentUser';
|
|
14
14
|
import { resolveServerAgentContext } from '@/src/utils/resolveServerAgentContext';
|
|
15
15
|
import { invalidateCachedActiveOrganizationSnapshots } from '@/src/utils/agentOrganization/loadAgentOrganizationState';
|
|
16
|
+
import type { OwnedAgentRow } from '@/src/utils/agentOwnership';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolves the stable identifier used when calling the agent collection.
|
|
20
|
+
*
|
|
21
|
+
* @param agent - Persisted agent row.
|
|
22
|
+
* @returns Permanent id when available, otherwise the legacy agent name.
|
|
23
|
+
*/
|
|
24
|
+
function getAgentCollectionIdentifier(agent: OwnedAgentRow): string {
|
|
25
|
+
return agent.permanentId || agent.agentName;
|
|
26
|
+
}
|
|
16
27
|
|
|
17
28
|
/**
|
|
18
29
|
* Handles patch.
|
|
@@ -25,6 +36,11 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ ag
|
|
|
25
36
|
return NextResponse.json({ success: false, error: 'Forbidden' }, { status: 403 });
|
|
26
37
|
}
|
|
27
38
|
|
|
39
|
+
const targetAgent = await findAgentForCallerWriteAccess(agentName);
|
|
40
|
+
if (!targetAgent || targetAgent.deletedAt) {
|
|
41
|
+
return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 });
|
|
42
|
+
}
|
|
43
|
+
|
|
28
44
|
const body = (await request.json()) as { visibility?: AgentVisibility; name?: string };
|
|
29
45
|
|
|
30
46
|
if (typeof body.name === 'string') {
|
|
@@ -34,9 +50,8 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ ag
|
|
|
34
50
|
}
|
|
35
51
|
|
|
36
52
|
const collection = await $provideAgentCollectionForServer();
|
|
37
|
-
const agentId =
|
|
38
|
-
const
|
|
39
|
-
const nextAgentSource = renameAgentSource(agentSource, trimmedName);
|
|
53
|
+
const agentId = getAgentCollectionIdentifier(targetAgent);
|
|
54
|
+
const nextAgentSource = renameAgentSource(targetAgent.agentSource as string_book, trimmedName);
|
|
40
55
|
|
|
41
56
|
await collection.updateAgentSource(agentId, nextAgentSource);
|
|
42
57
|
const baseAgentReferenceResolver = await $provideAgentReferenceResolver();
|
|
@@ -69,7 +84,7 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ ag
|
|
|
69
84
|
.from(await $getTableName(`Agent`))
|
|
70
85
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
71
86
|
.update({ visibility } as any)
|
|
72
|
-
.
|
|
87
|
+
.eq('id', targetAgent.id)
|
|
73
88
|
.is('deletedAt', null);
|
|
74
89
|
|
|
75
90
|
if (updateResult.error) {
|
|
@@ -92,15 +107,19 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ ag
|
|
|
92
107
|
*/
|
|
93
108
|
export async function DELETE(request: Request, { params }: { params: Promise<{ agentName: string }> }) {
|
|
94
109
|
const { agentName } = await params;
|
|
95
|
-
const collection = await $provideAgentCollectionForServer();
|
|
96
110
|
|
|
97
111
|
try {
|
|
98
112
|
if (!(await getCurrentUser())) {
|
|
99
113
|
return NextResponse.json({ success: false, error: 'Forbidden' }, { status: 403 });
|
|
100
114
|
}
|
|
101
115
|
|
|
102
|
-
const
|
|
103
|
-
|
|
116
|
+
const targetAgent = await findAgentForCallerWriteAccess(agentName);
|
|
117
|
+
if (!targetAgent || targetAgent.deletedAt) {
|
|
118
|
+
return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const collection = await $provideAgentCollectionForServer();
|
|
122
|
+
await collection.deleteAgent(getAgentCollectionIdentifier(targetAgent));
|
|
104
123
|
invalidateCachedActiveOrganizationSnapshots();
|
|
105
124
|
return NextResponse.json({ success: true });
|
|
106
125
|
} catch (error) {
|
|
@@ -1,9 +1,38 @@
|
|
|
1
1
|
import { createOpenAiExecutionTools } from '@promptbook-local/openai';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { spaceTrim } from 'spacetrim';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Whether this development-only OpenAI smoke test endpoint is reachable.
|
|
7
|
+
*
|
|
8
|
+
* The route exists to verify that the configured `OPENAI_API_KEY` actually
|
|
9
|
+
* talks to OpenAI, so it must never be exposed in production where it would
|
|
10
|
+
* let any anonymous client drain the server's paid OpenAI credit by repeatedly
|
|
11
|
+
* calling this unauthenticated `GET` with attacker-controlled `message`.
|
|
12
|
+
*
|
|
13
|
+
* @private internal helper of `/api/chat`
|
|
14
|
+
*/
|
|
15
|
+
const IS_DEVELOPMENT_CHAT_TEST_ENDPOINT_ENABLED = process.env.NODE_ENV !== 'production';
|
|
2
16
|
|
|
3
17
|
/**
|
|
4
18
|
* Handles get.
|
|
5
19
|
*/
|
|
6
20
|
export async function GET(request: Request) {
|
|
21
|
+
if (!IS_DEVELOPMENT_CHAT_TEST_ENDPOINT_ENABLED) {
|
|
22
|
+
return NextResponse.json(
|
|
23
|
+
{
|
|
24
|
+
error: {
|
|
25
|
+
message: spaceTrim(`
|
|
26
|
+
Endpoint \`/api/chat\` is a development-only OpenAI smoke test
|
|
27
|
+
and is disabled in production.
|
|
28
|
+
`),
|
|
29
|
+
type: 'not_available_in_production',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{ status: 404 },
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
7
36
|
const { searchParams } = new URL(request.url);
|
|
8
37
|
const message = searchParams.get('message') || 'Hello, who are you?';
|
|
9
38
|
|
|
@@ -21,13 +50,6 @@ export async function GET(request: Request) {
|
|
|
21
50
|
content: message,
|
|
22
51
|
});
|
|
23
52
|
|
|
24
|
-
/*
|
|
25
|
-
return new Response(JSON.stringify(response, null, 4), {
|
|
26
|
-
status: 200,
|
|
27
|
-
headers: { 'Content-Type': 'application/json' },
|
|
28
|
-
});
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
53
|
return new Response(response.content, {
|
|
32
54
|
status: 200,
|
|
33
55
|
headers: { 'Content-Type': 'text/markdown' },
|
|
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
2
2
|
import { $getTableName } from '../../../../database/$getTableName';
|
|
3
3
|
import { $provideSupabase } from '../../../../database/$provideSupabase';
|
|
4
4
|
import { convertToCsv } from '../../../../utils/convertToCsv';
|
|
5
|
-
import {
|
|
5
|
+
import { isUserAdmin } from '../../../../utils/isUserAdmin';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Export chat feedback as CSV.
|
|
@@ -11,7 +11,7 @@ import { getCurrentUser } from '../../../../utils/getCurrentUser';
|
|
|
11
11
|
* - agentName: filter by agent name (optional)
|
|
12
12
|
*/
|
|
13
13
|
export async function GET(request: NextRequest) {
|
|
14
|
-
if (!(await
|
|
14
|
+
if (!(await isUserAdmin())) {
|
|
15
15
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { $getTableName } from '../../../database/$getTableName';
|
|
3
3
|
import { $provideSupabase } from '../../../database/$provideSupabase';
|
|
4
|
-
import {
|
|
4
|
+
import { isUserAdmin } from '../../../utils/isUserAdmin';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Size of default page.
|
|
@@ -58,7 +58,7 @@ function parseSortOrder(value: string | null): SortOrder {
|
|
|
58
58
|
* - sortOrder: asc | desc (default: desc)
|
|
59
59
|
*/
|
|
60
60
|
export async function GET(request: NextRequest) {
|
|
61
|
-
if (!(await
|
|
61
|
+
if (!(await isUserAdmin())) {
|
|
62
62
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -145,7 +145,7 @@ export async function GET(request: NextRequest) {
|
|
|
145
145
|
* - agentName: name of the agent whose feedback should be removed
|
|
146
146
|
*/
|
|
147
147
|
export async function DELETE(request: NextRequest) {
|
|
148
|
-
if (!(await
|
|
148
|
+
if (!(await isUserAdmin())) {
|
|
149
149
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
150
150
|
}
|
|
151
151
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { $getTableName } from '../../../../database/$getTableName';
|
|
3
3
|
import { $provideSupabase } from '../../../../database/$provideSupabase';
|
|
4
|
-
import {
|
|
4
|
+
import { isUserAdmin } from '../../../../utils/isUserAdmin';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Delete a single chat history entry by ID.
|
|
8
8
|
*/
|
|
9
9
|
export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) {
|
|
10
|
-
if (!(await
|
|
10
|
+
if (!(await isUserAdmin())) {
|
|
11
11
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
2
2
|
import { $getTableName } from '../../../../database/$getTableName';
|
|
3
3
|
import { $provideSupabase } from '../../../../database/$provideSupabase';
|
|
4
4
|
import { convertToCsv } from '../../../../utils/convertToCsv';
|
|
5
|
-
import {
|
|
5
|
+
import { isUserAdmin } from '../../../../utils/isUserAdmin';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Export chat history as CSV.
|
|
@@ -11,7 +11,7 @@ import { getCurrentUser } from '../../../../utils/getCurrentUser';
|
|
|
11
11
|
* - agentName: filter by agent name (optional)
|
|
12
12
|
*/
|
|
13
13
|
export async function GET(request: NextRequest) {
|
|
14
|
-
if (!(await
|
|
14
|
+
if (!(await isUserAdmin())) {
|
|
15
15
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { $getTableName } from '../../../database/$getTableName';
|
|
3
3
|
import { $provideSupabase } from '../../../database/$provideSupabase';
|
|
4
|
-
import {
|
|
4
|
+
import { isUserAdmin } from '../../../utils/isUserAdmin';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Size of default page.
|
|
@@ -58,7 +58,7 @@ function parseSortOrder(value: string | null): SortOrder {
|
|
|
58
58
|
* - sortOrder: asc | desc (default: desc)
|
|
59
59
|
*/
|
|
60
60
|
export async function GET(request: NextRequest) {
|
|
61
|
-
if (!(await
|
|
61
|
+
if (!(await isUserAdmin())) {
|
|
62
62
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -135,7 +135,7 @@ export async function GET(request: NextRequest) {
|
|
|
135
135
|
* - agentName: name of the agent whose history should be removed
|
|
136
136
|
*/
|
|
137
137
|
export async function DELETE(request: NextRequest) {
|
|
138
|
-
if (!(await
|
|
138
|
+
if (!(await isUserAdmin())) {
|
|
139
139
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -1,10 +1,39 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
1
2
|
import { OpenAI } from 'openai';
|
|
3
|
+
import { spaceTrim } from 'spacetrim';
|
|
2
4
|
import { forTime } from 'waitasecond';
|
|
3
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Whether this development-only OpenAI streaming smoke test endpoint is reachable.
|
|
8
|
+
*
|
|
9
|
+
* The route exists to verify that streaming chat completions work end-to-end,
|
|
10
|
+
* so it must never be exposed in production where it would let any anonymous
|
|
11
|
+
* client drain the server's paid OpenAI credit by repeatedly calling this
|
|
12
|
+
* unauthenticated `GET` with attacker-controlled `message`.
|
|
13
|
+
*
|
|
14
|
+
* @private internal helper of `/api/chat-streaming`
|
|
15
|
+
*/
|
|
16
|
+
const IS_DEVELOPMENT_CHAT_STREAMING_TEST_ENDPOINT_ENABLED = process.env.NODE_ENV !== 'production';
|
|
17
|
+
|
|
4
18
|
/**
|
|
5
19
|
* Handles get.
|
|
6
20
|
*/
|
|
7
21
|
export async function GET(request: Request) {
|
|
22
|
+
if (!IS_DEVELOPMENT_CHAT_STREAMING_TEST_ENDPOINT_ENABLED) {
|
|
23
|
+
return NextResponse.json(
|
|
24
|
+
{
|
|
25
|
+
error: {
|
|
26
|
+
message: spaceTrim(`
|
|
27
|
+
Endpoint \`/api/chat-streaming\` is a development-only OpenAI smoke test
|
|
28
|
+
and is disabled in production.
|
|
29
|
+
`),
|
|
30
|
+
type: 'not_available_in_production',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{ status: 404 },
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
8
37
|
const { searchParams } = new URL(request.url);
|
|
9
38
|
const message = searchParams.get('message') || 'Hello, who are you?';
|
|
10
39
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { spaceTrim } from 'spacetrim';
|
|
1
3
|
import { respondIfClientVersionIsOutdated } from '../../../../utils/clientVersionGuard';
|
|
2
4
|
import { getMetadata } from '@/src/database/getMetadata';
|
|
5
|
+
import { guardPaidApiRequest } from '@/src/utils/paidApiRequestGuard';
|
|
3
6
|
import { textToSpeechText } from '../../../../utils/textToSpeechText';
|
|
4
7
|
|
|
5
8
|
/**
|
|
@@ -37,6 +40,19 @@ const ELEVEN_LABS_API_KEY = process.env.ELEVEN_LABS_API_KEY;
|
|
|
37
40
|
*/
|
|
38
41
|
const MAX_ELEVEN_LABS_TEXT_LENGTH = 4500;
|
|
39
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Pattern matching valid ElevenLabs voice identifiers.
|
|
45
|
+
*
|
|
46
|
+
* ElevenLabs voice IDs are 20-character alphanumeric strings (e.g.
|
|
47
|
+
* `21m00Tcm4TlvDq8ikWAM`). Validating with a strict allowlist prevents an
|
|
48
|
+
* attacker from injecting arbitrary path segments into the upstream URL
|
|
49
|
+
* (which is built by string interpolation below) and stops requests for
|
|
50
|
+
* unknown ids that would still bill our ElevenLabs account.
|
|
51
|
+
*
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
const ELEVEN_LABS_VOICE_ID_PATTERN = /^[A-Za-z0-9]{15,40}$/u;
|
|
55
|
+
|
|
40
56
|
/**
|
|
41
57
|
* Handles the OPTIONS request from the chat for CORS / preflight.
|
|
42
58
|
*
|
|
@@ -59,7 +75,7 @@ export function OPTIONS() {
|
|
|
59
75
|
*
|
|
60
76
|
* @private
|
|
61
77
|
*/
|
|
62
|
-
export async function POST(request:
|
|
78
|
+
export async function POST(request: NextRequest) {
|
|
63
79
|
const versionMismatchResponse = respondIfClientVersionIsOutdated(request, 'json');
|
|
64
80
|
if (versionMismatchResponse) {
|
|
65
81
|
return versionMismatchResponse;
|
|
@@ -87,10 +103,31 @@ export async function POST(request: Request) {
|
|
|
87
103
|
);
|
|
88
104
|
}
|
|
89
105
|
|
|
106
|
+
const guard = await guardPaidApiRequest(request, 'TEXT_TO_SPEECH');
|
|
107
|
+
if (!guard.ok) {
|
|
108
|
+
return guard.response;
|
|
109
|
+
}
|
|
110
|
+
|
|
90
111
|
const payload = (await request.json().catch(() => null)) as { text?: string; voiceId?: string } | null;
|
|
91
112
|
const rawText = (payload?.text?.toString() ?? '').trim();
|
|
92
113
|
const requestedVoiceId = payload?.voiceId?.toString().trim();
|
|
93
|
-
const voiceIdToUse = requestedVoiceId
|
|
114
|
+
const voiceIdToUse = resolveValidatedVoiceId(requestedVoiceId);
|
|
115
|
+
|
|
116
|
+
if (voiceIdToUse === null) {
|
|
117
|
+
return new Response(
|
|
118
|
+
JSON.stringify({
|
|
119
|
+
error: spaceTrim(`
|
|
120
|
+
Invalid \`voiceId\`.
|
|
121
|
+
|
|
122
|
+
ElevenLabs voice ids must be 15–40 alphanumeric characters.
|
|
123
|
+
`),
|
|
124
|
+
}),
|
|
125
|
+
{
|
|
126
|
+
status: 400,
|
|
127
|
+
headers: { 'Content-Type': 'application/json' },
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
}
|
|
94
131
|
|
|
95
132
|
if (!rawText) {
|
|
96
133
|
return new Response(
|
|
@@ -149,3 +186,24 @@ export async function POST(request: Request) {
|
|
|
149
186
|
},
|
|
150
187
|
});
|
|
151
188
|
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Validates the caller-supplied voice id and falls back to the configured default.
|
|
192
|
+
*
|
|
193
|
+
* Returns `null` if the caller provided a voice id that does not match the
|
|
194
|
+
* ElevenLabs format. The default value comes from a trusted environment
|
|
195
|
+
* variable, so it is forwarded as-is without re-validation.
|
|
196
|
+
*
|
|
197
|
+
* @private internal helper of `POST /api/elevenlabs/tts`
|
|
198
|
+
*/
|
|
199
|
+
function resolveValidatedVoiceId(requestedVoiceId: string | undefined): string | null {
|
|
200
|
+
if (!requestedVoiceId) {
|
|
201
|
+
return ELEVEN_LABS_VOICE_ID;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!ELEVEN_LABS_VOICE_ID_PATTERN.test(requestedVoiceId)) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return requestedVoiceId;
|
|
209
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { serializeError, computeHash } from '@promptbook-local/utils';
|
|
2
2
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import { spaceTrim } from 'spacetrim';
|
|
3
4
|
import { assertsError } from '../../../../../../../src/errors/assertsError';
|
|
4
5
|
import type { LlmExecutionTools } from '../../../../../../../src/execution/LlmExecutionTools';
|
|
5
6
|
import { getSingleLlmExecutionTools } from '../../../../../../../src/llm-providers/_multiple/getSingleLlmExecutionTools';
|
|
@@ -11,6 +12,50 @@ import { $provideServer } from '../../../../tools/$provideServer';
|
|
|
11
12
|
import { getGeneratedImageCdnKey } from '../../../../utils/cdn/utils/getGeneratedImageCdnKey';
|
|
12
13
|
import { ensureGeneratedImage } from '../../../../utils/imageGeneration/ensureGeneratedImage';
|
|
13
14
|
import { filenameToPrompt } from '../../../../utils/normalization/filenameToPrompt';
|
|
15
|
+
import { guardPaidApiRequest } from '../../../../utils/paidApiRequestGuard';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Allowlist of image generation models that may be requested by the caller.
|
|
19
|
+
*
|
|
20
|
+
* Image generation is one of the most expensive paid AI operations, so we
|
|
21
|
+
* reject any unknown model id before forwarding to the upstream provider.
|
|
22
|
+
*/
|
|
23
|
+
const ALLOWED_IMAGE_GENERATION_MODELS = new Set<string>([
|
|
24
|
+
'dall-e-3',
|
|
25
|
+
'dall-e-2',
|
|
26
|
+
'gpt-image-1',
|
|
27
|
+
'gemini-3-pro-image-preview',
|
|
28
|
+
'gemini-2.0-flash-preview-image-generation',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Allowlist of image sizes accepted from the caller.
|
|
33
|
+
*
|
|
34
|
+
* Larger sizes cost more on every supported provider, so callers must pick a
|
|
35
|
+
* value from this fixed list instead of supplying arbitrary `${number}x${number}`
|
|
36
|
+
* strings.
|
|
37
|
+
*/
|
|
38
|
+
const ALLOWED_IMAGE_GENERATION_SIZES = new Set<NonNullable<ImageGenerationModelRequirements['size']>>([
|
|
39
|
+
'1024x1024',
|
|
40
|
+
'1792x1024',
|
|
41
|
+
'1024x1792',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Allowlist of image quality values accepted from the caller.
|
|
46
|
+
*/
|
|
47
|
+
const ALLOWED_IMAGE_GENERATION_QUALITIES = new Set<NonNullable<ImageGenerationModelRequirements['quality']>>([
|
|
48
|
+
'standard',
|
|
49
|
+
'hd',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Allowlist of image style values accepted from the caller.
|
|
54
|
+
*/
|
|
55
|
+
const ALLOWED_IMAGE_GENERATION_STYLES = new Set<NonNullable<ImageGenerationModelRequirements['style']>>([
|
|
56
|
+
'vivid',
|
|
57
|
+
'natural',
|
|
58
|
+
]);
|
|
14
59
|
|
|
15
60
|
/**
|
|
16
61
|
* Chooses default image model from current provider metadata.
|
|
@@ -37,6 +82,63 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|
|
37
82
|
return NextResponse.json({ error: 'Filename is required' }, { status: 400 });
|
|
38
83
|
}
|
|
39
84
|
|
|
85
|
+
const guard = await guardPaidApiRequest(request, 'IMAGE_GENERATION');
|
|
86
|
+
if (!guard.ok) {
|
|
87
|
+
return guard.response;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (modelName !== null && !ALLOWED_IMAGE_GENERATION_MODELS.has(modelName)) {
|
|
91
|
+
return NextResponse.json(
|
|
92
|
+
{
|
|
93
|
+
error: spaceTrim(`
|
|
94
|
+
Requested image generation \`modelName\` is not allowed.
|
|
95
|
+
|
|
96
|
+
**Allowed models:** ${Array.from(ALLOWED_IMAGE_GENERATION_MODELS).join(', ')}
|
|
97
|
+
`),
|
|
98
|
+
},
|
|
99
|
+
{ status: 400 },
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (size !== null && !ALLOWED_IMAGE_GENERATION_SIZES.has(size as never)) {
|
|
104
|
+
return NextResponse.json(
|
|
105
|
+
{
|
|
106
|
+
error: spaceTrim(`
|
|
107
|
+
Requested image \`size\` is not allowed.
|
|
108
|
+
|
|
109
|
+
**Allowed sizes:** ${Array.from(ALLOWED_IMAGE_GENERATION_SIZES).join(', ')}
|
|
110
|
+
`),
|
|
111
|
+
},
|
|
112
|
+
{ status: 400 },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (quality !== null && !ALLOWED_IMAGE_GENERATION_QUALITIES.has(quality as never)) {
|
|
117
|
+
return NextResponse.json(
|
|
118
|
+
{
|
|
119
|
+
error: spaceTrim(`
|
|
120
|
+
Requested image \`quality\` is not allowed.
|
|
121
|
+
|
|
122
|
+
**Allowed values:** ${Array.from(ALLOWED_IMAGE_GENERATION_QUALITIES).join(', ')}
|
|
123
|
+
`),
|
|
124
|
+
},
|
|
125
|
+
{ status: 400 },
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (style !== null && !ALLOWED_IMAGE_GENERATION_STYLES.has(style as never)) {
|
|
130
|
+
return NextResponse.json(
|
|
131
|
+
{
|
|
132
|
+
error: spaceTrim(`
|
|
133
|
+
Requested image \`style\` is not allowed.
|
|
134
|
+
|
|
135
|
+
**Allowed values:** ${Array.from(ALLOWED_IMAGE_GENERATION_STYLES).join(', ')}
|
|
136
|
+
`),
|
|
137
|
+
},
|
|
138
|
+
{ status: 400 },
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
40
142
|
let attachments: unknown[] | undefined;
|
|
41
143
|
if (attachmentsRaw) {
|
|
42
144
|
try {
|