@promptbook/cli 0.104.0-2 → 0.104.0-3
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/package.json +3 -4
- package/apps/agents-server/public/swagger.json +115 -0
- package/apps/agents-server/scripts/generate-reserved-paths/generate-reserved-paths.ts +11 -7
- package/apps/agents-server/src/app/AddAgentButton.tsx +1 -2
- package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackClient.tsx +221 -274
- package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +94 -137
- package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +8 -8
- package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +15 -1
- package/apps/agents-server/src/app/agents/[agentName]/AgentOptionsMenu.tsx +1 -3
- package/apps/agents-server/src/app/agents/[agentName]/AgentProfileChat.tsx +29 -16
- package/apps/agents-server/src/app/agents/[agentName]/api/chat/route.ts +3 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/mcp/route.ts +6 -11
- package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +4 -1
- package/apps/agents-server/src/app/agents/[agentName]/code/api/route.ts +8 -6
- package/apps/agents-server/src/app/agents/[agentName]/code/page.tsx +33 -30
- package/apps/agents-server/src/app/api/agents/[agentName]/clone/route.ts +10 -12
- package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +1 -2
- package/apps/agents-server/src/app/api/agents/route.ts +1 -1
- package/apps/agents-server/src/app/api/api-tokens/route.ts +6 -7
- package/apps/agents-server/src/app/api/docs/book.md/route.ts +3 -0
- package/apps/agents-server/src/app/api/metadata/route.ts +5 -6
- package/apps/agents-server/src/app/api/upload/route.ts +9 -0
- package/apps/agents-server/src/app/page.tsx +1 -1
- package/apps/agents-server/src/app/swagger/page.tsx +14 -0
- package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +4 -2
- package/apps/agents-server/src/components/AgentProfile/QrCodeModal.tsx +0 -1
- package/apps/agents-server/src/components/Auth/AuthControls.tsx +5 -4
- package/apps/agents-server/src/components/Header/Header.tsx +27 -5
- package/apps/agents-server/src/components/Homepage/AgentCard.tsx +22 -3
- package/apps/agents-server/src/components/_utils/headlessParam.tsx +7 -3
- package/apps/agents-server/src/generated/reservedPaths.ts +6 -1
- package/apps/agents-server/src/middleware.ts +12 -3
- package/apps/agents-server/src/utils/auth.ts +117 -17
- package/apps/agents-server/src/utils/getUserIdFromRequest.ts +3 -1
- package/apps/agents-server/src/utils/handleChatCompletion.ts +9 -5
- package/apps/agents-server/src/utils/validateApiKey.ts +5 -10
- package/esm/index.es.js +55 -8
- package/esm/index.es.js.map +1 -1
- package/esm/typings/src/_packages/types.index.d.ts +2 -0
- package/esm/typings/src/book-components/Chat/types/ChatMessage.d.ts +7 -11
- package/esm/typings/src/types/Message.d.ts +49 -0
- package/esm/typings/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/umd/index.umd.js +55 -8
- package/umd/index.umd.js.map +1 -1
|
@@ -1,33 +1,133 @@
|
|
|
1
|
-
import { randomBytes, scrypt, timingSafeEqual } from 'crypto';
|
|
1
|
+
import { createHash, randomBytes, scrypt, timingSafeEqual } from 'crypto';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
|
+
import { PASSWORD_SECURITY_CONFIG } from '../../../../security.config';
|
|
3
4
|
|
|
4
5
|
const scryptAsync = promisify(scrypt);
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* @param password The
|
|
8
|
+
* Validates password input to prevent edge cases and DoS attacks
|
|
9
|
+
*
|
|
10
|
+
* @param password The password to validate
|
|
11
|
+
* @throws Error if password is invalid
|
|
12
|
+
*/
|
|
13
|
+
function validatePasswordInput(password: string): void {
|
|
14
|
+
if (typeof password !== 'string') {
|
|
15
|
+
throw new Error('Password must be a string');
|
|
16
|
+
}
|
|
17
|
+
if (password.length === 0) {
|
|
18
|
+
throw new Error('Password cannot be empty');
|
|
19
|
+
}
|
|
20
|
+
if (password.length < PASSWORD_SECURITY_CONFIG.MIN_PASSWORD_LENGTH) {
|
|
21
|
+
throw new Error(`Password must be at least ${PASSWORD_SECURITY_CONFIG.MIN_PASSWORD_LENGTH} characters`);
|
|
22
|
+
}
|
|
23
|
+
// Note: No hard max limit - long passwords are compacted via compactPassword()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Compacts a password for secure processing
|
|
28
|
+
*
|
|
29
|
+
* If the password is within MAX_PASSWORD_LENGTH, it is returned as-is.
|
|
30
|
+
* If longer, the password is split at MAX_PASSWORD_LENGTH and the second part
|
|
31
|
+
* is hashed with SHA256 before being appended to the first part.
|
|
32
|
+
*
|
|
33
|
+
* This prevents DoS attacks via extremely long passwords while still utilizing
|
|
34
|
+
* the full entropy of longer passwords.
|
|
35
|
+
*
|
|
36
|
+
* @param password The password to compact
|
|
37
|
+
* @returns The compacted password
|
|
38
|
+
*/
|
|
39
|
+
function compactPassword(password: string): string {
|
|
40
|
+
if (password.length <= PASSWORD_SECURITY_CONFIG.MAX_PASSWORD_LENGTH) {
|
|
41
|
+
return password;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const firstPart = password.slice(0, PASSWORD_SECURITY_CONFIG.MAX_PASSWORD_LENGTH);
|
|
45
|
+
const secondPart = password.slice(PASSWORD_SECURITY_CONFIG.MAX_PASSWORD_LENGTH);
|
|
46
|
+
|
|
47
|
+
// Hash the overflow part with SHA256 to bound its length while preserving entropy
|
|
48
|
+
const secondPartHash = createHash('sha256').update(secondPart, 'utf8').digest('hex');
|
|
49
|
+
|
|
50
|
+
return firstPart + secondPartHash;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Hashes a password using scrypt with secure parameters
|
|
55
|
+
*
|
|
56
|
+
* @param password The plain text password (minimum 8 characters, no maximum - long passwords are compacted)
|
|
10
57
|
* @returns The salt and hash formatted as "salt:hash"
|
|
58
|
+
* @throws Error if password validation fails
|
|
11
59
|
*/
|
|
12
60
|
export async function hashPassword(password: string): Promise<string> {
|
|
13
|
-
|
|
14
|
-
|
|
61
|
+
validatePasswordInput(password);
|
|
62
|
+
|
|
63
|
+
// Compact long passwords to prevent DoS while preserving entropy
|
|
64
|
+
const compactedPassword = compactPassword(password);
|
|
65
|
+
|
|
66
|
+
const salt = randomBytes(PASSWORD_SECURITY_CONFIG.SALT_LENGTH).toString('hex');
|
|
67
|
+
const derivedKey = (await scryptAsync(compactedPassword, salt, PASSWORD_SECURITY_CONFIG.KEY_LENGTH)) as Buffer;
|
|
68
|
+
|
|
69
|
+
// Clear password from memory as soon as possible (best effort)
|
|
70
|
+
// Note: JavaScript strings are immutable, so this is limited in effectiveness
|
|
15
71
|
return `${salt}:${derivedKey.toString('hex')}`;
|
|
16
72
|
}
|
|
17
73
|
|
|
18
74
|
/**
|
|
19
|
-
* Verifies a password against a stored hash
|
|
20
|
-
*
|
|
21
|
-
* @param password The plain text password
|
|
75
|
+
* Verifies a password against a stored hash using constant-time comparison
|
|
76
|
+
*
|
|
77
|
+
* @param password The plain text password to verify
|
|
22
78
|
* @param storedHash The stored hash in format "salt:hash"
|
|
23
|
-
* @returns True if the password matches
|
|
79
|
+
* @returns True if the password matches, false otherwise
|
|
24
80
|
*/
|
|
25
81
|
export async function verifyPassword(password: string, storedHash: string): Promise<boolean> {
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
82
|
+
// Validate inputs
|
|
83
|
+
if (typeof password !== 'string' || typeof storedHash !== 'string') {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (password.length === 0) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Compact long passwords the same way as during hashing
|
|
92
|
+
const compactedPassword = compactPassword(password);
|
|
93
|
+
|
|
94
|
+
const parts = storedHash.split(':');
|
|
95
|
+
if (parts.length !== 2) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const [salt, key] = parts;
|
|
100
|
+
if (!salt || !key) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate salt and key format (should be hex strings of expected length)
|
|
105
|
+
const expectedSaltLength = PASSWORD_SECURITY_CONFIG.SALT_LENGTH * 2; // hex encoding doubles length
|
|
106
|
+
const expectedKeyLength = PASSWORD_SECURITY_CONFIG.KEY_LENGTH * 2;
|
|
107
|
+
|
|
108
|
+
if (salt.length !== expectedSaltLength || key.length !== expectedKeyLength) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Validate hex format
|
|
113
|
+
const hexRegex = /^[0-9a-fA-F]+$/;
|
|
114
|
+
if (!hexRegex.test(salt) || !hexRegex.test(key)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const derivedKey = (await scryptAsync(compactedPassword, salt, PASSWORD_SECURITY_CONFIG.KEY_LENGTH)) as Buffer;
|
|
120
|
+
const keyBuffer = Buffer.from(key, 'hex');
|
|
121
|
+
|
|
122
|
+
// Ensure buffers are same length before timing-safe comparison
|
|
123
|
+
// This should always be true given our validation, but defense in depth
|
|
124
|
+
if (derivedKey.length !== keyBuffer.length) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return timingSafeEqual(derivedKey, keyBuffer);
|
|
129
|
+
} catch {
|
|
130
|
+
// Any error during verification should return false, not leak information
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
33
133
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { NextRequest } from 'next/server';
|
|
2
|
+
import { keepUnused } from '../../../../src/utils/organization/keepUnused';
|
|
2
3
|
import { $getTableName } from '../database/$getTableName';
|
|
3
4
|
import { $provideSupabaseForServer } from '../database/$provideSupabaseForServer';
|
|
4
5
|
import { getSession } from './session';
|
|
5
6
|
|
|
6
7
|
export async function getUserIdFromRequest(request: NextRequest): Promise<number | null> {
|
|
8
|
+
keepUnused(request); // Unused because we get user from session cookie for now
|
|
9
|
+
|
|
7
10
|
try {
|
|
8
11
|
// 1. Try to get user from session (cookie)
|
|
9
12
|
const session = await getSession();
|
|
@@ -24,7 +27,6 @@ export async function getUserIdFromRequest(request: NextRequest): Promise<number
|
|
|
24
27
|
// TODO: [🧠] Implement linking API keys to users if needed
|
|
25
28
|
// const authHeader = request.headers.get('authorization');
|
|
26
29
|
// ...
|
|
27
|
-
|
|
28
30
|
} catch (error) {
|
|
29
31
|
console.error('Error getting user ID from request:', error);
|
|
30
32
|
}
|
|
@@ -3,12 +3,11 @@ import { $provideSupabaseForServer } from '@/src/database/$provideSupabaseForSer
|
|
|
3
3
|
import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
|
|
4
4
|
import { $provideOpenAiAssistantExecutionToolsForServer } from '@/src/tools/$provideOpenAiAssistantExecutionToolsForServer';
|
|
5
5
|
import { Agent, computeAgentHash, parseAgentSource, PROMPTBOOK_ENGINE_VERSION } from '@promptbook-local/core';
|
|
6
|
-
import { OpenAiAssistantExecutionTools } from '@promptbook-local/openai';
|
|
7
6
|
import { ChatMessage, ChatPromptResult, Prompt, string_book, TODO_any } from '@promptbook-local/types';
|
|
8
7
|
import { computeHash } from '@promptbook-local/utils';
|
|
9
8
|
import { NextRequest, NextResponse } from 'next/server';
|
|
10
|
-
import { validateApiKey } from './validateApiKey';
|
|
11
9
|
import { isAgentDeleted } from '../app/agents/[agentName]/_utils';
|
|
10
|
+
import { validateApiKey } from './validateApiKey';
|
|
12
11
|
|
|
13
12
|
export async function handleChatCompletion(
|
|
14
13
|
request: NextRequest,
|
|
@@ -125,7 +124,9 @@ export async function handleChatCompletion(
|
|
|
125
124
|
let openAiAssistantExecutionTools = await $provideOpenAiAssistantExecutionToolsForServer();
|
|
126
125
|
|
|
127
126
|
if (assistantCache?.assistantId) {
|
|
128
|
-
console.log(
|
|
127
|
+
console.log(
|
|
128
|
+
`[🐱🚀] Reusing assistant ${assistantCache.assistantId} for agent ${agentName} (hash: ${agentHash})`,
|
|
129
|
+
);
|
|
129
130
|
openAiAssistantExecutionTools = openAiAssistantExecutionTools.getAssistant(assistantCache.assistantId);
|
|
130
131
|
} else {
|
|
131
132
|
console.log(`[🐱🚀] Creating NEW assistant for agent ${agentName} (hash: ${agentHash})`);
|
|
@@ -138,7 +139,9 @@ export async function handleChatCompletion(
|
|
|
138
139
|
// Note: Append context to instructions
|
|
139
140
|
const contextLines = agentSource.split('\n').filter((line) => line.startsWith('CONTEXT '));
|
|
140
141
|
const contextInstructions = contextLines.join('\n');
|
|
141
|
-
const instructions = contextInstructions
|
|
142
|
+
const instructions = contextInstructions
|
|
143
|
+
? `${baseInstructions}\n\n${contextInstructions}`
|
|
144
|
+
: baseInstructions;
|
|
142
145
|
|
|
143
146
|
// Create assistant
|
|
144
147
|
const newAssistantTools = await openAiAssistantExecutionTools.createNewAssistant({
|
|
@@ -182,8 +185,9 @@ export async function handleChatCompletion(
|
|
|
182
185
|
const previousMessages = threadMessages.slice(0, -1);
|
|
183
186
|
|
|
184
187
|
const thread: ChatMessage[] = previousMessages.map((msg: TODO_any, index: number) => ({
|
|
188
|
+
// channel: 'PROMPTBOOK_CHAT',
|
|
185
189
|
id: `msg-${index}`, // Placeholder ID
|
|
186
|
-
|
|
190
|
+
sender: msg.role === 'assistant' ? 'agent' : 'user', // Mapping standard OpenAI roles
|
|
187
191
|
content: msg.content,
|
|
188
192
|
isComplete: true,
|
|
189
193
|
date: new Date(), // We don't have the real date, using current
|
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
import { createClient } from '@supabase/supabase-js';
|
|
2
2
|
import { NextRequest } from 'next/server';
|
|
3
|
-
import { SERVERS, SUPABASE_TABLE_PREFIX } from '../../config';
|
|
4
3
|
import { $getTableName } from '../database/$getTableName';
|
|
5
4
|
|
|
6
|
-
// Note: Re-implementing normalizeTo_PascalCase to avoid importing from @promptbook-local/utils which might have Node.js dependencies
|
|
7
|
-
function normalizeTo_PascalCase(text: string): string {
|
|
8
|
-
return text
|
|
9
|
-
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word) => {
|
|
10
|
-
return word.toUpperCase();
|
|
11
|
-
})
|
|
12
|
-
.replace(/\s+/g, '');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
5
|
export type ApiKeyValidationResult = {
|
|
16
6
|
isValid: boolean;
|
|
17
7
|
token?: string;
|
|
@@ -63,10 +53,14 @@ export async function validateApiKey(request: NextRequest): Promise<ApiKeyValida
|
|
|
63
53
|
};
|
|
64
54
|
}
|
|
65
55
|
|
|
56
|
+
/*
|
|
57
|
+
Note: [🐔] This code was commented out because results of it are unused
|
|
58
|
+
|
|
66
59
|
// Determine the table prefix based on the host
|
|
67
60
|
const host = request.headers.get('host');
|
|
68
61
|
let tablePrefix = SUPABASE_TABLE_PREFIX;
|
|
69
62
|
|
|
63
|
+
|
|
70
64
|
if (host && SERVERS && SERVERS.length > 0) {
|
|
71
65
|
if (SERVERS.some((server) => server === host)) {
|
|
72
66
|
let serverName = host;
|
|
@@ -75,6 +69,7 @@ export async function validateApiKey(request: NextRequest): Promise<ApiKeyValida
|
|
|
75
69
|
tablePrefix = `server_${serverName}_`;
|
|
76
70
|
}
|
|
77
71
|
}
|
|
72
|
+
*/
|
|
78
73
|
|
|
79
74
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
80
75
|
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
package/esm/index.es.js
CHANGED
|
@@ -47,7 +47,7 @@ const BOOK_LANGUAGE_VERSION = '2.0.0';
|
|
|
47
47
|
* @generated
|
|
48
48
|
* @see https://github.com/webgptorg/promptbook
|
|
49
49
|
*/
|
|
50
|
-
const PROMPTBOOK_ENGINE_VERSION = '0.104.0-
|
|
50
|
+
const PROMPTBOOK_ENGINE_VERSION = '0.104.0-3';
|
|
51
51
|
/**
|
|
52
52
|
* TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
|
|
53
53
|
* Note: [💞] Ignore a discrepancy between file name and entity name
|
|
@@ -19448,7 +19448,7 @@ class OpenAiCompatibleExecutionTools {
|
|
|
19448
19448
|
let threadMessages = [];
|
|
19449
19449
|
if ('thread' in prompt && Array.isArray(prompt.thread)) {
|
|
19450
19450
|
threadMessages = prompt.thread.map((msg) => ({
|
|
19451
|
-
role: msg.
|
|
19451
|
+
role: msg.sender === 'assistant' ? 'assistant' : 'user',
|
|
19452
19452
|
content: msg.content,
|
|
19453
19453
|
}));
|
|
19454
19454
|
}
|
|
@@ -26114,17 +26114,64 @@ function parseAgentSourceWithCommitments(agentSource) {
|
|
|
26114
26114
|
};
|
|
26115
26115
|
}
|
|
26116
26116
|
const lines = agentSource.split('\n');
|
|
26117
|
-
|
|
26117
|
+
let agentName = null;
|
|
26118
|
+
let agentNameLineIndex = -1;
|
|
26119
|
+
// Find the agent name: first non-empty line that is not a commitment and not a horizontal line
|
|
26120
|
+
for (let i = 0; i < lines.length; i++) {
|
|
26121
|
+
const line = lines[i];
|
|
26122
|
+
if (line === undefined) {
|
|
26123
|
+
continue;
|
|
26124
|
+
}
|
|
26125
|
+
const trimmed = line.trim();
|
|
26126
|
+
if (!trimmed) {
|
|
26127
|
+
continue;
|
|
26128
|
+
}
|
|
26129
|
+
const isHorizontal = HORIZONTAL_LINE_PATTERN.test(line);
|
|
26130
|
+
if (isHorizontal) {
|
|
26131
|
+
continue;
|
|
26132
|
+
}
|
|
26133
|
+
let isCommitment = false;
|
|
26134
|
+
for (const definition of COMMITMENT_REGISTRY) {
|
|
26135
|
+
const typeRegex = definition.createTypeRegex();
|
|
26136
|
+
const match = typeRegex.exec(trimmed);
|
|
26137
|
+
if (match && ((_a = match.groups) === null || _a === void 0 ? void 0 : _a.type)) {
|
|
26138
|
+
isCommitment = true;
|
|
26139
|
+
break;
|
|
26140
|
+
}
|
|
26141
|
+
}
|
|
26142
|
+
if (!isCommitment) {
|
|
26143
|
+
agentName = trimmed;
|
|
26144
|
+
agentNameLineIndex = i;
|
|
26145
|
+
break;
|
|
26146
|
+
}
|
|
26147
|
+
}
|
|
26118
26148
|
const commitments = [];
|
|
26119
26149
|
const nonCommitmentLines = [];
|
|
26120
|
-
//
|
|
26121
|
-
|
|
26122
|
-
|
|
26150
|
+
// Add lines before agentName that are horizontal lines (they are non-commitment)
|
|
26151
|
+
for (let i = 0; i < agentNameLineIndex; i++) {
|
|
26152
|
+
const line = lines[i];
|
|
26153
|
+
if (line === undefined) {
|
|
26154
|
+
continue;
|
|
26155
|
+
}
|
|
26156
|
+
const trimmed = line.trim();
|
|
26157
|
+
if (!trimmed) {
|
|
26158
|
+
continue;
|
|
26159
|
+
}
|
|
26160
|
+
const isHorizontal = HORIZONTAL_LINE_PATTERN.test(line);
|
|
26161
|
+
if (isHorizontal) {
|
|
26162
|
+
nonCommitmentLines.push(line);
|
|
26163
|
+
}
|
|
26164
|
+
// Note: Commitments before agentName are not added to nonCommitmentLines
|
|
26165
|
+
}
|
|
26166
|
+
// Add the agent name line to non-commitment lines
|
|
26167
|
+
if (agentNameLineIndex >= 0) {
|
|
26168
|
+
nonCommitmentLines.push(lines[agentNameLineIndex]);
|
|
26123
26169
|
}
|
|
26124
26170
|
// Parse commitments with multiline support
|
|
26125
26171
|
let currentCommitment = null;
|
|
26126
|
-
// Process lines starting from the
|
|
26127
|
-
|
|
26172
|
+
// Process lines starting from after the agent name line
|
|
26173
|
+
const startIndex = agentNameLineIndex >= 0 ? agentNameLineIndex + 1 : 0;
|
|
26174
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
26128
26175
|
const line = lines[i];
|
|
26129
26176
|
if (line === undefined) {
|
|
26130
26177
|
continue;
|