@promptbook/cli 0.112.0-101 → 0.112.0-103

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 (91) hide show
  1. package/apps/agents-server/package.json +1 -1
  2. package/apps/agents-server/scripts/prerender-homepage.js +76 -1
  3. package/apps/agents-server/src/app/actions.ts +0 -6
  4. package/apps/agents-server/src/app/admin/about/page.tsx +1 -1
  5. package/apps/agents-server/src/app/admin/image-generator-test/ImageAttachmentsEditor.tsx +11 -6
  6. package/apps/agents-server/src/app/admin/login-methods/shibboleth/page.tsx +365 -0
  7. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +13 -15
  8. package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +3 -3
  9. package/apps/agents-server/src/app/admin/servers/useCreateServerWizard.ts +13 -14
  10. package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +12 -3
  11. package/apps/agents-server/src/app/admin/usage/UsageClientTimelineChart.tsx +1 -1
  12. package/apps/agents-server/src/app/admin/users/[userId]/UserDetailClient.tsx +21 -14
  13. package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatPageLayout.tsx +2 -2
  14. package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatSidebarDefault.tsx +11 -7
  15. package/apps/agents-server/src/app/api/admin/cli-access/route.ts +27 -123
  16. package/apps/agents-server/src/app/api/admin/code-runners/authentication/route.ts +33 -125
  17. package/apps/agents-server/src/app/api/auth/login/route.ts +0 -10
  18. package/apps/agents-server/src/app/api/auth/shibboleth/acs/route.ts +77 -57
  19. package/apps/agents-server/src/app/api/auth/shibboleth/login/route.ts +57 -33
  20. package/apps/agents-server/src/app/api/auth/shibboleth/metadata/route.ts +4 -29
  21. package/apps/agents-server/src/app/api/auth/shibboleth/status/route.ts +17 -0
  22. package/apps/agents-server/src/app/api/upload/route.ts +148 -209
  23. package/apps/agents-server/src/app/api/users/[username]/route.ts +1 -1
  24. package/apps/agents-server/src/app/api/users/route.ts +5 -5
  25. package/apps/agents-server/src/app/dashboard/page.tsx +1 -1
  26. package/apps/agents-server/src/app/docs/[docId]/page.tsx +1 -1
  27. package/apps/agents-server/src/app/docs/page.tsx +1 -1
  28. package/apps/agents-server/src/app/globals.css +100 -0
  29. package/apps/agents-server/src/app/layout.tsx +7 -0
  30. package/apps/agents-server/src/app/recycle-bin/page.tsx +1 -1
  31. package/apps/agents-server/src/app/system/settings/KeybindingsSettingsClient.tsx +13 -7
  32. package/apps/agents-server/src/components/AdminTerminal/useAdminTerminalSession.ts +29 -1
  33. package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +3 -3
  34. package/apps/agents-server/src/components/AgentProfile/AgentProfileImage.tsx +8 -2
  35. package/apps/agents-server/src/components/DocsToolbar/DocsToolbar.tsx +4 -4
  36. package/apps/agents-server/src/components/DocumentationContent/DocumentationContent.tsx +9 -9
  37. package/apps/agents-server/src/components/Footer/Footer.tsx +7 -7
  38. package/apps/agents-server/src/components/Header/Header.tsx +24 -4
  39. package/apps/agents-server/src/components/Header/HeaderTypes.ts +6 -0
  40. package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +51 -1
  41. package/apps/agents-server/src/components/Homepage/Card.tsx +1 -1
  42. package/apps/agents-server/src/components/Homepage/Section.tsx +3 -1
  43. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +12 -1
  44. package/apps/agents-server/src/components/LoginForm/LoginForm.tsx +100 -149
  45. package/apps/agents-server/src/components/Skeleton/ConsolePageLoadingSkeleton.tsx +1 -1
  46. package/apps/agents-server/src/components/Skeleton/DocumentationRouteLoadingSkeleton.tsx +1 -1
  47. package/apps/agents-server/src/components/Skeleton/HomepageLoadingSkeleton.tsx +1 -1
  48. package/apps/agents-server/src/components/UsersList/UsersList.tsx +20 -4
  49. package/apps/agents-server/src/components/UsersList/useUsersAdmin.ts +3 -0
  50. package/apps/agents-server/src/constants/shibbolethAuth.ts +139 -0
  51. package/apps/agents-server/src/database/metadataDefaults.ts +54 -80
  52. package/apps/agents-server/src/database/migrate.ts +30 -1
  53. package/apps/agents-server/src/database/migrations/2026-06-0100-shibboleth-auth.sql +136 -0
  54. package/apps/agents-server/src/database/sqlite/$provideLocalSqliteSupabase.ts +88 -36
  55. package/apps/agents-server/src/languages/ServerTranslationKeys.ts +4 -2
  56. package/apps/agents-server/src/languages/translations/czech.yaml +4 -2
  57. package/apps/agents-server/src/languages/translations/english.yaml +5 -3
  58. package/apps/agents-server/src/tools/$provideCdnForServer.ts +54 -11
  59. package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +18 -2
  60. package/apps/agents-server/src/utils/cdn/classes/TrackedFilesStorage.ts +6 -5
  61. package/apps/agents-server/src/utils/cdn/interfaces/IFilesStorage.ts +5 -0
  62. package/apps/agents-server/src/utils/chatExport/renderHtmlToPdfOnServer.ts +11 -0
  63. package/apps/agents-server/src/utils/createAdminTerminalRouteHandlers.ts +264 -0
  64. package/apps/agents-server/src/utils/shareTargetPayloads.ts +19 -66
  65. package/apps/agents-server/src/utils/shibbolethAuthentication.ts +729 -621
  66. package/apps/agents-server/src/utils/upload/createBookEditorUploadHandler.ts +19 -28
  67. package/apps/agents-server/src/utils/upload/uploadFileToServer.ts +113 -0
  68. package/esm/index.es.js +194 -35
  69. package/esm/index.es.js.map +1 -1
  70. package/esm/scripts/run-codex-prompts/common/waitForPause.d.ts +12 -0
  71. package/esm/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
  72. package/esm/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +1 -0
  73. package/esm/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +1 -1
  74. package/esm/src/book-components/Chat/MarkdownContent/MarkdownContent.d.ts +1 -0
  75. package/esm/src/version.d.ts +1 -1
  76. package/package.json +2 -2
  77. package/src/book-components/Chat/MarkdownContent/MarkdownContent.tsx +63 -4
  78. package/src/other/templates/getTemplatesPipelineCollection.ts +730 -739
  79. package/src/version.ts +2 -2
  80. package/src/versions.txt +2 -0
  81. package/umd/index.umd.js +194 -35
  82. package/umd/index.umd.js.map +1 -1
  83. package/umd/scripts/run-codex-prompts/common/waitForPause.d.ts +12 -0
  84. package/umd/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
  85. package/umd/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +1 -0
  86. package/umd/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +1 -1
  87. package/umd/src/book-components/Chat/MarkdownContent/MarkdownContent.d.ts +1 -0
  88. package/umd/src/version.d.ts +1 -1
  89. package/apps/agents-server/src/app/api/auth/methods/route.ts +0 -44
  90. package/apps/agents-server/src/constants/authenticationMethods.ts +0 -74
  91. package/apps/agents-server/src/constants/shibbolethAuthentication.ts +0 -107
@@ -1,31 +1,18 @@
1
- import { $getTableName } from '@/src/database/$getTableName';
2
- import { $provideSupabase } from '@/src/database/$provideSupabase';
3
1
  import { serializeError } from '@promptbook-local/utils';
4
- import type { PostgrestSingleResponse, SupabaseClient } from '@supabase/supabase-js';
5
- import { handleUpload, type HandleUploadBody } from '@vercel/blob/client';
6
2
  import { NextRequest, NextResponse } from 'next/server';
3
+ import { spaceTrim } from 'spacetrim';
7
4
  import { assertsError } from '../../../../../../src/errors/assertsError';
8
- import { getUserIdFromRequest } from '../../../../src/utils/getUserIdFromRequest';
5
+ import { LimitReachedError } from '../../../../../../src/errors/LimitReachedError';
6
+ import { UnexpectedError } from '../../../../../../src/errors/UnexpectedError';
7
+ import { $getTableName } from '../../../database/$getTableName';
8
+ import { $provideSupabase } from '../../../database/$provideSupabase';
9
9
  import type { AgentsServerDatabase } from '../../../database/schema';
10
+ import { $provideCdnForServer } from '../../../tools/$provideCdnForServer';
11
+ import { getSafeCdnPath } from '../../../utils/cdn/utils/getSafeCdnPath';
10
12
  import { FILE_SECURITY_CHECKERS } from '../../../file-security-checkers';
13
+ import { getUserIdFromRequest } from '../../../utils/getUserIdFromRequest';
11
14
  import { getMaxFileUploadSizeBytes } from '../../../utils/serverLimits';
12
-
13
- /**
14
- * Additional metadata accepted from the client-side upload helper.
15
- *
16
- * @private
17
- */
18
- type UploadClientPayload = {
19
- purpose?: unknown;
20
- contentType?: unknown;
21
- };
22
-
23
- /**
24
- * Generic object used for safe JSON parsing in upload payloads.
25
- *
26
- * @private
27
- */
28
- type JsonRecord = Record<string, unknown>;
15
+ import { validateMimeType } from '../../../utils/validators/validateMimeType';
29
16
 
30
17
  /**
31
18
  * Default purpose used for uploads when the client does not provide one.
@@ -42,40 +29,30 @@ const DEFAULT_UPLOAD_PURPOSE = 'GENERIC_UPLOAD';
42
29
  const DEFAULT_UPLOAD_CONTENT_TYPE = 'application/octet-stream';
43
30
 
44
31
  /**
45
- * Minimal MIME type validation for values provided by client payload.
32
+ * Regular expression for path segments that are safe to keep as public object keys.
46
33
  *
47
34
  * @private
48
35
  */
49
- const MIME_TYPE_PATTERN = /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/i;
36
+ const SAFE_UPLOAD_PATH_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~!$&'()*+,;=:@/-]*$/;
50
37
 
51
38
  /**
52
- * Safely parses a JSON string into an object; returns empty object on invalid payload.
39
+ * Parsed upload request.
53
40
  *
54
41
  * @private
55
42
  */
56
- function parseJsonRecord(rawJson: string | null | undefined): JsonRecord {
57
- if (!rawJson) {
58
- return {};
59
- }
60
-
61
- try {
62
- const parsed = JSON.parse(rawJson);
63
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
64
- return {};
65
- }
66
-
67
- return parsed as JsonRecord;
68
- } catch {
69
- return {};
70
- }
71
- }
43
+ type ParsedUploadRequest = {
44
+ file: File;
45
+ pathname: string;
46
+ purpose: string;
47
+ contentType: string;
48
+ };
72
49
 
73
50
  /**
74
51
  * Normalizes upload purpose to a non-empty string.
75
52
  *
76
53
  * @private
77
54
  */
78
- function normalizeUploadPurpose(value: unknown): string {
55
+ function normalizeUploadPurpose(value: FormDataEntryValue | null): string {
79
56
  if (typeof value !== 'string') {
80
57
  return DEFAULT_UPLOAD_PURPOSE;
81
58
  }
@@ -89,197 +66,166 @@ function normalizeUploadPurpose(value: unknown): string {
89
66
  *
90
67
  * @private
91
68
  */
92
- function normalizeUploadContentType(value: unknown): string {
93
- if (typeof value !== 'string') {
69
+ function normalizeUploadContentType(value: FormDataEntryValue | null, fallbackContentType: string): string {
70
+ const candidate = typeof value === 'string' && value.trim() ? value.trim() : fallbackContentType;
71
+
72
+ try {
73
+ return validateMimeType(candidate || DEFAULT_UPLOAD_CONTENT_TYPE);
74
+ } catch {
94
75
  return DEFAULT_UPLOAD_CONTENT_TYPE;
95
76
  }
77
+ }
78
+
79
+ /**
80
+ * Resolves and validates the storage key requested by the browser.
81
+ *
82
+ * @private
83
+ */
84
+ function resolveUploadPathname(value: FormDataEntryValue | null): string {
85
+ if (typeof value !== 'string') {
86
+ throw new UnexpectedError('Upload request is missing `pathname`.');
87
+ }
96
88
 
97
- const normalizedContentType = value.trim().toLowerCase();
98
- if (!MIME_TYPE_PATTERN.test(normalizedContentType)) {
99
- return DEFAULT_UPLOAD_CONTENT_TYPE;
89
+ const pathname = value.trim().replace(/\\/g, '/').replace(/^\/+/, '');
90
+
91
+ if (
92
+ pathname === '' ||
93
+ pathname.includes('/../') ||
94
+ pathname.startsWith('../') ||
95
+ pathname.endsWith('/..') ||
96
+ !SAFE_UPLOAD_PATH_PATTERN.test(pathname)
97
+ ) {
98
+ throw new UnexpectedError(
99
+ spaceTrim(`
100
+ Upload request contains an invalid \`pathname\`.
101
+
102
+ The upload key must be a relative CDN path without parent-directory segments.
103
+ `),
104
+ );
100
105
  }
101
106
 
102
- return normalizedContentType;
107
+ return getSafeCdnPath({
108
+ pathname,
109
+ pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX,
110
+ });
103
111
  }
104
112
 
105
113
  /**
106
- * Extracts normalized upload metadata from client payload.
114
+ * Parses the multipart request accepted by `/api/upload`.
107
115
  *
108
116
  * @private
109
117
  */
110
- function resolveUploadClientPayload(clientPayload: string | null | undefined): {
111
- purpose: string;
112
- contentType: string;
113
- } {
114
- const payload = parseJsonRecord(clientPayload) as UploadClientPayload;
118
+ async function parseUploadRequest(request: NextRequest): Promise<ParsedUploadRequest> {
119
+ const formData = await request.formData();
120
+ const file = formData.get('file');
121
+
122
+ if (!(file instanceof File)) {
123
+ throw new UnexpectedError('Upload request is missing `file`.');
124
+ }
115
125
 
116
126
  return {
117
- purpose: normalizeUploadPurpose(payload.purpose),
118
- contentType: normalizeUploadContentType(payload.contentType),
127
+ file,
128
+ pathname: resolveUploadPathname(formData.get('pathname')),
129
+ purpose: normalizeUploadPurpose(formData.get('purpose')),
130
+ contentType: normalizeUploadContentType(formData.get('contentType'), file.type),
119
131
  };
120
132
  }
121
133
 
122
134
  /**
123
- * Handles post.
135
+ * Runs all configured file-security checkers against the uploaded public URL.
136
+ *
137
+ * @private
124
138
  */
125
- export async function POST(request: NextRequest) {
126
- try {
127
- const body = (await request.json()) as HandleUploadBody;
128
- const userId = await getUserIdFromRequest(request);
129
- const supabase: SupabaseClient<AgentsServerDatabase> = $provideSupabase();
130
-
131
- // Handle Vercel Blob client upload protocol
132
- const jsonResponse = await handleUpload({
133
- body,
134
- request,
135
- token: process.env.VERCEL_BLOB_READ_WRITE_TOKEN!,
136
- onBeforeGenerateToken: async (pathname, clientPayload) => {
137
- // Authenticate user and validate upload
138
-
139
- // Parse client payload for additional metadata
140
- const { purpose, contentType } = resolveUploadClientPayload(clientPayload);
141
-
142
- const maxFileSize = await getMaxFileUploadSizeBytes();
143
-
144
- // Generate the proper path with prefix
145
- // Note: With client uploads, we use the original filename provided by the client
146
- // The file will be stored at: {pathPrefix}/user/files/{filename}
147
- const pathPrefix = process.env.NEXT_PUBLIC_CDN_PATH_PREFIX || '';
148
-
149
- // Create a DB record at the start of the upload to track it
150
- const uploadPurpose = purpose;
151
- const {
152
- data: insertedFile,
153
- error: insertError,
154
- }: PostgrestSingleResponse<Pick<AgentsServerDatabase['public']['Tables']['File']['Row'], 'id'>> =
155
- await supabase
156
- .from(await $getTableName('File'))
157
- .insert({
158
- userId: userId || null,
159
- fileName: pathname,
160
- fileSize: 0, // <- Will be updated when upload completes
161
- fileType: contentType,
162
- storageUrl: null, // <- To be updated on completion
163
- shortUrl: null, // <- To be updated on completion
164
- purpose: uploadPurpose,
165
- status: 'UPLOADING',
166
- })
167
- .select('id')
168
- .single();
139
+ async function checkUploadedFileSecurity(storageUrl: string): Promise<Record<string, unknown>> {
140
+ const securityResults: Record<string, unknown> = {};
141
+
142
+ for (const checkerId of Object.keys(FILE_SECURITY_CHECKERS)) {
143
+ try {
144
+ const checker = FILE_SECURITY_CHECKERS[checkerId]!;
145
+ securityResults[checkerId] = await checker.checkFile(storageUrl);
146
+ } catch (error) {
147
+ securityResults[checkerId] = {
148
+ isSafe: false,
149
+ status: 'ERROR',
150
+ confidence: 0,
151
+ message: error instanceof Error ? error.message : String(error),
152
+ };
153
+ }
154
+ }
169
155
 
170
- if (insertError) {
171
- console.error('🔼 Failed to create file record:', insertError);
172
- }
156
+ return securityResults;
157
+ }
173
158
 
174
- console.info('🔼 Upload started, tracking file:', {
175
- pathname,
176
- fileId: insertedFile?.id,
177
- purpose: uploadPurpose,
178
- });
159
+ /**
160
+ * Stores security results for the file row created by `TrackedFilesStorage`.
161
+ *
162
+ * @private
163
+ */
164
+ async function updateUploadedFileSecurityResult(
165
+ storageUrl: string,
166
+ securityResult: Record<string, unknown>,
167
+ ): Promise<void> {
168
+ if (Object.keys(securityResult).length === 0) {
169
+ return;
170
+ }
179
171
 
180
- return {
181
- maximumSizeInBytes: maxFileSize,
182
- addRandomSuffix: true, // Add random suffix to avoid filename collisions since we can't hash content
183
- tokenPayload: JSON.stringify({
184
- userId: userId || null,
185
- purpose: uploadPurpose,
186
- fileId: insertedFile?.id || null,
187
- uploadPath: pathname,
188
- pathPrefix,
189
- }),
190
- };
191
- },
192
- onUploadCompleted: async ({ blob, tokenPayload }) => {
193
- // !!!!
194
- // ⚠️ IMPORTANT: This callback is a WEBHOOK called by Vercel's servers AFTER the upload completes
195
- // - It runs in a DIFFERENT request context (not the original user request)
196
- // - It WON'T work in local development (Vercel can't reach localhost)
197
- // - All data must come from tokenPayload (userId, fileId, etc.)
198
- // - Need to create a fresh supabase client here
199
- console.info('🔼 Upload completed (webhook callback):', { blob, tokenPayload });
172
+ const supabase = $provideSupabase();
173
+ const securityResultForDatabase =
174
+ securityResult as AgentsServerDatabase['public']['Tables']['File']['Update']['securityResult'];
175
+ const { error } = await supabase
176
+ .from(await $getTableName('File'))
177
+ .update({ securityResult: securityResultForDatabase })
178
+ .eq('storageUrl', storageUrl);
200
179
 
201
- try {
202
- const payload = parseJsonRecord(tokenPayload);
203
- const fileId = typeof payload.fileId === 'number' ? payload.fileId : null;
204
- const tokenUserId = typeof payload.userId === 'number' ? payload.userId : null;
205
- const tokenPurpose = normalizeUploadPurpose(payload.purpose);
206
- const uploadPath = typeof payload.uploadPath === 'string' ? payload.uploadPath : null;
180
+ if (error) {
181
+ console.error('Failed to update uploaded file security result:', error);
182
+ }
183
+ }
207
184
 
208
- // Create fresh supabase client for this webhook context
209
- const supabase = $provideSupabase();
185
+ /**
186
+ * Handles file upload requests.
187
+ */
188
+ export async function POST(request: NextRequest) {
189
+ try {
190
+ const { file, pathname, purpose, contentType } = await parseUploadRequest(request);
191
+ const fileBuffer = Buffer.from(await file.arrayBuffer());
192
+ const maxFileSize = await getMaxFileUploadSizeBytes();
193
+
194
+ if (fileBuffer.byteLength > maxFileSize) {
195
+ throw new LimitReachedError(
196
+ spaceTrim(`
197
+ Uploaded file \`${file.name}\` exceeds the configured upload limit.
198
+
199
+ Maximum supported size: **${maxFileSize} bytes**
200
+ `),
201
+ );
202
+ }
210
203
 
211
- // Security checks
212
- const securityResults: Record<string, unknown> = {};
213
- const securityResultForDatabase =
214
- securityResults as AgentsServerDatabase['public']['Tables']['File']['Update']['securityResult'];
215
- for (const checkerId in FILE_SECURITY_CHECKERS) {
216
- try {
217
- const checker = FILE_SECURITY_CHECKERS[checkerId]!;
218
- console.info(`🛡️ Checking file security with ${checker.title} (${blob.url})...`);
219
- const result = await checker.checkFile(blob.url);
220
- securityResults[checkerId] = result;
221
- console.info(`🛡️ Security check result from ${checker.title}:`, result.status);
222
- } catch (error) {
223
- console.error(`🛡️ Security check failed for ${checkerId}:`, error);
224
- securityResults[checkerId] = {
225
- isSafe: false,
226
- status: 'ERROR',
227
- confidence: 0,
228
- message: error instanceof Error ? error.message : String(error),
229
- };
230
- }
231
- }
204
+ const cdn = $provideCdnForServer();
205
+ const storageUrl = cdn.getItemUrl(pathname).href;
206
+ const userId = await getUserIdFromRequest(request);
232
207
 
233
- if (fileId) {
234
- // Update the existing record by ID
235
- const { error: updateError } = await supabase
236
- .from(await $getTableName('File'))
237
- .update({
238
- userId: tokenUserId || null,
239
- fileSize: 0, // <- !!!!
240
- fileType: blob.contentType,
241
- storageUrl: blob.url,
242
- // <- TODO: !!!! Split between storageUrl and shortUrl
243
- purpose: tokenPurpose,
244
- status: 'COMPLETED',
245
- securityResult: securityResultForDatabase,
246
- })
247
- .eq('id', fileId);
208
+ await cdn.setItem(pathname, {
209
+ type: contentType,
210
+ data: fileBuffer,
211
+ purpose,
212
+ userId: userId || undefined,
213
+ fileSize: fileBuffer.byteLength,
214
+ });
248
215
 
249
- if (updateError) {
250
- console.error('🔼 Failed to update file record:', updateError);
251
- } else {
252
- console.info('🔼 File record updated successfully:', { fileId, shortUrl: blob.url });
253
- }
254
- } else if (uploadPath) {
255
- // Fallback: Update by uploadPath if fileId is not available
256
- const { error: updateError } = await supabase
257
- .from(await $getTableName('File'))
258
- .update({
259
- fileSize: 0, // <- !!!!
260
- fileType: blob.contentType,
261
- storageUrl: blob.url,
262
- status: 'COMPLETED',
263
- securityResult: securityResultForDatabase,
264
- })
265
- .eq('fileName', uploadPath)
266
- .eq('status', 'UPLOADING');
216
+ const securityResult = await checkUploadedFileSecurity(storageUrl);
217
+ await updateUploadedFileSecurityResult(storageUrl, securityResult);
267
218
 
268
- if (updateError) {
269
- console.error('🔼 Failed to update file record by uploadPath:', updateError);
270
- }
271
- }
272
- } catch (error) {
273
- console.error('🔼 Error in onUploadCompleted:', error);
274
- }
275
- },
219
+ return NextResponse.json({
220
+ url: storageUrl,
221
+ pathname,
222
+ contentType,
223
+ size: fileBuffer.byteLength,
276
224
  });
277
-
278
- return NextResponse.json(jsonResponse);
279
225
  } catch (error) {
280
226
  assertsError(error);
281
227
 
282
- console.error('🔼', error);
228
+ console.error('Upload failed:', error);
283
229
 
284
230
  return new Response(
285
231
  JSON.stringify(
@@ -296,10 +242,3 @@ export async function POST(request: NextRequest) {
296
242
  );
297
243
  }
298
244
  }
299
-
300
- // TODO: !!!! Change uploaded URLs from `storageUrl` to `shortUrl`
301
- // TODO: !!!! Record both `storageUrl` (actual storage location) and `shortUrl` in `File` table
302
- // TODO: !!!! Record `purpose` in `File` table
303
- // TODO: !!!! Record `userId` in `File` table
304
- // TODO: !!!! Record all things into `File` table
305
- // TODO: !!!! File type (mime type) of `.book` files should be `application/book` <- [🧠] !!!! Best mime type?!
@@ -34,7 +34,7 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ us
34
34
  .from(await $getTableName('User'))
35
35
  .update(updates)
36
36
  .eq('username', usernameParam)
37
- .select('id, username, createdAt, updatedAt, isAdmin')
37
+ .select('*')
38
38
  .single();
39
39
 
40
40
  if (error) {
@@ -16,7 +16,7 @@ export async function GET() {
16
16
  const supabase = $provideSupabaseForServer();
17
17
  const { data: users, error } = await supabase
18
18
  .from(await $getTableName('User'))
19
- .select('id, username, createdAt, updatedAt, isAdmin')
19
+ .select('*')
20
20
  .order('username');
21
21
 
22
22
  if (error) {
@@ -58,18 +58,18 @@ export async function POST(request: Request) {
58
58
  createdAt: new Date().toISOString(),
59
59
  updatedAt: new Date().toISOString(),
60
60
  })
61
- .select('id, username, createdAt, updatedAt, isAdmin')
61
+ .select('*')
62
62
  .single();
63
63
 
64
64
  if (error) {
65
- if (error.code === '23505') { // unique_violation
66
- return NextResponse.json({ error: 'Username already exists' }, { status: 409 });
65
+ if (error.code === '23505') {
66
+ // unique_violation
67
+ return NextResponse.json({ error: 'Username already exists' }, { status: 409 });
67
68
  }
68
69
  throw error;
69
70
  }
70
71
 
71
72
  return NextResponse.json(newUser);
72
-
73
73
  } catch (error) {
74
74
  console.error('Create user error:', error);
75
75
  const passwordValidationMessage = getPasswordValidationMessage(error);
@@ -56,7 +56,7 @@ export default async function DashboardPage() {
56
56
  const host = (await headers()).get('host') || 'unknown';
57
57
 
58
58
  return (
59
- <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50">
59
+ <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900">
60
60
  <div className="container mx-auto px-4 py-16">
61
61
  <HomepagePrimarySections
62
62
  agents={agents}
@@ -32,7 +32,7 @@ export default async function DocPage(props: DocPageProps) {
32
32
  const { primary, aliases } = group;
33
33
 
34
34
  return (
35
- <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50">
35
+ <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900">
36
36
  <div className="container mx-auto px-4 py-16">
37
37
  <DocsToolbar />
38
38
  <PrintHeader title={primary.type} />
@@ -19,7 +19,7 @@ export default function DocsPage() {
19
19
  const groupedCommitments = getVisibleCommitmentDefinitions();
20
20
 
21
21
  return (
22
- <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50">
22
+ <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900">
23
23
  <div className="container mx-auto px-4 py-16">
24
24
  <DocsToolbar />
25
25
  <PrintHeader title="Full Documentation" />
@@ -640,6 +640,26 @@ html.dark .agent-chat-route-surface .agent-chat-sidebar-toggle.agent-chat-solid-
640
640
  box-shadow: 0 20px 38px rgba(2, 6, 23, 0.6), inset 1px 0 0 rgba(191, 219, 254, 0.18);
641
641
  }
642
642
 
643
+ html.dark .agent-chat-sidebar-item-active {
644
+ border-color: rgba(125, 211, 252, 0.52) !important;
645
+ background: linear-gradient(145deg, rgba(8, 47, 73, 0.88), rgba(15, 23, 42, 0.96)) !important;
646
+ color: #e0f2fe !important;
647
+ box-shadow: 0 14px 30px rgba(2, 6, 23, 0.4), inset 0 0 0 1px rgba(125, 211, 252, 0.16) !important;
648
+ }
649
+
650
+ html.dark .agent-chat-sidebar-item-active .agent-chat-sidebar-item-preview-card {
651
+ border-color: rgba(125, 211, 252, 0.34) !important;
652
+ background: rgba(15, 23, 42, 0.82) !important;
653
+ }
654
+
655
+ html.dark .agent-chat-sidebar-item-active .agent-chat-sidebar-item-title {
656
+ color: #f8fafc !important;
657
+ }
658
+
659
+ html.dark .agent-chat-sidebar-item-active .agent-chat-sidebar-item-preview {
660
+ color: #bae6fd !important;
661
+ }
662
+
643
663
  .agent-chat-route-surface .chat-scrollToBottom.chat-scrollToBottom + [role='status'] {
644
664
  top: auto;
645
665
  right: auto;
@@ -807,6 +827,36 @@ html.dark .bg-slate-100\/80 {
807
827
  background-color: #111827 !important;
808
828
  }
809
829
 
830
+ html.dark .bg-gray-200,
831
+ html.dark .bg-slate-200 {
832
+ background-color: rgba(30, 41, 59, 0.92) !important;
833
+ }
834
+
835
+ html.dark .bg-gray-300,
836
+ html.dark .bg-slate-300 {
837
+ background-color: rgba(51, 65, 85, 0.94) !important;
838
+ }
839
+
840
+ html.dark .bg-blue-50,
841
+ html.dark .bg-blue-100 {
842
+ background-color: rgba(30, 64, 175, 0.24) !important;
843
+ }
844
+
845
+ html.dark .bg-amber-50,
846
+ html.dark .bg-amber-100 {
847
+ background-color: rgba(120, 53, 15, 0.28) !important;
848
+ }
849
+
850
+ html.dark .bg-red-50,
851
+ html.dark .bg-red-100 {
852
+ background-color: rgba(127, 29, 29, 0.3) !important;
853
+ }
854
+
855
+ html.dark .bg-emerald-50,
856
+ html.dark .bg-emerald-100 {
857
+ background-color: rgba(6, 78, 59, 0.28) !important;
858
+ }
859
+
810
860
  html.dark .border-gray-100,
811
861
  html.dark .border-gray-200,
812
862
  html.dark .border-slate-100,
@@ -844,9 +894,34 @@ html.dark .text-slate-400 {
844
894
  color: #64748b !important;
845
895
  }
846
896
 
897
+ html.dark .text-blue-600,
898
+ html.dark .text-blue-700,
899
+ html.dark .hover\:text-blue-700:hover {
900
+ color: #93c5fd !important;
901
+ }
902
+
903
+ html.dark .text-amber-700,
904
+ html.dark .text-amber-800,
905
+ html.dark .text-amber-900 {
906
+ color: #fcd34d !important;
907
+ }
908
+
909
+ html.dark .text-red-600,
910
+ html.dark .text-red-700,
911
+ html.dark .text-red-800,
912
+ html.dark .text-red-900 {
913
+ color: #fca5a5 !important;
914
+ }
915
+
916
+ html.dark .text-emerald-600,
917
+ html.dark .text-emerald-700 {
918
+ color: #86efac !important;
919
+ }
920
+
847
921
  html.dark .hover\:bg-white:hover,
848
922
  html.dark .hover\:bg-gray-50:hover,
849
923
  html.dark .hover\:bg-gray-100:hover,
924
+ html.dark .hover\:bg-gray-300:hover,
850
925
  html.dark .hover\:bg-slate-50:hover,
851
926
  html.dark .hover\:bg-slate-100:hover {
852
927
  background-color: #0f172a !important;
@@ -864,6 +939,31 @@ html.dark .hover\:border-slate-400:hover {
864
939
  border-color: rgba(125, 211, 252, 0.45) !important;
865
940
  }
866
941
 
942
+ html.dark input:not([type='checkbox']):not([type='radio']):not([type='range']),
943
+ html.dark textarea,
944
+ html.dark select {
945
+ background-color: #0f172a;
946
+ color: #e2e8f0;
947
+ border-color: rgba(71, 85, 105, 0.92);
948
+ }
949
+
950
+ html.dark input:not([type='checkbox']):not([type='radio']):not([type='range'])::placeholder,
951
+ html.dark textarea::placeholder {
952
+ color: #94a3b8;
953
+ }
954
+
955
+ html.dark input:not([type='checkbox']):not([type='radio']):not([type='range']):disabled,
956
+ html.dark textarea:disabled,
957
+ html.dark select:disabled {
958
+ background-color: #111827;
959
+ color: #64748b;
960
+ }
961
+
962
+ html.dark option {
963
+ background-color: #0f172a;
964
+ color: #e2e8f0;
965
+ }
966
+
867
967
  html.dark .prose {
868
968
  color: #cbd5e1;
869
969
  }
@@ -46,6 +46,10 @@ import {
46
46
  CONTROL_PANEL_OPTION_AVAILABILITY_METADATA_KEYS,
47
47
  getControlPanelOptionAvailability,
48
48
  } from '../utils/getControlPanelOptionAvailability';
49
+ import {
50
+ SHIBBOLETH_AUTHENTICATION_METADATA_KEYS,
51
+ resolveShibbolethAuthenticationMenuStatus,
52
+ } from '../constants/shibbolethAuth';
49
53
  import '@prisma/studio-core/ui/index.css';
50
54
  import './globals.css';
51
55
 
@@ -257,6 +261,7 @@ export default async function RootLayout({
257
261
  DEFAULT_THEME_METADATA_KEY,
258
262
  SERVER_LANGUAGE_METADATA_KEY,
259
263
  IS_SERVER_LANGUAGE_ENFORCED_METADATA_KEY,
264
+ ...SHIBBOLETH_AUTHENTICATION_METADATA_KEYS,
260
265
  ...CONTROL_PANEL_OPTION_AVAILABILITY_METADATA_KEYS,
261
266
  ]);
262
267
  const currentUserPromise = getCurrentUser();
@@ -380,6 +385,7 @@ export default async function RootLayout({
380
385
  metadata: layoutMetadata,
381
386
  isPushNotificationsConfigured: Boolean(webPushPublicKey),
382
387
  });
388
+ const shibbolethAuthenticationStatus = resolveShibbolethAuthenticationMenuStatus(layoutMetadata);
383
389
  const themeModeBootstrapScript = createThemeModeBootstrapScript(defaultThemeMode);
384
390
 
385
391
  return (
@@ -414,6 +420,7 @@ export default async function RootLayout({
414
420
  defaultIsNotificationsOn={defaultIsNotificationsOn}
415
421
  isExperimental={isExperimental}
416
422
  feedbackMode={feedbackMode}
423
+ shibbolethAuthenticationStatus={shibbolethAuthenticationStatus}
417
424
  isExperimentalPwaAppEnabled={isExperimentalPwaAppEnabled}
418
425
  controlPanelOptionAvailability={controlPanelOptionAvailability}
419
426
  defaultServerLanguage={serverLanguage}