@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.
Files changed (59) hide show
  1. package/apps/agents-server/src/app/api/page-preview/check/route.ts +31 -0
  2. package/apps/agents-server/src/app/api/page-preview/screenshot/route.ts +57 -0
  3. package/apps/agents-server/src/app/api/upload/route.ts +10 -1
  4. package/apps/agents-server/src/app/s3/[first]/[second]/[hash]/[filename]/route.ts +52 -0
  5. package/apps/agents-server/src/database/$provideClientSql.ts +37 -0
  6. package/apps/agents-server/src/database/$provideSupabaseForServer.ts +41 -0
  7. package/apps/agents-server/src/tools/$provideCdnForServer.ts +24 -0
  8. package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +30 -2
  9. package/apps/agents-server/src/utils/cdn/utils/getUserFileCdnKey.ts +10 -3
  10. package/apps/agents-server/src/utils/iframe/checkIfUrlCanBeEmbedded.ts +68 -0
  11. package/esm/index.es.js +26 -3
  12. package/esm/index.es.js.map +1 -1
  13. package/esm/src/book-components/BookEditor/BookEditor.d.ts +1 -1
  14. package/esm/src/book-components/BookEditor/BookEditorForClient.d.ts +1 -1
  15. package/esm/src/book-components/Chat/Chat/CitationIframePreview.d.ts +20 -0
  16. package/esm/src/book-components/_common/Dropdown/Dropdown.d.ts +1 -1
  17. package/esm/src/book-components/_common/MenuHoisting/MenuHoistingContext.d.ts +1 -1
  18. package/esm/src/book-components/_common/Modal/Modal.d.ts +1 -1
  19. package/esm/src/book-components/icons/AboutIcon.d.ts +1 -1
  20. package/esm/src/book-components/icons/DownloadIcon.d.ts +1 -1
  21. package/esm/src/book-components/icons/ExitFullscreenIcon.d.ts +1 -1
  22. package/esm/src/book-components/icons/FullscreenIcon.d.ts +1 -1
  23. package/esm/src/version.d.ts +1 -1
  24. package/package.json +1 -1
  25. package/src/book-3.0/LiteAgent.ts +1 -1
  26. package/src/book-components/BookEditor/BookEditor.tsx +6 -6
  27. package/src/book-components/BookEditor/BookEditorForClient.tsx +1 -1
  28. package/src/book-components/Chat/Chat/Chat.module.css +45 -0
  29. package/src/book-components/Chat/Chat/ChatCitationModal.tsx +2 -2
  30. package/src/book-components/Chat/Chat/CitationIframePreview.tsx +83 -0
  31. package/src/book-components/_common/Dropdown/Dropdown.tsx +1 -1
  32. package/src/book-components/_common/MenuHoisting/MenuHoistingContext.tsx +1 -1
  33. package/src/book-components/_common/Modal/Modal.tsx +1 -1
  34. package/src/book-components/icons/AboutIcon.tsx +1 -1
  35. package/src/book-components/icons/DownloadIcon.tsx +1 -1
  36. package/src/book-components/icons/ExitFullscreenIcon.tsx +1 -1
  37. package/src/book-components/icons/FullscreenIcon.tsx +1 -1
  38. package/src/cli/cli-commands/agents-server/buildAgentsServer.ts +31 -1
  39. package/src/execution/createPipelineExecutor/getKnowledgeForTask.ts +1 -1
  40. package/src/llm-providers/openai/OpenAiAgentKitExecutionToolsToolBuilder.ts +1 -1
  41. package/src/llm-providers/openai/OpenAiAssistantExecutionToolsToolRunner.ts +1 -1
  42. package/src/llm-providers/openai/OpenAiVectorStoreKnowledgeSourcePreparer.ts +1 -1
  43. package/src/other/templates/getTemplatesPipelineCollection.ts +732 -687
  44. package/src/scripting/javascript/JavascriptEvalExecutionTools.ts +1 -1
  45. package/src/version.ts +2 -2
  46. package/src/versions.txt +1 -0
  47. package/umd/index.umd.js +26 -3
  48. package/umd/index.umd.js.map +1 -1
  49. package/umd/src/book-components/BookEditor/BookEditor.d.ts +1 -1
  50. package/umd/src/book-components/BookEditor/BookEditorForClient.d.ts +1 -1
  51. package/umd/src/book-components/Chat/Chat/CitationIframePreview.d.ts +20 -0
  52. package/umd/src/book-components/_common/Dropdown/Dropdown.d.ts +1 -1
  53. package/umd/src/book-components/_common/MenuHoisting/MenuHoistingContext.d.ts +1 -1
  54. package/umd/src/book-components/_common/Modal/Modal.d.ts +1 -1
  55. package/umd/src/book-components/icons/AboutIcon.d.ts +1 -1
  56. package/umd/src/book-components/icons/DownloadIcon.d.ts +1 -1
  57. package/umd/src/book-components/icons/ExitFullscreenIcon.d.ts +1 -1
  58. package/umd/src/book-components/icons/FullscreenIcon.d.ts +1 -1
  59. 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, pathname, purpose, contentType } = await parseUploadRequest(request);
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
- return new URL(this.config.pathPrefix + '/' + key, this.cdnPublicUrl);
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 the user content
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
- return `user/files/${nameToSubfolderPath(hash).join('/')}/${filename}`;
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-118';
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: options.environment,
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,