@promptbook/cli 0.112.0-139 → 0.112.0-140
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/README.md +1 -1
- package/apps/agents-server/README.md +9 -0
- package/apps/agents-server/src/app/actions.ts +18 -7
- package/apps/agents-server/src/app/admin/login-methods/shibboleth/page.tsx +7 -17
- package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +395 -30
- package/apps/agents-server/src/app/api/agents/export/route.ts +28 -2
- package/apps/agents-server/src/app/api/auth/change-password/route.ts +53 -6
- package/apps/agents-server/src/app/api/auth/login/route.ts +17 -8
- package/apps/agents-server/src/app/api/chat/export/pdf/route.ts +167 -2
- package/apps/agents-server/src/app/api/emails/incoming/sendgrid/route.ts +24 -2
- package/apps/agents-server/src/app/api/metadata/import/route.ts +2 -1
- package/apps/agents-server/src/app/api/users/[username]/route.ts +3 -2
- package/apps/agents-server/src/app/api/users/route.ts +5 -4
- package/apps/agents-server/src/components/Homepage/AgentsList.tsx +1 -13
- package/apps/agents-server/src/components/Homepage/AgentsListHeader.tsx +3 -19
- package/apps/agents-server/src/components/Homepage/AgentsListListView.tsx +6 -0
- package/apps/agents-server/src/components/Homepage/AgentsListViewContent.tsx +6 -0
- package/apps/agents-server/src/components/Homepage/SortableFolderCard.tsx +7 -1
- package/apps/agents-server/src/components/Homepage/useAgentsListImportExportState.ts +45 -43
- package/apps/agents-server/src/components/Homepage/useAgentsListState.ts +1 -3
- package/apps/agents-server/src/components/UsersList/useUsersAdmin.ts +2 -10
- package/apps/agents-server/src/database/seedCoreAgents.ts +8 -7
- package/apps/agents-server/src/message-providers/email/sendgrid/verifySendgridInboundParseWebhook.ts +345 -0
- package/apps/agents-server/src/utils/agentsTransfer/createAgentsExportZipStream.ts +15 -2
- package/apps/agents-server/src/utils/authenticateUser.ts +110 -3
- package/apps/agents-server/src/utils/authenticationAttemptRateLimit.ts +504 -0
- package/apps/agents-server/src/utils/backup/createBooksBackupZipStream.ts +76 -5
- package/apps/agents-server/src/utils/chatExport/renderHtmlToPdfOnServer.ts +32 -0
- package/apps/agents-server/src/utils/chatExport/sanitizeChatPdfExportHtml.ts +193 -0
- package/apps/agents-server/src/utils/currentUserIdentity.ts +22 -9
- package/apps/agents-server/src/utils/externalChatRunner/processExternalUserChatJob.ts +5 -0
- package/apps/agents-server/src/utils/getUserById.ts +19 -5
- package/apps/agents-server/src/utils/localChatRunner/processLocalUserChatJob.ts +5 -0
- package/apps/agents-server/src/utils/metadataConfigurationTransfer.ts +32 -16
- package/apps/agents-server/src/utils/publicUser.ts +59 -0
- package/apps/agents-server/src/utils/shibbolethAuthentication.ts +17 -9
- package/apps/agents-server/src/utils/userChat/createRunUserChatJobPersistenceController.ts +17 -11
- package/apps/agents-server/src/utils/userChat/createUserChatHarnessProgressCard.ts +99 -0
- package/apps/agents-server/src/utils/userChat/createUserChatRunnerProgressCard.ts +63 -0
- package/apps/agents-server/src/utils/userChat/userChatMessageLifecycle.ts +0 -3
- package/apps/agents-server/src/utils/vpsConfiguration.ts +3 -0
- package/esm/index.es.js +14 -5
- package/esm/index.es.js.map +1 -1
- package/esm/src/book-components/Chat/utils/isVisibleChatToolCall.d.ts +10 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/book-components/Chat/Chat/createChatMessageToolCallRenderModel.ts +2 -9
- package/src/book-components/Chat/utils/isVisibleChatToolCall.ts +23 -0
- package/src/other/templates/getTemplatesPipelineCollection.ts +868 -755
- package/src/version.ts +2 -2
- package/src/versions.txt +1 -0
- package/umd/index.umd.js +14 -5
- package/umd/index.umd.js.map +1 -1
- package/umd/src/book-components/Chat/utils/isVisibleChatToolCall.d.ts +10 -0
- package/umd/src/version.d.ts +1 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { ParseError } from '@promptbook-local/core';
|
|
1
2
|
import JSZip from 'jszip';
|
|
2
3
|
import { $getTableName } from '../../database/$getTableName';
|
|
3
4
|
import { $provideSupabaseForServer } from '../../database/$provideSupabaseForServer';
|
|
4
5
|
import type { AgentsServerDatabase } from '../../database/schema';
|
|
6
|
+
import { buildFolderTree, collectDescendantFolderIds } from '../agentOrganization/folderTree';
|
|
5
7
|
import { sanitizeBackupPathSegment } from './sanitizeBackupPathSegment';
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -96,6 +98,18 @@ export type BooksBackupZipStream = {
|
|
|
96
98
|
readonly stream: NodeJS.ReadableStream;
|
|
97
99
|
};
|
|
98
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Options for appending book entries into an existing ZIP archive.
|
|
103
|
+
*/
|
|
104
|
+
export type AppendBooksBackupEntriesToZipOptions = {
|
|
105
|
+
/**
|
|
106
|
+
* Optional folder id used as the export root.
|
|
107
|
+
*
|
|
108
|
+
* When provided, only agents from this folder and its descendants are exported, with paths relative to this folder.
|
|
109
|
+
*/
|
|
110
|
+
readonly rootFolderId?: number;
|
|
111
|
+
};
|
|
112
|
+
|
|
99
113
|
/**
|
|
100
114
|
* Appends all exported books into a target ZIP folder while preserving the Agents Server folder tree.
|
|
101
115
|
*
|
|
@@ -104,8 +118,13 @@ export type BooksBackupZipStream = {
|
|
|
104
118
|
*
|
|
105
119
|
* @param zip - ZIP archive being assembled.
|
|
106
120
|
* @param archiveRootPath - ZIP folder path where books should be written.
|
|
121
|
+
* @param options - Optional folder scope for the exported books.
|
|
107
122
|
*/
|
|
108
|
-
export async function appendBooksBackupEntriesToZip(
|
|
123
|
+
export async function appendBooksBackupEntriesToZip(
|
|
124
|
+
zip: JSZip,
|
|
125
|
+
archiveRootPath: string,
|
|
126
|
+
options: AppendBooksBackupEntriesToZipOptions = {},
|
|
127
|
+
): Promise<void> {
|
|
109
128
|
const supabase = $provideSupabaseForServer();
|
|
110
129
|
const agentTable = await $getTableName('Agent');
|
|
111
130
|
const folderTable = await $getTableName('AgentFolder');
|
|
@@ -125,8 +144,11 @@ export async function appendBooksBackupEntriesToZip(zip: JSZip, archiveRootPath:
|
|
|
125
144
|
|
|
126
145
|
const agents = (agentsResult.data || []) as BackupAgentRow[];
|
|
127
146
|
const folders = (foldersResult.data || []) as BackupFolderRow[];
|
|
147
|
+
const agentsForExport =
|
|
148
|
+
options.rootFolderId === undefined ? agents : filterAgentsByRootFolder(agents, folders, options.rootFolderId);
|
|
128
149
|
const folderSegmentById = buildFolderSegmentById(folders);
|
|
129
150
|
const resolveFolderPath = createFolderPathResolver(folders, folderSegmentById);
|
|
151
|
+
const resolveArchiveFolderPath = createArchiveFolderPathResolver(resolveFolderPath, options.rootFolderId);
|
|
130
152
|
|
|
131
153
|
// Keep the root directory explicit even when there are no agents.
|
|
132
154
|
if (archiveRootPath) {
|
|
@@ -136,12 +158,12 @@ export async function appendBooksBackupEntriesToZip(zip: JSZip, archiveRootPath:
|
|
|
136
158
|
const relativeFilePaths = new Set<string>();
|
|
137
159
|
const folderPathByAgentId = new Map<number, string>();
|
|
138
160
|
|
|
139
|
-
for (const agent of
|
|
140
|
-
const folderPath =
|
|
161
|
+
for (const agent of agentsForExport) {
|
|
162
|
+
const folderPath = resolveArchiveFolderPath(agent.folderId ?? null).join('/');
|
|
141
163
|
folderPathByAgentId.set(agent.id, folderPath);
|
|
142
164
|
}
|
|
143
165
|
|
|
144
|
-
const sortedAgents = [...
|
|
166
|
+
const sortedAgents = [...agentsForExport].sort((left, right) =>
|
|
145
167
|
compareAgents(
|
|
146
168
|
left,
|
|
147
169
|
right,
|
|
@@ -151,7 +173,7 @@ export async function appendBooksBackupEntriesToZip(zip: JSZip, archiveRootPath:
|
|
|
151
173
|
);
|
|
152
174
|
|
|
153
175
|
for (const agent of sortedAgents) {
|
|
154
|
-
const folderSegments =
|
|
176
|
+
const folderSegments = resolveArchiveFolderPath(agent.folderId ?? null);
|
|
155
177
|
const initialBookFilename = createInitialBookFilename(agent);
|
|
156
178
|
const relativePath = createUniqueBookRelativePath(relativeFilePaths, folderSegments, initialBookFilename, agent.id);
|
|
157
179
|
zip.file(createArchivePath(archiveRootPath, relativePath), agent.agentSource || '');
|
|
@@ -169,6 +191,55 @@ function createArchivePath(archiveRootPath: string, relativePath: string): strin
|
|
|
169
191
|
return archiveRootPath ? `${archiveRootPath}/${relativePath}` : relativePath;
|
|
170
192
|
}
|
|
171
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Filters agents to a selected folder subtree.
|
|
196
|
+
*
|
|
197
|
+
* @param agents - All loaded agent rows.
|
|
198
|
+
* @param folders - All loaded folder rows.
|
|
199
|
+
* @param rootFolderId - Folder id selected as the export root.
|
|
200
|
+
* @returns Agents assigned to the root folder or its descendant folders.
|
|
201
|
+
*/
|
|
202
|
+
function filterAgentsByRootFolder(
|
|
203
|
+
agents: ReadonlyArray<BackupAgentRow>,
|
|
204
|
+
folders: ReadonlyArray<BackupFolderRow>,
|
|
205
|
+
rootFolderId: number,
|
|
206
|
+
): BackupAgentRow[] {
|
|
207
|
+
const { folderById, childrenByParentId } = buildFolderTree([...folders]);
|
|
208
|
+
if (!folderById.has(rootFolderId)) {
|
|
209
|
+
throw new ParseError(`Folder \`${rootFolderId}\` was not found.`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const scopedFolderIds = new Set(collectDescendantFolderIds(rootFolderId, childrenByParentId));
|
|
213
|
+
|
|
214
|
+
return agents.filter((agent) => agent.folderId !== null && scopedFolderIds.has(agent.folderId));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Creates a path resolver for full exports or folder-relative exports.
|
|
219
|
+
*
|
|
220
|
+
* @param resolveFolderPath - Full folder-path resolver.
|
|
221
|
+
* @param rootFolderId - Optional folder id selected as the export root.
|
|
222
|
+
* @returns Folder-path resolver for archive entry paths.
|
|
223
|
+
*/
|
|
224
|
+
function createArchiveFolderPathResolver(
|
|
225
|
+
resolveFolderPath: (folderId: number | null) => string[],
|
|
226
|
+
rootFolderId: number | undefined,
|
|
227
|
+
): (folderId: number | null) => string[] {
|
|
228
|
+
if (rootFolderId === undefined) {
|
|
229
|
+
return resolveFolderPath;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const rootFolderPath = resolveFolderPath(rootFolderId);
|
|
233
|
+
|
|
234
|
+
return (folderId: number | null): string[] => {
|
|
235
|
+
if (folderId === null) {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return resolveFolderPath(folderId).slice(rootFolderPath.length);
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
172
243
|
/**
|
|
173
244
|
* Builds a download-ready stream with all books organized by the Agents Server folder tree.
|
|
174
245
|
*
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { createServerChromiumLaunchOptions } from '@/src/tools/createServerChromiumLaunchOptions';
|
|
2
2
|
import { chromium } from 'playwright';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* URL protocols that can be allowed through the PDF renderer request
|
|
6
|
+
* interception because they cannot reach a server-side network destination.
|
|
7
|
+
*/
|
|
8
|
+
const CHAT_EXPORT_PDF_ALLOWED_REQUEST_PROTOCOLS = new Set(['about:', 'data:']);
|
|
9
|
+
|
|
4
10
|
/**
|
|
5
11
|
* Browser viewport used while rendering standalone HTML exports to PDF.
|
|
6
12
|
*/
|
|
@@ -32,6 +38,16 @@ export async function renderHtmlToPdfOnServer(html: string): Promise<Buffer> {
|
|
|
32
38
|
const page = await browser.newPage();
|
|
33
39
|
|
|
34
40
|
try {
|
|
41
|
+
await page.route('**/*', async (route) => {
|
|
42
|
+
const requestUrl = route.request().url();
|
|
43
|
+
|
|
44
|
+
if (isAllowedPdfRendererRequestUrl(requestUrl)) {
|
|
45
|
+
await route.continue();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await route.abort('blockedbyclient');
|
|
50
|
+
});
|
|
35
51
|
await page.setViewportSize(CHAT_EXPORT_PDF_VIEWPORT);
|
|
36
52
|
await page.emulateMedia({ media: 'print' });
|
|
37
53
|
await page.setContent(html, { waitUntil: 'load' });
|
|
@@ -52,3 +68,19 @@ export async function renderHtmlToPdfOnServer(html: string): Promise<Buffer> {
|
|
|
52
68
|
await browser.close();
|
|
53
69
|
}
|
|
54
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Returns whether a Playwright request can proceed while rendering a chat export PDF.
|
|
74
|
+
*
|
|
75
|
+
* @param requestUrl - URL reported by Playwright for one intercepted request.
|
|
76
|
+
* @returns Whether the request cannot reach an external or internal network host.
|
|
77
|
+
*
|
|
78
|
+
* @private internal helper of `renderHtmlToPdfOnServer`
|
|
79
|
+
*/
|
|
80
|
+
function isAllowedPdfRendererRequestUrl(requestUrl: string): boolean {
|
|
81
|
+
try {
|
|
82
|
+
return CHAT_EXPORT_PDF_ALLOWED_REQUEST_PROTOCOLS.has(new URL(requestUrl).protocol);
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { JSDOM } from 'jsdom';
|
|
2
|
+
import { assertSafeUrl } from '../assertSafeUrl';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* URL-bearing HTML attributes that can appear in standalone chat export markup.
|
|
6
|
+
*
|
|
7
|
+
* @private internal constant of chat PDF export sanitization
|
|
8
|
+
*/
|
|
9
|
+
const CHAT_PDF_EXPORT_URL_ATTRIBUTE_NAMES = new Set(['href', 'poster', 'src', 'xlink:href']);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Protocols that cannot make the PDF renderer issue server-side network requests.
|
|
13
|
+
*
|
|
14
|
+
* @private internal constant of chat PDF export sanitization
|
|
15
|
+
*/
|
|
16
|
+
const CHAT_PDF_EXPORT_NON_NETWORK_PROTOCOLS = new Set(['about:', 'data:']);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Link-only protocols that are safe to keep on anchors because Playwright does not fetch them while rendering.
|
|
20
|
+
*
|
|
21
|
+
* @private internal constant of chat PDF export sanitization
|
|
22
|
+
*/
|
|
23
|
+
const CHAT_PDF_EXPORT_LINK_ONLY_PROTOCOLS = new Set(['mailto:', 'tel:']);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Matches CSS `url(...)` values inside inline styles and style blocks.
|
|
27
|
+
*
|
|
28
|
+
* @private internal constant of chat PDF export sanitization
|
|
29
|
+
*/
|
|
30
|
+
const CHAT_PDF_EXPORT_CSS_URL_FUNCTION_REGEX = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parses the generated standalone chat export HTML and removes URL attributes
|
|
34
|
+
* that would let Chromium reach private or unsupported destinations while
|
|
35
|
+
* rendering the PDF.
|
|
36
|
+
*
|
|
37
|
+
* Markdown message bodies are already sanitized by the shared `renderMarkdown`
|
|
38
|
+
* DOMPurify pipeline; this pass covers structured export URLs such as avatars,
|
|
39
|
+
* citations, attachments, and markdown image `src` attributes after HTML
|
|
40
|
+
* generation.
|
|
41
|
+
*
|
|
42
|
+
* @param html - Standalone chat export HTML generated by `buildChatHtml`.
|
|
43
|
+
* @returns HTML safe to pass into the server-side PDF renderer.
|
|
44
|
+
*
|
|
45
|
+
* @private internal utility of the Agents Server chat PDF export route
|
|
46
|
+
*/
|
|
47
|
+
export function sanitizeChatPdfExportHtml(html: string): string {
|
|
48
|
+
const dom = new JSDOM(html);
|
|
49
|
+
const { document } = dom.window;
|
|
50
|
+
|
|
51
|
+
for (const element of Array.from(document.querySelectorAll('*'))) {
|
|
52
|
+
sanitizeChatPdfExportElementUrlAttributes(element);
|
|
53
|
+
sanitizeChatPdfExportStyleAttribute(element);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const styleElement of Array.from(document.querySelectorAll('style'))) {
|
|
57
|
+
styleElement.textContent = sanitizeChatPdfExportCssUrls(styleElement.textContent || '');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return dom.serialize();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Removes unsafe URL attributes from one element.
|
|
65
|
+
*
|
|
66
|
+
* @param element - HTML element to sanitize.
|
|
67
|
+
*
|
|
68
|
+
* @private internal helper of `sanitizeChatPdfExportHtml`
|
|
69
|
+
*/
|
|
70
|
+
function sanitizeChatPdfExportElementUrlAttributes(element: Element): void {
|
|
71
|
+
for (const attributeName of element.getAttributeNames()) {
|
|
72
|
+
const normalizedAttributeName = attributeName.toLowerCase();
|
|
73
|
+
|
|
74
|
+
if (!CHAT_PDF_EXPORT_URL_ATTRIBUTE_NAMES.has(normalizedAttributeName)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const attributeValue = element.getAttribute(attributeName);
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
attributeValue !== null &&
|
|
82
|
+
!isSafeChatPdfExportUrl(attributeValue, isChatPdfExportNetworkRequestAttribute(element, attributeName))
|
|
83
|
+
) {
|
|
84
|
+
element.removeAttribute(attributeName);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Removes unsafe CSS `url(...)` values from one inline style attribute.
|
|
91
|
+
*
|
|
92
|
+
* @param element - HTML element whose style attribute should be sanitized.
|
|
93
|
+
*
|
|
94
|
+
* @private internal helper of `sanitizeChatPdfExportHtml`
|
|
95
|
+
*/
|
|
96
|
+
function sanitizeChatPdfExportStyleAttribute(element: Element): void {
|
|
97
|
+
const styleValue = element.getAttribute('style');
|
|
98
|
+
|
|
99
|
+
if (styleValue === null) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
element.setAttribute('style', sanitizeChatPdfExportCssUrls(styleValue));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Replaces unsafe CSS `url(...)` functions with `none` so Chromium has no
|
|
108
|
+
* private/internal resource to request while keeping unrelated inline styles.
|
|
109
|
+
*
|
|
110
|
+
* @param cssValue - CSS text from a style attribute or style element.
|
|
111
|
+
* @returns CSS text without unsafe URL functions.
|
|
112
|
+
*
|
|
113
|
+
* @private internal helper of `sanitizeChatPdfExportHtml`
|
|
114
|
+
*/
|
|
115
|
+
function sanitizeChatPdfExportCssUrls(cssValue: string): string {
|
|
116
|
+
return cssValue.replace(CHAT_PDF_EXPORT_CSS_URL_FUNCTION_REGEX, (rawMatch: string, _quote: string, url: string) => {
|
|
117
|
+
if (isSafeChatPdfExportUrl(url, true)) {
|
|
118
|
+
return rawMatch;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return 'none';
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Returns whether an attribute value can be kept in the PDF-rendered HTML.
|
|
127
|
+
*
|
|
128
|
+
* Absolute HTTP(S) URLs are checked through the Agents Server SSRF guard.
|
|
129
|
+
* Relative resource URLs are removed because server-side rendering has no
|
|
130
|
+
* trusted origin for them, while relative anchor links remain useful inside the
|
|
131
|
+
* PDF table of sources.
|
|
132
|
+
*
|
|
133
|
+
* @param rawUrl - Raw attribute or CSS URL value.
|
|
134
|
+
* @param isNetworkRequestAttribute - Whether Chromium could fetch this value during rendering.
|
|
135
|
+
* @returns Whether the URL value is safe to keep.
|
|
136
|
+
*
|
|
137
|
+
* @private internal helper of `sanitizeChatPdfExportHtml`
|
|
138
|
+
*/
|
|
139
|
+
function isSafeChatPdfExportUrl(rawUrl: string, isNetworkRequestAttribute: boolean): boolean {
|
|
140
|
+
const trimmedUrl = rawUrl.trim();
|
|
141
|
+
|
|
142
|
+
if (trimmedUrl === '' || trimmedUrl.startsWith('#')) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const absoluteUrl = trimmedUrl.startsWith('//') ? `https:${trimmedUrl}` : trimmedUrl;
|
|
147
|
+
let parsedUrl: URL;
|
|
148
|
+
try {
|
|
149
|
+
parsedUrl = new URL(absoluteUrl);
|
|
150
|
+
} catch {
|
|
151
|
+
return !isNetworkRequestAttribute;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:') {
|
|
155
|
+
try {
|
|
156
|
+
assertSafeUrl(absoluteUrl);
|
|
157
|
+
return true;
|
|
158
|
+
} catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (CHAT_PDF_EXPORT_NON_NETWORK_PROTOCOLS.has(parsedUrl.protocol)) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return !isNetworkRequestAttribute && CHAT_PDF_EXPORT_LINK_ONLY_PROTOCOLS.has(parsedUrl.protocol);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Returns whether one URL attribute can trigger a network request during PDF rendering.
|
|
172
|
+
*
|
|
173
|
+
* @param element - Element that owns the attribute.
|
|
174
|
+
* @param attributeName - URL-bearing attribute name.
|
|
175
|
+
* @returns Whether Chromium may fetch the attribute while rendering.
|
|
176
|
+
*
|
|
177
|
+
* @private internal helper of `sanitizeChatPdfExportHtml`
|
|
178
|
+
*/
|
|
179
|
+
function isChatPdfExportNetworkRequestAttribute(element: Element, attributeName: string): boolean {
|
|
180
|
+
const normalizedAttributeName = attributeName.toLowerCase();
|
|
181
|
+
|
|
182
|
+
if (normalizedAttributeName === 'src' || normalizedAttributeName === 'poster') {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (normalizedAttributeName === 'xlink:href') {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return normalizedAttributeName === 'href' && element.tagName.toLowerCase() !== 'a';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Note: [🟢] Code for Agents Server chat PDF export sanitization should never be published into packages that could be imported into browser environment
|
|
@@ -16,15 +16,20 @@ const GENERATED_USER_PASSWORD_HASH_PLACEHOLDER = 'auto-generated-user';
|
|
|
16
16
|
const ENV_ADMIN_PASSWORD_HASH_PLACEHOLDER = 'env-admin-managed';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
19
|
+
* User row shape needed for request identity resolution.
|
|
20
20
|
*/
|
|
21
|
-
type
|
|
21
|
+
type UserIdentityRow = Pick<AgentsServerDatabase['public']['Tables']['User']['Row'], 'id' | 'username' | 'isAdmin'>;
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Database insert shape for the `User` table.
|
|
25
25
|
*/
|
|
26
26
|
type UserInsert = AgentsServerDatabase['public']['Tables']['User']['Insert'];
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Supabase projection for user fields needed by request identity resolution.
|
|
30
|
+
*/
|
|
31
|
+
const USER_IDENTITY_SELECT_COLUMNS = 'id, username, isAdmin';
|
|
32
|
+
|
|
28
33
|
/**
|
|
29
34
|
* Identity resolved for the current request, including database user id.
|
|
30
35
|
*/
|
|
@@ -109,7 +114,11 @@ export async function ensureChatHistoryIdentity(): Promise<boolean> {
|
|
|
109
114
|
/**
|
|
110
115
|
* Attempts to find an existing user row by username and creates one when missing.
|
|
111
116
|
*/
|
|
112
|
-
async function findOrCreateUserRow(
|
|
117
|
+
async function findOrCreateUserRow(
|
|
118
|
+
username: string,
|
|
119
|
+
isAdmin: boolean,
|
|
120
|
+
passwordHash: string,
|
|
121
|
+
): Promise<UserIdentityRow> {
|
|
113
122
|
const existing = await findUserRowByUsername(username);
|
|
114
123
|
if (existing) {
|
|
115
124
|
return existing;
|
|
@@ -154,22 +163,26 @@ function isDuplicateUsernameCreateError(error: unknown): boolean {
|
|
|
154
163
|
/**
|
|
155
164
|
* Finds one user row by its username.
|
|
156
165
|
*/
|
|
157
|
-
async function findUserRowByUsername(username: string): Promise<
|
|
166
|
+
async function findUserRowByUsername(username: string): Promise<UserIdentityRow | null> {
|
|
158
167
|
const supabase = $provideSupabaseForServer();
|
|
159
168
|
const tableName = await $getTableName('User');
|
|
160
|
-
const { data, error } = await supabase
|
|
169
|
+
const { data, error } = await supabase
|
|
170
|
+
.from(tableName)
|
|
171
|
+
.select(USER_IDENTITY_SELECT_COLUMNS)
|
|
172
|
+
.eq('username', username)
|
|
173
|
+
.maybeSingle();
|
|
161
174
|
|
|
162
175
|
if (error) {
|
|
163
176
|
throw new Error(`Failed to resolve user "${username}": ${error.message}`);
|
|
164
177
|
}
|
|
165
178
|
|
|
166
|
-
return (data as
|
|
179
|
+
return (data as UserIdentityRow | null) || null;
|
|
167
180
|
}
|
|
168
181
|
|
|
169
182
|
/**
|
|
170
183
|
* Inserts a new user row for the provided username.
|
|
171
184
|
*/
|
|
172
|
-
async function insertUserRow(payload: UserInsert): Promise<
|
|
185
|
+
async function insertUserRow(payload: UserInsert): Promise<UserIdentityRow> {
|
|
173
186
|
const supabase = $provideSupabaseForServer();
|
|
174
187
|
const tableName = await $getTableName('User');
|
|
175
188
|
const now = new Date().toISOString();
|
|
@@ -180,7 +193,7 @@ async function insertUserRow(payload: UserInsert): Promise<UserRow> {
|
|
|
180
193
|
createdAt: payload.createdAt ?? now,
|
|
181
194
|
updatedAt: payload.updatedAt ?? now,
|
|
182
195
|
})
|
|
183
|
-
.select(
|
|
196
|
+
.select(USER_IDENTITY_SELECT_COLUMNS)
|
|
184
197
|
.maybeSingle();
|
|
185
198
|
|
|
186
199
|
if (error) {
|
|
@@ -195,5 +208,5 @@ async function insertUserRow(payload: UserInsert): Promise<UserRow> {
|
|
|
195
208
|
throw new Error(`Failed to insert user "${payload.username}".`);
|
|
196
209
|
}
|
|
197
210
|
|
|
198
|
-
return data as
|
|
211
|
+
return data as UserIdentityRow;
|
|
199
212
|
}
|
|
@@ -7,6 +7,7 @@ import { getUserChat } from '../userChat/getUserChat';
|
|
|
7
7
|
import { getUserChatJobById } from '../userChat/getUserChatJobById';
|
|
8
8
|
import { persistUserChatJobTerminalState } from '../userChat/persistUserChatJobTerminalState';
|
|
9
9
|
import { provideUserChatJobTable } from '../userChat/provideUserChatJobTable';
|
|
10
|
+
import { createUserChatRunnerProgressCard } from '../userChat/createUserChatRunnerProgressCard';
|
|
10
11
|
import { updateUserChatAssistantMessage } from '../userChat/updateUserChatAssistantMessage';
|
|
11
12
|
import type { UserChatJobRecord } from '../userChat/UserChatJobRecord';
|
|
12
13
|
import type { UserChatJobRow } from '../userChat/UserChatJobRow';
|
|
@@ -196,6 +197,10 @@ async function enqueueExternalUserChatJob(job: UserChatJobRecord): Promise<Proce
|
|
|
196
197
|
isComplete: false,
|
|
197
198
|
lifecycleState: 'running',
|
|
198
199
|
lifecycleError: undefined,
|
|
200
|
+
progressCard: createUserChatRunnerProgressCard({
|
|
201
|
+
runnerKind: 'external',
|
|
202
|
+
phase: 'queued_for_runner',
|
|
203
|
+
}),
|
|
199
204
|
}),
|
|
200
205
|
});
|
|
201
206
|
|
|
@@ -3,21 +3,35 @@ import { $provideSupabaseForServer } from '@/src/database/$provideSupabaseForSer
|
|
|
3
3
|
import type { AgentsServerDatabase } from '@/src/database/schema';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* User row shape needed by durable chat execution.
|
|
7
7
|
*/
|
|
8
|
-
type
|
|
8
|
+
type UserByIdRow = Pick<
|
|
9
|
+
AgentsServerDatabase['public']['Tables']['User']['Row'],
|
|
10
|
+
'id' | 'username' | 'isAdmin' | 'profileImageUrl'
|
|
11
|
+
>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Supabase projection for user fields needed by durable chat execution.
|
|
15
|
+
*/
|
|
16
|
+
const USER_BY_ID_SELECT_COLUMNS = 'id, username, isAdmin, profileImageUrl';
|
|
9
17
|
|
|
10
18
|
/**
|
|
11
19
|
* Loads one user row by database id.
|
|
20
|
+
*
|
|
21
|
+
* @private internal Agents Server user lookup helper
|
|
12
22
|
*/
|
|
13
|
-
export async function getUserById(userId: number): Promise<
|
|
23
|
+
export async function getUserById(userId: number): Promise<UserByIdRow | null> {
|
|
14
24
|
const supabase = $provideSupabaseForServer();
|
|
15
25
|
const tableName = await $getTableName('User');
|
|
16
|
-
const { data, error } = await supabase
|
|
26
|
+
const { data, error } = await supabase
|
|
27
|
+
.from(tableName)
|
|
28
|
+
.select(USER_BY_ID_SELECT_COLUMNS)
|
|
29
|
+
.eq('id', userId)
|
|
30
|
+
.maybeSingle();
|
|
17
31
|
|
|
18
32
|
if (error) {
|
|
19
33
|
throw new Error(`Failed to resolve user "${userId}": ${error.message}`);
|
|
20
34
|
}
|
|
21
35
|
|
|
22
|
-
return (data as
|
|
36
|
+
return (data as UserByIdRow | null) || null;
|
|
23
37
|
}
|
|
@@ -9,6 +9,7 @@ import { getUserChat } from '../userChat/getUserChat';
|
|
|
9
9
|
import { getUserChatJobById } from '../userChat/getUserChatJobById';
|
|
10
10
|
import { persistUserChatJobTerminalState } from '../userChat/persistUserChatJobTerminalState';
|
|
11
11
|
import { provideUserChatJobTable } from '../userChat/provideUserChatJobTable';
|
|
12
|
+
import { createUserChatRunnerProgressCard } from '../userChat/createUserChatRunnerProgressCard';
|
|
12
13
|
import { updateUserChatAssistantMessage } from '../userChat/updateUserChatAssistantMessage';
|
|
13
14
|
import type { UserChatJobRecord } from '../userChat/UserChatJobRecord';
|
|
14
15
|
import type { UserChatJobRow } from '../userChat/UserChatJobRow';
|
|
@@ -188,6 +189,10 @@ async function enqueueLocalUserChatJob(job: UserChatJobRecord): Promise<ProcessL
|
|
|
188
189
|
isComplete: false,
|
|
189
190
|
lifecycleState: 'running',
|
|
190
191
|
lifecycleError: undefined,
|
|
192
|
+
progressCard: createUserChatRunnerProgressCard({
|
|
193
|
+
runnerKind: 'local',
|
|
194
|
+
phase: 'queued_for_runner',
|
|
195
|
+
}),
|
|
191
196
|
}),
|
|
192
197
|
});
|
|
193
198
|
|
|
@@ -103,6 +103,16 @@ export type MetadataConfigurationImportPlan = {
|
|
|
103
103
|
readonly defaultKeysToReset: ReadonlyArray<string>;
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Options controlling standalone metadata import behavior.
|
|
108
|
+
*/
|
|
109
|
+
export type MetadataConfigurationImportPlanOptions = {
|
|
110
|
+
/**
|
|
111
|
+
* When true, built-in metadata keys missing from the import payload keep their current persisted value.
|
|
112
|
+
*/
|
|
113
|
+
readonly isDefaultResetSkipped?: boolean;
|
|
114
|
+
};
|
|
115
|
+
|
|
106
116
|
/**
|
|
107
117
|
* Summary returned after importing metadata.
|
|
108
118
|
*/
|
|
@@ -152,18 +162,23 @@ export async function createMetadataConfigurationExport(): Promise<MetadataConfi
|
|
|
152
162
|
* Imports standalone metadata JSON into the current server database.
|
|
153
163
|
*
|
|
154
164
|
* @param payload - Parsed JSON import payload.
|
|
165
|
+
* @param options - Import behavior options.
|
|
155
166
|
* @returns Import summary for the UI.
|
|
156
167
|
*/
|
|
157
168
|
export async function importMetadataConfigurationPayload(
|
|
158
169
|
payload: unknown,
|
|
170
|
+
options: MetadataConfigurationImportPlanOptions = {},
|
|
159
171
|
): Promise<MetadataConfigurationImportSummary> {
|
|
160
|
-
const importPlan = createMetadataConfigurationImportPlan(payload);
|
|
172
|
+
const importPlan = createMetadataConfigurationImportPlan(payload, options);
|
|
161
173
|
const supabase = $provideSupabaseForServer();
|
|
162
174
|
const tableName = await $getTableName('Metadata');
|
|
163
175
|
const nowIso = new Date().toISOString();
|
|
164
176
|
|
|
165
177
|
if (importPlan.defaultKeysToReset.length > 0) {
|
|
166
|
-
const { error } = await supabase
|
|
178
|
+
const { error } = await supabase
|
|
179
|
+
.from(tableName)
|
|
180
|
+
.delete()
|
|
181
|
+
.in('key', [...importPlan.defaultKeysToReset]);
|
|
167
182
|
|
|
168
183
|
if (error) {
|
|
169
184
|
throw new DatabaseError(
|
|
@@ -247,9 +262,13 @@ export function createMetadataConfigurationExportFilename(serverName: string | n
|
|
|
247
262
|
* Validates and normalizes a standalone metadata import payload.
|
|
248
263
|
*
|
|
249
264
|
* @param payload - Parsed JSON import payload.
|
|
265
|
+
* @param options - Import behavior options.
|
|
250
266
|
* @returns Import plan ready for persistence.
|
|
251
267
|
*/
|
|
252
|
-
export function createMetadataConfigurationImportPlan(
|
|
268
|
+
export function createMetadataConfigurationImportPlan(
|
|
269
|
+
payload: unknown,
|
|
270
|
+
options: MetadataConfigurationImportPlanOptions = {},
|
|
271
|
+
): MetadataConfigurationImportPlan {
|
|
253
272
|
if (!payload || typeof payload !== 'object') {
|
|
254
273
|
throw new ParseError('Metadata import must be a JSON object.');
|
|
255
274
|
}
|
|
@@ -268,10 +287,12 @@ export function createMetadataConfigurationImportPlan(payload: unknown): Metadat
|
|
|
268
287
|
const rowsToUpsert = payloadRecord.metadata.map((rawMetadataEntry, index) =>
|
|
269
288
|
normalizeMetadataConfigurationImportEntry(rawMetadataEntry, index, importedMetadataKeys),
|
|
270
289
|
);
|
|
271
|
-
const defaultKeysToReset =
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
290
|
+
const defaultKeysToReset = options.isDefaultResetSkipped
|
|
291
|
+
? []
|
|
292
|
+
: metadataDefaults
|
|
293
|
+
.map((metadataDefinition) => metadataDefinition.key)
|
|
294
|
+
.filter((metadataKey) => !importedMetadataKeys.has(metadataKey))
|
|
295
|
+
.sort((leftKey, rightKey) => leftKey.localeCompare(rightKey));
|
|
275
296
|
|
|
276
297
|
return {
|
|
277
298
|
rowsToUpsert: rowsToUpsert.sort((leftRow, rightRow) => leftRow.key.localeCompare(rightRow.key)),
|
|
@@ -377,11 +398,7 @@ function normalizeMetadataConfigurationImportEntry(
|
|
|
377
398
|
throw new ParseError(`Metadata entry \`${key}\` has invalid \`value\`; expected a string.`);
|
|
378
399
|
}
|
|
379
400
|
|
|
380
|
-
if (
|
|
381
|
-
isNoteProvided &&
|
|
382
|
-
typeof rawMetadataEntryRecord.note !== 'string' &&
|
|
383
|
-
rawMetadataEntryRecord.note !== null
|
|
384
|
-
) {
|
|
401
|
+
if (isNoteProvided && typeof rawMetadataEntryRecord.note !== 'string' && rawMetadataEntryRecord.note !== null) {
|
|
385
402
|
throw new ParseError(`Metadata entry \`${key}\` has invalid \`note\`; expected a string or null.`);
|
|
386
403
|
}
|
|
387
404
|
|
|
@@ -396,9 +413,7 @@ function normalizeMetadataConfigurationImportEntry(
|
|
|
396
413
|
}
|
|
397
414
|
|
|
398
415
|
const value = isValueProvided ? (rawMetadataEntryRecord.value as string) : metadataDefinition!.value;
|
|
399
|
-
const note = isNoteProvided
|
|
400
|
-
? (rawMetadataEntryRecord.note as string | null)
|
|
401
|
-
: metadataDefinition?.note ?? null;
|
|
416
|
+
const note = isNoteProvided ? (rawMetadataEntryRecord.note as string | null) : metadataDefinition?.note ?? null;
|
|
402
417
|
const validationError = validateMetadataValue(key, value);
|
|
403
418
|
|
|
404
419
|
if (validationError) {
|
|
@@ -420,7 +435,8 @@ function normalizeMetadataConfigurationImportEntry(
|
|
|
420
435
|
*/
|
|
421
436
|
function resolveServerNameFromMetadataRows(metadataRows: ReadonlyArray<MetadataConfigurationRow>): string {
|
|
422
437
|
const metadataRow = metadataRows.find((candidateRow) => candidateRow.key === SERVER_NAME_METADATA_KEY);
|
|
423
|
-
const defaultServerName =
|
|
438
|
+
const defaultServerName =
|
|
439
|
+
getMetadataDefinition(SERVER_NAME_METADATA_KEY)?.value ?? DEFAULT_METADATA_EXPORT_FILENAME_STEM;
|
|
424
440
|
|
|
425
441
|
return metadataRow?.value.trim() || defaultServerName;
|
|
426
442
|
}
|