@promptbook/cli 0.112.0-101 → 0.112.0-102
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 +6 -0
- package/apps/agents-server/package.json +1 -1
- package/apps/agents-server/scripts/prerender-homepage.js +76 -1
- package/apps/agents-server/src/app/actions.ts +0 -6
- package/apps/agents-server/src/app/admin/about/page.tsx +1 -1
- package/apps/agents-server/src/app/admin/login-methods/shibboleth/page.tsx +365 -0
- package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +3 -3
- package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +12 -3
- package/apps/agents-server/src/app/admin/usage/UsageClientTimelineChart.tsx +1 -1
- package/apps/agents-server/src/app/admin/users/[userId]/UserDetailClient.tsx +21 -14
- package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatPageLayout.tsx +2 -2
- package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatSidebarDefault.tsx +11 -7
- package/apps/agents-server/src/app/api/admin/cli-access/route.ts +27 -123
- package/apps/agents-server/src/app/api/admin/code-runners/authentication/route.ts +33 -125
- package/apps/agents-server/src/app/api/auth/login/route.ts +0 -10
- package/apps/agents-server/src/app/api/auth/shibboleth/acs/route.ts +77 -57
- package/apps/agents-server/src/app/api/auth/shibboleth/login/route.ts +57 -33
- package/apps/agents-server/src/app/api/auth/shibboleth/metadata/route.ts +4 -29
- package/apps/agents-server/src/app/api/auth/shibboleth/status/route.ts +17 -0
- package/apps/agents-server/src/app/api/upload/route.ts +230 -18
- package/apps/agents-server/src/app/api/users/[username]/route.ts +1 -1
- package/apps/agents-server/src/app/api/users/route.ts +5 -5
- package/apps/agents-server/src/app/dashboard/page.tsx +1 -1
- package/apps/agents-server/src/app/docs/[docId]/page.tsx +1 -1
- package/apps/agents-server/src/app/docs/page.tsx +1 -1
- package/apps/agents-server/src/app/globals.css +100 -0
- package/apps/agents-server/src/app/layout.tsx +7 -0
- package/apps/agents-server/src/app/recycle-bin/page.tsx +1 -1
- package/apps/agents-server/src/app/system/settings/KeybindingsSettingsClient.tsx +13 -7
- package/apps/agents-server/src/components/AdminTerminal/useAdminTerminalSession.ts +29 -1
- package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +3 -3
- package/apps/agents-server/src/components/AgentProfile/AgentProfileImage.tsx +8 -2
- package/apps/agents-server/src/components/DocsToolbar/DocsToolbar.tsx +4 -4
- package/apps/agents-server/src/components/DocumentationContent/DocumentationContent.tsx +9 -9
- package/apps/agents-server/src/components/Footer/Footer.tsx +7 -7
- package/apps/agents-server/src/components/Header/Header.tsx +24 -4
- package/apps/agents-server/src/components/Header/HeaderTypes.ts +6 -0
- package/apps/agents-server/src/components/Header/buildHeaderSystemMenuItems.ts +51 -1
- package/apps/agents-server/src/components/Homepage/Card.tsx +1 -1
- package/apps/agents-server/src/components/Homepage/Section.tsx +3 -1
- package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +12 -1
- package/apps/agents-server/src/components/LoginForm/LoginForm.tsx +100 -149
- package/apps/agents-server/src/components/Skeleton/ConsolePageLoadingSkeleton.tsx +1 -1
- package/apps/agents-server/src/components/Skeleton/DocumentationRouteLoadingSkeleton.tsx +1 -1
- package/apps/agents-server/src/components/Skeleton/HomepageLoadingSkeleton.tsx +1 -1
- package/apps/agents-server/src/components/UsersList/UsersList.tsx +20 -4
- package/apps/agents-server/src/components/UsersList/useUsersAdmin.ts +3 -0
- package/apps/agents-server/src/constants/shibbolethAuth.ts +139 -0
- package/apps/agents-server/src/database/metadataDefaults.ts +54 -80
- package/apps/agents-server/src/database/migrate.ts +30 -1
- package/apps/agents-server/src/database/migrations/2026-06-0100-shibboleth-auth.sql +136 -0
- package/apps/agents-server/src/database/sqlite/$provideLocalSqliteSupabase.ts +88 -36
- package/apps/agents-server/src/languages/ServerTranslationKeys.ts +4 -2
- package/apps/agents-server/src/languages/translations/czech.yaml +4 -2
- package/apps/agents-server/src/languages/translations/english.yaml +5 -3
- package/apps/agents-server/src/tools/$provideCdnForServer.ts +69 -23
- package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +54 -6
- package/apps/agents-server/src/utils/cdn/classes/TrackedFilesStorage.ts +4 -6
- package/apps/agents-server/src/utils/cdn/resolveCdnStorageProvider.ts +40 -0
- package/apps/agents-server/src/utils/chatExport/renderHtmlToPdfOnServer.ts +11 -0
- package/apps/agents-server/src/utils/createAdminTerminalRouteHandlers.ts +264 -0
- package/apps/agents-server/src/utils/shareTargetPayloads.ts +11 -10
- package/apps/agents-server/src/utils/shibbolethAuthentication.ts +729 -621
- package/apps/agents-server/src/utils/upload/createBookEditorUploadHandler.ts +137 -19
- package/esm/index.es.js +1 -1
- package/esm/src/book-components/Chat/MarkdownContent/MarkdownContent.d.ts +1 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +2 -2
- package/src/book-components/Chat/MarkdownContent/MarkdownContent.tsx +65 -4
- package/src/other/templates/getTemplatesPipelineCollection.ts +788 -719
- package/src/version.ts +2 -2
- package/src/versions.txt +1 -0
- package/umd/index.umd.js +1 -1
- package/umd/src/book-components/Chat/MarkdownContent/MarkdownContent.d.ts +1 -0
- package/umd/src/version.d.ts +1 -1
- package/apps/agents-server/src/app/api/auth/methods/route.ts +0 -44
- package/apps/agents-server/src/constants/authenticationMethods.ts +0 -74
- package/apps/agents-server/src/constants/shibbolethAuthentication.ts +0 -107
|
@@ -1,46 +1,70 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
2
|
import {
|
|
3
3
|
createShibbolethSamlClient,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
getShibbolethRequestDetails,
|
|
5
|
+
recordShibbolethAuthenticationAttempt,
|
|
6
|
+
resolveShibbolethAuthenticationConfiguration,
|
|
7
|
+
sanitizeShibbolethRelayState,
|
|
8
8
|
} from '../../../../../utils/shibbolethAuthentication';
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* Forces this route to run in Node.js because node-saml depends on Node crypto/zlib APIs.
|
|
12
|
-
*/
|
|
13
|
-
export const runtime = 'nodejs';
|
|
14
|
-
|
|
15
10
|
/**
|
|
16
11
|
* Handles get.
|
|
17
12
|
*/
|
|
18
|
-
export async function GET(request:
|
|
19
|
-
const
|
|
13
|
+
export async function GET(request: Request) {
|
|
14
|
+
const requestDetails = getShibbolethRequestDetails(request);
|
|
15
|
+
const relayState = sanitizeShibbolethRelayState(new URL(request.url).searchParams.get('returnTo'));
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (!configuration.isConfigured) {
|
|
27
|
-
const error = new ShibbolethConfigurationError(configuration.missingConfiguration);
|
|
28
|
-
logShibbolethAuthenticationEvent('login_rejected_incomplete_configuration', {
|
|
29
|
-
missingConfiguration: configuration.missingConfiguration,
|
|
17
|
+
try {
|
|
18
|
+
const configuration = await resolveShibbolethAuthenticationConfiguration({
|
|
19
|
+
requestUrl: request.url,
|
|
20
|
+
isIdentityProviderMetadataValidationEnabled: true,
|
|
30
21
|
});
|
|
31
|
-
return NextResponse.json({ error: error.message }, { status: 503 });
|
|
32
|
-
}
|
|
33
22
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
23
|
+
if (!configuration.isActive) {
|
|
24
|
+
await recordShibbolethAuthenticationAttempt({
|
|
25
|
+
stage: 'LOGIN_REQUEST',
|
|
26
|
+
status: 'REJECTED',
|
|
27
|
+
requestDetails,
|
|
28
|
+
relayState,
|
|
29
|
+
errorMessage: 'Shibboleth authentication is not active.',
|
|
30
|
+
});
|
|
31
|
+
return NextResponse.json({ error: 'Shibboleth authentication is not active.' }, { status: 404 });
|
|
32
|
+
}
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
if (!configuration.isConfigured) {
|
|
35
|
+
const errorMessage = configuration.errors.join(' ');
|
|
36
|
+
await recordShibbolethAuthenticationAttempt({
|
|
37
|
+
stage: 'LOGIN_REQUEST',
|
|
38
|
+
status: 'FAILED',
|
|
39
|
+
requestDetails,
|
|
40
|
+
relayState,
|
|
41
|
+
errorMessage,
|
|
42
|
+
});
|
|
43
|
+
return NextResponse.json({ error: errorMessage }, { status: 503 });
|
|
44
|
+
}
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
const saml = createShibbolethSamlClient(configuration);
|
|
47
|
+
const authorizeUrl = await saml.getAuthorizeUrlAsync(relayState, undefined, {});
|
|
48
|
+
|
|
49
|
+
await recordShibbolethAuthenticationAttempt({
|
|
50
|
+
stage: 'LOGIN_REQUEST',
|
|
51
|
+
status: 'REDIRECTED',
|
|
52
|
+
requestDetails,
|
|
53
|
+
relayState,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return NextResponse.redirect(authorizeUrl);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to start Shibboleth authentication.';
|
|
59
|
+
await recordShibbolethAuthenticationAttempt({
|
|
60
|
+
stage: 'LOGIN_REQUEST',
|
|
61
|
+
status: 'FAILED',
|
|
62
|
+
requestDetails,
|
|
63
|
+
relayState,
|
|
64
|
+
errorMessage,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
console.error('Shibboleth login start error:', error);
|
|
68
|
+
return NextResponse.json({ error: errorMessage }, { status: 500 });
|
|
69
|
+
}
|
|
46
70
|
}
|
|
@@ -1,40 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
createShibbolethServiceProviderMetadata,
|
|
4
|
-
loadShibbolethConfiguration,
|
|
5
|
-
logShibbolethAuthenticationEvent,
|
|
6
|
-
} from '../../../../../utils/shibbolethAuthentication';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Forces this route to run in Node.js because node-saml metadata generation depends on Node APIs.
|
|
10
|
-
*/
|
|
11
|
-
export const runtime = 'nodejs';
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { createShibbolethServiceProviderMetadataXml } from '../../../../../utils/shibbolethAuthentication';
|
|
12
3
|
|
|
13
4
|
/**
|
|
14
5
|
* Handles get.
|
|
15
6
|
*/
|
|
16
|
-
export async function GET(request:
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
if (!configuration.isEnabled || !configuration.issuer || !configuration.callbackUrl) {
|
|
20
|
-
logShibbolethAuthenticationEvent('metadata_rejected_disabled');
|
|
21
|
-
return NextResponse.json({ error: 'Shibboleth authentication is not enabled.' }, { status: 404 });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const metadataXml = createShibbolethServiceProviderMetadata({
|
|
25
|
-
issuer: configuration.issuer,
|
|
26
|
-
callbackUrl: configuration.callbackUrl,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
logShibbolethAuthenticationEvent('metadata_served', {
|
|
30
|
-
issuer: configuration.issuer,
|
|
31
|
-
callbackUrl: configuration.callbackUrl,
|
|
32
|
-
});
|
|
7
|
+
export async function GET(request: Request) {
|
|
8
|
+
const metadataXml = await createShibbolethServiceProviderMetadataXml(request.url);
|
|
33
9
|
|
|
34
10
|
return new NextResponse(metadataXml, {
|
|
35
11
|
headers: {
|
|
36
12
|
'Content-Type': 'application/samlmetadata+xml; charset=utf-8',
|
|
37
|
-
'Cache-Control': 'no-store',
|
|
38
13
|
},
|
|
39
14
|
});
|
|
40
15
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { resolveShibbolethAuthenticationConfiguration } from '../../../../../utils/shibbolethAuthentication';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handles get.
|
|
6
|
+
*/
|
|
7
|
+
export async function GET(request: Request) {
|
|
8
|
+
const configuration = await resolveShibbolethAuthenticationConfiguration({
|
|
9
|
+
requestUrl: request.url,
|
|
10
|
+
isIdentityProviderMetadataValidationEnabled: false,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
return NextResponse.json({
|
|
14
|
+
isActive: configuration.isActive,
|
|
15
|
+
isConfigured: configuration.isConfigured,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { $getTableName } from '@/src/database/$getTableName';
|
|
2
2
|
import { $provideSupabase } from '@/src/database/$provideSupabase';
|
|
3
|
+
import { $provideUntrackedCdnForServer } from '@/src/tools/$provideCdnForServer';
|
|
4
|
+
import { getSafeCdnPath } from '@/src/utils/cdn/utils/getSafeCdnPath';
|
|
3
5
|
import { serializeError } from '@promptbook-local/utils';
|
|
4
6
|
import type { PostgrestSingleResponse, SupabaseClient } from '@supabase/supabase-js';
|
|
5
7
|
import { handleUpload, type HandleUploadBody } from '@vercel/blob/client';
|
|
6
8
|
import { NextRequest, NextResponse } from 'next/server';
|
|
7
9
|
import { assertsError } from '../../../../../../src/errors/assertsError';
|
|
10
|
+
import { DatabaseError } from '../../../../../../src/errors/DatabaseError';
|
|
11
|
+
import { LimitReachedError } from '../../../../../../src/errors/LimitReachedError';
|
|
12
|
+
import { NotAllowed } from '../../../../../../src/errors/NotAllowed';
|
|
8
13
|
import { getUserIdFromRequest } from '../../../../src/utils/getUserIdFromRequest';
|
|
9
14
|
import type { AgentsServerDatabase } from '../../../database/schema';
|
|
10
15
|
import { FILE_SECURITY_CHECKERS } from '../../../file-security-checkers';
|
|
@@ -48,6 +53,41 @@ const DEFAULT_UPLOAD_CONTENT_TYPE = 'application/octet-stream';
|
|
|
48
53
|
*/
|
|
49
54
|
const MIME_TYPE_PATTERN = /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/i;
|
|
50
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.
|
|
86
|
+
*
|
|
87
|
+
* @private
|
|
88
|
+
*/
|
|
89
|
+
const MULTIPART_FORM_DATA_CONTENT_TYPE = 'multipart/form-data';
|
|
90
|
+
|
|
51
91
|
/**
|
|
52
92
|
* Safely parses a JSON string into an object; returns empty object on invalid payload.
|
|
53
93
|
*
|
|
@@ -119,11 +159,200 @@ function resolveUploadClientPayload(clientPayload: string | null | undefined): {
|
|
|
119
159
|
};
|
|
120
160
|
}
|
|
121
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
|
+
}
|
|
170
|
+
|
|
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);
|
|
178
|
+
|
|
179
|
+
return typeof value === 'string' ? value : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Normalizes a client-provided CDN pathname into a relative object key.
|
|
184
|
+
*
|
|
185
|
+
* @private
|
|
186
|
+
*/
|
|
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, '/');
|
|
194
|
+
|
|
195
|
+
if (!normalizedPathname || normalizedPathname.split('/').includes('..')) {
|
|
196
|
+
throw new NotAllowed('Upload pathname must be a safe relative CDN path.');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return getSafeCdnPath({ pathname: normalizedPathname });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Runs configured file-security checks for an uploaded public URL.
|
|
204
|
+
*
|
|
205
|
+
* @private
|
|
206
|
+
*/
|
|
207
|
+
async function checkUploadedFileSecurity(storageUrl: string): Promise<Record<string, unknown>> {
|
|
208
|
+
const securityResults: Record<string, unknown> = {};
|
|
209
|
+
|
|
210
|
+
for (const checkerId in FILE_SECURITY_CHECKERS) {
|
|
211
|
+
try {
|
|
212
|
+
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);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error(`🛡️ Security check failed for ${checkerId}:`, error);
|
|
219
|
+
securityResults[checkerId] = {
|
|
220
|
+
isSafe: false,
|
|
221
|
+
status: 'ERROR',
|
|
222
|
+
confidence: 0,
|
|
223
|
+
message: error instanceof Error ? error.message : String(error),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return securityResults;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Inserts a completed file row for a server-routed upload.
|
|
233
|
+
*
|
|
234
|
+
* @private
|
|
235
|
+
*/
|
|
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}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
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);
|
|
324
|
+
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
|
+
});
|
|
337
|
+
|
|
338
|
+
return NextResponse.json({
|
|
339
|
+
url: storageUrl,
|
|
340
|
+
pathname,
|
|
341
|
+
fileId,
|
|
342
|
+
contentType,
|
|
343
|
+
size: buffer.byteLength,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
122
347
|
/**
|
|
123
348
|
* Handles post.
|
|
124
349
|
*/
|
|
125
350
|
export async function POST(request: NextRequest) {
|
|
126
351
|
try {
|
|
352
|
+
if (isServerRoutedUploadRequest(request)) {
|
|
353
|
+
return await handleServerRoutedUpload(request);
|
|
354
|
+
}
|
|
355
|
+
|
|
127
356
|
const body = (await request.json()) as HandleUploadBody;
|
|
128
357
|
const userId = await getUserIdFromRequest(request);
|
|
129
358
|
const supabase: SupabaseClient<AgentsServerDatabase> = $provideSupabase();
|
|
@@ -209,26 +438,9 @@ export async function POST(request: NextRequest) {
|
|
|
209
438
|
const supabase = $provideSupabase();
|
|
210
439
|
|
|
211
440
|
// Security checks
|
|
212
|
-
const securityResults
|
|
441
|
+
const securityResults = await checkUploadedFileSecurity(blob.url);
|
|
213
442
|
const securityResultForDatabase =
|
|
214
443
|
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
|
-
}
|
|
232
444
|
|
|
233
445
|
if (fileId) {
|
|
234
446
|
// Update the existing record by ID
|
|
@@ -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('
|
|
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('
|
|
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('
|
|
61
|
+
.select('*')
|
|
62
62
|
.single();
|
|
63
63
|
|
|
64
64
|
if (error) {
|
|
65
|
-
if (error.code === '23505') {
|
|
66
|
-
|
|
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
|
}
|