veryfront 0.1.124 → 0.1.127

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 (41) hide show
  1. package/esm/deno.js +1 -1
  2. package/esm/src/html/hydration-script-builder/templates/router.d.ts.map +1 -1
  3. package/esm/src/html/hydration-script-builder/templates/router.js +9 -0
  4. package/esm/src/react/components/Head.d.ts.map +1 -1
  5. package/esm/src/react/components/Head.js +5 -0
  6. package/esm/src/react/components/ai/chat/composition/chat-root.d.ts.map +1 -1
  7. package/esm/src/react/components/ai/chat/composition/chat-root.js +3 -1
  8. package/esm/src/react/components/ai/chat-with-sidebar.d.ts.map +1 -1
  9. package/esm/src/react/components/ai/chat-with-sidebar.js +3 -1
  10. package/esm/src/react/components/ai/csp-nonce.d.ts +6 -0
  11. package/esm/src/react/components/ai/csp-nonce.d.ts.map +1 -0
  12. package/esm/src/react/components/ai/csp-nonce.js +13 -0
  13. package/esm/src/security/http/response/security-handler.d.ts.map +1 -1
  14. package/esm/src/security/http/response/security-handler.js +12 -4
  15. package/esm/src/server/handlers/dev/framework-candidates.generated.d.ts.map +1 -1
  16. package/esm/src/server/handlers/dev/framework-candidates.generated.js +186 -0
  17. package/esm/src/transforms/mdx/esm-module-loader/import-transformer.js +1 -1
  18. package/esm/src/transforms/mdx/esm-module-loader/jsx-cache.js +1 -1
  19. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/import-rewriter.d.ts +1 -14
  20. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/import-rewriter.d.ts.map +1 -1
  21. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/import-rewriter.js +50 -8
  22. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/index.js +1 -1
  23. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.d.ts.map +1 -1
  24. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.js +18 -17
  25. package/esm/src/utils/version-constant.d.ts +1 -1
  26. package/esm/src/utils/version-constant.js +1 -1
  27. package/package.json +1 -1
  28. package/src/deno.js +1 -1
  29. package/src/src/html/hydration-script-builder/templates/router.ts +9 -0
  30. package/src/src/react/components/Head.tsx +5 -0
  31. package/src/src/react/components/ai/chat/composition/chat-root.tsx +3 -1
  32. package/src/src/react/components/ai/chat-with-sidebar.tsx +3 -1
  33. package/src/src/react/components/ai/csp-nonce.ts +13 -0
  34. package/src/src/security/http/response/security-handler.ts +12 -4
  35. package/src/src/server/handlers/dev/framework-candidates.generated.ts +186 -0
  36. package/src/src/transforms/mdx/esm-module-loader/import-transformer.ts +1 -1
  37. package/src/src/transforms/mdx/esm-module-loader/jsx-cache.ts +1 -1
  38. package/src/src/transforms/mdx/esm-module-loader/module-fetcher/import-rewriter.ts +54 -12
  39. package/src/src/transforms/mdx/esm-module-loader/module-fetcher/index.ts +1 -1
  40. package/src/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.ts +17 -13
  41. package/src/src/utils/version-constant.ts +1 -1
@@ -143,7 +143,7 @@ export async function transformJsxImports(code, adapter, esmCacheDir) {
143
143
  // Rewrite _dnt.polyfills.js / _dnt.shims.js relative imports to absolute file:// paths.
144
144
  // Framework files from the npm package contain relative dnt imports that resolve
145
145
  // incorrectly when cached to a different directory.
146
- transformed = rewriteDntImports(transformed, filePath);
146
+ transformed = await rewriteDntImports(transformed, filePath);
147
147
  await getLocalFs().writeTextFile(transformedPath, transformed);
148
148
  return {
149
149
  original: fullMatch,
@@ -17,7 +17,7 @@ export async function ensureCachedJsxModulePatched(transformedPath, sourceFilePa
17
17
  const fs = getLocalFs();
18
18
  try {
19
19
  const cachedCode = await fs.readTextFile(transformedPath);
20
- const rewritten = rewriteDntImports(cachedCode, sourceFilePath);
20
+ const rewritten = await rewriteDntImports(cachedCode, sourceFilePath);
21
21
  if (rewritten === cachedCode)
22
22
  return true;
23
23
  await fs.writeTextFile(transformedPath, rewritten);
@@ -8,18 +8,5 @@
8
8
  * Uses deno.json exports/imports as the source of truth and appends ?ssr=true.
9
9
  */
10
10
  export declare function rewriteVeryfrontImports(code: string): string;
11
- /**
12
- * Rewrite relative imports in framework files to absolute file:// paths.
13
- *
14
- * Framework files from the npm package (e.g., Head.js) contain relative imports like:
15
- * import "../../../_dnt.polyfills.js"
16
- * import { collectHead } from "../head-collector.js"
17
- *
18
- * These resolve correctly when loaded from the npm package directory, but break when
19
- * the transformed code is cached to a different directory (e.g., /app/.cache/veryfront-mdx-esm/...).
20
- * The relative path would resolve to /app/.cache/head-collector.js which doesn't exist.
21
- *
22
- * Fix: Replace ALL relative imports with absolute file:// paths resolved from the source file's directory.
23
- */
24
- export declare function rewriteDntImports(code: string, sourceFilePath: string): string;
11
+ export declare function rewriteDntImports(code: string, sourceFilePath: string): Promise<string>;
25
12
  //# sourceMappingURL=import-rewriter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"import-rewriter.d.ts","sourceRoot":"","sources":["../../../../../../src/src/transforms/mdx/esm-module-loader/module-fetcher/import-rewriter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM5D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CA8B9E"}
1
+ {"version":3,"file":"import-rewriter.d.ts","sourceRoot":"","sources":["../../../../../../src/src/transforms/mdx/esm-module-loader/module-fetcher/import-rewriter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM5D;AAwCD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA8C7F"}
@@ -6,6 +6,7 @@
6
6
  import { dirname, join, resolve } from "../../../../platform/compat/path/index.js";
7
7
  import { FRAMEWORK_ROOT } from "../constants.js";
8
8
  import { resolveVeryfrontModuleUrl } from "../../../veryfront-module-urls.js";
9
+ import { getLocalFs } from "../cache/index.js";
9
10
  /**
10
11
  * Rewrite veryfront/* imports to /_vf_modules/_veryfront/ paths for MDX module loading.
11
12
  * Uses deno.json exports/imports as the source of truth and appends ?ssr=true.
@@ -31,7 +32,27 @@ export function rewriteVeryfrontImports(code) {
31
32
  *
32
33
  * Fix: Replace ALL relative imports with absolute file:// paths resolved from the source file's directory.
33
34
  */
34
- export function rewriteDntImports(code, sourceFilePath) {
35
+ async function findExistingFrameworkRelativeTarget(absolutePath) {
36
+ const fs = getLocalFs();
37
+ const candidates = [absolutePath, `${absolutePath}.src`];
38
+ if (absolutePath.endsWith(".js") || absolutePath.endsWith(".mjs")) {
39
+ const stem = absolutePath.replace(/\.(?:m?js)$/, "");
40
+ for (const ext of [".ts", ".tsx", ".jsx", ".js", ".mjs"]) {
41
+ candidates.push(`${stem}${ext}.src`, `${stem}${ext}`);
42
+ }
43
+ }
44
+ for (const candidate of candidates) {
45
+ try {
46
+ await fs.stat(candidate);
47
+ return candidate;
48
+ }
49
+ catch {
50
+ /* expected: candidate may not exist */
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+ export async function rewriteDntImports(code, sourceFilePath) {
35
56
  // Only needed for framework files that come from the npm package.
36
57
  // IMPORTANT: Use FRAMEWORK_ROOT + "src/" or dist/framework-src to avoid matching project source files
37
58
  // that live under FRAMEWORK_ROOT (e.g., projects/myproject/components/...).
@@ -46,11 +67,32 @@ export function rewriteDntImports(code, sourceFilePath) {
46
67
  return code;
47
68
  }
48
69
  const sourceDir = dirname(sourceFilePath);
49
- return code.replace(/from\s*["'](\.\.?\/[^"']+)["']/g, (_match, relativePath) => {
50
- const absolutePath = resolve(sourceDir, relativePath);
51
- return `from "file://${absolutePath}"`;
52
- }).replace(/import\s*["'](\.\.?\/[^"']+)["']/g, (_match, relativePath) => {
53
- const absolutePath = resolve(sourceDir, relativePath);
54
- return `import "file://${absolutePath}"`;
55
- });
70
+ const needsFrameworkSourceFallback = sourceFilePath.startsWith(frameworkSrcRoot) ||
71
+ sourceFilePath.startsWith(embeddedSrcRoot);
72
+ let rewritten = code;
73
+ const patterns = [
74
+ {
75
+ regex: /from\s*["'](\.\.?\/[^"']+)["']/g,
76
+ buildReplacement: (path) => `from "file://${path}"`,
77
+ },
78
+ {
79
+ regex: /import\s*["'](\.\.?\/[^"']+)["']/g,
80
+ buildReplacement: (path) => `import "file://${path}"`,
81
+ },
82
+ ];
83
+ for (const { regex, buildReplacement } of patterns) {
84
+ const matches = [...rewritten.matchAll(regex)];
85
+ for (const match of matches) {
86
+ const original = match[0];
87
+ const relativePath = match[1];
88
+ if (!relativePath)
89
+ continue;
90
+ const absolutePath = resolve(sourceDir, relativePath);
91
+ const resolvedPath = needsFrameworkSourceFallback
92
+ ? await findExistingFrameworkRelativeTarget(absolutePath) ?? absolutePath
93
+ : absolutePath;
94
+ rewritten = rewritten.replace(original, buildReplacement(resolvedPath));
95
+ }
96
+ }
97
+ return rewritten;
56
98
  }
@@ -244,7 +244,7 @@ async function doFetchAndCacheModule(normalizedPath, context, fetchAndCacheModul
244
244
  outputLength: moduleCode.length,
245
245
  });
246
246
  // Rewrite _dnt.polyfills.js / _dnt.shims.js relative imports to absolute file:// paths
247
- moduleCode = rewriteDntImports(moduleCode, actualFilePath);
247
+ moduleCode = await rewriteDntImports(moduleCode, actualFilePath);
248
248
  // Cache HTTP imports (esm.sh URLs) to local file:// paths.
249
249
  // This ensures the same cache works for both compiled and non-compiled Deno.
250
250
  // Compiled binaries cannot do dynamic HTTP imports, but non-compiled Deno
@@ -1 +1 @@
1
- {"version":3,"file":"path-resolver.d.ts","sourceRoot":"","sources":["../../../../../../src/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,gBAAgB,EAAU,MAAM,mCAAmC,CAAC;AAY7E,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,EACvC,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAU,GACpD,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAmBzD;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,YAAY,EAAE,MAAM,EACpB,EAAE,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,EACvC,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAU,GACpD,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA6DzD;AAED;;;;;;;GAOG;AACH,wBAAsB,0BAA0B,CAC9C,SAAS,EAAE,MAAM,EACjB,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAU,GACpD,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAkExB;AAED;;;;;;GAMG;AACH,wBAAsB,8BAA8B,CAClD,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,GAAG,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,EACxC,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAU,GACpD,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiExB"}
1
+ {"version":3,"file":"path-resolver.d.ts","sourceRoot":"","sources":["../../../../../../src/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,gBAAgB,EAAU,MAAM,mCAAmC,CAAC;AAY7E,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,EACvC,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAU,GACpD,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAmBzD;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,YAAY,EAAE,MAAM,EACpB,EAAE,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,EACvC,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAU,GACpD,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA6DzD;AAED;;;;;;;GAOG;AACH,wBAAsB,0BAA0B,CAC9C,SAAS,EAAE,MAAM,EACjB,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAU,GACpD,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAkExB;AAED;;;;;;GAMG;AACH,wBAAsB,8BAA8B,CAClD,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,GAAG,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,EACxC,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAU,GACpD,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAqExB"}
@@ -184,26 +184,27 @@ export async function resolveRelativeFrameworkImport(specifier, fromSourcePath,
184
184
  // If specifier already has extension (e.g., ./Head.tsx), we need to try:
185
185
  // 1. The exact path (basePath)
186
186
  // 2. The path with .src suffix (basePath.src) for embedded sources
187
- // 3. Fall back to extension probing
187
+ // 3. For transpiled .js/.mjs imports, fall back to sibling TS/TSX/JSX sources
188
188
  if (/\.(tsx?|jsx?|mjs)$/.test(specifier)) {
189
- // Try exact path first
190
- try {
191
- if (await existsFn(basePath))
192
- return basePath;
193
- }
194
- catch (_) {
195
- /* expected: file may not exist at this path */
196
- }
197
- // Try with .src suffix for embedded sources
198
- try {
199
- const srcPath = basePath + ".src";
200
- if (await existsFn(srcPath))
201
- return srcPath;
189
+ const explicitCandidates = [basePath, `${basePath}.src`];
190
+ // esbuild rewrites TS/TSX relative imports to .js in transformed output.
191
+ // When the original source only exists as .ts/.tsx (or embedded .src),
192
+ // probe those sibling source extensions before giving up.
193
+ if (basePath.endsWith(".js") || basePath.endsWith(".mjs")) {
194
+ const stem = basePath.replace(/\.(?:m?js)$/, "");
195
+ for (const ext of [".ts", ".tsx", ".jsx", ".js", ".mjs"]) {
196
+ explicitCandidates.push(`${stem}${ext}.src`, `${stem}${ext}`);
197
+ }
202
198
  }
203
- catch (_) {
204
- /* expected: file may not exist at this path */
199
+ for (const candidate of explicitCandidates) {
200
+ try {
201
+ if (await existsFn(candidate))
202
+ return candidate;
203
+ }
204
+ catch (_) {
205
+ /* expected: file may not exist at this path */
206
+ }
205
207
  }
206
- // Not found with explicit extension
207
208
  return null;
208
209
  }
209
210
  // No extension provided - try all extensions (including .src for embedded sources)
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.1.124";
1
+ export declare const VERSION = "0.1.127";
2
2
  //# sourceMappingURL=version-constant.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
- export const VERSION = "0.1.124";
3
+ export const VERSION = "0.1.127";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.124",
3
+ "version": "0.1.127",
4
4
  "description": "The simplest way to build AI-powered apps",
5
5
  "keywords": [
6
6
  "react",
package/src/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.124",
3
+ "version": "0.1.127",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "exclude": [
@@ -42,6 +42,13 @@ export const getRouterScript = () => `
42
42
  const log = DEBUG ? console.log.bind(console, '[Veryfront]') : () => {};
43
43
  const logError = console.error.bind(console, '[Veryfront]');
44
44
 
45
+ function getDocumentNonce() {
46
+ const element = document.querySelector('script[nonce], style[nonce], link[nonce]');
47
+ if (!element) return undefined;
48
+
49
+ return element.nonce || element.getAttribute('nonce') || undefined;
50
+ }
51
+
45
52
  // ============================================
46
53
  // Version tracking for cache invalidation
47
54
  // ============================================
@@ -417,6 +424,8 @@ export const getRouterScript = () => `
417
424
  existingStyle.textContent = pageData.css;
418
425
  } else {
419
426
  const styleEl = document.createElement('style');
427
+ const nonce = getDocumentNonce();
428
+ if (nonce) styleEl.setAttribute('nonce', nonce);
420
429
  styleEl.id = 'veryfront-spa-css';
421
430
  styleEl.textContent = pageData.css;
422
431
  document.head.appendChild(styleEl);
@@ -25,6 +25,7 @@ import "../../../_dnt.polyfills.js";
25
25
  import React, { useEffect, useRef } from "react";
26
26
  import { collectHead } from "../head-collector.js";
27
27
  import { isServerEnvironment } from "../../platform/compat/runtime.js";
28
+ import { getDocumentNonce } from "./ai/csp-nonce.js";
28
29
 
29
30
  export function Head({ children }: { children: React.ReactNode }): React.ReactElement {
30
31
  const mountedRef = useRef(false);
@@ -94,6 +95,7 @@ export function Head({ children }: { children: React.ReactNode }): React.ReactEl
94
95
  if (!children) return;
95
96
 
96
97
  const addedElements: Element[] = [];
98
+ const nonce = getDocumentNonce();
97
99
 
98
100
  React.Children.forEach(children, (child) => {
99
101
  if (!React.isValidElement(child)) return;
@@ -109,6 +111,9 @@ export function Head({ children }: { children: React.ReactNode }): React.ReactEl
109
111
  }
110
112
 
111
113
  const element = document.createElement(type);
114
+ if ((type === "style" || type === "script") && !props.nonce && nonce) {
115
+ element.setAttribute("nonce", nonce);
116
+ }
112
117
 
113
118
  // For scripts, check if already SSR'd via <Head> to avoid double execution
114
119
  if (type === "script") {
@@ -11,6 +11,7 @@ import * as React from "react";
11
11
  import { ChatContainer } from "../../../../primitives/index.js";
12
12
  import type { UIMessage } from "../../../../../agent/react/index.js";
13
13
  import type { ChatTheme } from "../../theme.js";
14
+ import { getDocumentNonce } from "../../csp-nonce.js";
14
15
  import { cn, defaultChatTheme, generateTokenCSS, mergeThemes } from "../../theme.js";
15
16
  import type { ModelOption } from "../../model-selector.js";
16
17
  import type { AttachmentInfo } from "../components/attachment-pill.js";
@@ -97,6 +98,7 @@ export const ChatRoot = React.forwardRef<HTMLDivElement, ChatRootProps>(
97
98
  ref,
98
99
  ) {
99
100
  const theme = React.useMemo(() => mergeThemes(defaultChatTheme, userTheme), [userTheme]);
101
+ const nonce = getDocumentNonce();
100
102
  const tokenCSS = React.useMemo(() => generateTokenCSS(), []);
101
103
  const [isAtBottom, _setIsAtBottom] = React.useState(true);
102
104
  const scrollAreaRef = React.useRef<HTMLDivElement>(null);
@@ -164,7 +166,7 @@ export const ChatRoot = React.forwardRef<HTMLDivElement, ChatRootProps>(
164
166
 
165
167
  return (
166
168
  <ChatContextProvider value={contextValue}>
167
- <style dangerouslySetInnerHTML={{ __html: tokenCSS }} />
169
+ <style nonce={nonce} dangerouslySetInnerHTML={{ __html: tokenCSS }} />
168
170
  <ChatContainer
169
171
  ref={ref}
170
172
  data-vf-chat=""
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import { getDocumentNonce } from "./csp-nonce.js";
2
3
  import { cn, generateTokenCSS } from "./theme.js";
3
4
  import { Chat, type ChatProps } from "./chat/index.js";
4
5
  import { ChatSidebar } from "./chat/components/sidebar.js";
@@ -142,6 +143,7 @@ export const ChatWithSidebar = React.forwardRef<HTMLDivElement, ChatWithSidebarP
142
143
  },
143
144
  ref,
144
145
  ): React.ReactElement {
146
+ const nonce = getDocumentNonce();
145
147
  const storageKey = sidebar?.storageKey;
146
148
  const controlledOpen = sidebar?.open;
147
149
  const onSidebarToggle = sidebar?.onToggle;
@@ -320,7 +322,7 @@ export const ChatWithSidebar = React.forwardRef<HTMLDivElement, ChatWithSidebarP
320
322
  className={cn("flex h-full bg-[var(--background)]", className)}
321
323
  data-vf-chat=""
322
324
  >
323
- <style dangerouslySetInnerHTML={{ __html: tokenCSS }} />
325
+ <style nonce={nonce} dangerouslySetInnerHTML={{ __html: tokenCSS }} />
324
326
  {sidebarOpen && (
325
327
  <ChatSidebar
326
328
  threads={threads}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Reuse the server-issued CSP nonce for client-created style/script elements
3
+ * during hydration and SPA updates.
4
+ */
5
+ export function getDocumentNonce(): string | undefined {
6
+ if (typeof document === "undefined") return undefined;
7
+
8
+ const element = document.querySelector<HTMLElement>("script[nonce], style[nonce], link[nonce]");
9
+ if (!element) return undefined;
10
+
11
+ const nonce = element.nonce || element.getAttribute("nonce") || "";
12
+ return nonce || undefined;
13
+ }
@@ -20,9 +20,16 @@ export function generateNonce(): string {
20
20
  *
21
21
  * - Scripts: nonce-based + cdn.jsdelivr.net + esm.sh (Scalar API docs,
22
22
  * html2canvas, legacy/browser ESM hydration)
23
- * - Styles: 'self' + 'unsafe-inline' + nonce + Google Fonts + cdn.veryfront.com
24
- * plus style-src-attr 'unsafe-inline' so React style="" attributes remain
25
- * compatible while inline <style> tags continue to use the nonce
23
+ * - Styles:
24
+ * - style-src: 'self' + 'unsafe-inline' + Google Fonts + cdn.veryfront.com
25
+ * so React style="" attributes and framework inline styles remain
26
+ * compatible. Do not include a nonce in style-src here: browsers ignore
27
+ * 'unsafe-inline' when a nonce/hash is present on the directive, which
28
+ * breaks React style attributes.
29
+ * - style-src-elem: nonce-based + Google Fonts + cdn.veryfront.com for
30
+ * inline <style> tags and stylesheet elements
31
+ * - style-src-attr: 'unsafe-inline' for modern browsers with directive-level
32
+ * style attribute support
26
33
  * - Images/media/fonts: 'self' + data: + https: + cdn.veryfront.com
27
34
  * - Connections: 'self' + wss: + https: (WebSocket for HMR/live reload, API calls)
28
35
  * - Objects: 'none' (block Flash/plugins)
@@ -35,7 +42,8 @@ function buildDefaultCSP(nonce: string): string {
35
42
  return [
36
43
  `default-src 'self'`,
37
44
  `script-src 'self' 'nonce-${nonce}' https://cdn.jsdelivr.net https://esm.sh`,
38
- `style-src 'self' 'unsafe-inline' 'nonce-${nonce}' https://fonts.googleapis.com https://cdn.veryfront.com`,
45
+ `style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.veryfront.com`,
46
+ `style-src-elem 'self' 'nonce-${nonce}' https://fonts.googleapis.com https://cdn.veryfront.com`,
39
47
  `style-src-attr 'unsafe-inline'`,
40
48
  `img-src 'self' data: https:`,
41
49
  `font-src 'self' data: https://fonts.gstatic.com https://cdn.veryfront.com`,