@promptbook/cli 0.112.0-130 → 0.112.0-132

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.
@@ -53,6 +53,13 @@ export function CustomJsClient() {
53
53
  Scripts entered here are concatenated and injected on every Agents Server page. Use them to add helpers,
54
54
  integrations, or instrumentation, but keep them lean since runtime errors can impact the UI.
55
55
  </p>
56
+ <p className="text-sm text-gray-600 mb-6">
57
+ Scripts run inside a strict Content Security Policy: they are loaded via a per-request{' '}
58
+ <code>nonce</code> on the inline <code>&lt;script&gt;</code> tag, and any extra scripts they create at
59
+ runtime inherit that trust via <code>script-src &apos;strict-dynamic&apos;</code>. Closing{' '}
60
+ <code>&lt;/script&gt;</code> sequences are no longer rewritten — write valid JavaScript and avoid raw
61
+ HTML payloads here.
62
+ </p>
56
63
 
57
64
  {error && (
58
65
  <div className="mb-4 rounded-md border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700">
@@ -3,7 +3,8 @@ import { LayoutWrapper } from '@/src/components/LayoutWrapper/LayoutWrapper';
3
3
  import { createThemeModeBootstrapScript } from '@/src/components/ThemeMode/createThemeModeBootstrapScript';
4
4
  import { APPLICATION_FONT_VARIABLE_CLASS_NAME } from '@/src/utils/applicationFonts';
5
5
  import type { Metadata } from 'next';
6
- import { cookies } from 'next/headers';
6
+ import { cookies, headers } from 'next/headers';
7
+ import { CONTENT_SECURITY_POLICY_NONCE_REQUEST_HEADER } from '../middleware/contentSecurityPolicy';
7
8
  import { getCustomJavascriptWithIntegrations } from '../database/customJavascript';
8
9
  import { getAggregatedCustomStylesheetCss } from '../database/customStylesheet';
9
10
  import { getMetadataMap } from '../database/getMetadata';
@@ -293,6 +294,7 @@ export default async function RootLayout({
293
294
  getCustomJavascriptWithIntegrations,
294
295
  );
295
296
  const cookieStorePromise = cookies();
297
+ const requestHeadersPromise = headers();
296
298
  const defaultThemeModePromise = Promise.all([currentUserPromise, cookieStorePromise, layoutMetadataPromise]).then(
297
299
  async ([currentUser, cookieStore, layoutMetadata]) => {
298
300
  const cookieThemeMode = cookieStore.get(THEME_MODE_COOKIE_NAME)?.value || null;
@@ -346,6 +348,7 @@ export default async function RootLayout({
346
348
  customStylesheetCss,
347
349
  customJavascript,
348
350
  cookieStore,
351
+ requestHeaders,
349
352
  defaultThemeMode,
350
353
  federatedServers,
351
354
  footerLinks,
@@ -363,6 +366,7 @@ export default async function RootLayout({
363
366
  customStylesheetCssPromise,
364
367
  customJavascriptPromise,
365
368
  cookieStorePromise,
369
+ requestHeadersPromise,
366
370
  defaultThemeModePromise,
367
371
  federatedServersPromise,
368
372
  footerLinksPromise,
@@ -379,7 +383,10 @@ export default async function RootLayout({
379
383
  const feedbackMode = parseChatFeedbackMode(layoutMetadata.CHAT_FEEDBACK_MODE, layoutMetadata.IS_FEEDBACK_ENABLED);
380
384
  const isExperimentalPwaAppEnabled = (layoutMetadata.IS_EXPERIMENTAL_PWA_APP_ENABLED ?? 'true') === 'true';
381
385
  const isPublicServer = isPublicServerVisibility(serverVisibility);
382
- const safeCustomJavascript = customJavascript.replace(/<\/script>/gi, '<\\/script>');
386
+ // Note: The custom JavaScript is executed only when the browser sees the matching
387
+ // `script-src 'nonce-...'` directive that the middleware sets per request, so a
388
+ // fragile string-level sanitization is intentionally not applied here.
389
+ const contentSecurityPolicyNonce = requestHeaders.get(CONTENT_SECURITY_POLICY_NONCE_REQUEST_HEADER) ?? undefined;
383
390
  const webPushPublicKey = process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY?.trim() || null;
384
391
  const chatVisualModeCookie = cookieStore.get(CHAT_VISUAL_MODE_COOKIE_NAME)?.value || null;
385
392
  const cookieLanguage = cookieStore.get(SERVER_LANGUAGE_COOKIE_NAME)?.value || null;
@@ -412,6 +419,7 @@ export default async function RootLayout({
412
419
  <body className={`${APPLICATION_FONT_VARIABLE_CLASS_NAME} bg-background text-foreground antialiased`}>
413
420
  <script
414
421
  id="agents-server-theme-mode"
422
+ nonce={contentSecurityPolicyNonce}
415
423
  dangerouslySetInnerHTML={{ __html: themeModeBootstrapScript }}
416
424
  />
417
425
  {customStylesheetCss && <style id="agents-server-custom-css">{customStylesheetCss}</style>}
@@ -448,7 +456,8 @@ export default async function RootLayout({
448
456
  {customJavascript && (
449
457
  <script
450
458
  id="agents-server-custom-js"
451
- dangerouslySetInnerHTML={{ __html: safeCustomJavascript }}
459
+ nonce={contentSecurityPolicyNonce}
460
+ dangerouslySetInnerHTML={{ __html: customJavascript }}
452
461
  />
453
462
  )}
454
463
  {/* Global portal root for modals/popups */}
@@ -1,9 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF } from '@/config';
3
+ import { NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF, NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA } from '@/config';
4
4
  import { CLAIM, NAME, PROMPTBOOK_ENGINE_VERSION } from '@promptbook-local/core';
5
5
  import { useServerLanguage } from '../ServerLanguage/ServerLanguageProvider';
6
6
  import { HeadlessLink } from '../_utils/headlessParam';
7
+ import { getCommitFooterEmoji } from './getCommitFooterEmoji';
7
8
 
8
9
  /**
9
10
  * Title/url pair used to extend the footer via metadata.
@@ -169,7 +170,7 @@ export function Footer(props: FooterProps) {
169
170
  <br />
170
171
  {t('footer.allRightsReserved')}
171
172
  <br />
172
- {t('footer.madeInCzechRepublic')}
173
+ {t('footer.madeInCzechRepublic', { emoji: getCommitFooterEmoji(NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA) })}
173
174
  {/* <- TODO: !!!!!!!! Put here Prague outline */}
174
175
  </p>
175
176
  <p className="mt-2 text-xs text-gray-400">
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Curated list of positive or neutral emojis displayed in the footer's "Made with {emoji} in Europe" line.
3
+ *
4
+ * The order of this list is part of the deterministic mapping from commit SHA to emoji, so existing
5
+ * entries should not be reordered. New emojis can be appended at the end without affecting the
6
+ * indexes of existing ones, but each commit SHA will then map to a different emoji.
7
+ *
8
+ * Hearts, cute animals, plants, fruits, sweets, and celebratory symbols are intentionally chosen so
9
+ * that the footer never shows anything aggressive, scary, or negative.
10
+ */
11
+ const POSITIVE_FOOTER_EMOJIS = [
12
+ '❤️',
13
+ '🧡',
14
+ '💛',
15
+ '💚',
16
+ '💙',
17
+ '💜',
18
+ '🤍',
19
+ '💖',
20
+ '💝',
21
+ '🐙',
22
+ '🦄',
23
+ '🐶',
24
+ '🐱',
25
+ '🐰',
26
+ '🦊',
27
+ '🐻',
28
+ '🐼',
29
+ '🐨',
30
+ '🐯',
31
+ '🦁',
32
+ '🐸',
33
+ '🐵',
34
+ '🐧',
35
+ '🦉',
36
+ '🐢',
37
+ '🦋',
38
+ '🐝',
39
+ '🐞',
40
+ '🐳',
41
+ '🐬',
42
+ '🦒',
43
+ '🐘',
44
+ '🌸',
45
+ '🌻',
46
+ '🌷',
47
+ '🌹',
48
+ '🌈',
49
+ '🌟',
50
+ '✨',
51
+ '☀️',
52
+ '🌙',
53
+ '🍀',
54
+ '🌳',
55
+ '🍎',
56
+ '🍓',
57
+ '🍑',
58
+ '🍒',
59
+ '🍋',
60
+ '🍊',
61
+ '🍇',
62
+ '🍉',
63
+ '🍰',
64
+ '🍪',
65
+ '🍩',
66
+ '🍕',
67
+ '🍔',
68
+ '🍦',
69
+ '🍫',
70
+ '🎉',
71
+ '🎈',
72
+ '🎁',
73
+ '🎨',
74
+ '🚀',
75
+ '⭐',
76
+ '💎',
77
+ '🪐',
78
+ '🎵',
79
+ ] as const;
80
+
81
+ /**
82
+ * Default emoji used in the footer when no commit SHA is available (for example during local
83
+ * development outside Vercel). Kept as the historic `❤️` so the footer text does not look
84
+ * unusual when the deployment metadata is missing.
85
+ */
86
+ const DEFAULT_FOOTER_EMOJI: string = POSITIVE_FOOTER_EMOJIS[0]!;
87
+
88
+ /**
89
+ * Derives a stable emoji from a commit SHA using a small djb2-style string hash.
90
+ *
91
+ * The same SHA always produces the same emoji, so users can spot a server update by noticing that
92
+ * the footer emoji changed. The result is taken modulo the curated list, so it is always one of
93
+ * the positive/neutral emojis in {@link POSITIVE_FOOTER_EMOJIS}.
94
+ *
95
+ * @param commitSha - Full or short commit SHA. May be `null` or `undefined` when running outside
96
+ * a Vercel deployment, in which case {@link DEFAULT_FOOTER_EMOJI} is returned.
97
+ * @returns One emoji from {@link POSITIVE_FOOTER_EMOJIS}.
98
+ * @private Internal to `apps/agents-server`
99
+ */
100
+ export function getCommitFooterEmoji(commitSha: string | null | undefined): string {
101
+ if (!commitSha) {
102
+ return DEFAULT_FOOTER_EMOJI;
103
+ }
104
+
105
+ let hash = 0;
106
+ for (let characterIndex = 0; characterIndex < commitSha.length; characterIndex++) {
107
+ hash = (hash * 31 + commitSha.charCodeAt(characterIndex)) | 0;
108
+ }
109
+
110
+ const emojiIndex = Math.abs(hash) % POSITIVE_FOOTER_EMOJIS.length;
111
+ return POSITIVE_FOOTER_EMOJIS[emojiIndex]!;
112
+ }
@@ -380,7 +380,7 @@ footer.logosAndBranding: Loga a vizuální identita
380
380
  footer.connectSectionTitle: Spojte se s námi
381
381
  footer.linksSectionTitle: Odkazy
382
382
  footer.allRightsReserved: Všechna práva vyhrazena.
383
- footer.madeInCzechRepublic: Vytvořeno s ❤️ v České republice.
383
+ footer.madeInCzechRepublic: Vytvořeno s {emoji} v České republice.
384
384
  footer.engineVersion: Verze jádra Promptbooku
385
385
  forbidden.title: 403 Zakázáno
386
386
  forbidden.message: Nemáte oprávnění k přístupu na tuto stránku.
@@ -384,7 +384,7 @@ footer.logosAndBranding: Logos & Branding
384
384
  footer.connectSectionTitle: Connect
385
385
  footer.linksSectionTitle: Links
386
386
  footer.allRightsReserved: All rights reserved.
387
- footer.madeInCzechRepublic: Made with ❤️ in Europe
387
+ footer.madeInCzechRepublic: Made with {emoji} in Europe
388
388
  footer.engineVersion: Promptbook engine version
389
389
  forbidden.title: 403 Forbidden
390
390
  forbidden.message: You do not have permission to access this page.
@@ -21,13 +21,15 @@ export function applyEmbeddingHeader(response: NextResponse, url: URL, isAllowed
21
21
  return;
22
22
  }
23
23
 
24
+ // Note: The base strict CSP (script-src with nonce) is preserved by appending a separate
25
+ // Content-Security-Policy header that only carries the `frame-ancestors` directive.
24
26
  if (isAllowed) {
25
- response.headers.set('Content-Security-Policy', 'frame-ancestors https: http:');
27
+ response.headers.append('Content-Security-Policy', 'frame-ancestors https: http:');
26
28
  response.headers.delete('X-Frame-Options');
27
29
  return;
28
30
  }
29
31
 
30
- response.headers.set('Content-Security-Policy', "frame-ancestors 'none'");
32
+ response.headers.append('Content-Security-Policy', "frame-ancestors 'none'");
31
33
  response.headers.set('X-Frame-Options', 'DENY');
32
34
  }
33
35
 
@@ -0,0 +1,97 @@
1
+ import type { NextResponse } from 'next/server';
2
+
3
+ /**
4
+ * Name of the request header that carries the per-request CSP nonce to server components.
5
+ *
6
+ * Server components read it via `next/headers` and must pass it to every inline `<script>` tag so
7
+ * the browser executes only scripts explicitly rendered by the server.
8
+ *
9
+ * @private function of middleware
10
+ */
11
+ export const CONTENT_SECURITY_POLICY_NONCE_REQUEST_HEADER = 'x-csp-nonce';
12
+
13
+ /**
14
+ * Length (in bytes) of the random material used to derive each per-request CSP nonce.
15
+ *
16
+ * 16 bytes (128 bits) is the value recommended by the W3C CSP3 specification — large enough to
17
+ * make guessing infeasible while keeping the base64 representation short.
18
+ *
19
+ * @private function of middleware
20
+ */
21
+ const CONTENT_SECURITY_POLICY_NONCE_BYTE_LENGTH = 16;
22
+
23
+ /**
24
+ * Generates a fresh, single-use CSP nonce encoded as base64.
25
+ *
26
+ * @returns Cryptographically random nonce safe to inline into a CSP header.
27
+ *
28
+ * @private function of middleware
29
+ */
30
+ export function generateContentSecurityPolicyNonce(): string {
31
+ const randomBytes = new Uint8Array(CONTENT_SECURITY_POLICY_NONCE_BYTE_LENGTH);
32
+ crypto.getRandomValues(randomBytes);
33
+
34
+ let binaryString = '';
35
+ for (const byte of randomBytes) {
36
+ binaryString += String.fromCharCode(byte);
37
+ }
38
+
39
+ // Note: `btoa` is available in the Next.js Edge runtime where this middleware executes.
40
+ return btoa(binaryString);
41
+ }
42
+
43
+ /**
44
+ * Builds the strict Content Security Policy header value for the given nonce.
45
+ *
46
+ * The policy locks down `script-src` to nonced inline scripts and scripts loaded by them
47
+ * (`'strict-dynamic'`). The legacy fallbacks (`'unsafe-inline'`, `http:`, `https:`) are ignored
48
+ * by modern browsers that honor the nonce, but keep the page functional in older browsers
49
+ * without weakening security in modern ones.
50
+ *
51
+ * @param nonce - Per-request CSP nonce.
52
+ * @returns Serialized CSP header value.
53
+ *
54
+ * @private function of middleware
55
+ */
56
+ function buildContentSecurityPolicyHeaderValue(nonce: string): string {
57
+ const directives = [
58
+ `script-src 'nonce-${nonce}' 'strict-dynamic' 'unsafe-inline' 'unsafe-eval' https: http:`,
59
+ `object-src 'none'`,
60
+ `base-uri 'self'`,
61
+ ];
62
+
63
+ return directives.join('; ');
64
+ }
65
+
66
+ /**
67
+ * Adds the CSP nonce headers to the forwarded request so server components and the Next.js
68
+ * runtime can detect the nonce and apply it to their own inline scripts.
69
+ *
70
+ * @param requestHeaders - Mutable headers object that is forwarded to the route handler.
71
+ * @param nonce - Per-request CSP nonce.
72
+ *
73
+ * @private function of middleware
74
+ */
75
+ export function applyContentSecurityPolicyToRequestHeaders(requestHeaders: Headers, nonce: string): void {
76
+ requestHeaders.set(CONTENT_SECURITY_POLICY_NONCE_REQUEST_HEADER, nonce);
77
+
78
+ // Note: Next.js detects the nonce automatically when the CSP is also present on the
79
+ // forwarded request headers — see https://nextjs.org/docs/app/guides/content-security-policy
80
+ requestHeaders.set('Content-Security-Policy', buildContentSecurityPolicyHeaderValue(nonce));
81
+ }
82
+
83
+ /**
84
+ * Adds the strict CSP response header so the browser enforces the nonced script policy.
85
+ *
86
+ * Helpers that add narrower directives (e.g. `applyEmbeddingHeader` with `frame-ancestors`)
87
+ * must `append` a separate CSP header rather than `set` so the strict `script-src` policy is
88
+ * preserved.
89
+ *
90
+ * @param response - Outgoing response.
91
+ * @param nonce - Per-request CSP nonce.
92
+ *
93
+ * @private function of middleware
94
+ */
95
+ export function applyContentSecurityPolicyHeader(response: NextResponse, nonce: string): void {
96
+ response.headers.set('Content-Security-Policy', buildContentSecurityPolicyHeaderValue(nonce));
97
+ }
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-130';
61
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-132';
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
@@ -15,7 +15,7 @@ export declare const BOOK_LANGUAGE_VERSION: string_semantic_version;
15
15
  export declare const PROMPTBOOK_ENGINE_VERSION: string_promptbook_version;
16
16
  /**
17
17
  * Represents the version string of the Promptbook engine.
18
- * It follows semantic versioning (e.g., `0.112.0-128`).
18
+ * It follows semantic versioning (e.g., `0.112.0-131`).
19
19
  *
20
20
  * @generated
21
21
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptbook/cli",
3
- "version": "0.112.0-130",
3
+ "version": "0.112.0-132",
4
4
  "description": "Promptbook: Create persistent AI agents that turn your company's scattered knowledge into action",
5
5
  "private": false,
6
6
  "sideEffects": false,