Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "veryfront",
"version": "0.1.1208",
"version": "0.1.1209",
"license": "Apache-2.0",
"nodeModulesDir": "auto",
"minimumDependencyAge": {
Expand Down
6 changes: 3 additions & 3 deletions docs/api-reference/veryfront/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ applySecurityHeaders(response.headers, false, generateNonce(), null);
| `applyCORSHeaders` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/cors/headers.ts#L86) |
| `applyCORSHeadersSync` | Apply CORS synchronously. Promise-returning values still fail closed at runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/cors/headers.ts#L112) |
| `applyCsrfCookie` | Set CSRF cookie on GET/HEAD responses when not already present. Uses httpOnly: false so client JS can read the cookie for double-submit. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/csrf/helpers.ts#L150) |
| `applySecurityHeaders` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/response/security-handler.ts#L262) |
| `applySecurityHeaders` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/response/security-handler.ts#L266) |
| `buildCacheControl` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/response/cache-handler.ts#L86) |
| `cors` | Create CORS middleware. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/cors/middleware.ts#L10) |
| `corsSimple` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/cors/middleware.ts#L39) |
Expand All @@ -65,8 +65,8 @@ applySecurityHeaders(response.headers, false, generateNonce(), null);
| `createValidationError` | Create an input validation error. Convenience wrapper around INPUT_VALIDATION_FAILED.create(). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/input-validation/errors.ts#L12) |
| `createValidator` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/path-validation/index.ts#L446) |
| `generateCsrfToken` | Generate a CSRF token and return value + Set-Cookie header string | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/csrf/helpers.ts#L70) |
| `generateNonce` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/response/security-handler.ts#L49) |
| `getSecurityHeader` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/response/security-handler.ts#L249) |
| `generateNonce` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/response/security-handler.ts#L51) |
| `getSecurityHeader` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/response/security-handler.ts#L253) |
| `handleCORSPreflight` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/cors/preflight.ts#L126) |
| `isPreflightRequest` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/cors/preflight.ts#L186) |
| `isRequestBodyTooLargeError` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/input-validation/limits.ts#L100) |
Expand Down
1 change: 1 addition & 0 deletions docs/api-reference/veryfront/ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export default function App({ children }: { children: React.ReactNode }) {
| `getFileTypeLabel` | Human label for a file extension, falling back to the media type. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/file-type.tsx#L233) |
| `useAdapter` | Resolve the active UI-primitive adapter. Never returns null (defaults to builtin). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/adapter/context.tsx#L53) |
| `useColorModeOptional` | Non-throwing variant - returns `null` when there is no `ColorModeProvider`. Use for components that should render standalone (e.g. a `CodeBlock` dropped into markdown) and fall back to light mode. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/color-mode.tsx#L143) |
| `useDocumentNonce` | Read the nonce from the Suspense-safe server provider or the browser DOM. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/csp-nonce.ts#L23) |
| `useToast` | Returns `{ toast, dismiss }`. Call `toast(options)` to enqueue (returns the new id), `toast.custom((id) => node)` for a fully custom toast, and `dismiss(id)` to remove one early. Must be used within a `ToastProvider`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/toast.tsx#L136) |

### Types
Expand Down
34 changes: 23 additions & 11 deletions docs/guides/security-headers.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: "Security headers and CSP"
description: "Veryfront applies a Content-Security-Policy by default. Use this guide to allow Google Fonts, analytics, and other third-party origins."
description: "Veryfront applies a Content-Security-Policy by default. Use this guide to allow analytics, embeds, and other third-party origins your site needs."
order: 11
---

Expand All @@ -13,10 +13,10 @@ In production, Veryfront serves this policy:
```http
default-src 'self';
script-src 'self' 'nonce-<generated>' https://esm.sh;
style-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
style-src-attr 'unsafe-inline';
img-src 'self' https://images.veryfront.com https://cdn.veryfront.com data:;
font-src 'self' data:;
font-src 'self' data: https://fonts.gstatic.com;
connect-src 'self' https://esm.sh;
media-src 'self' blob:;
worker-src 'self' blob:;
Expand All @@ -31,9 +31,10 @@ Alongside it: `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Refer

Development serves no CSP at all, so HMR and dev tooling are never blocked and a local allowance can never widen your production policy.

Two directives are worth understanding:
Three directives are worth understanding:

- **`script-src` includes `https://esm.sh`** because the renderer writes React imports from that CDN into every document. A fresh nonce is generated per response for the framework's own inline bootstrap.
- **`style-src` and `font-src` include the Google Fonts origins** because `veryfront/fonts` writes those tags into the document itself. Google Fonts therefore works with no configuration. If your project never uses it, see [Tightening the policy](#tightening-the-policy).
- **`frame-ancestors`** is `'none'` on your own domain. On `*.veryfront.com` addresses it instead allows the Studio origins, so the Studio preview iframe works.

## Adding an origin
Expand All @@ -44,25 +45,36 @@ Set `security.csp` in `veryfront.config.ts`. Values are **added to** the default
export default {
security: {
csp: {
styleSrc: ["https://fonts.googleapis.com"],
fontSrc: ["https://fonts.gstatic.com"],
// An analytics endpoint your client code posts to
connectSrc: ["https://analytics.example.com"],
},
},
};
```

That is the complete Google Fonts setup: `fonts.googleapis.com` serves the stylesheet, `fonts.gstatic.com` serves the font files, and both directives keep everything they already had.
`connect-src` keeps everything it already had and gains your origin.

Directive names may be camelCase (`fontSrc`) or the CSP spelling (`font-src`). Both work; camelCase matches the rest of your config. You do not need to repeat `'self'`; it is already there.

A font service other than Google's needs both halves, the stylesheet origin and the font-file origin:

```ts
export default {
security: {
csp: {
styleSrc: ["https://use.typekit.net"],
fontSrc: ["https://use.typekit.net"],
},
},
};
```

A few more examples:

```ts
export default {
security: {
csp: {
// An analytics endpoint your client code posts to
connectSrc: ["https://analytics.example.com"],
// Embedding YouTube
frameSrc: ["https://www.youtube.com"],
// Images from your own CDN
Expand All @@ -88,7 +100,7 @@ To remove the platform's optional sources for one directive, set it to `null`:
export default {
security: {
csp: {
// Serve no inline styles. Keeps 'self', drops 'unsafe-inline'.
// Keeps 'self'. Drops 'unsafe-inline' and the Google Fonts origin.
styleSrc: null,
},
},
Expand All @@ -97,7 +109,7 @@ export default {

`null` removes the optional half of a directive and keeps the required half. It cannot lock you out of your own site.

Before doing this, check what your components actually need. `'unsafe-inline'` is in the default `style-src` because many React component libraries, including Veryfront's own, create styles at runtime. Removing it is safe only if you are certain yours do not.
Before doing this, check what your components actually need. `'unsafe-inline'` is in the default `style-src` because many React component libraries, including Veryfront's own, create styles at runtime. Removing it is safe only if you are certain yours do not. The same setting drops the Google Fonts stylesheet origin, so only reach for it if your project does not use `veryfront/fonts`.

## Replacing the policy entirely

Expand Down

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/html/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,10 @@ describe("html-generation/utils", () => {
assertEquals(imports["veryfront/head"], "/_vf_modules/_veryfront/react/runtime/core.js");
assertEquals(imports["veryfront/context"], "/_vf_modules/_veryfront/react/runtime/core.js");
assertEquals(imports["veryfront/fonts"], "/_vf_modules/_veryfront/react/fonts/index.js");
assertEquals(
imports["veryfront/ui"],
"/_vf_modules/_veryfront/react/components/ui/index.js",
);

// React must come from esm.sh even under unpkg — unpkg only ships UMD
// globals, which cannot be loaded through an import map, so hydration would
Expand Down
6 changes: 6 additions & 0 deletions src/html/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const PLATFORM_UTILITY_PATHS = {
router: CORE_REACT_RUNTIME_PATH,
context: CORE_REACT_RUNTIME_PATH,
fonts: "/_vf_modules/_veryfront/react/fonts/index.js",
ui: "/_vf_modules/_veryfront/react/components/ui/index.js",
// Client-side AI/chat modules - use local module server in dev for faster iteration
// NOTE: These are NOT available in compiled binaries, so we use CDN URLs there instead
chat: "/_vf_modules/_veryfront/chat/index.js",
Expand All @@ -101,6 +102,11 @@ const CORE_PLATFORM_UTILITIES: Record<string, string> = {
"veryfront/router": PLATFORM_UTILITY_PATHS.router,
"veryfront/context": PLATFORM_UTILITY_PATHS.context,
"veryfront/fonts": PLATFORM_UTILITY_PATHS.fonts,
// deno.json has exported `veryfront/ui` for as long as the barrel has
// existed, but no import map named it, so the bare specifier reached the
// browser unresolved and the release build counted the importing module as
// uncovered. Anything a project can import must be resolvable here.
"veryfront/ui": PLATFORM_UTILITY_PATHS.ui,
"veryfront/react/head": PLATFORM_UTILITY_PATHS.head,
"veryfront/react/router": PLATFORM_UTILITY_PATHS.router,
"veryfront/react/context": PLATFORM_UTILITY_PATHS.context,
Expand Down
14 changes: 14 additions & 0 deletions src/modules/import-map/default-import-map.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ describe("modules/import-map/default-import-map", () => {
assert("veryfront/router" in imports, "should have 'veryfront/router' mapping");
assert("veryfront/context" in imports, "should have 'veryfront/context' mapping");
assert("veryfront/fonts" in imports, "should have 'veryfront/fonts' mapping");
assert("veryfront/ui" in imports, "should have 'veryfront/ui' mapping");
});

it("maps every React-bearing deno.json export so the specifier resolves", () => {
// veryfront/ui was exported from deno.json but named in no import map, so
// a project importing it shipped a bare specifier the browser could not
// resolve -- and the release build then counted the importing module as
// uncovered, which is fatal to the whole manifest.
const imports = getImports();

assertEquals(
imports["veryfront/ui"],
"/_vf_modules/_veryfront/react/components/ui/index.js?ssr=true",
);
});

it("should map veryfront/react to the browser public barrel", () => {
Expand Down
2 changes: 2 additions & 0 deletions src/modules/import-map/default-import-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ function getVeryfrontSsrImportMap(): Record<string, string> {
const router = coreReact;
const context = coreReact;
const fonts = `${base}/react/fonts/index.js${ssr}`;
const ui = `${base}/react/components/ui/index.js${ssr}`;

const markdown = `${base}/markdown/index.js${ssr}`;
const chat = `${base}/chat/index.js${ssr}`;
Expand All @@ -38,6 +39,7 @@ function getVeryfrontSsrImportMap(): Record<string, string> {
"veryfront/router": router,
"veryfront/context": context,
"veryfront/fonts": fonts,
"veryfront/ui": ui,
"veryfront/markdown": markdown,
"veryfront/chat": chat,
"veryfront/mdx": mdx,
Expand Down
1 change: 1 addition & 0 deletions src/react/components/ui/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ const expectedRuntimeExports = [
"useAppShell",
"useColorMode",
"useColorModeOptional",
"useDocumentNonce",
"useToast",
];

Expand Down
6 changes: 5 additions & 1 deletion src/react/components/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@
export { cva, cx, type VariantProps } from "./cva.ts";
export { generateTokenCSS } from "./design-tokens.ts";
export { DesignTokenStyle } from "./tokens.tsx";
export { getDocumentNonce } from "./csp-nonce.ts";
// `useDocumentNonce` ships alongside the getter because third-party providers
// that render their own inline script -- next-themes is the common one -- take
// the nonce as a prop and emit an unusable empty attribute without it, which
// the default CSP then blocks with nothing but a console error to show for it.
export { getDocumentNonce, useDocumentNonce } from "./csp-nonce.ts";
export {
ColorModeProvider,
type ColorModeProviderProps,
Expand Down
Loading