@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.
Files changed (110) hide show
  1. package/apps/agents-server/README.md +1 -1
  2. package/apps/agents-server/TODO.txt +6 -5
  3. package/apps/agents-server/config.ts +130 -0
  4. package/apps/agents-server/next.config.ts +1 -1
  5. package/apps/agents-server/public/fonts/OpenMoji-black-glyf.woff2 +0 -0
  6. package/apps/agents-server/public/fonts/download-font.js +22 -0
  7. package/apps/agents-server/src/app/[agentName]/[...rest]/page.tsx +6 -0
  8. package/apps/agents-server/src/app/[agentName]/page.tsx +1 -0
  9. package/apps/agents-server/src/app/actions.ts +37 -2
  10. package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +68 -0
  11. package/apps/agents-server/src/app/agents/[agentName]/AgentQrCode.tsx +55 -0
  12. package/apps/agents-server/src/app/agents/[agentName]/AgentUrlCopy.tsx +4 -5
  13. package/apps/agents-server/src/app/agents/[agentName]/CopyField.tsx +44 -0
  14. package/apps/agents-server/src/app/agents/[agentName]/api/book/route.ts +8 -8
  15. package/apps/agents-server/src/app/agents/[agentName]/api/chat/route.ts +100 -24
  16. package/apps/agents-server/src/app/agents/[agentName]/api/feedback/route.ts +54 -0
  17. package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/route.ts +6 -6
  18. package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/systemMessage/route.ts +3 -3
  19. package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +6 -7
  20. package/apps/agents-server/src/app/agents/[agentName]/book/BookEditorWrapper.tsx +4 -5
  21. package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +9 -2
  22. package/apps/agents-server/src/app/agents/[agentName]/book+chat/AgentBookAndChat.tsx +23 -0
  23. package/apps/agents-server/src/app/agents/[agentName]/book+chat/{AgentBookAndChatComponent.tsx → AgentBookAndChatComponent.tsx.todo} +4 -4
  24. package/apps/agents-server/src/app/agents/[agentName]/book+chat/page.tsx +28 -17
  25. package/apps/agents-server/src/app/agents/[agentName]/book+chat/page.tsx.todo +21 -0
  26. package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatWrapper.tsx +34 -4
  27. package/apps/agents-server/src/app/agents/[agentName]/chat/page.tsx +4 -1
  28. package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +42 -0
  29. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +109 -106
  30. package/apps/agents-server/src/app/agents/page.tsx +1 -1
  31. package/apps/agents-server/src/app/api/auth/login/route.ts +65 -0
  32. package/apps/agents-server/src/app/api/auth/logout/route.ts +7 -0
  33. package/apps/agents-server/src/app/api/metadata/route.ts +116 -0
  34. package/apps/agents-server/src/app/api/upload/route.ts +7 -3
  35. package/apps/agents-server/src/app/api/users/[username]/route.ts +75 -0
  36. package/apps/agents-server/src/app/api/users/route.ts +71 -0
  37. package/apps/agents-server/src/app/globals.css +35 -1
  38. package/apps/agents-server/src/app/layout.tsx +43 -23
  39. package/apps/agents-server/src/app/metadata/MetadataClient.tsx +271 -0
  40. package/apps/agents-server/src/app/metadata/page.tsx +13 -0
  41. package/apps/agents-server/src/app/not-found.tsx +5 -0
  42. package/apps/agents-server/src/app/page.tsx +84 -46
  43. package/apps/agents-server/src/components/Auth/AuthControls.tsx +123 -0
  44. package/apps/agents-server/src/components/ErrorPage/ErrorPage.tsx +33 -0
  45. package/apps/agents-server/src/components/ForbiddenPage/ForbiddenPage.tsx +15 -0
  46. package/apps/agents-server/src/components/Header/Header.tsx +146 -0
  47. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +27 -0
  48. package/apps/agents-server/src/components/LoginDialog/LoginDialog.tsx +40 -0
  49. package/apps/agents-server/src/components/LoginForm/LoginForm.tsx +109 -0
  50. package/apps/agents-server/src/components/NotFoundPage/NotFoundPage.tsx +17 -0
  51. package/apps/agents-server/src/components/UsersList/UsersList.tsx +190 -0
  52. package/apps/agents-server/src/components/VercelDeploymentCard/VercelDeploymentCard.tsx +60 -0
  53. package/apps/agents-server/src/database/$getTableName.ts +18 -0
  54. package/apps/agents-server/src/database/$provideSupabase.ts +2 -2
  55. package/apps/agents-server/src/database/$provideSupabaseForServer.ts +3 -3
  56. package/apps/agents-server/src/database/getMetadata.ts +31 -0
  57. package/apps/agents-server/src/database/metadataDefaults.ts +32 -0
  58. package/apps/agents-server/src/database/schema.sql +81 -33
  59. package/apps/agents-server/src/database/schema.ts +35 -1
  60. package/apps/agents-server/src/middleware.ts +162 -0
  61. package/apps/agents-server/src/tools/$provideAgentCollectionForServer.ts +11 -7
  62. package/apps/agents-server/src/tools/$provideCdnForServer.ts +1 -1
  63. package/apps/agents-server/src/tools/$provideExecutionToolsForServer.ts +11 -13
  64. package/apps/agents-server/src/tools/$provideOpenAiAssistantExecutionToolsForServer.ts +7 -7
  65. package/apps/agents-server/src/tools/$provideServer.ts +39 -0
  66. package/apps/agents-server/src/utils/auth.ts +33 -0
  67. package/apps/agents-server/src/utils/cdn/utils/nameToSubfolderPath.ts +1 -1
  68. package/apps/agents-server/src/utils/getCurrentUser.ts +32 -0
  69. package/apps/agents-server/src/utils/isIpAllowed.ts +101 -0
  70. package/apps/agents-server/src/utils/isUserAdmin.ts +31 -0
  71. package/apps/agents-server/src/utils/session.ts +50 -0
  72. package/apps/agents-server/tailwind.config.ts +2 -0
  73. package/esm/index.es.js +147 -31
  74. package/esm/index.es.js.map +1 -1
  75. package/esm/typings/servers.d.ts +1 -0
  76. package/esm/typings/src/_packages/types.index.d.ts +2 -0
  77. package/esm/typings/src/_packages/utils.index.d.ts +2 -0
  78. package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +12 -2
  79. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +14 -8
  80. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabaseOptions.d.ts +10 -0
  81. package/esm/typings/src/commitments/MESSAGE/InitialMessageCommitmentDefinition.d.ts +28 -0
  82. package/esm/typings/src/commitments/index.d.ts +2 -1
  83. package/esm/typings/src/config.d.ts +1 -0
  84. package/esm/typings/src/errors/DatabaseError.d.ts +2 -2
  85. package/esm/typings/src/errors/WrappedError.d.ts +2 -2
  86. package/esm/typings/src/execution/ExecutionTask.d.ts +2 -2
  87. package/esm/typings/src/execution/LlmExecutionTools.d.ts +6 -1
  88. package/esm/typings/src/llm-providers/_common/register/$provideLlmToolsForWizardOrCli.d.ts +2 -2
  89. package/esm/typings/src/llm-providers/agent/Agent.d.ts +11 -3
  90. package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +6 -1
  91. package/esm/typings/src/llm-providers/agent/RemoteAgent.d.ts +6 -2
  92. package/esm/typings/src/llm-providers/openai/OpenAiAssistantExecutionTools.d.ts +6 -1
  93. package/esm/typings/src/remote-server/startAgentServer.d.ts +2 -2
  94. package/esm/typings/src/utils/color/Color.d.ts +7 -0
  95. package/esm/typings/src/utils/color/Color.test.d.ts +1 -0
  96. package/esm/typings/src/utils/environment/$getGlobalScope.d.ts +2 -2
  97. package/esm/typings/src/utils/misc/computeHash.d.ts +11 -0
  98. package/esm/typings/src/utils/misc/computeHash.test.d.ts +1 -0
  99. package/esm/typings/src/utils/organization/$sideEffect.d.ts +2 -2
  100. package/esm/typings/src/utils/organization/$side_effect.d.ts +2 -2
  101. package/esm/typings/src/utils/organization/TODO_USE.d.ts +2 -2
  102. package/esm/typings/src/utils/organization/keepUnused.d.ts +2 -2
  103. package/esm/typings/src/utils/organization/preserve.d.ts +3 -3
  104. package/esm/typings/src/utils/organization/really_any.d.ts +7 -0
  105. package/esm/typings/src/utils/serialization/asSerializable.d.ts +2 -2
  106. package/esm/typings/src/version.d.ts +1 -1
  107. package/package.json +1 -1
  108. package/umd/index.umd.js +147 -31
  109. package/umd/index.umd.js.map +1 -1
  110. 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: !!!! [🌕] DRY
22
+ // TODO: [🐱‍🚀] [🌕] DRY
21
23
 
22
- const isVerbose = true; // <- TODO: !!!! Pass
24
+ const isVerbose = true; // <- TODO: [🐱‍🚀] Pass
23
25
 
24
- if (agentCollection !== null) {
25
- console.log('!!! Returning cached agent collection');
26
+ if (agentCollection !== null && just(false /* <- TODO: [🐱‍🚀] Fix caching */)) {
27
+ console.log('[🐱‍🚀] Returning cached agent collection');
26
28
  return agentCollection;
27
- // TODO: !!!! Be aware of options changes
29
+ // TODO: [🐱‍🚀] Be aware of options changes
28
30
  }
29
31
 
30
- console.log('!!! Creating NEW agent collection');
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;
@@ -9,7 +9,7 @@ import { IIFilesStorageWithCdn } from '../utils/cdn/interfaces/IFilesStorage';
9
9
  let cdn: IIFilesStorageWithCdn | null = null;
10
10
 
11
11
  /**
12
- * !!!
12
+ * [🐱‍🚀]
13
13
  */
14
14
  export function $provideCdnForServer(): IIFilesStorageWithCdn {
15
15
  if (!cdn) {
@@ -43,11 +43,11 @@ $sideEffect(
43
43
  _MarkitdownScraperMetadataRegistration,
44
44
  _PdfScraperMetadataRegistration,
45
45
  _WebsiteScraperMetadataRegistration,
46
- // <- TODO: !!! Export all registrations from one variabile in `@promptbook/core`
46
+ // <- TODO: [🐱‍🚀] Export all registrations from one variabile in `@promptbook/core`
47
47
  );
48
48
  $sideEffect(/* [㊗] */ _OpenAiRegistration);
49
49
  $sideEffect(/* [㊗] */ _GoogleRegistration);
50
- // <- TODO: !!!! Allow to dynamically install required metadata
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: !!!! [🌕] DRY
70
+ // TODO: [🐱‍🚀] [🌕] DRY
73
71
 
74
- // const path = '../../agents'; // <- TODO: !!!! Pass
75
- const isVerbose = true; // <- TODO: !!!! Pass
76
- const isCacheReloaded = false; // <- TODO: !!!! Pass
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: !!!! Pass
77
+ } as TODO_any; // <- TODO: [🐱‍🚀] Pass
80
78
 
81
79
  if (executionTools !== null) {
82
- console.log('!!! Returning cached execution tools');
80
+ console.log('[🐱‍🚀] Returning cached execution tools');
83
81
  return executionTools;
84
- // TODO: !!!! Be aware of options changes
82
+ // TODO: [🐱‍🚀] Be aware of options changes
85
83
  }
86
84
 
87
- console.log('!!! Creating NEW execution tools');
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: !!!! [🌕] DRY
17
- const isVerbose = true; // <- TODO: !!!! Pass
16
+ // TODO: [🐱‍🚀] [🌕] DRY
17
+ const isVerbose = true; // <- TODO: [🐱‍🚀] Pass
18
18
 
19
19
  if (executionTools !== null) {
20
- console.log('!!! Returning cached OpenAiAssistantExecutionTools');
20
+ console.log('[🐱‍🚀] Returning cached OpenAiAssistantExecutionTools');
21
21
  return executionTools;
22
- // TODO: !!!! Be aware of options changes
22
+ // TODO: [🐱‍🚀] Be aware of options changes
23
23
  }
24
24
 
25
- console.log('!!! Creating NEW OpenAiAssistantExecutionTools');
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: !!!! In `OpenAiAssistantExecutionTools` Allow to create abstract assistants with `isCreatingNewAssistantsAllowed`
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
+ }
@@ -5,5 +5,5 @@ export function nameToSubfolderPath(name: string_name): Array<string> {
5
5
  }
6
6
 
7
7
  /**
8
- * TODO: !!! Use `nameToSubfolderPath` from src
8
+ * TODO: [🐱‍🚀] Use `nameToSubfolderPath` from src
9
9
  */
@@ -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
+ }
@@ -15,6 +15,8 @@ const config: Config = {
15
15
  colors: {
16
16
  background: 'var(--background)',
17
17
  foreground: 'var(--foreground)',
18
+ 'promptbook-blue': '#7aebff',
19
+ 'promptbook-blue-dark': '#30a8bd',
18
20
  },
19
21
  },
20
22
  },