@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.
- 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/image-generator-test/ImageAttachmentsEditor.tsx +11 -6
- package/apps/agents-server/src/app/admin/login-methods/shibboleth/page.tsx +365 -0
- package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +13 -15
- package/apps/agents-server/src/app/admin/servers/ServersRegistryTable.tsx +3 -3
- package/apps/agents-server/src/app/admin/servers/useCreateServerWizard.ts +13 -14
- 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 +148 -209
- 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 +54 -11
- package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +18 -2
- package/apps/agents-server/src/utils/cdn/classes/TrackedFilesStorage.ts +6 -5
- package/apps/agents-server/src/utils/cdn/interfaces/IFilesStorage.ts +5 -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 +19 -66
- package/apps/agents-server/src/utils/shibbolethAuthentication.ts +729 -621
- package/apps/agents-server/src/utils/upload/createBookEditorUploadHandler.ts +19 -28
- package/apps/agents-server/src/utils/upload/uploadFileToServer.ts +113 -0
- package/esm/index.es.js +194 -35
- package/esm/index.es.js.map +1 -1
- package/esm/scripts/run-codex-prompts/common/waitForPause.d.ts +12 -0
- package/esm/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
- package/esm/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +1 -0
- package/esm/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +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 +63 -4
- package/src/other/templates/getTemplatesPipelineCollection.ts +730 -739
- package/src/version.ts +2 -2
- package/src/versions.txt +2 -0
- package/umd/index.umd.js +194 -35
- package/umd/index.umd.js.map +1 -1
- package/umd/scripts/run-codex-prompts/common/waitForPause.d.ts +12 -0
- package/umd/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
- package/umd/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +1 -0
- package/umd/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +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
|
@@ -124,6 +124,7 @@ header.menuLabel: Menu
|
|
|
124
124
|
header.myAccount: My Account
|
|
125
125
|
header.superAdmin: Super Admin
|
|
126
126
|
header.administration: Administration
|
|
127
|
+
header.loginMethods: Login Methods
|
|
127
128
|
header.monitoringAndUsage: Monitoring & Usage
|
|
128
129
|
header.integrationsAndKeys: Integrations & Keys
|
|
129
130
|
header.developerDebug: Developer / Debug
|
|
@@ -164,6 +165,7 @@ header.errorSimulation: Error simulation
|
|
|
164
165
|
header.imagesGallery: Images gallery
|
|
165
166
|
header.files: Files
|
|
166
167
|
header.users: Users
|
|
168
|
+
header.shibboleth: Shibboleth
|
|
167
169
|
header.versionInfo: Version info
|
|
168
170
|
header.experiments: Experiments
|
|
169
171
|
header.story: Story
|
|
@@ -194,8 +196,8 @@ login.passwordLabel: Password
|
|
|
194
196
|
login.passwordPlaceholder: Enter your password
|
|
195
197
|
login.loggingIn: Logging in...
|
|
196
198
|
login.loginAction: Log in
|
|
197
|
-
login.
|
|
198
|
-
login.
|
|
199
|
+
login.shibbolethLoginAction: Continue with Shibboleth
|
|
200
|
+
login.shibbolethNotConfigured: Shibboleth login is active but needs administrator setup.
|
|
199
201
|
login.forgottenPassword: Forgotten password?
|
|
200
202
|
login.registerNewUser: Register new user
|
|
201
203
|
login.errorOccurred: An error occurred
|
|
@@ -382,7 +384,7 @@ footer.logosAndBranding: Logos & Branding
|
|
|
382
384
|
footer.connectSectionTitle: Connect
|
|
383
385
|
footer.linksSectionTitle: Links
|
|
384
386
|
footer.allRightsReserved: All rights reserved.
|
|
385
|
-
footer.madeInCzechRepublic: Made with ❤️ in the Czech Republic
|
|
387
|
+
footer.madeInCzechRepublic: Made with ❤️ in the Czech Republic
|
|
386
388
|
footer.engineVersion: Promptbook engine version
|
|
387
389
|
forbidden.title: 403 Forbidden
|
|
388
390
|
forbidden.message: You do not have permission to access this page.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { $provideSupabaseForServer } from '../database/$provideSupabaseForServer';
|
|
2
|
+
import { DigitalOceanSpaces } from '../utils/cdn/classes/DigitalOceanSpaces';
|
|
2
3
|
import { TrackedFilesStorage } from '../utils/cdn/classes/TrackedFilesStorage';
|
|
3
4
|
import { VercelBlobStorage } from '../utils/cdn/classes/VercelBlobStorage';
|
|
4
5
|
import { IIFilesStorageWithCdn } from '../utils/cdn/interfaces/IFilesStorage';
|
|
@@ -15,29 +16,71 @@ let cdn: IIFilesStorageWithCdn | null = null;
|
|
|
15
16
|
*/
|
|
16
17
|
export function $provideCdnForServer(): IIFilesStorageWithCdn {
|
|
17
18
|
if (!cdn) {
|
|
18
|
-
const inner =
|
|
19
|
-
token: process.env.VERCEL_BLOB_READ_WRITE_TOKEN!,
|
|
20
|
-
pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX!,
|
|
21
|
-
cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
|
|
22
|
-
});
|
|
23
|
-
|
|
19
|
+
const inner = createCdnStorageForServer();
|
|
24
20
|
const supabase = $provideSupabaseForServer();
|
|
25
21
|
cdn = new TrackedFilesStorage(inner, supabase);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return cdn;
|
|
25
|
+
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Creates the configured CDN storage implementation for server-side file operations.
|
|
29
|
+
*
|
|
30
|
+
* @private helper of `$provideCdnForServer`
|
|
31
|
+
*/
|
|
32
|
+
function createCdnStorageForServer(): IIFilesStorageWithCdn {
|
|
33
|
+
if (isS3CompatibleStorageSelected()) {
|
|
34
|
+
return new DigitalOceanSpaces({
|
|
29
35
|
bucket: process.env.CDN_BUCKET!,
|
|
30
|
-
pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX
|
|
36
|
+
pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX || '',
|
|
31
37
|
endpoint: process.env.CDN_ENDPOINT!,
|
|
32
38
|
accessKeyId: process.env.CDN_ACCESS_KEY_ID!,
|
|
33
39
|
secretAccessKey: process.env.CDN_SECRET_ACCESS_KEY!,
|
|
34
40
|
cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
|
|
35
41
|
gzip: true,
|
|
42
|
+
forcePathStyle: process.env.CDN_FORCE_PATH_STYLE === 'true',
|
|
43
|
+
region: process.env.CDN_REGION || 'auto',
|
|
36
44
|
});
|
|
37
|
-
*/
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
return
|
|
47
|
+
return new VercelBlobStorage({
|
|
48
|
+
token: process.env.VERCEL_BLOB_READ_WRITE_TOKEN!,
|
|
49
|
+
pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX || '',
|
|
50
|
+
cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Checks whether the current environment should use the S3-compatible storage implementation.
|
|
56
|
+
*
|
|
57
|
+
* @private helper of `$provideCdnForServer`
|
|
58
|
+
*/
|
|
59
|
+
function isS3CompatibleStorageSelected(): boolean {
|
|
60
|
+
const storageMode = (process.env.PTBK_FILE_STORAGE_MODE || process.env.CDN_PROVIDER || '').toLowerCase();
|
|
61
|
+
const isS3StorageMode =
|
|
62
|
+
storageMode === 's3' || storageMode === 'external-s3' || storageMode === 'self-contained-s3';
|
|
63
|
+
|
|
64
|
+
if (isS3StorageMode) {
|
|
65
|
+
return hasS3CompatibleStorageConfiguration();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return !process.env.VERCEL_BLOB_READ_WRITE_TOKEN && hasS3CompatibleStorageConfiguration();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Checks whether all S3-compatible storage environment variables are present.
|
|
73
|
+
*
|
|
74
|
+
* @private helper of `$provideCdnForServer`
|
|
75
|
+
*/
|
|
76
|
+
function hasS3CompatibleStorageConfiguration(): boolean {
|
|
77
|
+
return Boolean(
|
|
78
|
+
process.env.CDN_BUCKET &&
|
|
79
|
+
process.env.CDN_ENDPOINT &&
|
|
80
|
+
process.env.CDN_ACCESS_KEY_ID &&
|
|
81
|
+
process.env.CDN_SECRET_ACCESS_KEY &&
|
|
82
|
+
process.env.NEXT_PUBLIC_CDN_PUBLIC_URL,
|
|
83
|
+
);
|
|
41
84
|
}
|
|
42
85
|
|
|
43
86
|
// TODO: [🏓] Unite `xxxForServer` and `xxxForNode` naming
|
|
@@ -16,6 +16,8 @@ type IDigitalOceanSpacesConfig = {
|
|
|
16
16
|
readonly secretAccessKey: string;
|
|
17
17
|
readonly cdnPublicUrl: URL;
|
|
18
18
|
readonly gzip: boolean;
|
|
19
|
+
readonly forcePathStyle?: boolean;
|
|
20
|
+
readonly region?: string;
|
|
19
21
|
|
|
20
22
|
// TODO: [⛳️] Probbably prefix should be in this config not on the consumer side
|
|
21
23
|
};
|
|
@@ -32,8 +34,9 @@ export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
|
|
|
32
34
|
|
|
33
35
|
public constructor(private readonly config: IDigitalOceanSpacesConfig) {
|
|
34
36
|
this.s3 = new S3Client({
|
|
35
|
-
region: 'auto',
|
|
36
|
-
endpoint:
|
|
37
|
+
region: config.region || 'auto',
|
|
38
|
+
endpoint: normalizeS3Endpoint(config.endpoint),
|
|
39
|
+
forcePathStyle: config.forcePathStyle,
|
|
37
40
|
credentials: {
|
|
38
41
|
accessKeyId: config.accessKeyId,
|
|
39
42
|
secretAccessKey: config.secretAccessKey,
|
|
@@ -119,5 +122,18 @@ export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
|
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Normalizes endpoint values from legacy host-only configuration and full S3 URLs.
|
|
127
|
+
*
|
|
128
|
+
* @private helper of `DigitalOceanSpaces`
|
|
129
|
+
*/
|
|
130
|
+
function normalizeS3Endpoint(endpoint: string): string {
|
|
131
|
+
if (/^https?:\/\//i.test(endpoint)) {
|
|
132
|
+
return endpoint;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return `https://${endpoint}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
122
138
|
// TODO: Implement Read-only mode
|
|
123
139
|
// TODO: [☹️] Unite with `PromptbookStorage` and move to `/src/...`
|
|
@@ -33,24 +33,25 @@ export class TrackedFilesStorage implements IIFilesStorageWithCdn {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
public async setItem(key: string, file: IFile): Promise<void> {
|
|
36
|
-
|
|
37
36
|
await this.inner.setItem(key, file);
|
|
38
37
|
|
|
39
38
|
try {
|
|
40
39
|
const { userId, purpose } = file;
|
|
41
40
|
const cdnUrl = this.getItemUrl(key).href;
|
|
41
|
+
const securityResult =
|
|
42
|
+
file.securityResult as AgentsServerDatabase['public']['Tables']['File']['Insert']['securityResult'];
|
|
42
43
|
|
|
43
|
-
|
|
44
44
|
await this.supabase.from(await $getTableName('File')).insert({
|
|
45
45
|
userId: userId || null,
|
|
46
46
|
fileName: key,
|
|
47
47
|
fileSize: file.fileSize ?? file.data.length,
|
|
48
48
|
fileType: file.type,
|
|
49
|
-
cdnUrl,
|
|
49
|
+
storageUrl: cdnUrl,
|
|
50
|
+
shortUrl: null,
|
|
50
51
|
purpose: purpose || 'UNKNOWN',
|
|
52
|
+
status: 'COMPLETED',
|
|
53
|
+
securityResult: securityResult || null,
|
|
51
54
|
});
|
|
52
|
-
|
|
53
|
-
|
|
54
55
|
} catch (error) {
|
|
55
56
|
console.error('Failed to track upload:', error);
|
|
56
57
|
}
|
|
@@ -25,6 +25,11 @@ export type IFile = {
|
|
|
25
25
|
* Note: This is optional, if not provided, the size of the buffer is used
|
|
26
26
|
*/
|
|
27
27
|
fileSize?: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Optional file security result collected before the file is tracked.
|
|
31
|
+
*/
|
|
32
|
+
securityResult?: Record<string, unknown>;
|
|
28
33
|
};
|
|
29
34
|
|
|
30
35
|
/**
|
|
@@ -9,6 +9,16 @@ const CHAT_EXPORT_PDF_VIEWPORT = {
|
|
|
9
9
|
height: 1600,
|
|
10
10
|
} as const;
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Page margins applied to every exported PDF page.
|
|
14
|
+
*/
|
|
15
|
+
const CHAT_EXPORT_PDF_MARGIN = {
|
|
16
|
+
top: '0.5in',
|
|
17
|
+
right: '0.5in',
|
|
18
|
+
bottom: '0.5in',
|
|
19
|
+
left: '0.5in',
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
12
22
|
/**
|
|
13
23
|
* Prints standalone HTML into a PDF using a short-lived headless Chromium instance.
|
|
14
24
|
*
|
|
@@ -31,6 +41,7 @@ export async function renderHtmlToPdfOnServer(html: string): Promise<Buffer> {
|
|
|
31
41
|
|
|
32
42
|
return await page.pdf({
|
|
33
43
|
format: 'Letter',
|
|
44
|
+
margin: CHAT_EXPORT_PDF_MARGIN,
|
|
34
45
|
printBackground: true,
|
|
35
46
|
preferCSSPageSize: true,
|
|
36
47
|
});
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { createInteractiveTerminalEventStream } from './createInteractiveTerminalEventStream';
|
|
3
|
+
import { isUserGlobalAdmin } from './isUserGlobalAdmin';
|
|
4
|
+
import type { InteractiveTerminalSessionSubscriber } from './interactiveTerminalSession';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Minimal browser-safe terminal session shape handled by the shared admin API routes.
|
|
8
|
+
*/
|
|
9
|
+
type AdminTerminalRouteSession = {
|
|
10
|
+
/**
|
|
11
|
+
* Whether the terminal process is still active.
|
|
12
|
+
*/
|
|
13
|
+
readonly isRunning: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Browser stream callbacks accepted by one admin terminal route.
|
|
18
|
+
*/
|
|
19
|
+
type AdminTerminalRouteSubscriber<TSession extends AdminTerminalRouteSession> = {
|
|
20
|
+
/**
|
|
21
|
+
* Called whenever the terminal backend emits output.
|
|
22
|
+
*/
|
|
23
|
+
readonly onOutput: InteractiveTerminalSessionSubscriber['onOutput'];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Called once the terminal backend exits.
|
|
27
|
+
*/
|
|
28
|
+
readonly onExit: (event: { readonly type: 'exit'; readonly snapshot: TSession }) => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Terminal backend operations provided by each specific admin terminal.
|
|
33
|
+
*/
|
|
34
|
+
type AdminTerminalRouteBackend<TSession extends AdminTerminalRouteSession> = {
|
|
35
|
+
/**
|
|
36
|
+
* Returns the latest session for the terminal purpose.
|
|
37
|
+
*/
|
|
38
|
+
readonly getLatestSession: (request: Request) => Promise<TSession | null> | TSession | null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Looks up one session by id.
|
|
42
|
+
*/
|
|
43
|
+
readonly getSession: (sessionId: string) => Promise<TSession | null> | TSession | null;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Starts or reconnects to the terminal session.
|
|
47
|
+
*/
|
|
48
|
+
readonly startSession: (request: Request) => Promise<TSession> | TSession;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Sends raw input to the terminal session.
|
|
52
|
+
*/
|
|
53
|
+
readonly writeSessionInput: (sessionId: string, input: string) => Promise<TSession> | TSession;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Stops one terminal session.
|
|
57
|
+
*/
|
|
58
|
+
readonly stopSession: (sessionId: string) => Promise<TSession> | TSession;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Subscribes one browser stream to live terminal events.
|
|
62
|
+
*/
|
|
63
|
+
readonly subscribeToSession: (
|
|
64
|
+
sessionId: string,
|
|
65
|
+
subscriber: AdminTerminalRouteSubscriber<TSession>,
|
|
66
|
+
) => (() => void) | null;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* User-facing error messages used by the generated terminal route handlers.
|
|
71
|
+
*/
|
|
72
|
+
type AdminTerminalRouteMessages = {
|
|
73
|
+
/**
|
|
74
|
+
* Error used when loading the latest session fails.
|
|
75
|
+
*/
|
|
76
|
+
readonly loadErrorMessage: string;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Error used when the stream request omits a session id.
|
|
80
|
+
*/
|
|
81
|
+
readonly missingStreamSessionIdMessage: string;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Error used when the requested stream session does not exist.
|
|
85
|
+
*/
|
|
86
|
+
readonly sessionNotFoundMessage: string;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Error used when starting a session fails.
|
|
90
|
+
*/
|
|
91
|
+
readonly startErrorMessage: string;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Error used when the input request is malformed.
|
|
95
|
+
*/
|
|
96
|
+
readonly missingInputMessage: string;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Error used when writing input fails.
|
|
100
|
+
*/
|
|
101
|
+
readonly sendErrorMessage: string;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Error used when the stop request omits a session id.
|
|
105
|
+
*/
|
|
106
|
+
readonly missingStopSessionIdMessage: string;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Error used when stopping a session fails.
|
|
110
|
+
*/
|
|
111
|
+
readonly stopErrorMessage: string;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Generated Next.js route handlers for one admin terminal endpoint.
|
|
116
|
+
*/
|
|
117
|
+
type AdminTerminalRouteHandlers = {
|
|
118
|
+
/**
|
|
119
|
+
* Loads the latest terminal session or streams a specific session.
|
|
120
|
+
*/
|
|
121
|
+
readonly GET: (request: Request) => Promise<Response>;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Starts or reconnects to the terminal session.
|
|
125
|
+
*/
|
|
126
|
+
readonly POST: (request: Request) => Promise<Response>;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Sends raw input to a running terminal session.
|
|
130
|
+
*/
|
|
131
|
+
readonly PATCH: (request: Request) => Promise<Response>;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Stops one terminal session.
|
|
135
|
+
*/
|
|
136
|
+
readonly DELETE: (request: Request) => Promise<Response>;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Creates consistent admin terminal API handlers for GET/stream, POST, PATCH, and DELETE.
|
|
141
|
+
*
|
|
142
|
+
* @param backend - Terminal-specific backend operations.
|
|
143
|
+
* @param messages - Terminal-specific error messages.
|
|
144
|
+
* @returns Next.js route handlers ready to export from an API route.
|
|
145
|
+
*/
|
|
146
|
+
export function createAdminTerminalRouteHandlers<TSession extends AdminTerminalRouteSession>(
|
|
147
|
+
backend: AdminTerminalRouteBackend<TSession>,
|
|
148
|
+
messages: AdminTerminalRouteMessages,
|
|
149
|
+
): AdminTerminalRouteHandlers {
|
|
150
|
+
return {
|
|
151
|
+
async GET(request: Request): Promise<Response> {
|
|
152
|
+
if (!(await isUserGlobalAdmin())) {
|
|
153
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const { searchParams } = new URL(request.url);
|
|
158
|
+
const sessionId = searchParams.get('sessionId')?.trim() || '';
|
|
159
|
+
const isStreamRequested = searchParams.get('stream') === '1';
|
|
160
|
+
|
|
161
|
+
if (isStreamRequested) {
|
|
162
|
+
if (!sessionId) {
|
|
163
|
+
return NextResponse.json({ error: messages.missingStreamSessionIdMessage }, { status: 400 });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const session = await backend.getSession(sessionId);
|
|
167
|
+
if (!session) {
|
|
168
|
+
return NextResponse.json({ error: messages.sessionNotFoundMessage }, { status: 404 });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return createInteractiveTerminalEventStream(
|
|
172
|
+
request,
|
|
173
|
+
sessionId,
|
|
174
|
+
session,
|
|
175
|
+
backend.subscribeToSession,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return NextResponse.json({
|
|
180
|
+
session: await backend.getLatestSession(request),
|
|
181
|
+
});
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return createAdminTerminalErrorResponse(error, messages.loadErrorMessage);
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
async POST(request: Request): Promise<Response> {
|
|
188
|
+
if (!(await isUserGlobalAdmin())) {
|
|
189
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
return NextResponse.json({
|
|
194
|
+
session: await backend.startSession(request),
|
|
195
|
+
});
|
|
196
|
+
} catch (error) {
|
|
197
|
+
return createAdminTerminalErrorResponse(error, messages.startErrorMessage);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
async PATCH(request: Request): Promise<Response> {
|
|
202
|
+
if (!(await isUserGlobalAdmin())) {
|
|
203
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const body = (await request.json().catch(() => null)) as
|
|
208
|
+
| {
|
|
209
|
+
readonly sessionId?: string;
|
|
210
|
+
readonly input?: string;
|
|
211
|
+
}
|
|
212
|
+
| null;
|
|
213
|
+
|
|
214
|
+
if (!body?.sessionId || typeof body.input !== 'string') {
|
|
215
|
+
return NextResponse.json({ error: messages.missingInputMessage }, { status: 400 });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return NextResponse.json({
|
|
219
|
+
session: await backend.writeSessionInput(body.sessionId, body.input),
|
|
220
|
+
});
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return createAdminTerminalErrorResponse(error, messages.sendErrorMessage);
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
async DELETE(request: Request): Promise<Response> {
|
|
227
|
+
if (!(await isUserGlobalAdmin())) {
|
|
228
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const body = (await request.json().catch(() => null)) as
|
|
233
|
+
| {
|
|
234
|
+
readonly sessionId?: string;
|
|
235
|
+
}
|
|
236
|
+
| null;
|
|
237
|
+
|
|
238
|
+
if (!body?.sessionId) {
|
|
239
|
+
return NextResponse.json({ error: messages.missingStopSessionIdMessage }, { status: 400 });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return NextResponse.json({
|
|
243
|
+
session: await backend.stopSession(body.sessionId),
|
|
244
|
+
});
|
|
245
|
+
} catch (error) {
|
|
246
|
+
return createAdminTerminalErrorResponse(error, messages.stopErrorMessage);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Converts one thrown backend failure into a consistent JSON error response.
|
|
254
|
+
*
|
|
255
|
+
* @param error - Thrown backend error.
|
|
256
|
+
* @param fallbackMessage - Message used when the thrown value is not an `Error`.
|
|
257
|
+
* @returns JSON error response.
|
|
258
|
+
*/
|
|
259
|
+
function createAdminTerminalErrorResponse(error: unknown, fallbackMessage: string): Response {
|
|
260
|
+
return NextResponse.json(
|
|
261
|
+
{ error: error instanceof Error ? error.message : fallbackMessage },
|
|
262
|
+
{ status: 500 },
|
|
263
|
+
);
|
|
264
|
+
}
|
|
@@ -2,12 +2,12 @@ import { $getTableName } from '@/src/database/$getTableName';
|
|
|
2
2
|
import { $provideSupabaseForServer } from '@/src/database/$provideSupabaseForServer';
|
|
3
3
|
import type { Json } from '@/src/database/schema';
|
|
4
4
|
import { FILE_SECURITY_CHECKERS } from '@/src/file-security-checkers';
|
|
5
|
+
import { $provideCdnForServer } from '@/src/tools/$provideCdnForServer';
|
|
5
6
|
import { $provideServer } from '@/src/tools/$provideServer';
|
|
6
7
|
import { getUserFileCdnKey } from '@/src/utils/cdn/utils/getUserFileCdnKey';
|
|
7
8
|
import { validateMimeType } from '@/src/utils/validators/validateMimeType';
|
|
8
9
|
import { normalizeChatAttachments } from '@promptbook-local/core';
|
|
9
10
|
import type { TODO_any } from '@promptbook-local/types';
|
|
10
|
-
import { put } from '@vercel/blob';
|
|
11
11
|
import { after } from 'next/server';
|
|
12
12
|
import { spaceTrim } from 'spacetrim';
|
|
13
13
|
import { DatabaseError } from '../../../../src/errors/DatabaseError';
|
|
@@ -231,12 +231,14 @@ async function createShareTargetAttachment(file: File, maxFileUploadBytes: numbe
|
|
|
231
231
|
|
|
232
232
|
const mimeType = resolveShareTargetMimeType(file.type);
|
|
233
233
|
const blobPath = getUserFileCdnKey(buffer, normalizedFilename);
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
234
|
+
const cdn = $provideCdnForServer();
|
|
235
|
+
const storageUrl = cdn.getItemUrl(blobPath).href;
|
|
236
|
+
|
|
237
|
+
await cdn.setItem(blobPath, {
|
|
238
|
+
type: mimeType,
|
|
239
|
+
data: buffer,
|
|
240
|
+
purpose: SHARE_TARGET_FILE_PURPOSE,
|
|
241
|
+
fileSize: buffer.byteLength,
|
|
240
242
|
}).catch((error) => {
|
|
241
243
|
throw new DatabaseError(
|
|
242
244
|
spaceTrim(`
|
|
@@ -246,75 +248,26 @@ async function createShareTargetAttachment(file: File, maxFileUploadBytes: numbe
|
|
|
246
248
|
`),
|
|
247
249
|
);
|
|
248
250
|
});
|
|
249
|
-
const fileRecordId = await insertShareTargetFileRecord({
|
|
250
|
-
fileName: normalizedFilename,
|
|
251
|
-
fileSize: buffer.byteLength,
|
|
252
|
-
fileType: mimeType,
|
|
253
|
-
storageUrl: uploadedBlob.url,
|
|
254
|
-
});
|
|
255
251
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}),
|
|
264
|
-
);
|
|
265
|
-
}
|
|
252
|
+
after(() =>
|
|
253
|
+
populateShareTargetFileSecurityResult({
|
|
254
|
+
storageUrl,
|
|
255
|
+
}).catch((error) => {
|
|
256
|
+
console.error('[share-target] Failed to finalize file security result', error);
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
266
259
|
|
|
267
260
|
return {
|
|
268
261
|
name: normalizedFilename,
|
|
269
262
|
type: mimeType,
|
|
270
|
-
url:
|
|
263
|
+
url: storageUrl,
|
|
271
264
|
};
|
|
272
265
|
}
|
|
273
266
|
|
|
274
|
-
/**
|
|
275
|
-
* Inserts one admin-visible file row for a shared attachment.
|
|
276
|
-
*/
|
|
277
|
-
async function insertShareTargetFileRecord(options: {
|
|
278
|
-
fileName: string;
|
|
279
|
-
fileSize: number;
|
|
280
|
-
fileType: string;
|
|
281
|
-
storageUrl: string;
|
|
282
|
-
}): Promise<number | null> {
|
|
283
|
-
try {
|
|
284
|
-
const supabase = $provideSupabaseForServer() as TODO_any;
|
|
285
|
-
const fileTableName = await $getTableName('File');
|
|
286
|
-
const { data, error } = await supabase
|
|
287
|
-
.from(fileTableName)
|
|
288
|
-
.insert({
|
|
289
|
-
fileName: options.fileName,
|
|
290
|
-
fileSize: options.fileSize,
|
|
291
|
-
fileType: options.fileType,
|
|
292
|
-
storageUrl: options.storageUrl,
|
|
293
|
-
shortUrl: null,
|
|
294
|
-
purpose: SHARE_TARGET_FILE_PURPOSE,
|
|
295
|
-
status: 'COMPLETED',
|
|
296
|
-
agentId: null,
|
|
297
|
-
securityResult: null,
|
|
298
|
-
})
|
|
299
|
-
.select('id')
|
|
300
|
-
.maybeSingle();
|
|
301
|
-
|
|
302
|
-
if (error) {
|
|
303
|
-
console.error('[share-target] Failed to store uploaded file metadata', error);
|
|
304
|
-
return null;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return typeof data?.id === 'number' ? data.id : null;
|
|
308
|
-
} catch (error) {
|
|
309
|
-
console.error('[share-target] Failed to insert shared file metadata', error);
|
|
310
|
-
return null;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
267
|
/**
|
|
315
268
|
* Populates best-effort file-security results after the share redirect has already returned.
|
|
316
269
|
*/
|
|
317
|
-
async function populateShareTargetFileSecurityResult(options: {
|
|
270
|
+
async function populateShareTargetFileSecurityResult(options: { storageUrl: string }): Promise<void> {
|
|
318
271
|
const securityResult: Record<string, unknown> = {};
|
|
319
272
|
|
|
320
273
|
for (const checkerId of Object.keys(FILE_SECURITY_CHECKERS)) {
|
|
@@ -342,7 +295,7 @@ async function populateShareTargetFileSecurityResult(options: { fileId: number;
|
|
|
342
295
|
.update({
|
|
343
296
|
securityResult,
|
|
344
297
|
})
|
|
345
|
-
.eq('
|
|
298
|
+
.eq('storageUrl', options.storageUrl);
|
|
346
299
|
|
|
347
300
|
if (error) {
|
|
348
301
|
throw new DatabaseError(
|