@promptbook/cli 0.112.0-118 → 0.112.0-119
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/src/app/api/page-preview/check/route.ts +31 -0
- package/apps/agents-server/src/app/api/page-preview/screenshot/route.ts +57 -0
- package/apps/agents-server/src/app/api/upload/route.ts +10 -1
- package/apps/agents-server/src/app/s3/[first]/[second]/[hash]/[filename]/route.ts +52 -0
- package/apps/agents-server/src/database/$provideClientSql.ts +37 -0
- package/apps/agents-server/src/database/$provideSupabaseForServer.ts +41 -0
- package/apps/agents-server/src/tools/$provideCdnForServer.ts +24 -0
- package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +30 -2
- package/apps/agents-server/src/utils/cdn/utils/getUserFileCdnKey.ts +10 -3
- package/apps/agents-server/src/utils/iframe/checkIfUrlCanBeEmbedded.ts +68 -0
- package/esm/index.es.js +26 -3
- package/esm/index.es.js.map +1 -1
- package/esm/src/book-components/BookEditor/BookEditor.d.ts +1 -1
- package/esm/src/book-components/BookEditor/BookEditorForClient.d.ts +1 -1
- package/esm/src/book-components/Chat/Chat/CitationIframePreview.d.ts +20 -0
- package/esm/src/book-components/_common/Dropdown/Dropdown.d.ts +1 -1
- package/esm/src/book-components/_common/MenuHoisting/MenuHoistingContext.d.ts +1 -1
- package/esm/src/book-components/_common/Modal/Modal.d.ts +1 -1
- package/esm/src/book-components/icons/AboutIcon.d.ts +1 -1
- package/esm/src/book-components/icons/DownloadIcon.d.ts +1 -1
- package/esm/src/book-components/icons/ExitFullscreenIcon.d.ts +1 -1
- package/esm/src/book-components/icons/FullscreenIcon.d.ts +1 -1
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/book-3.0/LiteAgent.ts +1 -1
- package/src/book-components/BookEditor/BookEditor.tsx +6 -6
- package/src/book-components/BookEditor/BookEditorForClient.tsx +1 -1
- package/src/book-components/Chat/Chat/Chat.module.css +45 -0
- package/src/book-components/Chat/Chat/ChatCitationModal.tsx +2 -2
- package/src/book-components/Chat/Chat/CitationIframePreview.tsx +83 -0
- package/src/book-components/_common/Dropdown/Dropdown.tsx +1 -1
- package/src/book-components/_common/MenuHoisting/MenuHoistingContext.tsx +1 -1
- package/src/book-components/_common/Modal/Modal.tsx +1 -1
- package/src/book-components/icons/AboutIcon.tsx +1 -1
- package/src/book-components/icons/DownloadIcon.tsx +1 -1
- package/src/book-components/icons/ExitFullscreenIcon.tsx +1 -1
- package/src/book-components/icons/FullscreenIcon.tsx +1 -1
- package/src/cli/cli-commands/agents-server/buildAgentsServer.ts +31 -1
- package/src/execution/createPipelineExecutor/getKnowledgeForTask.ts +1 -1
- package/src/llm-providers/openai/OpenAiAgentKitExecutionToolsToolBuilder.ts +1 -1
- package/src/llm-providers/openai/OpenAiAssistantExecutionToolsToolRunner.ts +1 -1
- package/src/llm-providers/openai/OpenAiVectorStoreKnowledgeSourcePreparer.ts +1 -1
- package/src/other/templates/getTemplatesPipelineCollection.ts +732 -687
- package/src/scripting/javascript/JavascriptEvalExecutionTools.ts +1 -1
- package/src/version.ts +2 -2
- package/src/versions.txt +1 -0
- package/umd/index.umd.js +26 -3
- package/umd/index.umd.js.map +1 -1
- package/umd/src/book-components/BookEditor/BookEditor.d.ts +1 -1
- package/umd/src/book-components/BookEditor/BookEditorForClient.d.ts +1 -1
- package/umd/src/book-components/Chat/Chat/CitationIframePreview.d.ts +20 -0
- package/umd/src/book-components/_common/Dropdown/Dropdown.d.ts +1 -1
- package/umd/src/book-components/_common/MenuHoisting/MenuHoistingContext.d.ts +1 -1
- package/umd/src/book-components/_common/Modal/Modal.d.ts +1 -1
- package/umd/src/book-components/icons/AboutIcon.d.ts +1 -1
- package/umd/src/book-components/icons/DownloadIcon.d.ts +1 -1
- package/umd/src/book-components/icons/ExitFullscreenIcon.d.ts +1 -1
- package/umd/src/book-components/icons/FullscreenIcon.d.ts +1 -1
- package/umd/src/version.d.ts +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { NextRequest } from 'next/server';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { assertsError } from '../../../../../../../src/errors/assertsError';
|
|
4
|
+
import { checkIfUrlCanBeEmbedded } from '../../../../utils/iframe/checkIfUrlCanBeEmbedded';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Checks whether a given URL can be embedded in an iframe by inspecting
|
|
8
|
+
* `X-Frame-Options` and `Content-Security-Policy` `frame-ancestors` headers.
|
|
9
|
+
*
|
|
10
|
+
* Query parameters:
|
|
11
|
+
* - `url` — the fully-qualified HTTP(S) URL to check
|
|
12
|
+
*
|
|
13
|
+
* Returns `{ canEmbed: boolean }`.
|
|
14
|
+
*/
|
|
15
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
16
|
+
const url = request.nextUrl.searchParams.get('url');
|
|
17
|
+
|
|
18
|
+
if (!url) {
|
|
19
|
+
return NextResponse.json({ error: 'Missing required query parameter: url' }, { status: 400 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const canEmbed = await checkIfUrlCanBeEmbedded(url);
|
|
24
|
+
return NextResponse.json({ canEmbed });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
assertsError(error);
|
|
27
|
+
console.warn('Failed to check if URL can be embedded:', error.message);
|
|
28
|
+
// When the check fails, allow the iframe to try — the browser will handle it
|
|
29
|
+
return NextResponse.json({ canEmbed: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { $provideBrowserForServer } from '@/src/tools/$provideBrowserForServer';
|
|
2
|
+
import { serializeError } from '@promptbook-local/utils';
|
|
3
|
+
import type { NextRequest } from 'next/server';
|
|
4
|
+
import { NextResponse } from 'next/server';
|
|
5
|
+
import { assertsError } from '../../../../../../../src/errors/assertsError';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Takes a screenshot of the given URL using a headless browser.
|
|
9
|
+
*
|
|
10
|
+
* Query parameters:
|
|
11
|
+
* - `url` — the fully-qualified HTTP(S) URL to screenshot
|
|
12
|
+
*
|
|
13
|
+
* Returns a PNG image.
|
|
14
|
+
*/
|
|
15
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
16
|
+
const url = request.nextUrl.searchParams.get('url');
|
|
17
|
+
|
|
18
|
+
if (!url) {
|
|
19
|
+
return NextResponse.json({ error: 'Missing required query parameter: url' }, { status: 400 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let parsedUrl: URL;
|
|
23
|
+
try {
|
|
24
|
+
parsedUrl = new URL(url);
|
|
25
|
+
} catch {
|
|
26
|
+
return NextResponse.json({ error: 'Invalid URL' }, { status: 400 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
|
30
|
+
return NextResponse.json({ error: 'Only http and https URLs are supported' }, { status: 400 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const browserContext = await $provideBrowserForServer();
|
|
35
|
+
const page = await browserContext.newPage();
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await page.setViewportSize({ width: 1280, height: 800 });
|
|
39
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
40
|
+
const screenshotBuffer = await page.screenshot({ type: 'png' });
|
|
41
|
+
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
return new NextResponse(new Blob([screenshotBuffer as any]), {
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'image/png',
|
|
46
|
+
'Cache-Control': 'public, max-age=300',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
} finally {
|
|
50
|
+
await page.close().catch(() => {});
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
assertsError(error);
|
|
54
|
+
console.error('Error taking page screenshot:', error);
|
|
55
|
+
return NextResponse.json({ error: serializeError(error) }, { status: 500 });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from '../../../tools/$provideCdnForServer';
|
|
16
16
|
import { $provideServer } from '../../../tools/$provideServer';
|
|
17
17
|
import { getSafeCdnPath } from '../../../utils/cdn/utils/getSafeCdnPath';
|
|
18
|
+
import { getUserFileCdnKey } from '../../../utils/cdn/utils/getUserFileCdnKey';
|
|
18
19
|
import { FILE_SECURITY_CHECKERS } from '../../../file-security-checkers';
|
|
19
20
|
import { getUserIdFromRequest } from '../../../utils/getUserIdFromRequest';
|
|
20
21
|
import { getMaxFileUploadSizeBytes } from '../../../utils/serverLimits';
|
|
@@ -223,7 +224,7 @@ export async function POST(request: NextRequest) {
|
|
|
223
224
|
const providedServer = await $provideServer();
|
|
224
225
|
assertFileUploadAvailable(providedServer);
|
|
225
226
|
|
|
226
|
-
const { file,
|
|
227
|
+
const { file, purpose, contentType } = await parseUploadRequest(request);
|
|
227
228
|
const fileBuffer = Buffer.from(await file.arrayBuffer());
|
|
228
229
|
const maxFileSize = await getMaxFileUploadSizeBytes();
|
|
229
230
|
|
|
@@ -237,6 +238,14 @@ export async function POST(request: NextRequest) {
|
|
|
237
238
|
);
|
|
238
239
|
}
|
|
239
240
|
|
|
241
|
+
// [✨🏣] Compute a content-addressed CDN key so the public URL contains
|
|
242
|
+
// the file hash and does not expose the internal S3 bucket or path prefix.
|
|
243
|
+
const rawKey = getUserFileCdnKey(fileBuffer, file.name);
|
|
244
|
+
const pathname = getSafeCdnPath({
|
|
245
|
+
pathname: rawKey,
|
|
246
|
+
pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX,
|
|
247
|
+
});
|
|
248
|
+
|
|
240
249
|
const cdn = $provideCdnForServer({
|
|
241
250
|
cdnPublicUrl: resolveCdnPublicUrlForServer(providedServer.publicUrl),
|
|
242
251
|
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import {
|
|
3
|
+
$provideCdnForServer,
|
|
4
|
+
resolveCdnPublicUrlForServer,
|
|
5
|
+
} from '../../../../../../tools/$provideCdnForServer';
|
|
6
|
+
import { $provideServer } from '../../../../../../tools/$provideServer';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Serves files stored under the hash-based CDN key format:
|
|
10
|
+
* `{hash[0]}/{hash[1]}/{sha256-hash}/{filename}`
|
|
11
|
+
*
|
|
12
|
+
* This route is reached when nginx routes a request matching
|
|
13
|
+
* `^/s3/[0-9a-f]/[0-9a-f]/[0-9a-f]{64}/` to Next.js instead of VersityGW,
|
|
14
|
+
* which keeps the internal S3 bucket and path prefix hidden from the public URL.
|
|
15
|
+
*
|
|
16
|
+
* [✨🏣] Companion route for `getUserFileCdnKey` and the `publicCdnBaseUrl`
|
|
17
|
+
* configuration in `$provideCdnForServer`.
|
|
18
|
+
*/
|
|
19
|
+
export async function GET(
|
|
20
|
+
_request: NextRequest,
|
|
21
|
+
{
|
|
22
|
+
params,
|
|
23
|
+
}: {
|
|
24
|
+
params: Promise<{
|
|
25
|
+
first: string;
|
|
26
|
+
second: string;
|
|
27
|
+
hash: string;
|
|
28
|
+
filename: string;
|
|
29
|
+
}>;
|
|
30
|
+
},
|
|
31
|
+
) {
|
|
32
|
+
const { first, second, hash, filename } = await params;
|
|
33
|
+
const key = `${first}/${second}/${hash}/${filename}`;
|
|
34
|
+
|
|
35
|
+
const providedServer = await $provideServer();
|
|
36
|
+
const cdn = $provideCdnForServer({
|
|
37
|
+
cdnPublicUrl: resolveCdnPublicUrlForServer(providedServer.publicUrl),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const file = await cdn.getItem(key);
|
|
41
|
+
|
|
42
|
+
if (!file) {
|
|
43
|
+
return new NextResponse(null, { status: 404 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return new NextResponse(file.data, {
|
|
47
|
+
headers: {
|
|
48
|
+
'Content-Type': file.type,
|
|
49
|
+
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -31,6 +31,33 @@ export type ClientSqlExecutor = ClientSql & {
|
|
|
31
31
|
readonly raw: ClientSqlRaw;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Maximum number of PostgreSQL connections in the shared pool.
|
|
36
|
+
*
|
|
37
|
+
* Configurable via `DATABASE_POOL_MAX` environment variable.
|
|
38
|
+
* Defaults to 20 which comfortably handles concurrent chat-stream polling loads.
|
|
39
|
+
*
|
|
40
|
+
* @private internal constant of Agents Server database layer
|
|
41
|
+
*/
|
|
42
|
+
const DATABASE_POOL_MAX = Math.max(1, parseInt(process.env.DATABASE_POOL_MAX || '20', 10));
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Milliseconds to wait for a free pool connection before failing.
|
|
46
|
+
*
|
|
47
|
+
* Without a timeout the default `pg` behaviour is to queue requests indefinitely,
|
|
48
|
+
* which causes the server to become unresponsive under pool exhaustion.
|
|
49
|
+
*
|
|
50
|
+
* @private internal constant of Agents Server database layer
|
|
51
|
+
*/
|
|
52
|
+
const POOL_CONNECTION_TIMEOUT_MS = 15_000;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Milliseconds an idle client stays in the pool before being closed.
|
|
56
|
+
*
|
|
57
|
+
* @private internal constant of Agents Server database layer
|
|
58
|
+
*/
|
|
59
|
+
const POOL_IDLE_TIMEOUT_MS = 30_000;
|
|
60
|
+
|
|
34
61
|
/**
|
|
35
62
|
* Shared PostgreSQL pool reused across all requests in the server process.
|
|
36
63
|
*
|
|
@@ -52,6 +79,16 @@ export async function $provideClientSql(): Promise<ClientSqlExecutor> {
|
|
|
52
79
|
clientPool = new Pool({
|
|
53
80
|
connectionString: resolvePostgresConnectionString(),
|
|
54
81
|
ssl: { rejectUnauthorized: false },
|
|
82
|
+
max: DATABASE_POOL_MAX,
|
|
83
|
+
idleTimeoutMillis: POOL_IDLE_TIMEOUT_MS,
|
|
84
|
+
connectionTimeoutMillis: POOL_CONNECTION_TIMEOUT_MS,
|
|
85
|
+
allowExitOnIdle: true,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Prevent unhandled errors from crashing the pool or the process.
|
|
89
|
+
// Individual query errors are still thrown at the call site.
|
|
90
|
+
clientPool.on('error', (error) => {
|
|
91
|
+
console.error('[database] Unexpected error on idle PostgreSQL client:', error);
|
|
55
92
|
});
|
|
56
93
|
}
|
|
57
94
|
|
|
@@ -4,6 +4,44 @@ import { isAgentsServerSqliteMode } from './agentsServerDatabaseMode';
|
|
|
4
4
|
import { $provideLocalSqliteSupabase } from './sqlite/$provideLocalSqliteSupabase';
|
|
5
5
|
import { AgentsServerDatabase } from './schema';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Hard timeout for every Supabase HTTP request.
|
|
9
|
+
*
|
|
10
|
+
* Without this, a slow or unreachable PostgREST endpoint causes requests to
|
|
11
|
+
* hang indefinitely, eventually making the server appear unresponsive.
|
|
12
|
+
*
|
|
13
|
+
* @private internal constant of Agents Server database layer
|
|
14
|
+
*/
|
|
15
|
+
const SUPABASE_REQUEST_TIMEOUT_MS = 30_000;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wraps the native `fetch` with a per-request timeout so Supabase queries
|
|
19
|
+
* always resolve (with an error) within `SUPABASE_REQUEST_TIMEOUT_MS`.
|
|
20
|
+
*
|
|
21
|
+
* When the caller already passes an AbortSignal the request is aborted when
|
|
22
|
+
* either that signal fires or the timeout fires — whichever comes first.
|
|
23
|
+
*
|
|
24
|
+
* Uses manual composition instead of `AbortSignal.any` for Node.js 18 compatibility
|
|
25
|
+
* (`AbortSignal.any` is only available in Node.js 20.3+).
|
|
26
|
+
*
|
|
27
|
+
* @private internal helper of Agents Server database layer
|
|
28
|
+
*/
|
|
29
|
+
function fetchWithSupabaseTimeout(url: RequestInfo | URL, options?: RequestInit): Promise<Response> {
|
|
30
|
+
const existingSignal = options?.signal instanceof AbortSignal ? options.signal : null;
|
|
31
|
+
|
|
32
|
+
if (!existingSignal) {
|
|
33
|
+
return fetch(url, { ...options, signal: AbortSignal.timeout(SUPABASE_REQUEST_TIMEOUT_MS) });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Compose the caller signal with the timeout: abort when either fires.
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const abort = () => controller.abort();
|
|
39
|
+
existingSignal.addEventListener('abort', abort, { once: true });
|
|
40
|
+
AbortSignal.timeout(SUPABASE_REQUEST_TIMEOUT_MS).addEventListener('abort', abort, { once: true });
|
|
41
|
+
|
|
42
|
+
return fetch(url, { ...options, signal: controller.signal });
|
|
43
|
+
}
|
|
44
|
+
|
|
7
45
|
/**
|
|
8
46
|
* Internal cache for `$provideSupabaseForServer`
|
|
9
47
|
*
|
|
@@ -42,6 +80,9 @@ export function $provideSupabaseForServer(): SupabaseClient<AgentsServerDatabase
|
|
|
42
80
|
autoRefreshToken: false,
|
|
43
81
|
persistSession: false,
|
|
44
82
|
},
|
|
83
|
+
global: {
|
|
84
|
+
fetch: fetchWithSupabaseTimeout,
|
|
85
|
+
},
|
|
45
86
|
},
|
|
46
87
|
);
|
|
47
88
|
}
|
|
@@ -74,6 +74,13 @@ function createCdnStorageForServer(cdnPublicUrl: URL): IIFilesStorageWithCdn {
|
|
|
74
74
|
gzip: true,
|
|
75
75
|
forcePathStyle: process.env.CDN_FORCE_PATH_STYLE === 'true',
|
|
76
76
|
region: resolveS3CompatibleStorageRegion(),
|
|
77
|
+
// [✨🏣] For self-contained S3: hash-based CDN keys get a public URL that
|
|
78
|
+
// omits the internal bucket and pathPrefix so users can't infer the storage
|
|
79
|
+
// structure. The `/s3/` segment is preserved so nginx routes those requests
|
|
80
|
+
// to the Next.js hash-file route instead of directly to VersityGW.
|
|
81
|
+
publicCdnBaseUrl: isSelfContainedS3StorageSelected()
|
|
82
|
+
? resolveHashBasedPublicCdnBaseUrl(cdnPublicUrl)
|
|
83
|
+
: undefined,
|
|
77
84
|
});
|
|
78
85
|
}
|
|
79
86
|
|
|
@@ -84,6 +91,23 @@ function createCdnStorageForServer(cdnPublicUrl: URL): IIFilesStorageWithCdn {
|
|
|
84
91
|
});
|
|
85
92
|
}
|
|
86
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Derives the public base URL used for hash-based file URLs on self-contained S3.
|
|
96
|
+
*
|
|
97
|
+
* Strips the bucket segment from the configured CDN URL so that the public URL
|
|
98
|
+
* exposes only the `/s3/` prefix — the internal bucket name and path prefix
|
|
99
|
+
* remain hidden from users.
|
|
100
|
+
*
|
|
101
|
+
* Example: `https://s24.ptbk.io/s3/promptbook-files/` → `https://s24.ptbk.io/s3/`
|
|
102
|
+
*
|
|
103
|
+
* [✨🏣] Companion to `isHashBasedCdnKey` in `DigitalOceanSpaces`.
|
|
104
|
+
*
|
|
105
|
+
* @private helper of `createCdnStorageForServer`
|
|
106
|
+
*/
|
|
107
|
+
function resolveHashBasedPublicCdnBaseUrl(cdnPublicUrl: URL): URL {
|
|
108
|
+
return new URL('/s3/', cdnPublicUrl);
|
|
109
|
+
}
|
|
110
|
+
|
|
87
111
|
/**
|
|
88
112
|
* Resolves the CDN public URL from environment configuration.
|
|
89
113
|
*
|
|
@@ -19,6 +19,16 @@ type IDigitalOceanSpacesConfig = {
|
|
|
19
19
|
readonly forcePathStyle?: boolean;
|
|
20
20
|
readonly region?: string;
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* When set, `getItemUrl` generates `new URL(key, publicCdnBaseUrl)` for
|
|
24
|
+
* hash-based CDN keys instead of the default formula that prepends the
|
|
25
|
+
* `pathPrefix`. This allows self-contained S3 to expose an unguessable URL
|
|
26
|
+
* that hides the internal bucket name and path prefix.
|
|
27
|
+
*
|
|
28
|
+
* [✨🏣] Used by self-contained S3 to serve files via the Next.js hash route.
|
|
29
|
+
*/
|
|
30
|
+
readonly publicCdnBaseUrl?: URL;
|
|
31
|
+
|
|
22
32
|
// TODO: [⛳️] Probbably prefix should be in this config not on the consumer side
|
|
23
33
|
};
|
|
24
34
|
|
|
@@ -27,7 +37,7 @@ type IDigitalOceanSpacesConfig = {
|
|
|
27
37
|
*/
|
|
28
38
|
export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
|
|
29
39
|
public get cdnPublicUrl() {
|
|
30
|
-
return this.config.cdnPublicUrl;
|
|
40
|
+
return this.config.publicCdnBaseUrl ?? this.config.cdnPublicUrl;
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
private s3: S3Client;
|
|
@@ -45,7 +55,13 @@ export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
|
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
public getItemUrl(key: string): URL {
|
|
48
|
-
|
|
58
|
+
// [✨🏣] For hash-based keys, omit the pathPrefix from the public URL so
|
|
59
|
+
// the internal S3 structure (bucket, prefix, upload directory) is hidden.
|
|
60
|
+
if (this.config.publicCdnBaseUrl && isHashBasedCdnKey(key)) {
|
|
61
|
+
return new URL(key, this.config.publicCdnBaseUrl);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return new URL(this.config.pathPrefix + '/' + key, this.config.cdnPublicUrl);
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
public async getItem(key: string): Promise<IFile | null> {
|
|
@@ -122,6 +138,18 @@ export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
|
|
|
122
138
|
}
|
|
123
139
|
}
|
|
124
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Returns `true` when `key` matches the hash-based CDN key format produced by
|
|
143
|
+
* `getUserFileCdnKey`: `{hex}/{hex}/{sha256-64-chars}/{filename}`.
|
|
144
|
+
*
|
|
145
|
+
* [✨🏣] Used by `getItemUrl` to decide whether to strip the internal path prefix.
|
|
146
|
+
*
|
|
147
|
+
* @private helper of `DigitalOceanSpaces`
|
|
148
|
+
*/
|
|
149
|
+
function isHashBasedCdnKey(key: string): boolean {
|
|
150
|
+
return /^[0-9a-f]\/[0-9a-f]\/[0-9a-f]{64}\//.test(key);
|
|
151
|
+
}
|
|
152
|
+
|
|
125
153
|
/**
|
|
126
154
|
* Normalizes endpoint values from legacy host-only configuration and full S3 URLs.
|
|
127
155
|
*
|
|
@@ -2,10 +2,15 @@ import hexEncoder from 'crypto-js/enc-hex';
|
|
|
2
2
|
import sha256 from 'crypto-js/sha256';
|
|
3
3
|
import type { string_uri } from '../../../../../../src/types/typeAliases';
|
|
4
4
|
import { titleToName } from '../../../../../../src/utils/normalization/titleToName';
|
|
5
|
-
import { nameToSubfolderPath } from './nameToSubfolderPath';
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
|
-
* Generates a path for
|
|
7
|
+
* Generates a content-addressed path for user-uploaded files.
|
|
8
|
+
*
|
|
9
|
+
* The returned key uses the SHA-256 hash of the file buffer so the URL is
|
|
10
|
+
* unguessable and does not expose internal storage structure (bucket, prefix,
|
|
11
|
+
* or upload directory). The first two hex characters are used as single-level
|
|
12
|
+
* shard directories, matching the same convention as the public URL served by
|
|
13
|
+
* the hash-based file route.
|
|
9
14
|
*/
|
|
10
15
|
export function getUserFileCdnKey(file: Buffer, originalFilename: string): string_uri {
|
|
11
16
|
const hash = sha256(hexEncoder.parse(file.toString('hex'))).toString(/* hex */);
|
|
@@ -18,7 +23,9 @@ export function getUserFileCdnKey(file: Buffer, originalFilename: string): strin
|
|
|
18
23
|
const filename = name + '.' + extension;
|
|
19
24
|
// <- Note: [⛳️] Preserving original file name
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
// [✨🏣] Hash-based key: {hash[0]}/{hash[1]}/{fullHash}/{filename}
|
|
27
|
+
// The single-char shard dirs and full hash hide the internal S3 structure.
|
|
28
|
+
return `${hash[0]}/${hash[1]}/${hash}/${filename}`;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
// TODO: [🌍] Unite this logic in one place
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses the X-Frame-Options header value and returns whether embedding is allowed.
|
|
3
|
+
*
|
|
4
|
+
* Some servers set multiple values (e.g. "DENY, SAMEORIGIN") which results in a
|
|
5
|
+
* comma-separated string. Any DENY or SAMEORIGIN token blocks embedding.
|
|
6
|
+
*/
|
|
7
|
+
function canEmbedByXFrameOptions(headerValue: string): boolean {
|
|
8
|
+
const values = headerValue
|
|
9
|
+
.split(',')
|
|
10
|
+
.map((v) => v.trim().toUpperCase());
|
|
11
|
+
return !values.some((v) => v === 'DENY' || v === 'SAMEORIGIN');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parses the Content-Security-Policy header and checks the `frame-ancestors` directive.
|
|
16
|
+
*
|
|
17
|
+
* Returns false when `frame-ancestors` is present and does not include a wildcard or
|
|
18
|
+
* protocol-level allow (`*`, `https:`, `http:`).
|
|
19
|
+
*/
|
|
20
|
+
function canEmbedByCsp(cspHeader: string): boolean {
|
|
21
|
+
const match = cspHeader.match(/frame-ancestors\s+([^;]+)/i);
|
|
22
|
+
if (!match) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const directive = (match[1] ?? '').trim();
|
|
27
|
+
const tokens = directive.split(/\s+/);
|
|
28
|
+
|
|
29
|
+
for (const token of tokens) {
|
|
30
|
+
if (token === '*' || token === 'https:' || token === 'http:') {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Checks whether a given HTTP(S) URL allows being embedded in an iframe by
|
|
40
|
+
* inspecting `X-Frame-Options` and `Content-Security-Policy` `frame-ancestors`.
|
|
41
|
+
*
|
|
42
|
+
* Returns `true` when the page is embeddable, `false` when it is blocked.
|
|
43
|
+
* Throws when the URL is not HTTP(S) or the network request fails.
|
|
44
|
+
*/
|
|
45
|
+
export async function checkIfUrlCanBeEmbedded(url: string): Promise<boolean> {
|
|
46
|
+
const parsedUrl = new URL(url);
|
|
47
|
+
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
|
48
|
+
throw new Error(`Only http and https URLs are supported, got: ${parsedUrl.protocol}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const response = await fetch(url, {
|
|
52
|
+
method: 'HEAD',
|
|
53
|
+
signal: AbortSignal.timeout(10_000),
|
|
54
|
+
redirect: 'follow',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const xFrameOptions = response.headers.get('X-Frame-Options');
|
|
58
|
+
if (xFrameOptions !== null && !canEmbedByXFrameOptions(xFrameOptions)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const csp = response.headers.get('Content-Security-Policy');
|
|
63
|
+
if (csp !== null && !canEmbedByCsp(csp)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return true;
|
|
68
|
+
}
|
package/esm/index.es.js
CHANGED
|
@@ -58,7 +58,7 @@ const BOOK_LANGUAGE_VERSION = '2.0.0';
|
|
|
58
58
|
* @generated
|
|
59
59
|
* @see https://github.com/webgptorg/promptbook
|
|
60
60
|
*/
|
|
61
|
-
const PROMPTBOOK_ENGINE_VERSION = '0.112.0-
|
|
61
|
+
const PROMPTBOOK_ENGINE_VERSION = '0.112.0-119';
|
|
62
62
|
/**
|
|
63
63
|
* TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
|
|
64
64
|
* Note: [💞] Ignore a discrepancy between file name and entity name
|
|
@@ -3272,6 +3272,16 @@ const AGENTS_SERVER_NEXT_BUILD_ID_FILENAME = 'BUILD_ID';
|
|
|
3272
3272
|
* @private internal constant of `ptbk agents-server`
|
|
3273
3273
|
*/
|
|
3274
3274
|
const DEFAULT_AGENTS_SERVER_NEXT_DIST_DIRECTORY_NAME = '.next';
|
|
3275
|
+
/**
|
|
3276
|
+
* Node.js heap size limit (in MiB) injected into `NODE_OPTIONS` for the Next.js production build.
|
|
3277
|
+
*
|
|
3278
|
+
* Next.js webpack peaks at ~1.9 GiB on a fresh install; the Node.js default cap is ~1.7 GiB,
|
|
3279
|
+
* which causes an OOM crash on first-run VPS installations where swap is the primary resource.
|
|
3280
|
+
* Raising the limit lets the build complete on the first attempt.
|
|
3281
|
+
*
|
|
3282
|
+
* @private internal constant of `ptbk agents-server`
|
|
3283
|
+
*/
|
|
3284
|
+
const AGENTS_SERVER_BUILD_MAX_OLD_SPACE_MIB = 4096;
|
|
3275
3285
|
/**
|
|
3276
3286
|
* Environment variable passed to the bundled Next app so webpack can resolve dependencies
|
|
3277
3287
|
* installed beside `ptbk` even when the app sources are materialized into a project cache.
|
|
@@ -3669,7 +3679,10 @@ async function runNextBuild(options) {
|
|
|
3669
3679
|
var _a, _b;
|
|
3670
3680
|
const buildProcess = spawn(process.execPath, [options.nextCliPath, 'build'], {
|
|
3671
3681
|
cwd: options.appPath,
|
|
3672
|
-
env:
|
|
3682
|
+
env: {
|
|
3683
|
+
...options.environment,
|
|
3684
|
+
NODE_OPTIONS: mergeNodeOptionsWithHeapSize(options.environment.NODE_OPTIONS, AGENTS_SERVER_BUILD_MAX_OLD_SPACE_MIB),
|
|
3685
|
+
},
|
|
3673
3686
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
3674
3687
|
});
|
|
3675
3688
|
(_a = buildProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (chunk) => {
|
|
@@ -3688,6 +3701,16 @@ async function runNextBuild(options) {
|
|
|
3688
3701
|
});
|
|
3689
3702
|
});
|
|
3690
3703
|
}
|
|
3704
|
+
/**
|
|
3705
|
+
* Prepends `--max-old-space-size=<mib>` to `NODE_OPTIONS` unless the caller already set one.
|
|
3706
|
+
*/
|
|
3707
|
+
function mergeNodeOptionsWithHeapSize(existingNodeOptions, maxOldSpaceMib) {
|
|
3708
|
+
if (existingNodeOptions !== undefined && /--max-old-space-size[= ]/u.test(existingNodeOptions)) {
|
|
3709
|
+
return existingNodeOptions;
|
|
3710
|
+
}
|
|
3711
|
+
const heapFlag = `--max-old-space-size=${maxOldSpaceMib}`;
|
|
3712
|
+
return existingNodeOptions ? `${heapFlag} ${existingNodeOptions}` : heapFlag;
|
|
3713
|
+
}
|
|
3691
3714
|
/**
|
|
3692
3715
|
* Sends one Next build output chunk through the caller handler or to the foreground terminal.
|
|
3693
3716
|
*/
|
|
@@ -58459,7 +58482,7 @@ class OpenAiVectorStoreKnowledgeSourcePreparer {
|
|
|
58459
58482
|
return { skippedReason: 'invalid_data_url' };
|
|
58460
58483
|
}
|
|
58461
58484
|
return {
|
|
58462
|
-
file: new File([parsed.buffer], parsed.filename, {
|
|
58485
|
+
file: new File([new Uint8Array(parsed.buffer)], parsed.filename, {
|
|
58463
58486
|
type: parsed.mimeType,
|
|
58464
58487
|
}),
|
|
58465
58488
|
sizeBytes: parsed.buffer.length,
|