@promptbook/cli 0.103.0-55 → 0.103.0-66
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/TODO.txt +5 -1
- package/apps/agents-server/package-lock.json +2336 -0
- package/apps/agents-server/package.json +9 -0
- package/apps/agents-server/src/app/actions.ts +3 -1
- package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +3 -1
- package/apps/agents-server/src/app/agents/[agentName]/AgentOptionsMenu.tsx +282 -0
- package/apps/agents-server/src/app/agents/[agentName]/AgentProfileChat.tsx +91 -0
- package/apps/agents-server/src/app/agents/[agentName]/AgentProfileWrapper.tsx +44 -0
- package/apps/agents-server/src/app/agents/[agentName]/CloneAgentButton.tsx +4 -4
- package/apps/agents-server/src/app/agents/[agentName]/InstallPwaButton.tsx +2 -2
- package/apps/agents-server/src/app/agents/[agentName]/agentLinks.tsx +80 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/book/route.ts +3 -1
- package/apps/agents-server/src/app/agents/[agentName]/api/chat/route.ts +11 -1
- package/apps/agents-server/src/app/agents/[agentName]/api/openai/models/route.ts +93 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/openai/v1/chat/completions/route.ts +10 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/openai/v1/models/route.ts +93 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +4 -0
- package/apps/agents-server/src/app/agents/[agentName]/chat/page.tsx +9 -2
- package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +7 -3
- package/apps/agents-server/src/app/agents/[agentName]/integration/SdkCodeTabs.tsx +31 -0
- package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +271 -30
- package/apps/agents-server/src/app/agents/[agentName]/layout.tsx +41 -0
- package/apps/agents-server/src/app/agents/[agentName]/links/page.tsx +61 -97
- package/apps/agents-server/src/app/agents/[agentName]/page.tsx +47 -157
- package/apps/agents-server/src/app/agents/[agentName]/website-integration/page.tsx +70 -0
- package/apps/agents-server/src/app/api/openai/v1/chat/completions/route.ts +6 -0
- package/apps/agents-server/src/app/api/openai/v1/models/route.ts +65 -0
- package/apps/agents-server/src/app/docs/[docId]/page.tsx +12 -32
- package/apps/agents-server/src/app/docs/page.tsx +42 -17
- package/apps/agents-server/src/app/embed/page.tsx +2 -2
- package/apps/agents-server/src/app/globals.css +129 -0
- package/apps/agents-server/src/app/layout.tsx +16 -26
- package/apps/agents-server/src/app/manifest.ts +9 -4
- package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +334 -0
- package/apps/agents-server/src/components/AgentProfile/AgentProfileFromSource.tsx +23 -0
- package/apps/agents-server/src/{app/agents/[agentName] → components/AgentProfile}/AgentQrCode.tsx +8 -1
- package/apps/agents-server/src/components/AgentProfile/QrCodeModal.tsx +90 -0
- package/apps/agents-server/src/components/DocumentationContent/DocumentationContent.tsx +87 -0
- package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +7 -6
- package/apps/agents-server/src/components/OpenMojiIcon/OpenMojiIcon.tsx +20 -0
- package/apps/agents-server/src/components/PrintButton/PrintButton.tsx +18 -0
- package/apps/agents-server/src/components/PrintHeader/PrintHeader.tsx +18 -0
- package/apps/agents-server/src/database/metadataDefaults.ts +6 -0
- package/apps/agents-server/src/database/migrations/2025-12-0070-chat-history-source.sql +2 -0
- package/apps/agents-server/src/database/schema.ts +6 -0
- package/apps/agents-server/src/utils/handleChatCompletion.ts +186 -14
- package/apps/agents-server/src/utils/resolveInheritedAgentSource.ts +13 -6
- package/apps/agents-server/src/utils/validateApiKey.ts +128 -0
- package/apps/agents-server/tailwind.config.ts +1 -1
- package/esm/index.es.js +953 -474
- package/esm/index.es.js.map +1 -1
- package/esm/typings/src/_packages/components.index.d.ts +2 -2
- package/esm/typings/src/_packages/core.index.d.ts +6 -8
- package/esm/typings/src/_packages/types.index.d.ts +7 -1
- package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +2 -1
- package/esm/typings/src/book-2.0/agent-source/createCommitmentRegex.d.ts +1 -1
- package/esm/typings/src/book-components/Chat/AgentChat/AgentChat.d.ts +3 -0
- package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +6 -0
- package/esm/typings/src/book-components/Chat/LlmChat/LlmChatProps.d.ts +5 -0
- package/esm/typings/src/book-components/PromptbookAgent/PromptbookAgentIntegration.d.ts +52 -0
- package/esm/typings/src/book-components/PromptbookAgent/PromptbookAgentSeamlessIntegration.d.ts +14 -0
- package/esm/typings/src/book-components/icons/SendIcon.d.ts +3 -0
- package/esm/typings/src/commitments/CLOSED/CLOSED.d.ts +4 -0
- package/esm/typings/src/commitments/CLOSED/CLOSED.test.d.ts +4 -0
- package/esm/typings/src/commitments/META_COLOR/META_COLOR.d.ts +6 -0
- package/esm/typings/src/commitments/META_FONT/META_FONT.d.ts +42 -0
- package/esm/typings/src/commitments/USE/USE.d.ts +53 -0
- package/esm/typings/src/commitments/USE_BROWSER/USE_BROWSER.d.ts +42 -0
- package/esm/typings/src/commitments/USE_BROWSER/USE_BROWSER.test.d.ts +1 -0
- package/esm/typings/src/commitments/{IMPORTANT/IMPORTANT.d.ts → USE_MCP/USE_MCP.d.ts} +16 -5
- package/esm/typings/src/commitments/USE_SEARCH_ENGINE/USE_SEARCH_ENGINE.d.ts +38 -0
- package/esm/typings/src/commitments/_base/BaseCommitmentDefinition.d.ts +6 -0
- package/esm/typings/src/commitments/index.d.ts +93 -1
- package/esm/typings/src/llm-providers/agent/Agent.d.ts +3 -1
- package/esm/typings/src/other/templates/getTemplatesPipelineCollection.d.ts +1 -1
- package/esm/typings/src/playground/playground.d.ts +3 -0
- package/esm/typings/src/types/typeAliases.d.ts +6 -0
- package/esm/typings/src/utils/color/Color.d.ts +9 -1
- package/esm/typings/src/utils/color/css-colors.d.ts +1 -0
- package/esm/typings/src/utils/random/$generateBookBoilerplate.d.ts +6 -0
- package/esm/typings/src/utils/random/CzechNamePool.d.ts +7 -0
- package/esm/typings/src/utils/random/EnglishNamePool.d.ts +7 -0
- package/esm/typings/src/utils/random/NamePool.d.ts +17 -0
- package/esm/typings/src/utils/random/getNamePool.d.ts +10 -0
- package/esm/typings/src/version.d.ts +1 -1
- package/package.json +2 -2
- package/umd/index.umd.js +902 -423
- package/umd/index.umd.js.map +1 -1
- package/esm/typings/src/book-components/PromptbookAgent/PromptbookAgent.d.ts +0 -29
- package/esm/typings/src/commitments/registry.d.ts +0 -68
- package/esm/typings/src/playground/playground1.d.ts +0 -2
|
@@ -128,6 +128,8 @@ export type AgentsServerDatabase = {
|
|
|
128
128
|
userAgent: string | null;
|
|
129
129
|
language: string | null;
|
|
130
130
|
platform: string | null;
|
|
131
|
+
source: 'AGENT_PAGE_CHAT' | 'OPENAI_API_COMPATIBILITY' | null;
|
|
132
|
+
apiKey: string | null;
|
|
131
133
|
};
|
|
132
134
|
Insert: {
|
|
133
135
|
id?: number;
|
|
@@ -143,6 +145,8 @@ export type AgentsServerDatabase = {
|
|
|
143
145
|
userAgent?: string | null;
|
|
144
146
|
language?: string | null;
|
|
145
147
|
platform?: string | null;
|
|
148
|
+
source?: 'AGENT_PAGE_CHAT' | 'OPENAI_API_COMPATIBILITY' | null;
|
|
149
|
+
apiKey?: string | null;
|
|
146
150
|
};
|
|
147
151
|
Update: {
|
|
148
152
|
id?: number;
|
|
@@ -158,6 +162,8 @@ export type AgentsServerDatabase = {
|
|
|
158
162
|
userAgent?: string | null;
|
|
159
163
|
language?: string | null;
|
|
160
164
|
platform?: string | null;
|
|
165
|
+
source?: 'AGENT_PAGE_CHAT' | 'OPENAI_API_COMPATIBILITY' | null;
|
|
166
|
+
apiKey?: string | null;
|
|
161
167
|
};
|
|
162
168
|
Relationships: [];
|
|
163
169
|
};
|
|
@@ -1,23 +1,53 @@
|
|
|
1
|
+
import { $getTableName } from '@/src/database/$getTableName';
|
|
2
|
+
import { $provideSupabaseForServer } from '@/src/database/$provideSupabaseForServer';
|
|
1
3
|
import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
|
|
2
|
-
import { $
|
|
3
|
-
import { Agent } from '@promptbook-local/core';
|
|
4
|
-
import { ChatMessage, ChatPromptResult, Prompt, TODO_any } from '@promptbook-local/types';
|
|
4
|
+
import { $provideOpenAiAssistantExecutionToolsForServer } from '@/src/tools/$provideOpenAiAssistantExecutionToolsForServer';
|
|
5
|
+
import { Agent, computeAgentHash, PROMPTBOOK_ENGINE_VERSION } from '@promptbook-local/core';
|
|
6
|
+
import { ChatMessage, ChatPromptResult, Prompt, string_book, TODO_any } from '@promptbook-local/types';
|
|
7
|
+
import { computeHash } from '@promptbook-local/utils';
|
|
5
8
|
import { NextRequest, NextResponse } from 'next/server';
|
|
9
|
+
import { validateApiKey } from './validateApiKey';
|
|
6
10
|
|
|
7
11
|
export async function handleChatCompletion(
|
|
8
12
|
request: NextRequest,
|
|
9
|
-
params: { agentName
|
|
10
|
-
title: string = 'API Chat Completion'
|
|
13
|
+
params: { agentName?: string },
|
|
14
|
+
title: string = 'API Chat Completion',
|
|
11
15
|
) {
|
|
12
|
-
const { agentName } = params;
|
|
16
|
+
const { agentName: agentNameFromParams } = params;
|
|
13
17
|
|
|
14
|
-
//
|
|
15
|
-
|
|
18
|
+
// Validate API key explicitly (in addition to middleware)
|
|
19
|
+
const apiKeyValidation = await validateApiKey(request);
|
|
20
|
+
if (!apiKeyValidation.isValid) {
|
|
21
|
+
return NextResponse.json(
|
|
22
|
+
{
|
|
23
|
+
error: {
|
|
24
|
+
message: apiKeyValidation.error || 'Invalid API key',
|
|
25
|
+
type: 'authentication_error',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{ status: 401 },
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const apiKey = apiKeyValidation.token || null;
|
|
16
32
|
|
|
17
33
|
try {
|
|
18
34
|
const body = await request.json();
|
|
19
35
|
const { messages, stream, model } = body;
|
|
20
36
|
|
|
37
|
+
const agentName = agentNameFromParams || model;
|
|
38
|
+
|
|
39
|
+
if (!agentName) {
|
|
40
|
+
return NextResponse.json(
|
|
41
|
+
{
|
|
42
|
+
error: {
|
|
43
|
+
message: 'Agent name is required. Please provide it in the URL or as the "model" parameter.',
|
|
44
|
+
type: 'invalid_request_error',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{ status: 400 },
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
21
51
|
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
|
22
52
|
return NextResponse.json(
|
|
23
53
|
{
|
|
@@ -31,7 +61,7 @@ export async function handleChatCompletion(
|
|
|
31
61
|
}
|
|
32
62
|
|
|
33
63
|
const collection = await $provideAgentCollectionForServer();
|
|
34
|
-
let agentSource;
|
|
64
|
+
let agentSource: string_book;
|
|
35
65
|
try {
|
|
36
66
|
agentSource = await collection.getAgentSource(agentName);
|
|
37
67
|
} catch (error) {
|
|
@@ -48,16 +78,51 @@ export async function handleChatCompletion(
|
|
|
48
78
|
);
|
|
49
79
|
}
|
|
50
80
|
|
|
51
|
-
|
|
81
|
+
// Note: Handle system messages as CONTEXT
|
|
82
|
+
const systemMessages = messages.filter((msg: TODO_any) => msg.role === 'system');
|
|
83
|
+
if (systemMessages.length > 0) {
|
|
84
|
+
const contextString = systemMessages.map((msg: TODO_any) => `CONTEXT ${msg.content}`).join('\n');
|
|
85
|
+
agentSource = `${agentSource}\n\n${contextString}` as string_book;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const threadMessages = messages.filter((msg: TODO_any) => msg.role !== 'system');
|
|
89
|
+
|
|
90
|
+
if (threadMessages.length === 0) {
|
|
91
|
+
return NextResponse.json(
|
|
92
|
+
{
|
|
93
|
+
error: {
|
|
94
|
+
message: 'Messages array must contain at least one non-system message.',
|
|
95
|
+
type: 'invalid_request_error',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{ status: 400 },
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const openAiAssistantExecutionTools = await $provideOpenAiAssistantExecutionToolsForServer();
|
|
52
103
|
const agent = new Agent({
|
|
53
104
|
agentSource,
|
|
54
|
-
executionTools
|
|
105
|
+
executionTools: {
|
|
106
|
+
llm: openAiAssistantExecutionTools, // Note: Use the same OpenAI Assistant LLM tools as the chat route
|
|
107
|
+
},
|
|
55
108
|
isVerbose: true, // or false
|
|
56
109
|
});
|
|
57
110
|
|
|
111
|
+
const agentHash = computeAgentHash(agentSource);
|
|
112
|
+
const userAgent = request.headers.get('user-agent');
|
|
113
|
+
const ip =
|
|
114
|
+
request.headers.get('x-forwarded-for') ||
|
|
115
|
+
request.headers.get('x-real-ip') ||
|
|
116
|
+
request.headers.get('x-client-ip');
|
|
117
|
+
|
|
118
|
+
// Note: Capture language and platform information
|
|
119
|
+
const language = request.headers.get('accept-language');
|
|
120
|
+
// Simple platform extraction from userAgent parentheses content (e.g., Windows NT 10.0; Win64; x64)
|
|
121
|
+
const platform = userAgent ? userAgent.match(/\(([^)]+)\)/)?.[1] : undefined; // <- TODO: [🧠] Improve platform parsing
|
|
122
|
+
|
|
58
123
|
// Prepare thread and content
|
|
59
|
-
const lastMessage =
|
|
60
|
-
const previousMessages =
|
|
124
|
+
const lastMessage = threadMessages[threadMessages.length - 1];
|
|
125
|
+
const previousMessages = threadMessages.slice(0, -1);
|
|
61
126
|
|
|
62
127
|
const thread: ChatMessage[] = previousMessages.map((msg: TODO_any, index: number) => ({
|
|
63
128
|
id: `msg-${index}`, // Placeholder ID
|
|
@@ -67,6 +132,30 @@ export async function handleChatCompletion(
|
|
|
67
132
|
date: new Date(), // We don't have the real date, using current
|
|
68
133
|
}));
|
|
69
134
|
|
|
135
|
+
// Note: Identify the user message
|
|
136
|
+
const userMessageContent = {
|
|
137
|
+
role: 'USER',
|
|
138
|
+
content: lastMessage.content,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const supabase = $provideSupabaseForServer();
|
|
142
|
+
await supabase.from(await $getTableName('ChatHistory')).insert({
|
|
143
|
+
createdAt: new Date().toISOString(),
|
|
144
|
+
messageHash: computeHash(userMessageContent),
|
|
145
|
+
previousMessageHash: null,
|
|
146
|
+
agentName,
|
|
147
|
+
agentHash,
|
|
148
|
+
message: userMessageContent,
|
|
149
|
+
promptbookEngineVersion: PROMPTBOOK_ENGINE_VERSION,
|
|
150
|
+
url: request.url,
|
|
151
|
+
ip,
|
|
152
|
+
userAgent,
|
|
153
|
+
language,
|
|
154
|
+
platform,
|
|
155
|
+
source: 'OPENAI_API_COMPATIBILITY',
|
|
156
|
+
apiKey,
|
|
157
|
+
});
|
|
158
|
+
|
|
70
159
|
const prompt: Prompt = {
|
|
71
160
|
title,
|
|
72
161
|
content: lastMessage.content,
|
|
@@ -86,10 +175,29 @@ export async function handleChatCompletion(
|
|
|
86
175
|
const runId = `chatcmpl-${Math.random().toString(36).substring(2, 15)}`;
|
|
87
176
|
const created = Math.floor(Date.now() / 1000);
|
|
88
177
|
|
|
178
|
+
// Note: Send the initial chunk with role
|
|
179
|
+
const initialChunkData = {
|
|
180
|
+
id: runId,
|
|
181
|
+
object: 'chat.completion.chunk',
|
|
182
|
+
created,
|
|
183
|
+
model: model || 'promptbook-agent',
|
|
184
|
+
choices: [
|
|
185
|
+
{
|
|
186
|
+
index: 0,
|
|
187
|
+
delta: {
|
|
188
|
+
role: 'assistant',
|
|
189
|
+
content: '',
|
|
190
|
+
},
|
|
191
|
+
finish_reason: null,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(initialChunkData)}\n\n`));
|
|
196
|
+
|
|
89
197
|
let previousContent = '';
|
|
90
198
|
|
|
91
199
|
try {
|
|
92
|
-
await agent.callChatModelStream(prompt, (chunk: ChatPromptResult) => {
|
|
200
|
+
const result = await agent.callChatModelStream(prompt, (chunk: ChatPromptResult) => {
|
|
93
201
|
const fullContent = chunk.content;
|
|
94
202
|
const deltaContent = fullContent.substring(previousContent.length);
|
|
95
203
|
previousContent = fullContent;
|
|
@@ -114,6 +222,36 @@ export async function handleChatCompletion(
|
|
|
114
222
|
}
|
|
115
223
|
});
|
|
116
224
|
|
|
225
|
+
// Note: Identify the agent message
|
|
226
|
+
const agentMessageContent = {
|
|
227
|
+
role: 'MODEL',
|
|
228
|
+
content: result.content,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Record the agent message
|
|
232
|
+
await supabase.from(await $getTableName('ChatHistory')).insert({
|
|
233
|
+
createdAt: new Date().toISOString(),
|
|
234
|
+
messageHash: computeHash(agentMessageContent),
|
|
235
|
+
previousMessageHash: computeHash(userMessageContent),
|
|
236
|
+
agentName,
|
|
237
|
+
agentHash,
|
|
238
|
+
message: agentMessageContent,
|
|
239
|
+
promptbookEngineVersion: PROMPTBOOK_ENGINE_VERSION,
|
|
240
|
+
url: request.url,
|
|
241
|
+
ip,
|
|
242
|
+
userAgent,
|
|
243
|
+
language,
|
|
244
|
+
platform,
|
|
245
|
+
source: 'OPENAI_API_COMPATIBILITY',
|
|
246
|
+
apiKey,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Note: [🐱🚀] Save the learned data
|
|
250
|
+
const newAgentSource = agent.agentSource.value;
|
|
251
|
+
if (newAgentSource !== agentSource) {
|
|
252
|
+
await collection.updateAgentSource(agentName, newAgentSource);
|
|
253
|
+
}
|
|
254
|
+
|
|
117
255
|
const doneChunkData = {
|
|
118
256
|
id: runId,
|
|
119
257
|
object: 'chat.completion.chunk',
|
|
@@ -149,6 +287,36 @@ export async function handleChatCompletion(
|
|
|
149
287
|
} else {
|
|
150
288
|
const result = await agent.callChatModel(prompt);
|
|
151
289
|
|
|
290
|
+
// Note: Identify the agent message
|
|
291
|
+
const agentMessageContent = {
|
|
292
|
+
role: 'MODEL',
|
|
293
|
+
content: result.content,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// Record the agent message
|
|
297
|
+
await supabase.from(await $getTableName('ChatHistory')).insert({
|
|
298
|
+
createdAt: new Date().toISOString(),
|
|
299
|
+
messageHash: computeHash(agentMessageContent),
|
|
300
|
+
previousMessageHash: computeHash(userMessageContent),
|
|
301
|
+
agentName,
|
|
302
|
+
agentHash,
|
|
303
|
+
message: agentMessageContent,
|
|
304
|
+
promptbookEngineVersion: PROMPTBOOK_ENGINE_VERSION,
|
|
305
|
+
url: request.url,
|
|
306
|
+
ip,
|
|
307
|
+
userAgent,
|
|
308
|
+
language,
|
|
309
|
+
platform,
|
|
310
|
+
source: 'OPENAI_API_COMPATIBILITY',
|
|
311
|
+
apiKey,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Note: [🐱🚀] Save the learned data
|
|
315
|
+
const newAgentSource = agent.agentSource.value;
|
|
316
|
+
if (newAgentSource !== agentSource) {
|
|
317
|
+
await collection.updateAgentSource(agentName, newAgentSource);
|
|
318
|
+
}
|
|
319
|
+
|
|
152
320
|
return NextResponse.json({
|
|
153
321
|
id: `chatcmpl-${Math.random().toString(36).substring(2, 15)}`,
|
|
154
322
|
object: 'chat.completion',
|
|
@@ -181,3 +349,7 @@ export async function handleChatCompletion(
|
|
|
181
349
|
);
|
|
182
350
|
}
|
|
183
351
|
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* TODO: [🈹] Maybe move chat thread handling here
|
|
355
|
+
*/
|
|
@@ -35,13 +35,20 @@ export async function resolveInheritedAgentSource(
|
|
|
35
35
|
// So we might need to parse the URL to extract agent name if it matches expected pattern.
|
|
36
36
|
// For now, let's rely on fetch for external and check collection if it looks like a local reference (though FROM expects URL)
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const response = await fetch(parentUrl);
|
|
38
|
+
// If the URL is valid, we try to fetch it
|
|
39
|
+
// TODO: Handle authentication/tokens for private agents if needed
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
// TODO: [🧠] Do this logic more robustly
|
|
42
|
+
let fetchUrl = parentUrl;
|
|
43
|
+
if (!fetchUrl.endsWith('/api/book') && !fetchUrl.endsWith('.book') && !fetchUrl.endsWith('.md')) {
|
|
44
|
+
fetchUrl = `${fetchUrl.replace(/\/$/, '')}/api/book`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const response = await fetch(fetchUrl);
|
|
48
|
+
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(`Failed to fetch parent agent from ${fetchUrl}: ${response.status} ${response.statusText}`);
|
|
51
|
+
}
|
|
45
52
|
|
|
46
53
|
// We assume the response is the agent source text
|
|
47
54
|
// TODO: Handle content negotiation or JSON responses if the server returns JSON
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
import { NextRequest } from 'next/server';
|
|
3
|
+
import { SERVERS, SUPABASE_TABLE_PREFIX } from '../../config';
|
|
4
|
+
|
|
5
|
+
// Note: Re-implementing normalizeTo_PascalCase to avoid importing from @promptbook-local/utils which might have Node.js dependencies
|
|
6
|
+
function normalizeTo_PascalCase(text: string): string {
|
|
7
|
+
return text
|
|
8
|
+
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word) => {
|
|
9
|
+
return word.toUpperCase();
|
|
10
|
+
})
|
|
11
|
+
.replace(/\s+/g, '');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ApiKeyValidationResult = {
|
|
15
|
+
isValid: boolean;
|
|
16
|
+
token?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates an API key from the Authorization header.
|
|
22
|
+
* Returns validation result with status and optional error message.
|
|
23
|
+
*
|
|
24
|
+
* Note: This function provides explicit API key validation in addition to middleware.
|
|
25
|
+
* Use this when you need to verify API key validity and return specific error messages.
|
|
26
|
+
*/
|
|
27
|
+
export async function validateApiKey(request: NextRequest): Promise<ApiKeyValidationResult> {
|
|
28
|
+
const authHeader = request.headers.get('authorization');
|
|
29
|
+
|
|
30
|
+
// If no auth header, check if user has a session cookie (logged in via web)
|
|
31
|
+
if (!authHeader) {
|
|
32
|
+
const hasSession = request.cookies.has('sessionToken');
|
|
33
|
+
if (hasSession) {
|
|
34
|
+
return { isValid: true };
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
isValid: false,
|
|
38
|
+
error: 'Missing Authorization header. Provide a valid API key using "Authorization: Bearer ptbk_..."',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!authHeader.startsWith('Bearer ')) {
|
|
43
|
+
return {
|
|
44
|
+
isValid: false,
|
|
45
|
+
error: 'Invalid Authorization header format. Expected "Bearer <token>"',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const token = authHeader.split(' ')[1];
|
|
50
|
+
|
|
51
|
+
if (!token) {
|
|
52
|
+
return {
|
|
53
|
+
isValid: false,
|
|
54
|
+
error: 'No token provided in Authorization header',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!token.startsWith('ptbk_')) {
|
|
59
|
+
return {
|
|
60
|
+
isValid: false,
|
|
61
|
+
error: 'Invalid API key format. API keys must start with "ptbk_"',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Determine the table prefix based on the host
|
|
66
|
+
const host = request.headers.get('host');
|
|
67
|
+
let tablePrefix = SUPABASE_TABLE_PREFIX;
|
|
68
|
+
|
|
69
|
+
if (host && SERVERS && SERVERS.length > 0) {
|
|
70
|
+
if (SERVERS.some((server) => server === host)) {
|
|
71
|
+
let serverName = host;
|
|
72
|
+
serverName = serverName.replace(/\.ptbk\.io$/, '');
|
|
73
|
+
serverName = normalizeTo_PascalCase(serverName);
|
|
74
|
+
tablePrefix = `server_${serverName}_`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
79
|
+
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
80
|
+
|
|
81
|
+
if (!supabaseUrl || !supabaseKey) {
|
|
82
|
+
console.error('Supabase configuration missing for API key validation');
|
|
83
|
+
return {
|
|
84
|
+
isValid: false,
|
|
85
|
+
error: 'Server configuration error',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const supabase = createClient(supabaseUrl, supabaseKey, {
|
|
91
|
+
auth: {
|
|
92
|
+
persistSession: false,
|
|
93
|
+
autoRefreshToken: false,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const { data, error } = await supabase
|
|
98
|
+
.from(`${tablePrefix}ApiTokens`)
|
|
99
|
+
.select('id, isRevoked')
|
|
100
|
+
.eq('token', token)
|
|
101
|
+
.single();
|
|
102
|
+
|
|
103
|
+
if (error || !data) {
|
|
104
|
+
return {
|
|
105
|
+
isValid: false,
|
|
106
|
+
error: 'Invalid API key',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (data.isRevoked) {
|
|
111
|
+
return {
|
|
112
|
+
isValid: false,
|
|
113
|
+
error: 'API key has been revoked',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
isValid: true,
|
|
119
|
+
token,
|
|
120
|
+
};
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('Error validating API key:', error);
|
|
123
|
+
return {
|
|
124
|
+
isValid: false,
|
|
125
|
+
error: 'Error validating API key',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|