@promptbook/cli 0.103.0-48 → 0.103.0-49
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/README.md +1 -1
- package/apps/agents-server/TODO.txt +6 -5
- package/apps/agents-server/config.ts +130 -0
- package/apps/agents-server/next.config.ts +1 -1
- package/apps/agents-server/public/fonts/OpenMoji-black-glyf.woff2 +0 -0
- package/apps/agents-server/public/fonts/download-font.js +22 -0
- package/apps/agents-server/src/app/[agentName]/[...rest]/page.tsx +6 -0
- package/apps/agents-server/src/app/[agentName]/page.tsx +1 -0
- package/apps/agents-server/src/app/actions.ts +37 -2
- package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +68 -0
- package/apps/agents-server/src/app/agents/[agentName]/AgentQrCode.tsx +55 -0
- package/apps/agents-server/src/app/agents/[agentName]/AgentUrlCopy.tsx +4 -5
- package/apps/agents-server/src/app/agents/[agentName]/CopyField.tsx +44 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/book/route.ts +8 -8
- package/apps/agents-server/src/app/agents/[agentName]/api/chat/route.ts +100 -24
- package/apps/agents-server/src/app/agents/[agentName]/api/feedback/route.ts +54 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/route.ts +6 -6
- package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/systemMessage/route.ts +3 -3
- package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +6 -7
- package/apps/agents-server/src/app/agents/[agentName]/book/BookEditorWrapper.tsx +4 -5
- package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +9 -2
- package/apps/agents-server/src/app/agents/[agentName]/book+chat/AgentBookAndChat.tsx +23 -0
- package/apps/agents-server/src/app/agents/[agentName]/book+chat/{AgentBookAndChatComponent.tsx → AgentBookAndChatComponent.tsx.todo} +4 -4
- package/apps/agents-server/src/app/agents/[agentName]/book+chat/page.tsx +28 -17
- package/apps/agents-server/src/app/agents/[agentName]/book+chat/page.tsx.todo +21 -0
- package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatWrapper.tsx +34 -4
- package/apps/agents-server/src/app/agents/[agentName]/chat/page.tsx +4 -1
- package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +42 -0
- package/apps/agents-server/src/app/agents/[agentName]/page.tsx +109 -106
- package/apps/agents-server/src/app/agents/page.tsx +1 -1
- package/apps/agents-server/src/app/api/auth/login/route.ts +65 -0
- package/apps/agents-server/src/app/api/auth/logout/route.ts +7 -0
- package/apps/agents-server/src/app/api/metadata/route.ts +116 -0
- package/apps/agents-server/src/app/api/upload/route.ts +7 -3
- package/apps/agents-server/src/app/api/users/[username]/route.ts +75 -0
- package/apps/agents-server/src/app/api/users/route.ts +71 -0
- package/apps/agents-server/src/app/globals.css +35 -1
- package/apps/agents-server/src/app/layout.tsx +43 -23
- package/apps/agents-server/src/app/metadata/MetadataClient.tsx +271 -0
- package/apps/agents-server/src/app/metadata/page.tsx +13 -0
- package/apps/agents-server/src/app/not-found.tsx +5 -0
- package/apps/agents-server/src/app/page.tsx +84 -46
- package/apps/agents-server/src/components/Auth/AuthControls.tsx +123 -0
- package/apps/agents-server/src/components/ErrorPage/ErrorPage.tsx +33 -0
- package/apps/agents-server/src/components/ForbiddenPage/ForbiddenPage.tsx +15 -0
- package/apps/agents-server/src/components/Header/Header.tsx +146 -0
- package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +27 -0
- package/apps/agents-server/src/components/LoginDialog/LoginDialog.tsx +40 -0
- package/apps/agents-server/src/components/LoginForm/LoginForm.tsx +109 -0
- package/apps/agents-server/src/components/NotFoundPage/NotFoundPage.tsx +17 -0
- package/apps/agents-server/src/components/UsersList/UsersList.tsx +190 -0
- package/apps/agents-server/src/components/VercelDeploymentCard/VercelDeploymentCard.tsx +60 -0
- package/apps/agents-server/src/database/$getTableName.ts +18 -0
- package/apps/agents-server/src/database/$provideSupabase.ts +2 -2
- package/apps/agents-server/src/database/$provideSupabaseForServer.ts +3 -3
- package/apps/agents-server/src/database/getMetadata.ts +31 -0
- package/apps/agents-server/src/database/metadataDefaults.ts +32 -0
- package/apps/agents-server/src/database/schema.sql +81 -33
- package/apps/agents-server/src/database/schema.ts +35 -1
- package/apps/agents-server/src/middleware.ts +162 -0
- package/apps/agents-server/src/tools/$provideAgentCollectionForServer.ts +11 -7
- package/apps/agents-server/src/tools/$provideCdnForServer.ts +1 -1
- package/apps/agents-server/src/tools/$provideExecutionToolsForServer.ts +11 -13
- package/apps/agents-server/src/tools/$provideOpenAiAssistantExecutionToolsForServer.ts +7 -7
- package/apps/agents-server/src/tools/$provideServer.ts +39 -0
- package/apps/agents-server/src/utils/auth.ts +33 -0
- package/apps/agents-server/src/utils/cdn/utils/nameToSubfolderPath.ts +1 -1
- package/apps/agents-server/src/utils/getCurrentUser.ts +32 -0
- package/apps/agents-server/src/utils/isIpAllowed.ts +101 -0
- package/apps/agents-server/src/utils/isUserAdmin.ts +31 -0
- package/apps/agents-server/src/utils/session.ts +50 -0
- package/apps/agents-server/tailwind.config.ts +2 -0
- package/esm/index.es.js +147 -31
- package/esm/index.es.js.map +1 -1
- package/esm/typings/servers.d.ts +1 -0
- package/esm/typings/src/_packages/types.index.d.ts +2 -0
- package/esm/typings/src/_packages/utils.index.d.ts +2 -0
- package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +12 -2
- package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +14 -8
- package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabaseOptions.d.ts +10 -0
- package/esm/typings/src/commitments/MESSAGE/InitialMessageCommitmentDefinition.d.ts +28 -0
- package/esm/typings/src/commitments/index.d.ts +2 -1
- package/esm/typings/src/config.d.ts +1 -0
- package/esm/typings/src/errors/DatabaseError.d.ts +2 -2
- package/esm/typings/src/errors/WrappedError.d.ts +2 -2
- package/esm/typings/src/execution/ExecutionTask.d.ts +2 -2
- package/esm/typings/src/execution/LlmExecutionTools.d.ts +6 -1
- package/esm/typings/src/llm-providers/_common/register/$provideLlmToolsForWizardOrCli.d.ts +2 -2
- package/esm/typings/src/llm-providers/agent/Agent.d.ts +11 -3
- package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +6 -1
- package/esm/typings/src/llm-providers/agent/RemoteAgent.d.ts +6 -2
- package/esm/typings/src/llm-providers/openai/OpenAiAssistantExecutionTools.d.ts +6 -1
- package/esm/typings/src/remote-server/startAgentServer.d.ts +2 -2
- package/esm/typings/src/utils/color/Color.d.ts +7 -0
- package/esm/typings/src/utils/color/Color.test.d.ts +1 -0
- package/esm/typings/src/utils/environment/$getGlobalScope.d.ts +2 -2
- package/esm/typings/src/utils/misc/computeHash.d.ts +11 -0
- package/esm/typings/src/utils/misc/computeHash.test.d.ts +1 -0
- package/esm/typings/src/utils/organization/$sideEffect.d.ts +2 -2
- package/esm/typings/src/utils/organization/$side_effect.d.ts +2 -2
- package/esm/typings/src/utils/organization/TODO_USE.d.ts +2 -2
- package/esm/typings/src/utils/organization/keepUnused.d.ts +2 -2
- package/esm/typings/src/utils/organization/preserve.d.ts +3 -3
- package/esm/typings/src/utils/organization/really_any.d.ts +7 -0
- package/esm/typings/src/utils/serialization/asSerializable.d.ts +2 -2
- package/esm/typings/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/umd/index.umd.js +147 -31
- package/umd/index.umd.js.map +1 -1
- package/apps/agents-server/config.ts.todo +0 -38
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { TODO_any } from '@promptbook-local/types';
|
|
2
|
+
import { createClient } from '@supabase/supabase-js';
|
|
3
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
4
|
+
import { SERVERS, SUPABASE_TABLE_PREFIX } from '../config';
|
|
5
|
+
import { isIpAllowed } from './utils/isIpAllowed';
|
|
6
|
+
|
|
7
|
+
// Note: Re-implementing normalizeTo_PascalCase to avoid importing from @promptbook-local/utils which might have Node.js dependencies
|
|
8
|
+
function normalizeTo_PascalCase(text: string): string {
|
|
9
|
+
return text
|
|
10
|
+
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => {
|
|
11
|
+
return word.toUpperCase();
|
|
12
|
+
})
|
|
13
|
+
.replace(/\s+/g, '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function middleware(req: NextRequest) {
|
|
17
|
+
// 1. Get client IP
|
|
18
|
+
let ip = (req as TODO_any).ip;
|
|
19
|
+
const xForwardedFor = req.headers.get('x-forwarded-for');
|
|
20
|
+
if (!ip && xForwardedFor) {
|
|
21
|
+
ip = xForwardedFor.split(',')[0].trim();
|
|
22
|
+
}
|
|
23
|
+
// Fallback for local development if needed, though req.ip is usually ::1 or 127.0.0.1
|
|
24
|
+
ip = ip || '127.0.0.1';
|
|
25
|
+
|
|
26
|
+
// 2. Determine allowed IPs
|
|
27
|
+
// Priority: Metadata > Environment Variable
|
|
28
|
+
|
|
29
|
+
const allowedIpsEnv = process.env.RESTRICT_IP;
|
|
30
|
+
let allowedIpsMetadata: string | null = null;
|
|
31
|
+
|
|
32
|
+
// To fetch metadata, we need to know the table name, which depends on the host
|
|
33
|
+
const host = req.headers.get('host');
|
|
34
|
+
|
|
35
|
+
if (host) {
|
|
36
|
+
let tablePrefix = SUPABASE_TABLE_PREFIX;
|
|
37
|
+
|
|
38
|
+
if (SERVERS && SERVERS.length > 0) {
|
|
39
|
+
// Logic mirrored from src/tools/$provideServer.ts
|
|
40
|
+
if (SERVERS.some((server) => server === host)) {
|
|
41
|
+
let serverName = host;
|
|
42
|
+
serverName = serverName.replace(/\.ptbk\.io$/, '');
|
|
43
|
+
serverName = normalizeTo_PascalCase(serverName);
|
|
44
|
+
tablePrefix = `server_${serverName}_`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
49
|
+
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
50
|
+
|
|
51
|
+
if (supabaseUrl && supabaseKey) {
|
|
52
|
+
try {
|
|
53
|
+
const supabase = createClient(supabaseUrl, supabaseKey, {
|
|
54
|
+
auth: {
|
|
55
|
+
persistSession: false,
|
|
56
|
+
autoRefreshToken: false,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const { data } = await supabase
|
|
61
|
+
.from(`${tablePrefix}Metadata`)
|
|
62
|
+
.select('value')
|
|
63
|
+
.eq('key', 'RESTRICT_IP')
|
|
64
|
+
.single();
|
|
65
|
+
|
|
66
|
+
if (data && data.value) {
|
|
67
|
+
allowedIpsMetadata = data.value;
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('Error fetching metadata in middleware:', error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const allowedIps = allowedIpsMetadata !== null && allowedIpsMetadata !== undefined ? allowedIpsMetadata : allowedIpsEnv;
|
|
76
|
+
|
|
77
|
+
if (isIpAllowed(ip, allowedIps)) {
|
|
78
|
+
// 3. Custom Domain Routing
|
|
79
|
+
// If the host is not one of the configured SERVERS, try to find an agent with a matching META LINK
|
|
80
|
+
|
|
81
|
+
if (host && SERVERS && !SERVERS.some((server) => server === host)) {
|
|
82
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
83
|
+
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
84
|
+
|
|
85
|
+
if (supabaseUrl && supabaseKey) {
|
|
86
|
+
const supabase = createClient(supabaseUrl, supabaseKey, {
|
|
87
|
+
auth: {
|
|
88
|
+
persistSession: false,
|
|
89
|
+
autoRefreshToken: false,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Determine prefixes to check
|
|
94
|
+
// We check all configured servers because the custom domain could point to any of them
|
|
95
|
+
// (or if they share the database, we need to check the relevant tables)
|
|
96
|
+
const serversToCheck = SERVERS;
|
|
97
|
+
|
|
98
|
+
// TODO: [🧠] If there are many servers, this loop might be slow. Optimize if needed.
|
|
99
|
+
for (const serverHost of serversToCheck) {
|
|
100
|
+
let serverName = serverHost;
|
|
101
|
+
serverName = serverName.replace(/\.ptbk\.io$/, '');
|
|
102
|
+
serverName = normalizeTo_PascalCase(serverName);
|
|
103
|
+
const prefix = `server_${serverName}_`;
|
|
104
|
+
|
|
105
|
+
// Search for agent with matching META LINK
|
|
106
|
+
// agentProfile->links is an array of strings
|
|
107
|
+
// We check if it contains the host, or https://host, or http://host
|
|
108
|
+
|
|
109
|
+
const searchLinks = [host, `https://${host}`, `http://${host}`];
|
|
110
|
+
|
|
111
|
+
// Construct OR filter: agentProfile.cs.{"links":["link1"]},agentProfile.cs.{"links":["link2"]},...
|
|
112
|
+
const orFilter = searchLinks.map(link => `agentProfile.cs.{"links":["${link}"]}`).join(',');
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const { data } = await supabase
|
|
116
|
+
.from(`${prefix}Agent`)
|
|
117
|
+
.select('agentName')
|
|
118
|
+
.or(orFilter)
|
|
119
|
+
.limit(1)
|
|
120
|
+
.single();
|
|
121
|
+
|
|
122
|
+
if (data && data.agentName) {
|
|
123
|
+
// Found the agent!
|
|
124
|
+
const url = req.nextUrl.clone();
|
|
125
|
+
url.pathname = `/${data.agentName}`;
|
|
126
|
+
|
|
127
|
+
// Pass the server context to the app via header
|
|
128
|
+
const requestHeaders = new Headers(req.headers);
|
|
129
|
+
requestHeaders.set('x-promptbook-server', serverHost);
|
|
130
|
+
|
|
131
|
+
return NextResponse.rewrite(url, {
|
|
132
|
+
request: {
|
|
133
|
+
headers: requestHeaders,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// Ignore error (e.g. table not found, or agent not found) and continue to next server
|
|
139
|
+
// console.error(`Error checking server ${serverHost} for custom domain ${host}:`, error);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return NextResponse.next();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return new NextResponse('Forbidden', { status: 403 });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const config = {
|
|
152
|
+
matcher: [
|
|
153
|
+
/*
|
|
154
|
+
* Match all request paths except for the ones starting with:
|
|
155
|
+
* - _next/static (static files)
|
|
156
|
+
* - _next/image (image optimization files)
|
|
157
|
+
* - favicon.ico (favicon file)
|
|
158
|
+
* - public folder
|
|
159
|
+
*/
|
|
160
|
+
'/((?!_next/static|_next/image|favicon.ico|logo-|fonts/).*)',
|
|
161
|
+
],
|
|
162
|
+
};
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { AgentCollectionInSupabase } from '@promptbook-local/core';
|
|
4
4
|
import { AgentCollection } from '@promptbook-local/types';
|
|
5
|
+
import { just } from '../../../../src/utils/organization/just';
|
|
5
6
|
import { $provideSupabaseForServer } from '../database/$provideSupabaseForServer';
|
|
7
|
+
import { $provideServer } from './$provideServer';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Cache of provided agent collection
|
|
@@ -12,22 +14,22 @@ import { $provideSupabaseForServer } from '../database/$provideSupabaseForServer
|
|
|
12
14
|
let agentCollection: null | AgentCollection = null;
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
|
-
*
|
|
17
|
+
* [🐱🚀]
|
|
16
18
|
*/
|
|
17
19
|
export async function $provideAgentCollectionForServer(): Promise<AgentCollection> {
|
|
18
20
|
// <- Note: This function is potentially async
|
|
19
21
|
|
|
20
|
-
// TODO:
|
|
22
|
+
// TODO: [🐱🚀] [🌕] DRY
|
|
21
23
|
|
|
22
|
-
const isVerbose = true; // <- TODO:
|
|
24
|
+
const isVerbose = true; // <- TODO: [🐱🚀] Pass
|
|
23
25
|
|
|
24
|
-
if (agentCollection !== null) {
|
|
25
|
-
console.log('
|
|
26
|
+
if (agentCollection !== null && just(false /* <- TODO: [🐱🚀] Fix caching */)) {
|
|
27
|
+
console.log('[🐱🚀] Returning cached agent collection');
|
|
26
28
|
return agentCollection;
|
|
27
|
-
// TODO:
|
|
29
|
+
// TODO: [🐱🚀] Be aware of options changes
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
console.log('
|
|
32
|
+
console.log('[🐱🚀] Creating NEW agent collection');
|
|
31
33
|
|
|
32
34
|
/*
|
|
33
35
|
// TODO: [🧟♂️][◽] DRY:
|
|
@@ -41,9 +43,11 @@ export async function $provideAgentCollectionForServer(): Promise<AgentCollectio
|
|
|
41
43
|
*/
|
|
42
44
|
|
|
43
45
|
const supabase = $provideSupabaseForServer();
|
|
46
|
+
const { tablePrefix } = await $provideServer();
|
|
44
47
|
|
|
45
48
|
agentCollection = new AgentCollectionInSupabase(supabase, {
|
|
46
49
|
isVerbose,
|
|
50
|
+
tablePrefix,
|
|
47
51
|
});
|
|
48
52
|
|
|
49
53
|
return agentCollection;
|
|
@@ -43,11 +43,11 @@ $sideEffect(
|
|
|
43
43
|
_MarkitdownScraperMetadataRegistration,
|
|
44
44
|
_PdfScraperMetadataRegistration,
|
|
45
45
|
_WebsiteScraperMetadataRegistration,
|
|
46
|
-
// <- TODO:
|
|
46
|
+
// <- TODO: [🐱🚀] Export all registrations from one variabile in `@promptbook/core`
|
|
47
47
|
);
|
|
48
48
|
$sideEffect(/* [㊗] */ _OpenAiRegistration);
|
|
49
49
|
$sideEffect(/* [㊗] */ _GoogleRegistration);
|
|
50
|
-
// <- TODO:
|
|
50
|
+
// <- TODO: [🐱🚀] Allow to dynamically install required metadata
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Cache of provided execution tools
|
|
@@ -56,8 +56,6 @@ $sideEffect(/* [㊗] */ _GoogleRegistration);
|
|
|
56
56
|
*/
|
|
57
57
|
let executionTools: null | ExecutionTools = null;
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
59
|
/*
|
|
62
60
|
TODO: [▶️]
|
|
63
61
|
type ProvideExecutionToolsForServerOptions = {
|
|
@@ -66,25 +64,25 @@ type ProvideExecutionToolsForServerOptions = {
|
|
|
66
64
|
*/
|
|
67
65
|
|
|
68
66
|
/**
|
|
69
|
-
*
|
|
67
|
+
* [🐱🚀]
|
|
70
68
|
*/
|
|
71
69
|
export async function $provideExecutionToolsForServer(): Promise<ExecutionTools> {
|
|
72
|
-
// TODO:
|
|
70
|
+
// TODO: [🐱🚀] [🌕] DRY
|
|
73
71
|
|
|
74
|
-
// const path = '../../agents'; // <- TODO:
|
|
75
|
-
const isVerbose = true; // <- TODO:
|
|
76
|
-
const isCacheReloaded = false; // <- TODO:
|
|
72
|
+
// const path = '../../agents'; // <- TODO: [🐱🚀] Pass
|
|
73
|
+
const isVerbose = true; // <- TODO: [🐱🚀] Pass
|
|
74
|
+
const isCacheReloaded = false; // <- TODO: [🐱🚀] Pass
|
|
77
75
|
const cliOptions = {
|
|
78
76
|
provider: 'BRING_YOUR_OWN_KEYS',
|
|
79
|
-
} as TODO_any; // <- TODO:
|
|
77
|
+
} as TODO_any; // <- TODO: [🐱🚀] Pass
|
|
80
78
|
|
|
81
79
|
if (executionTools !== null) {
|
|
82
|
-
console.log('
|
|
80
|
+
console.log('[🐱🚀] Returning cached execution tools');
|
|
83
81
|
return executionTools;
|
|
84
|
-
// TODO:
|
|
82
|
+
// TODO: [🐱🚀] Be aware of options changes
|
|
85
83
|
}
|
|
86
84
|
|
|
87
|
-
console.log('
|
|
85
|
+
console.log('[🐱🚀] Creating NEW execution tools');
|
|
88
86
|
|
|
89
87
|
// TODO: DRY [◽]
|
|
90
88
|
const prepareAndScrapeOptions = {
|
|
@@ -10,23 +10,23 @@ import { OpenAiAssistantExecutionTools } from '@promptbook-local/openai';
|
|
|
10
10
|
let executionTools: null | OpenAiAssistantExecutionTools = null;
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* [🐱🚀]
|
|
14
14
|
*/
|
|
15
15
|
export async function $provideOpenAiAssistantExecutionToolsForServer(): Promise<OpenAiAssistantExecutionTools> {
|
|
16
|
-
// TODO:
|
|
17
|
-
const isVerbose = true; // <- TODO:
|
|
16
|
+
// TODO: [🐱🚀] [🌕] DRY
|
|
17
|
+
const isVerbose = true; // <- TODO: [🐱🚀] Pass
|
|
18
18
|
|
|
19
19
|
if (executionTools !== null) {
|
|
20
|
-
console.log('
|
|
20
|
+
console.log('[🐱🚀] Returning cached OpenAiAssistantExecutionTools');
|
|
21
21
|
return executionTools;
|
|
22
|
-
// TODO:
|
|
22
|
+
// TODO: [🐱🚀] Be aware of options changes
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
console.log('
|
|
25
|
+
console.log('[🐱🚀] Creating NEW OpenAiAssistantExecutionTools');
|
|
26
26
|
|
|
27
27
|
executionTools = new OpenAiAssistantExecutionTools({
|
|
28
28
|
apiKey: process.env.OPENAI_API_KEY,
|
|
29
|
-
assistantId: 'abstract_assistant', // <- TODO:
|
|
29
|
+
assistantId: 'abstract_assistant', // <- TODO: [🐱🚀] In `OpenAiAssistantExecutionTools` Allow to create abstract assistants with `isCreatingNewAssistantsAllowed`
|
|
30
30
|
isCreatingNewAssistantsAllowed: true,
|
|
31
31
|
isVerbose,
|
|
32
32
|
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NEXT_PUBLIC_URL, SERVERS, SUPABASE_TABLE_PREFIX } from '@/config';
|
|
2
|
+
import { normalizeTo_PascalCase } from '@promptbook-local/utils';
|
|
3
|
+
import { headers } from 'next/headers';
|
|
4
|
+
|
|
5
|
+
export async function $provideServer() {
|
|
6
|
+
if (!SERVERS) {
|
|
7
|
+
return {
|
|
8
|
+
publicUrl: NEXT_PUBLIC_URL || new URL(`https://${(await headers()).get('host') || 'localhost:4440'}`),
|
|
9
|
+
tablePrefix: SUPABASE_TABLE_PREFIX,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const headersList = await headers();
|
|
14
|
+
let host = headersList.get('host');
|
|
15
|
+
const xPromptbookServer = headersList.get('x-promptbook-server');
|
|
16
|
+
|
|
17
|
+
if (host === null) {
|
|
18
|
+
throw new Error('Host header is missing');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// If host is not in known servers, check if we have a context header from middleware
|
|
22
|
+
if (!SERVERS.some((server) => server === host)) {
|
|
23
|
+
if (xPromptbookServer && SERVERS.some((server) => server === xPromptbookServer)) {
|
|
24
|
+
host = xPromptbookServer;
|
|
25
|
+
} else {
|
|
26
|
+
throw new Error(`Server with host "${host}" is not configured in SERVERS`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let serverName = host;
|
|
31
|
+
|
|
32
|
+
serverName = serverName.replace(/\.ptbk\.io$/, '');
|
|
33
|
+
serverName = normalizeTo_PascalCase(serverName);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
publicUrl: new URL(`https://${host}`),
|
|
37
|
+
tablePrefix: `server_${serverName}_`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { randomBytes, scrypt, timingSafeEqual } from 'crypto';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
|
|
4
|
+
const scryptAsync = promisify(scrypt);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hashes a password using scrypt
|
|
8
|
+
*
|
|
9
|
+
* @param password The plain text password
|
|
10
|
+
* @returns The salt and hash formatted as "salt:hash"
|
|
11
|
+
*/
|
|
12
|
+
export async function hashPassword(password: string): Promise<string> {
|
|
13
|
+
const salt = randomBytes(16).toString('hex');
|
|
14
|
+
const derivedKey = (await scryptAsync(password, salt, 64)) as Buffer;
|
|
15
|
+
return `${salt}:${derivedKey.toString('hex')}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Verifies a password against a stored hash
|
|
20
|
+
*
|
|
21
|
+
* @param password The plain text password
|
|
22
|
+
* @param storedHash The stored hash in format "salt:hash"
|
|
23
|
+
* @returns True if the password matches
|
|
24
|
+
*/
|
|
25
|
+
export async function verifyPassword(password: string, storedHash: string): Promise<boolean> {
|
|
26
|
+
const [salt, key] = storedHash.split(':');
|
|
27
|
+
if (!salt || !key) return false;
|
|
28
|
+
|
|
29
|
+
const derivedKey = (await scryptAsync(password, salt, 64)) as Buffer;
|
|
30
|
+
const keyBuffer = Buffer.from(key, 'hex');
|
|
31
|
+
|
|
32
|
+
return timingSafeEqual(derivedKey, keyBuffer);
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { cookies } from 'next/headers';
|
|
2
|
+
import { getSession } from './session';
|
|
3
|
+
|
|
4
|
+
export type UserInfo = {
|
|
5
|
+
username: string;
|
|
6
|
+
isAdmin: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export async function getCurrentUser(): Promise<UserInfo | null> {
|
|
10
|
+
// Check session
|
|
11
|
+
const session = await getSession();
|
|
12
|
+
if (session) {
|
|
13
|
+
return {
|
|
14
|
+
username: session.username,
|
|
15
|
+
isAdmin: session.isAdmin
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Check legacy admin token
|
|
20
|
+
if (process.env.ADMIN_PASSWORD) {
|
|
21
|
+
const cookieStore = await cookies();
|
|
22
|
+
const adminToken = cookieStore.get('adminToken');
|
|
23
|
+
if (adminToken?.value === process.env.ADMIN_PASSWORD) {
|
|
24
|
+
return {
|
|
25
|
+
username: 'admin',
|
|
26
|
+
isAdmin: true
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { isIPv4, isIPv6 } from 'net';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if the IP address is allowed based on the allowed IPs list
|
|
5
|
+
*
|
|
6
|
+
* @param clientIp - The IP address of the client
|
|
7
|
+
* @param allowedIps - Comma separated list of allowed IPs or CIDR ranges
|
|
8
|
+
* @returns true if the IP is allowed, false otherwise
|
|
9
|
+
*/
|
|
10
|
+
export function isIpAllowed(clientIp: string, allowedIps: string | null | undefined): boolean {
|
|
11
|
+
if (!allowedIps || allowedIps.trim() === '') {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const allowedList = allowedIps.split(',').map((ip) => ip.trim());
|
|
16
|
+
|
|
17
|
+
for (const allowed of allowedList) {
|
|
18
|
+
if (allowed === '') {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (allowed.includes('/')) {
|
|
23
|
+
// CIDR
|
|
24
|
+
if (isIpInCidr(clientIp, allowed)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
// Single IP
|
|
29
|
+
if (clientIp === allowed) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isIpInCidr(ip: string, cidr: string): boolean {
|
|
39
|
+
try {
|
|
40
|
+
const [range, bitsStr] = cidr.split('/');
|
|
41
|
+
const bits = parseInt(bitsStr, 10);
|
|
42
|
+
|
|
43
|
+
if (isIPv4(ip) && isIPv4(range)) {
|
|
44
|
+
return isIPv4InCidr(ip, range, bits);
|
|
45
|
+
} else if (isIPv6(ip) && isIPv6(range)) {
|
|
46
|
+
return isIPv6InCidr(ip, range, bits);
|
|
47
|
+
} else if (isIPv6(ip) && isIPv4(range)) {
|
|
48
|
+
// Check if IPv6 is IPv4-mapped
|
|
49
|
+
if (ip.startsWith('::ffff:')) {
|
|
50
|
+
return isIPv4InCidr(ip.substring(7), range, bits);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(`Error checking CIDR ${cidr} for IP ${ip}:`, error);
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function ipToLong(ip: string): number {
|
|
60
|
+
return (
|
|
61
|
+
ip.split('.').reduce((acc, octet) => {
|
|
62
|
+
return (acc << 8) + parseInt(octet, 10);
|
|
63
|
+
}, 0) >>> 0
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isIPv4InCidr(ip: string, range: string, bits: number): boolean {
|
|
68
|
+
const mask = ~((1 << (32 - bits)) - 1);
|
|
69
|
+
const ipLong = ipToLong(ip);
|
|
70
|
+
const rangeLong = ipToLong(range);
|
|
71
|
+
|
|
72
|
+
return (ipLong & mask) === (rangeLong & mask);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseIPv6(ip: string): bigint {
|
|
76
|
+
// Expand ::
|
|
77
|
+
let fullIp = ip;
|
|
78
|
+
if (ip.includes('::')) {
|
|
79
|
+
const parts = ip.split('::');
|
|
80
|
+
const left = parts[0].split(':').filter(Boolean);
|
|
81
|
+
const right = parts[1].split(':').filter(Boolean);
|
|
82
|
+
const missing = 8 - (left.length + right.length);
|
|
83
|
+
const zeros = Array(missing).fill('0');
|
|
84
|
+
fullIp = [...left, ...zeros, ...right].join(':');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const parts = fullIp.split(':');
|
|
88
|
+
let value = BigInt(0);
|
|
89
|
+
for (const part of parts) {
|
|
90
|
+
value = (value << BigInt(16)) + BigInt(parseInt(part || '0', 16));
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isIPv6InCidr(ip: string, range: string, bits: number): boolean {
|
|
96
|
+
const ipBigInt = parseIPv6(ip);
|
|
97
|
+
const rangeBigInt = parseIPv6(range);
|
|
98
|
+
const mask = (BigInt(1) << BigInt(128)) - BigInt(1) ^ ((BigInt(1) << BigInt(128 - bits)) - BigInt(1));
|
|
99
|
+
|
|
100
|
+
return (ipBigInt & mask) === (rangeBigInt & mask);
|
|
101
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { cookies } from 'next/headers';
|
|
2
|
+
import { getSession } from './session';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Checks if the current user is an admin
|
|
6
|
+
*
|
|
7
|
+
* Note: If `process.env.ADMIN_PASSWORD` is not set, everyone is admin
|
|
8
|
+
*
|
|
9
|
+
* @returns true if the user is admin
|
|
10
|
+
*/
|
|
11
|
+
export async function isUserAdmin(): Promise<boolean> {
|
|
12
|
+
if (!process.env.ADMIN_PASSWORD) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check legacy admin token
|
|
17
|
+
const cookieStore = await cookies();
|
|
18
|
+
const adminToken = cookieStore.get('adminToken');
|
|
19
|
+
|
|
20
|
+
if (adminToken?.value === process.env.ADMIN_PASSWORD) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check session
|
|
25
|
+
const session = await getSession();
|
|
26
|
+
if (session?.isAdmin) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createHmac } from 'crypto';
|
|
2
|
+
import { cookies } from 'next/headers';
|
|
3
|
+
|
|
4
|
+
const SESSION_COOKIE_NAME = 'sessionToken';
|
|
5
|
+
const SECRET_KEY = process.env.ADMIN_PASSWORD || 'default-secret-key-change-me';
|
|
6
|
+
|
|
7
|
+
type SessionUser = {
|
|
8
|
+
username: string;
|
|
9
|
+
isAdmin: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function setSession(user: SessionUser) {
|
|
13
|
+
const payload = JSON.stringify(user);
|
|
14
|
+
const signature = createHmac('sha256', SECRET_KEY).update(payload).digest('hex');
|
|
15
|
+
const token = `${Buffer.from(payload).toString('base64')}.${signature}`;
|
|
16
|
+
|
|
17
|
+
(await cookies()).set(SESSION_COOKIE_NAME, token, {
|
|
18
|
+
httpOnly: true,
|
|
19
|
+
secure: process.env.NODE_ENV === 'production',
|
|
20
|
+
path: '/',
|
|
21
|
+
maxAge: 60 * 60 * 24 * 7, // 1 week
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function clearSession() {
|
|
26
|
+
(await cookies()).delete(SESSION_COOKIE_NAME);
|
|
27
|
+
// Also clear legacy adminToken
|
|
28
|
+
(await cookies()).delete('adminToken');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function getSession(): Promise<SessionUser | null> {
|
|
32
|
+
const cookieStore = await cookies();
|
|
33
|
+
const token = cookieStore.get(SESSION_COOKIE_NAME)?.value;
|
|
34
|
+
|
|
35
|
+
if (!token) return null;
|
|
36
|
+
|
|
37
|
+
const [payloadBase64, signature] = token.split('.');
|
|
38
|
+
if (!payloadBase64 || !signature) return null;
|
|
39
|
+
|
|
40
|
+
const payload = Buffer.from(payloadBase64, 'base64').toString('utf-8');
|
|
41
|
+
const expectedSignature = createHmac('sha256', SECRET_KEY).update(payload).digest('hex');
|
|
42
|
+
|
|
43
|
+
if (signature !== expectedSignature) return null;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(payload) as SessionUser;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|