@promptbook/cli 0.112.0-102 → 0.112.0-104

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 (54) hide show
  1. package/apps/agents-server/README.md +0 -6
  2. package/apps/agents-server/src/app/AddAgentButton.tsx +0 -5
  3. package/apps/agents-server/src/app/actions.ts +50 -0
  4. package/apps/agents-server/src/app/admin/image-generator-test/ImageAttachmentsEditor.tsx +11 -6
  5. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +13 -15
  6. package/apps/agents-server/src/app/admin/servers/useCreateServerWizard.ts +13 -14
  7. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileChat.tsx +3 -4
  8. package/apps/agents-server/src/app/api/health/route.ts +18 -0
  9. package/apps/agents-server/src/app/api/upload/route.ts +110 -383
  10. package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +1 -4
  11. package/apps/agents-server/src/components/Header/Header.tsx +0 -11
  12. package/apps/agents-server/src/components/Header/useHeaderAgentMenus.tsx +0 -5
  13. package/apps/agents-server/src/components/NewAgentDialog/useNewAgentDialog.tsx +39 -16
  14. package/apps/agents-server/src/constants/defaultAgentAvatarVisual.ts +1 -1
  15. package/apps/agents-server/src/database/migrations/2026-06-0200-default-agent-avatar-visual-octopus3d3.sql +16 -0
  16. package/apps/agents-server/src/middleware.ts +2 -1
  17. package/apps/agents-server/src/tools/$provideCdnForServer.ts +87 -49
  18. package/apps/agents-server/src/utils/agentRouting/resolveAgentRouteTarget.ts +27 -4
  19. package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +17 -49
  20. package/apps/agents-server/src/utils/cdn/classes/TrackedFilesStorage.ts +5 -2
  21. package/apps/agents-server/src/utils/cdn/interfaces/IFilesStorage.ts +5 -0
  22. package/apps/agents-server/src/utils/defaultAgents/defaultAgents.ts +168 -0
  23. package/apps/agents-server/src/utils/defaultAgents/installDefaultAgents.ts +139 -0
  24. package/apps/agents-server/src/utils/shareTargetPayloads.ts +15 -63
  25. package/apps/agents-server/src/utils/upload/createBookEditorUploadHandler.ts +23 -150
  26. package/apps/agents-server/src/utils/upload/uploadFileToServer.ts +113 -0
  27. package/esm/index.es.js +711 -41
  28. package/esm/index.es.js.map +1 -1
  29. package/esm/scripts/run-codex-prompts/common/waitForPause.d.ts +12 -0
  30. package/esm/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
  31. package/esm/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +1 -0
  32. package/esm/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +1 -1
  33. package/esm/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
  34. package/esm/src/avatars/visuals/octopus3d3AvatarVisual.d.ts +7 -0
  35. package/esm/src/version.d.ts +1 -1
  36. package/package.json +1 -1
  37. package/src/avatars/types/AvatarVisualDefinition.ts +1 -0
  38. package/src/avatars/visuals/avatarVisualRegistry.ts +2 -0
  39. package/src/avatars/visuals/octopus3d3AvatarVisual.ts +903 -0
  40. package/src/book-components/Chat/MarkdownContent/MarkdownContent.tsx +1 -3
  41. package/src/other/templates/getTemplatesPipelineCollection.ts +799 -809
  42. package/src/utils/agents/resolveAgentAvatarImageUrl.ts +1 -1
  43. package/src/version.ts +2 -2
  44. package/src/versions.txt +1 -0
  45. package/umd/index.umd.js +711 -41
  46. package/umd/index.umd.js.map +1 -1
  47. package/umd/scripts/run-codex-prompts/common/waitForPause.d.ts +12 -0
  48. package/umd/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
  49. package/umd/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +1 -0
  50. package/umd/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +1 -1
  51. package/umd/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
  52. package/umd/src/avatars/visuals/octopus3d3AvatarVisual.d.ts +7 -0
  53. package/umd/src/version.d.ts +1 -1
  54. package/apps/agents-server/src/utils/cdn/resolveCdnStorageProvider.ts +0 -40
@@ -1,36 +1,18 @@
1
- import { $getTableName } from '@/src/database/$getTableName';
2
- import { $provideSupabase } from '@/src/database/$provideSupabase';
3
- import { $provideUntrackedCdnForServer } from '@/src/tools/$provideCdnForServer';
4
- import { getSafeCdnPath } from '@/src/utils/cdn/utils/getSafeCdnPath';
5
1
  import { serializeError } from '@promptbook-local/utils';
6
- import type { PostgrestSingleResponse, SupabaseClient } from '@supabase/supabase-js';
7
- import { handleUpload, type HandleUploadBody } from '@vercel/blob/client';
8
2
  import { NextRequest, NextResponse } from 'next/server';
3
+ import { spaceTrim } from 'spacetrim';
9
4
  import { assertsError } from '../../../../../../src/errors/assertsError';
10
- import { DatabaseError } from '../../../../../../src/errors/DatabaseError';
11
5
  import { LimitReachedError } from '../../../../../../src/errors/LimitReachedError';
12
- import { NotAllowed } from '../../../../../../src/errors/NotAllowed';
13
- import { getUserIdFromRequest } from '../../../../src/utils/getUserIdFromRequest';
6
+ import { UnexpectedError } from '../../../../../../src/errors/UnexpectedError';
7
+ import { $getTableName } from '../../../database/$getTableName';
8
+ import { $provideSupabase } from '../../../database/$provideSupabase';
14
9
  import type { AgentsServerDatabase } from '../../../database/schema';
10
+ import { $provideCdnForServer } from '../../../tools/$provideCdnForServer';
11
+ import { getSafeCdnPath } from '../../../utils/cdn/utils/getSafeCdnPath';
15
12
  import { FILE_SECURITY_CHECKERS } from '../../../file-security-checkers';
13
+ import { getUserIdFromRequest } from '../../../utils/getUserIdFromRequest';
16
14
  import { getMaxFileUploadSizeBytes } from '../../../utils/serverLimits';
17
-
18
- /**
19
- * Additional metadata accepted from the client-side upload helper.
20
- *
21
- * @private
22
- */
23
- type UploadClientPayload = {
24
- purpose?: unknown;
25
- contentType?: unknown;
26
- };
27
-
28
- /**
29
- * Generic object used for safe JSON parsing in upload payloads.
30
- *
31
- * @private
32
- */
33
- type JsonRecord = Record<string, unknown>;
15
+ import { validateMimeType } from '../../../utils/validators/validateMimeType';
34
16
 
35
17
  /**
36
18
  * Default purpose used for uploads when the client does not provide one.
@@ -47,75 +29,30 @@ const DEFAULT_UPLOAD_PURPOSE = 'GENERIC_UPLOAD';
47
29
  const DEFAULT_UPLOAD_CONTENT_TYPE = 'application/octet-stream';
48
30
 
49
31
  /**
50
- * Minimal MIME type validation for values provided by client payload.
51
- *
52
- * @private
53
- */
54
- const MIME_TYPE_PATTERN = /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/i;
55
-
56
- /**
57
- * Form-data field containing the uploaded file for S3-backed server uploads.
58
- *
59
- * @private
60
- */
61
- const SERVER_ROUTED_UPLOAD_FILE_FIELD = 'file';
62
-
63
- /**
64
- * Form-data field containing the requested CDN object key for S3-backed server uploads.
65
- *
66
- * @private
67
- */
68
- const SERVER_ROUTED_UPLOAD_PATHNAME_FIELD = 'pathname';
69
-
70
- /**
71
- * Form-data field containing upload purpose for S3-backed server uploads.
72
- *
73
- * @private
74
- */
75
- const SERVER_ROUTED_UPLOAD_PURPOSE_FIELD = 'purpose';
76
-
77
- /**
78
- * Form-data field containing content type for S3-backed server uploads.
79
- *
80
- * @private
81
- */
82
- const SERVER_ROUTED_UPLOAD_CONTENT_TYPE_FIELD = 'contentType';
83
-
84
- /**
85
- * Multipart content type prefix used by server-routed S3 uploads.
32
+ * Regular expression for path segments that are safe to keep as public object keys.
86
33
  *
87
34
  * @private
88
35
  */
89
- const MULTIPART_FORM_DATA_CONTENT_TYPE = 'multipart/form-data';
36
+ const SAFE_UPLOAD_PATH_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~!$&'()*+,;=:@/-]*$/;
90
37
 
91
38
  /**
92
- * Safely parses a JSON string into an object; returns empty object on invalid payload.
39
+ * Parsed upload request.
93
40
  *
94
41
  * @private
95
42
  */
96
- function parseJsonRecord(rawJson: string | null | undefined): JsonRecord {
97
- if (!rawJson) {
98
- return {};
99
- }
100
-
101
- try {
102
- const parsed = JSON.parse(rawJson);
103
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
104
- return {};
105
- }
106
-
107
- return parsed as JsonRecord;
108
- } catch {
109
- return {};
110
- }
111
- }
43
+ type ParsedUploadRequest = {
44
+ file: File;
45
+ pathname: string;
46
+ purpose: string;
47
+ contentType: string;
48
+ };
112
49
 
113
50
  /**
114
51
  * Normalizes upload purpose to a non-empty string.
115
52
  *
116
53
  * @private
117
54
  */
118
- function normalizeUploadPurpose(value: unknown): string {
55
+ function normalizeUploadPurpose(value: FormDataEntryValue | null): string {
119
56
  if (typeof value !== 'string') {
120
57
  return DEFAULT_UPLOAD_PURPOSE;
121
58
  }
@@ -129,93 +66,84 @@ function normalizeUploadPurpose(value: unknown): string {
129
66
  *
130
67
  * @private
131
68
  */
132
- function normalizeUploadContentType(value: unknown): string {
133
- if (typeof value !== 'string') {
134
- return DEFAULT_UPLOAD_CONTENT_TYPE;
135
- }
69
+ function normalizeUploadContentType(value: FormDataEntryValue | null, fallbackContentType: string): string {
70
+ const candidate = typeof value === 'string' && value.trim() ? value.trim() : fallbackContentType;
136
71
 
137
- const normalizedContentType = value.trim().toLowerCase();
138
- if (!MIME_TYPE_PATTERN.test(normalizedContentType)) {
72
+ try {
73
+ return validateMimeType(candidate || DEFAULT_UPLOAD_CONTENT_TYPE);
74
+ } catch {
139
75
  return DEFAULT_UPLOAD_CONTENT_TYPE;
140
76
  }
141
-
142
- return normalizedContentType;
143
77
  }
144
78
 
145
79
  /**
146
- * Extracts normalized upload metadata from client payload.
80
+ * Resolves and validates the storage key requested by the browser.
147
81
  *
148
82
  * @private
149
83
  */
150
- function resolveUploadClientPayload(clientPayload: string | null | undefined): {
151
- purpose: string;
152
- contentType: string;
153
- } {
154
- const payload = parseJsonRecord(clientPayload) as UploadClientPayload;
155
-
156
- return {
157
- purpose: normalizeUploadPurpose(payload.purpose),
158
- contentType: normalizeUploadContentType(payload.contentType),
159
- };
160
- }
161
-
162
- /**
163
- * Checks whether the upload request uses the server-routed multipart protocol.
164
- *
165
- * @private
166
- */
167
- function isServerRoutedUploadRequest(request: NextRequest): boolean {
168
- return (request.headers.get('content-type') || '').toLowerCase().includes(MULTIPART_FORM_DATA_CONTENT_TYPE);
169
- }
84
+ function resolveUploadPathname(value: FormDataEntryValue | null): string {
85
+ if (typeof value !== 'string') {
86
+ throw new UnexpectedError('Upload request is missing `pathname`.');
87
+ }
170
88
 
171
- /**
172
- * Reads a string field from multipart form data.
173
- *
174
- * @private
175
- */
176
- function getFormDataString(formData: FormData, fieldName: string): string | null {
177
- const value = formData.get(fieldName);
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
+ );
105
+ }
178
106
 
179
- return typeof value === 'string' ? value : null;
107
+ return getSafeCdnPath({
108
+ pathname,
109
+ pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX,
110
+ });
180
111
  }
181
112
 
182
113
  /**
183
- * Normalizes a client-provided CDN pathname into a relative object key.
114
+ * Parses the multipart request accepted by `/api/upload`.
184
115
  *
185
116
  * @private
186
117
  */
187
- function normalizeServerUploadPathname(value: unknown, fallbackFilename: string): string {
188
- const rawPathname = typeof value === 'string' ? value : fallbackFilename;
189
- const normalizedPathname = rawPathname
190
- .trim()
191
- .replace(/\\/g, '/')
192
- .replace(/^\/+/g, '')
193
- .replace(/\/+/g, '/');
118
+ async function parseUploadRequest(request: NextRequest): Promise<ParsedUploadRequest> {
119
+ const formData = await request.formData();
120
+ const file = formData.get('file');
194
121
 
195
- if (!normalizedPathname || normalizedPathname.split('/').includes('..')) {
196
- throw new NotAllowed('Upload pathname must be a safe relative CDN path.');
122
+ if (!(file instanceof File)) {
123
+ throw new UnexpectedError('Upload request is missing `file`.');
197
124
  }
198
125
 
199
- return getSafeCdnPath({ pathname: normalizedPathname });
126
+ return {
127
+ file,
128
+ pathname: resolveUploadPathname(formData.get('pathname')),
129
+ purpose: normalizeUploadPurpose(formData.get('purpose')),
130
+ contentType: normalizeUploadContentType(formData.get('contentType'), file.type),
131
+ };
200
132
  }
201
133
 
202
134
  /**
203
- * Runs configured file-security checks for an uploaded public URL.
135
+ * Runs all configured file-security checkers against the uploaded public URL.
204
136
  *
205
137
  * @private
206
138
  */
207
139
  async function checkUploadedFileSecurity(storageUrl: string): Promise<Record<string, unknown>> {
208
140
  const securityResults: Record<string, unknown> = {};
209
141
 
210
- for (const checkerId in FILE_SECURITY_CHECKERS) {
142
+ for (const checkerId of Object.keys(FILE_SECURITY_CHECKERS)) {
211
143
  try {
212
144
  const checker = FILE_SECURITY_CHECKERS[checkerId]!;
213
- console.info(`🛡️ Checking file security with ${checker.title} (${storageUrl})...`);
214
- const result = await checker.checkFile(storageUrl);
215
- securityResults[checkerId] = result;
216
- console.info(`🛡️ Security check result from ${checker.title}:`, result.status);
145
+ securityResults[checkerId] = await checker.checkFile(storageUrl);
217
146
  } catch (error) {
218
- console.error(`🛡️ Security check failed for ${checkerId}:`, error);
219
147
  securityResults[checkerId] = {
220
148
  isSafe: false,
221
149
  status: 'ERROR',
@@ -229,269 +157,75 @@ async function checkUploadedFileSecurity(storageUrl: string): Promise<Record<str
229
157
  }
230
158
 
231
159
  /**
232
- * Inserts a completed file row for a server-routed upload.
160
+ * Stores security results for the file row created by `TrackedFilesStorage`.
233
161
  *
234
162
  * @private
235
163
  */
236
- async function insertCompletedUploadFileRecord(options: {
237
- supabase: SupabaseClient<AgentsServerDatabase>;
238
- userId: number | null;
239
- fileName: string;
240
- fileSize: number;
241
- fileType: string;
242
- storageUrl: string;
243
- purpose: string;
244
- securityResult: AgentsServerDatabase['public']['Tables']['File']['Insert']['securityResult'];
245
- }): Promise<number | null> {
246
- const {
247
- supabase,
248
- userId,
249
- fileName,
250
- fileSize,
251
- fileType,
252
- storageUrl,
253
- purpose,
254
- securityResult,
255
- } = options;
256
- const { data, error }: PostgrestSingleResponse<Pick<AgentsServerDatabase['public']['Tables']['File']['Row'], 'id'>> =
257
- await supabase
258
- .from(await $getTableName('File'))
259
- .insert({
260
- userId,
261
- fileName,
262
- fileSize,
263
- fileType,
264
- storageUrl,
265
- shortUrl: null,
266
- purpose,
267
- status: 'COMPLETED',
268
- securityResult,
269
- })
270
- .select('id')
271
- .single();
272
-
273
- if (error) {
274
- throw new DatabaseError(`Failed to create completed file record: ${error.message}`);
164
+ async function updateUploadedFileSecurityResult(
165
+ storageUrl: string,
166
+ securityResult: Record<string, unknown>,
167
+ ): Promise<void> {
168
+ if (Object.keys(securityResult).length === 0) {
169
+ return;
275
170
  }
276
171
 
277
- return data?.id ?? null;
278
- }
279
-
280
- /**
281
- * Handles direct multipart uploads for S3-compatible storage backends.
282
- *
283
- * @private
284
- */
285
- async function handleServerRoutedUpload(request: NextRequest): Promise<NextResponse> {
286
- const formData = await request.formData();
287
- const uploadFile = formData.get(SERVER_ROUTED_UPLOAD_FILE_FIELD);
288
-
289
- if (!(uploadFile instanceof File)) {
290
- throw new NotAllowed('Upload request must contain a file.');
291
- }
292
-
293
- const userId = await getUserIdFromRequest(request);
294
- const purpose = normalizeUploadPurpose(getFormDataString(formData, SERVER_ROUTED_UPLOAD_PURPOSE_FIELD));
295
- const contentType = normalizeUploadContentType(
296
- getFormDataString(formData, SERVER_ROUTED_UPLOAD_CONTENT_TYPE_FIELD) || uploadFile.type,
297
- );
298
- const pathname = normalizeServerUploadPathname(
299
- getFormDataString(formData, SERVER_ROUTED_UPLOAD_PATHNAME_FIELD),
300
- uploadFile.name,
301
- );
302
- const maxFileSize = await getMaxFileUploadSizeBytes();
303
-
304
- if (uploadFile.size > maxFileSize) {
305
- throw new LimitReachedError(`Uploaded file exceeds the configured maximum size of ${maxFileSize} bytes.`);
306
- }
307
-
308
- const buffer = Buffer.from(await uploadFile.arrayBuffer());
309
- if (buffer.byteLength > maxFileSize) {
310
- throw new LimitReachedError(`Uploaded file exceeds the configured maximum size of ${maxFileSize} bytes.`);
311
- }
312
-
313
- const cdn = $provideUntrackedCdnForServer();
314
- await cdn.setItem(pathname, {
315
- type: contentType,
316
- data: buffer,
317
- userId: userId || undefined,
318
- purpose,
319
- fileSize: buffer.byteLength,
320
- });
321
-
322
- const storageUrl = cdn.getItemUrl(pathname).href;
323
- const securityResults = await checkUploadedFileSecurity(storageUrl);
172
+ const supabase = $provideSupabase();
324
173
  const securityResultForDatabase =
325
- securityResults as AgentsServerDatabase['public']['Tables']['File']['Insert']['securityResult'];
326
- const supabase: SupabaseClient<AgentsServerDatabase> = $provideSupabase();
327
- const fileId = await insertCompletedUploadFileRecord({
328
- supabase,
329
- userId: userId || null,
330
- fileName: pathname,
331
- fileSize: buffer.byteLength,
332
- fileType: contentType,
333
- storageUrl,
334
- purpose,
335
- securityResult: securityResultForDatabase,
336
- });
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);
337
179
 
338
- return NextResponse.json({
339
- url: storageUrl,
340
- pathname,
341
- fileId,
342
- contentType,
343
- size: buffer.byteLength,
344
- });
180
+ if (error) {
181
+ console.error('Failed to update uploaded file security result:', error);
182
+ }
345
183
  }
346
184
 
347
185
  /**
348
- * Handles post.
186
+ * Handles file upload requests.
349
187
  */
350
188
  export async function POST(request: NextRequest) {
351
189
  try {
352
- if (isServerRoutedUploadRequest(request)) {
353
- return await handleServerRoutedUpload(request);
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
+ );
354
202
  }
355
203
 
356
- const body = (await request.json()) as HandleUploadBody;
204
+ const cdn = $provideCdnForServer();
205
+ const storageUrl = cdn.getItemUrl(pathname).href;
357
206
  const userId = await getUserIdFromRequest(request);
358
- const supabase: SupabaseClient<AgentsServerDatabase> = $provideSupabase();
359
-
360
- // Handle Vercel Blob client upload protocol
361
- const jsonResponse = await handleUpload({
362
- body,
363
- request,
364
- token: process.env.VERCEL_BLOB_READ_WRITE_TOKEN!,
365
- onBeforeGenerateToken: async (pathname, clientPayload) => {
366
- // Authenticate user and validate upload
367
-
368
- // Parse client payload for additional metadata
369
- const { purpose, contentType } = resolveUploadClientPayload(clientPayload);
370
207
 
371
- const maxFileSize = await getMaxFileUploadSizeBytes();
372
-
373
- // Generate the proper path with prefix
374
- // Note: With client uploads, we use the original filename provided by the client
375
- // The file will be stored at: {pathPrefix}/user/files/{filename}
376
- const pathPrefix = process.env.NEXT_PUBLIC_CDN_PATH_PREFIX || '';
377
-
378
- // Create a DB record at the start of the upload to track it
379
- const uploadPurpose = purpose;
380
- const {
381
- data: insertedFile,
382
- error: insertError,
383
- }: PostgrestSingleResponse<Pick<AgentsServerDatabase['public']['Tables']['File']['Row'], 'id'>> =
384
- await supabase
385
- .from(await $getTableName('File'))
386
- .insert({
387
- userId: userId || null,
388
- fileName: pathname,
389
- fileSize: 0, // <- Will be updated when upload completes
390
- fileType: contentType,
391
- storageUrl: null, // <- To be updated on completion
392
- shortUrl: null, // <- To be updated on completion
393
- purpose: uploadPurpose,
394
- status: 'UPLOADING',
395
- })
396
- .select('id')
397
- .single();
398
-
399
- if (insertError) {
400
- console.error('🔼 Failed to create file record:', insertError);
401
- }
402
-
403
- console.info('🔼 Upload started, tracking file:', {
404
- pathname,
405
- fileId: insertedFile?.id,
406
- purpose: uploadPurpose,
407
- });
408
-
409
- return {
410
- maximumSizeInBytes: maxFileSize,
411
- addRandomSuffix: true, // Add random suffix to avoid filename collisions since we can't hash content
412
- tokenPayload: JSON.stringify({
413
- userId: userId || null,
414
- purpose: uploadPurpose,
415
- fileId: insertedFile?.id || null,
416
- uploadPath: pathname,
417
- pathPrefix,
418
- }),
419
- };
420
- },
421
- onUploadCompleted: async ({ blob, tokenPayload }) => {
422
- // !!!!
423
- // ⚠️ IMPORTANT: This callback is a WEBHOOK called by Vercel's servers AFTER the upload completes
424
- // - It runs in a DIFFERENT request context (not the original user request)
425
- // - It WON'T work in local development (Vercel can't reach localhost)
426
- // - All data must come from tokenPayload (userId, fileId, etc.)
427
- // - Need to create a fresh supabase client here
428
- console.info('🔼 Upload completed (webhook callback):', { blob, tokenPayload });
429
-
430
- try {
431
- const payload = parseJsonRecord(tokenPayload);
432
- const fileId = typeof payload.fileId === 'number' ? payload.fileId : null;
433
- const tokenUserId = typeof payload.userId === 'number' ? payload.userId : null;
434
- const tokenPurpose = normalizeUploadPurpose(payload.purpose);
435
- const uploadPath = typeof payload.uploadPath === 'string' ? payload.uploadPath : null;
436
-
437
- // Create fresh supabase client for this webhook context
438
- const supabase = $provideSupabase();
439
-
440
- // Security checks
441
- const securityResults = await checkUploadedFileSecurity(blob.url);
442
- const securityResultForDatabase =
443
- securityResults as AgentsServerDatabase['public']['Tables']['File']['Update']['securityResult'];
444
-
445
- if (fileId) {
446
- // Update the existing record by ID
447
- const { error: updateError } = await supabase
448
- .from(await $getTableName('File'))
449
- .update({
450
- userId: tokenUserId || null,
451
- fileSize: 0, // <- !!!!
452
- fileType: blob.contentType,
453
- storageUrl: blob.url,
454
- // <- TODO: !!!! Split between storageUrl and shortUrl
455
- purpose: tokenPurpose,
456
- status: 'COMPLETED',
457
- securityResult: securityResultForDatabase,
458
- })
459
- .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
+ });
460
215
 
461
- if (updateError) {
462
- console.error('🔼 Failed to update file record:', updateError);
463
- } else {
464
- console.info('🔼 File record updated successfully:', { fileId, shortUrl: blob.url });
465
- }
466
- } else if (uploadPath) {
467
- // Fallback: Update by uploadPath if fileId is not available
468
- const { error: updateError } = await supabase
469
- .from(await $getTableName('File'))
470
- .update({
471
- fileSize: 0, // <- !!!!
472
- fileType: blob.contentType,
473
- storageUrl: blob.url,
474
- status: 'COMPLETED',
475
- securityResult: securityResultForDatabase,
476
- })
477
- .eq('fileName', uploadPath)
478
- .eq('status', 'UPLOADING');
216
+ const securityResult = await checkUploadedFileSecurity(storageUrl);
217
+ await updateUploadedFileSecurityResult(storageUrl, securityResult);
479
218
 
480
- if (updateError) {
481
- console.error('🔼 Failed to update file record by uploadPath:', updateError);
482
- }
483
- }
484
- } catch (error) {
485
- console.error('🔼 Error in onUploadCompleted:', error);
486
- }
487
- },
219
+ return NextResponse.json({
220
+ url: storageUrl,
221
+ pathname,
222
+ contentType,
223
+ size: fileBuffer.byteLength,
488
224
  });
489
-
490
- return NextResponse.json(jsonResponse);
491
225
  } catch (error) {
492
226
  assertsError(error);
493
227
 
494
- console.error('🔼', error);
228
+ console.error('Upload failed:', error);
495
229
 
496
230
  return new Response(
497
231
  JSON.stringify(
@@ -508,10 +242,3 @@ export async function POST(request: NextRequest) {
508
242
  );
509
243
  }
510
244
  }
511
-
512
- // TODO: !!!! Change uploaded URLs from `storageUrl` to `shortUrl`
513
- // TODO: !!!! Record both `storageUrl` (actual storage location) and `shortUrl` in `File` table
514
- // TODO: !!!! Record `purpose` in `File` table
515
- // TODO: !!!! Record `userId` in `File` table
516
- // TODO: !!!! Record all things into `File` table
517
- // TODO: !!!! File type (mime type) of `.book` files should be `application/book` <- [🧠] !!!! Best mime type?!
@@ -186,10 +186,7 @@ export function AgentProfile(props: AgentProfileProps) {
186
186
  ) : (
187
187
  <div className="flex h-full w-full items-center justify-center overflow-hidden p-4 md:p-8">
188
188
  {/* Keep built-in visuals inside a centered square stage so different avatar renderers fit the tall profile card consistently. */}
189
- <div
190
- className="flex h-full w-full max-h-[80%] max-w-[80%] items-center justify-center"
191
- style={{ aspectRatio: '1 / 1' }}
192
- >
189
+ <div className="flex aspect-square w-[80%] items-center justify-center">
193
190
  <AgentAvatar
194
191
  agent={agent}
195
192
  baseUrl={publicUrl}
@@ -9,7 +9,6 @@ import { HamburgerMenu } from '../../../../../src/book-components/_common/Hambur
9
9
  import { useMenuHoisting } from '../../../../../src/book-components/_common/MenuHoisting/MenuHoistingContext';
10
10
  import { just } from '../../../../../src/utils/organization/just';
11
11
  import { getVisibleCommitmentDefinitions } from '../../utils/getVisibleCommitmentDefinitions';
12
- import { pushWithHeadless, useIsHeadless } from '../_utils/headlessParam';
13
12
  import { useInstallPromptState, type AgentContextMenuRenamePayload } from '../AgentContextMenu/AgentContextMenu';
14
13
  import { useAgentNaming } from '../AgentNaming/AgentNamingContext';
15
14
  import { QrCodeModal } from '../AgentProfile/QrCodeModal';
@@ -58,11 +57,9 @@ export function Header(props: HeaderProps) {
58
57
  feedbackMode = 'stars',
59
58
  shibbolethAuthenticationStatus,
60
59
  } = props;
61
-
62
60
  const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
63
61
  const router = useRouter();
64
62
  const pathname = usePathname();
65
- const isHeadless = useIsHeadless();
66
63
  const menuHoisting = useMenuHoisting();
67
64
  const mobileMenuHoisting = useMobileMenuHoisting();
68
65
  const { naming } = useAgentNaming();
@@ -73,13 +70,6 @@ export function Header(props: HeaderProps) {
73
70
  () => buildDocumentationDropdownItems(visibleDocumentationCommitments, t),
74
71
  [t, visibleDocumentationCommitments],
75
72
  );
76
-
77
- const fallbackNavigateToHref = useCallback(
78
- (href: string) => {
79
- pushWithHeadless(router, href, isHeadless);
80
- },
81
- [isHeadless, router],
82
- );
83
73
  const hoistedMobileMenuItems = mobileMenuHoisting?.menuItems || EMPTY_HOISTED_MOBILE_MENU_ITEMS;
84
74
  const {
85
75
  cancelMenuClose,
@@ -201,7 +191,6 @@ export function Header(props: HeaderProps) {
201
191
  isAdmin,
202
192
  isAuthenticated: Boolean(currentUser),
203
193
  isInstalled,
204
- navigateToHref: fallbackNavigateToHref,
205
194
  namingPlural: naming.plural,
206
195
  namingSingular: naming.singular,
207
196
  onAgentRenamed: handleAgentRenamedFromHeader,