@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
|
@@ -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,43 +1,89 @@
|
|
|
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';
|
|
6
|
+
import { resolveCdnStorageProvider } from '../utils/cdn/resolveCdnStorageProvider';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
9
|
+
* Environment value that enables path-style S3 requests.
|
|
8
10
|
*
|
|
9
11
|
* @private internal cache for `$provideCdnForServer`
|
|
10
12
|
*/
|
|
11
|
-
|
|
13
|
+
const TRUE_ENV_VALUE = 'true';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Cache of raw CDN instance.
|
|
17
|
+
*
|
|
18
|
+
* @private internal cache for `$provideCdnForServer`
|
|
19
|
+
*/
|
|
20
|
+
let rawCdn: IIFilesStorageWithCdn | null = null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Cache of tracked CDN instance.
|
|
24
|
+
*
|
|
25
|
+
* @private internal cache for `$provideCdnForServer`
|
|
26
|
+
*/
|
|
27
|
+
let trackedCdn: IIFilesStorageWithCdn | null = null;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates a CDN storage instance from environment variables.
|
|
31
|
+
*
|
|
32
|
+
* @private internal factory for `$provideCdnForServer`
|
|
33
|
+
*/
|
|
34
|
+
function createCdnForServer(): IIFilesStorageWithCdn {
|
|
35
|
+
const provider = resolveCdnStorageProvider();
|
|
36
|
+
|
|
37
|
+
switch (provider) {
|
|
38
|
+
case 's3':
|
|
39
|
+
return new DigitalOceanSpaces({
|
|
40
|
+
bucket: process.env.CDN_BUCKET!,
|
|
41
|
+
pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX || '',
|
|
42
|
+
endpoint: process.env.CDN_ENDPOINT!,
|
|
43
|
+
region: process.env.CDN_REGION || 'auto',
|
|
44
|
+
accessKeyId: process.env.CDN_ACCESS_KEY_ID!,
|
|
45
|
+
secretAccessKey: process.env.CDN_SECRET_ACCESS_KEY!,
|
|
46
|
+
cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
|
|
47
|
+
gzip: process.env.CDN_GZIP !== 'false',
|
|
48
|
+
forcePathStyle: process.env.CDN_FORCE_PATH_STYLE === TRUE_ENV_VALUE,
|
|
49
|
+
isPublicReadAclEnabled: process.env.CDN_ENABLE_PUBLIC_READ_ACL !== 'false',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
case 'vercel':
|
|
53
|
+
return new VercelBlobStorage({
|
|
54
|
+
token: process.env.VERCEL_BLOB_READ_WRITE_TOKEN!,
|
|
55
|
+
pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX!,
|
|
56
|
+
cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Provides an untracked CDN storage interface for code paths that manage `File` rows themselves.
|
|
63
|
+
*
|
|
64
|
+
* @private internal cache for `$provideCdnForServer`
|
|
65
|
+
*/
|
|
66
|
+
export function $provideUntrackedCdnForServer(): IIFilesStorageWithCdn {
|
|
67
|
+
if (!rawCdn) {
|
|
68
|
+
rawCdn = createCdnForServer();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return rawCdn;
|
|
72
|
+
}
|
|
12
73
|
|
|
13
74
|
/**
|
|
14
75
|
* Provides a CDN storage interface for server-side file operations, with caching to reuse instances.
|
|
76
|
+
*
|
|
77
|
+
* @private internal cache for `$provideCdnForServer`
|
|
15
78
|
*/
|
|
16
79
|
export function $provideCdnForServer(): IIFilesStorageWithCdn {
|
|
17
|
-
if (!
|
|
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
|
-
|
|
80
|
+
if (!trackedCdn) {
|
|
81
|
+
const inner = $provideUntrackedCdnForServer();
|
|
24
82
|
const supabase = $provideSupabaseForServer();
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/*
|
|
28
|
-
cdn = new DigitalOceanSpaces({
|
|
29
|
-
bucket: process.env.CDN_BUCKET!,
|
|
30
|
-
pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX!,
|
|
31
|
-
endpoint: process.env.CDN_ENDPOINT!,
|
|
32
|
-
accessKeyId: process.env.CDN_ACCESS_KEY_ID!,
|
|
33
|
-
secretAccessKey: process.env.CDN_SECRET_ACCESS_KEY!,
|
|
34
|
-
cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
|
|
35
|
-
gzip: true,
|
|
36
|
-
});
|
|
37
|
-
*/
|
|
83
|
+
trackedCdn = new TrackedFilesStorage(inner, supabase);
|
|
38
84
|
}
|
|
39
85
|
|
|
40
|
-
return
|
|
86
|
+
return trackedCdn;
|
|
41
87
|
}
|
|
42
88
|
|
|
43
89
|
// TODO: [🏓] Unite `xxxForServer` and `xxxForNode` naming
|
|
@@ -12,14 +12,45 @@ type IDigitalOceanSpacesConfig = {
|
|
|
12
12
|
readonly bucket: string;
|
|
13
13
|
readonly pathPrefix: string;
|
|
14
14
|
readonly endpoint: string;
|
|
15
|
+
readonly region?: string;
|
|
15
16
|
readonly accessKeyId: string;
|
|
16
17
|
readonly secretAccessKey: string;
|
|
17
18
|
readonly cdnPublicUrl: URL;
|
|
18
19
|
readonly gzip: boolean;
|
|
20
|
+
readonly forcePathStyle?: boolean;
|
|
21
|
+
readonly isPublicReadAclEnabled?: boolean;
|
|
19
22
|
|
|
20
23
|
// TODO: [⛳️] Probbably prefix should be in this config not on the consumer side
|
|
21
24
|
};
|
|
22
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Resolves the S3 endpoint URL, preserving explicit `http` endpoints for local MinIO.
|
|
28
|
+
*
|
|
29
|
+
* @private internal helper for DigitalOceanSpaces.
|
|
30
|
+
*/
|
|
31
|
+
function resolveS3Endpoint(endpoint: string): string {
|
|
32
|
+
if (/^https?:\/\//i.test(endpoint)) {
|
|
33
|
+
return endpoint;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return `https://${endpoint}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns a URL object whose pathname ends with `/`.
|
|
41
|
+
*
|
|
42
|
+
* @private internal helper for DigitalOceanSpaces.
|
|
43
|
+
*/
|
|
44
|
+
function ensureTrailingSlashUrl(url: URL): URL {
|
|
45
|
+
const normalizedUrl = new URL(url.href);
|
|
46
|
+
|
|
47
|
+
if (!normalizedUrl.pathname.endsWith('/')) {
|
|
48
|
+
normalizedUrl.pathname = `${normalizedUrl.pathname}/`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return normalizedUrl;
|
|
52
|
+
}
|
|
53
|
+
|
|
23
54
|
/**
|
|
24
55
|
* Class implementing digital ocean spaces.
|
|
25
56
|
*/
|
|
@@ -32,8 +63,9 @@ export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
|
|
|
32
63
|
|
|
33
64
|
public constructor(private readonly config: IDigitalOceanSpacesConfig) {
|
|
34
65
|
this.s3 = new S3Client({
|
|
35
|
-
region: 'auto',
|
|
36
|
-
endpoint:
|
|
66
|
+
region: config.region || 'auto',
|
|
67
|
+
endpoint: resolveS3Endpoint(config.endpoint),
|
|
68
|
+
forcePathStyle: config.forcePathStyle,
|
|
37
69
|
credentials: {
|
|
38
70
|
accessKeyId: config.accessKeyId,
|
|
39
71
|
secretAccessKey: config.secretAccessKey,
|
|
@@ -42,13 +74,13 @@ export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
|
|
|
42
74
|
}
|
|
43
75
|
|
|
44
76
|
public getItemUrl(key: string): URL {
|
|
45
|
-
return new URL(this.
|
|
77
|
+
return new URL(this.getStorageKey(key), ensureTrailingSlashUrl(this.cdnPublicUrl));
|
|
46
78
|
}
|
|
47
79
|
|
|
48
80
|
public async getItem(key: string): Promise<IFile | null> {
|
|
49
81
|
const parameters = {
|
|
50
82
|
Bucket: this.config.bucket,
|
|
51
|
-
Key: this.
|
|
83
|
+
Key: this.getStorageKey(key),
|
|
52
84
|
};
|
|
53
85
|
|
|
54
86
|
try {
|
|
@@ -104,12 +136,12 @@ export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
|
|
|
104
136
|
const uploadResult = await this.s3.send(
|
|
105
137
|
new PutObjectCommand({
|
|
106
138
|
Bucket: this.config.bucket,
|
|
107
|
-
Key: this.
|
|
139
|
+
Key: this.getStorageKey(key),
|
|
108
140
|
ContentType: processedFile.type,
|
|
109
141
|
...putObjectRequestAdditional,
|
|
110
142
|
Body: processedFile.data,
|
|
111
143
|
// TODO: Public read access / just private to extending class
|
|
112
|
-
ACL: 'public-read',
|
|
144
|
+
...(this.config.isPublicReadAclEnabled === false ? {} : { ACL: 'public-read' }),
|
|
113
145
|
}),
|
|
114
146
|
);
|
|
115
147
|
|
|
@@ -117,6 +149,22 @@ export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
|
|
|
117
149
|
throw new Error(`Upload result does not contain ETag`);
|
|
118
150
|
}
|
|
119
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Builds the final object key used in the S3 bucket.
|
|
155
|
+
*
|
|
156
|
+
* @private internal helper for DigitalOceanSpaces.
|
|
157
|
+
*/
|
|
158
|
+
private getStorageKey(key: string): string {
|
|
159
|
+
const normalizedKey = key.replace(/^\/+/g, '');
|
|
160
|
+
const normalizedPathPrefix = this.config.pathPrefix.replace(/^\/+|\/+$/g, '');
|
|
161
|
+
|
|
162
|
+
if (!normalizedPathPrefix) {
|
|
163
|
+
return normalizedKey;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return `${normalizedPathPrefix}/${normalizedKey}`;
|
|
167
|
+
}
|
|
120
168
|
}
|
|
121
169
|
|
|
122
170
|
// TODO: Implement Read-only mode
|
|
@@ -33,24 +33,22 @@ 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
|
-
const
|
|
40
|
+
const storageUrl = this.getItemUrl(key).href;
|
|
42
41
|
|
|
43
|
-
|
|
44
42
|
await this.supabase.from(await $getTableName('File')).insert({
|
|
45
43
|
userId: userId || null,
|
|
46
44
|
fileName: key,
|
|
47
45
|
fileSize: file.fileSize ?? file.data.length,
|
|
48
46
|
fileType: file.type,
|
|
49
|
-
|
|
47
|
+
storageUrl,
|
|
48
|
+
shortUrl: null,
|
|
50
49
|
purpose: purpose || 'UNKNOWN',
|
|
50
|
+
status: 'COMPLETED',
|
|
51
51
|
});
|
|
52
|
-
|
|
53
|
-
|
|
54
52
|
} catch (error) {
|
|
55
53
|
console.error('Failed to track upload:', error);
|
|
56
54
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported file storage backends for Agents Server CDN uploads.
|
|
3
|
+
*
|
|
4
|
+
* @private internal CDN configuration helper.
|
|
5
|
+
*/
|
|
6
|
+
export type CdnStorageProvider = 's3' | 'vercel';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolves the configured CDN storage provider.
|
|
10
|
+
*
|
|
11
|
+
* @private internal CDN configuration helper.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveCdnStorageProvider(): CdnStorageProvider {
|
|
14
|
+
const rawProvider = (process.env.NEXT_PUBLIC_CDN_STORAGE_PROVIDER || '').trim().toLowerCase();
|
|
15
|
+
|
|
16
|
+
switch (rawProvider) {
|
|
17
|
+
case 's3':
|
|
18
|
+
case 'minio':
|
|
19
|
+
case 'external-s3':
|
|
20
|
+
case 'self-contained-s3':
|
|
21
|
+
return 's3';
|
|
22
|
+
|
|
23
|
+
case 'vercel':
|
|
24
|
+
case 'vercel-blob':
|
|
25
|
+
case '':
|
|
26
|
+
return 'vercel';
|
|
27
|
+
|
|
28
|
+
default:
|
|
29
|
+
throw new Error(`Unsupported CDN storage provider \`${rawProvider}\`.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Checks whether browser uploads should be routed through the Agents Server API.
|
|
35
|
+
*
|
|
36
|
+
* @private internal CDN configuration helper.
|
|
37
|
+
*/
|
|
38
|
+
export function isServerRoutedCdnUploadProvider(provider: CdnStorageProvider = resolveCdnStorageProvider()): boolean {
|
|
39
|
+
return provider === 's3';
|
|
40
|
+
}
|
|
@@ -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 { $provideUntrackedCdnForServer } 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,12 @@ 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 = $provideUntrackedCdnForServer();
|
|
235
|
+
await cdn.setItem(blobPath, {
|
|
236
|
+
data: buffer,
|
|
237
|
+
type: mimeType,
|
|
238
|
+
fileSize: buffer.byteLength,
|
|
239
|
+
purpose: SHARE_TARGET_FILE_PURPOSE,
|
|
240
240
|
}).catch((error) => {
|
|
241
241
|
throw new DatabaseError(
|
|
242
242
|
spaceTrim(`
|
|
@@ -246,18 +246,19 @@ async function createShareTargetAttachment(file: File, maxFileUploadBytes: numbe
|
|
|
246
246
|
`),
|
|
247
247
|
);
|
|
248
248
|
});
|
|
249
|
+
const storageUrl = cdn.getItemUrl(blobPath).href;
|
|
249
250
|
const fileRecordId = await insertShareTargetFileRecord({
|
|
250
251
|
fileName: normalizedFilename,
|
|
251
252
|
fileSize: buffer.byteLength,
|
|
252
253
|
fileType: mimeType,
|
|
253
|
-
storageUrl
|
|
254
|
+
storageUrl,
|
|
254
255
|
});
|
|
255
256
|
|
|
256
257
|
if (fileRecordId !== null) {
|
|
257
258
|
after(() =>
|
|
258
259
|
populateShareTargetFileSecurityResult({
|
|
259
260
|
fileId: fileRecordId,
|
|
260
|
-
storageUrl
|
|
261
|
+
storageUrl,
|
|
261
262
|
}).catch((error) => {
|
|
262
263
|
console.error('[share-target] Failed to finalize file security result', error);
|
|
263
264
|
}),
|
|
@@ -267,7 +268,7 @@ async function createShareTargetAttachment(file: File, maxFileUploadBytes: numbe
|
|
|
267
268
|
return {
|
|
268
269
|
name: normalizedFilename,
|
|
269
270
|
type: mimeType,
|
|
270
|
-
url:
|
|
271
|
+
url: storageUrl,
|
|
271
272
|
};
|
|
272
273
|
}
|
|
273
274
|
|