diff --git a/.changeset/honest-ghosts-obey.md b/.changeset/honest-ghosts-obey.md
new file mode 100644
index 000000000..d13ca3599
--- /dev/null
+++ b/.changeset/honest-ghosts-obey.md
@@ -0,0 +1,7 @@
+---
+'@quilted/preact-browser': patch
+'@quilted/browser': patch
+'@quilted/create': patch
+---
+
+Move headers to be server-only in default templates
diff --git a/packages/browser/package.json b/packages/browser/package.json
index f49931738..647c0f05f 100644
--- a/packages/browser/package.json
+++ b/packages/browser/package.json
@@ -61,6 +61,7 @@
},
"dependencies": {
"@quilted/assets": "workspace:^0.1.5",
+ "@quilted/http": "workspace:^0.3.0",
"@quilted/signals": "workspace:^0.2.1",
"js-cookie": "^3.0.0"
},
diff --git a/packages/browser/source/headers.ts b/packages/browser/source/headers.ts
new file mode 100644
index 000000000..c85b0eb50
--- /dev/null
+++ b/packages/browser/source/headers.ts
@@ -0,0 +1,916 @@
+import type {
+ ContentSecurityPolicySandboxAllow,
+ ContentSecurityPolicySpecialSource,
+} from '@quilted/http';
+
+export type {
+ ContentSecurityPolicySandboxAllow,
+ ContentSecurityPolicySpecialSource,
+ ContentSecurityPolicyDirective,
+ CrossOriginEmbedderPolicyHeaderValue,
+ CrossOriginOpenerPolicyHeaderValue,
+ CrossOriginResourcePolicyHeaderValue,
+ PermissionsPolicyDirective,
+ PermissionsPolicySpecialSource,
+} from '@quilted/http';
+
+const SPECIAL_SOURCES = new Set(['*', 'self', 'src']);
+
+// The recommendation for being added to Google’s preload list is
+// two years. See https://hstspreload.org/ for details.
+const STRICT_TRANSPORT_SECURITY_DEFAULT_MAX_AGE = 63_072_000;
+
+/**
+ * Options for controlling the Cache-Control header of the current
+ * response.
+ *
+ * @see https://csswizardry.com/2019/03/cache-control-for-civilians/
+ */
+export type CacheControlOptions =
+ | {
+ /**
+ * Whether this cache control directive applies only to “private”
+ * caches, which is typically the end user’s browser cache for
+ * applications using Quilt. When not set, the cache-control directive
+ * will be set to `public`, which allows all caches (including CDNs,
+ * proxies, and the like) to cache the content.
+ */
+ private?: true;
+ /**
+ * Completely disable caching of this response. Passing this option
+ * sets the `Cache-Control` header to `no-store`.
+ */
+ cache: false;
+ maxAge?: never;
+ immutable?: never;
+ revalidate?: never;
+ }
+ | {
+ /**
+ * Whether this cache control directive applies only to “private”
+ * caches, which is typically the end user’s browser cache for
+ * applications using Quilt. When not set, the cache-control directive
+ * will be set to `public`, which allows all caches (including CDNs,
+ * proxies, and the like) to cache the content.
+ */
+ private?: true;
+ cache?: never;
+ /**
+ * The number of seconds, from the time of this request, that
+ * the content is considered “fresh”.
+ */
+ maxAge?: number;
+ immutable?: never;
+ /**
+ * Controls how clients should revalidate their cached content.
+ * If this option is set to `true`, the resulting `Cache-Control`
+ * header will have the `must-revalidate` directive, which asks
+ * clients to revalidate their content after the `maxAge` period
+ * has expired.
+ *
+ * If you instead pass `{allowStale: number}`, the resulting
+ * `Cache-Control` header will have the `stale-while-revalidate`
+ * directive, which allows caches that support it to use the stale
+ * version from the cache while they perform revalidation in the
+ * background.
+ */
+ revalidate?: boolean | {allowStale: number};
+ }
+ | {
+ /**
+ * Whether this cache control directive applies only to “private”
+ * caches, which is typically the end user’s browser cache for
+ * applications using Quilt. When not set, the cache-control directive
+ * will be set to `public`, which allows all caches (including CDNs,
+ * proxies, and the like) to cache the content.
+ */
+ private?: true;
+ cache?: never;
+ maxAge?: number;
+ /**
+ * Declares that this request is immutable. This means that it is
+ * assumed to never change, and clients will never attempt to
+ * revalidate the content. Be careful when using this option!
+ */
+ immutable?: boolean;
+ revalidate?: never;
+ };
+
+export function cacheControlHeader(options: CacheControlOptions) {
+ let headerValue = '';
+
+ const {
+ private: isPrivate,
+ cache,
+ immutable,
+ maxAge = immutable ? 31536000 : undefined,
+ revalidate,
+ } = options;
+
+ headerValue = isPrivate ? 'private' : 'public';
+
+ const appendToHeader = (value: string) => {
+ headerValue = `${headerValue}, ${value}`;
+ };
+
+ if (cache === false) {
+ appendToHeader('no-store');
+ }
+
+ if (maxAge === 0 && revalidate === true) {
+ appendToHeader('no-cache');
+ } else if (typeof maxAge === 'number' || revalidate) {
+ appendToHeader(`max-age=${maxAge ?? 0}`);
+
+ if (revalidate === true) {
+ appendToHeader('must-revalidate');
+ } else if (typeof revalidate === 'object') {
+ appendToHeader(`stale-while-revalidate=${revalidate.allowStale}`);
+ }
+ }
+
+ if (immutable) appendToHeader('immutable');
+
+ return headerValue;
+}
+
+/**
+ * Options for creating a content security policy.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
+ */
+export interface ContentSecurityPolicyOptions {
+ /**
+ * Sets the child-src content security policy directive, which determines what sources
+ * are allowed for nested browsing contexts, including workers and iframes.
+ *
+ * Passing `false` or an empty array is equivalent to passing `["'none'"]`, disallowing
+ * all sources.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/child-src
+ */
+ childSources?: false | (ContentSecurityPolicySpecialSource | string)[];
+
+ /**
+ * Sets the connect-src content security policy directive, which determines what sources
+ * can be connected to from script sources (for example, when using `fetch()`).
+ *
+ * Passing `false` or an empty array is equivalent to passing `["'none'"]`, disallowing
+ * all sources.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src
+ */
+ connectSources?: (ContentSecurityPolicySpecialSource | string)[];
+
+ /**
+ * Sets the default-src content security policy directive, which determines what sources
+ * are allowed for resources where a more specific policy has not been set.
+ *
+ * Passing `false` or an empty array is equivalent to passing `["'none'"]`, disallowing
+ * all sources.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src
+ */
+ defaultSources?: false | (ContentSecurityPolicySpecialSource | string)[];
+
+ /**
+ * Sets the font-src content security policy directive, which determines what sources
+ * are allowed for fonts used on this page.
+ *
+ * Passing `false` or an empty array is equivalent to passing `["'none'"]`, disallowing
+ * all sources.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/font-src
+ */
+ fontSources?: false | (ContentSecurityPolicySpecialSource | string)[];
+
+ /**
+ * Sets the frame-src content security policy directive, which determines what sources
+ * are allowed for frames embedded in this page.
+ *
+ * Passing `false` or an empty array is equivalent to passing `["'none'"]`, disallowing
+ * all sources.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src
+ */
+ frameSources?: false | (ContentSecurityPolicySpecialSource | string)[];
+
+ /**
+ * Sets the img-src content security policy directive, which determines what sources
+ * are allowed for images.
+ *
+ * Passing `false` or an empty array is equivalent to passing `["'none'"]`, disallowing
+ * all sources.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/img-src
+ */
+ imageSources?: false | (ContentSecurityPolicySpecialSource | string)[];
+
+ /**
+ * Sets the manifest-src content security policy directive, which determines what sources
+ * are allowed for the app manifest for this page.
+ *
+ * Passing `false` or an empty array is equivalent to passing `["'none'"]`, disallowing
+ * all sources.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/manifest-src
+ */
+ manifestSources?: false | (ContentSecurityPolicySpecialSource | string)[];
+
+ /**
+ * Sets the media-src content security policy directive, which determines what sources
+ * are allowed for audio and video.
+ *
+ * Passing `false` or an empty array is equivalent to passing `["'none'"]`, disallowing
+ * all sources.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/media-src
+ */
+ mediaSources?: false | (ContentSecurityPolicySpecialSource | string)[];
+
+ /**
+ * Sets the object-src content security policy directive, which determines what sources
+ * are allowed for `);
-
- expect(headers).toContainPreactComponent(ContentSecurityPolicy, {
- defaultSources: ["'self'"],
- });
- });
-});
diff --git a/packages/create/templates/app-basic/foundation/html/HTML.tsx b/packages/create/templates/app-basic/foundation/html/HTML.tsx
deleted file mode 100644
index 383892ed1..000000000
--- a/packages/create/templates/app-basic/foundation/html/HTML.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-import type {RenderableProps} from 'preact';
-import Env from 'quilt:module/env';
-import {Title, Favicon, useBrowserRequest} from '@quilted/quilt/browser';
-import {
- CacheControl,
- ResponseHeader,
- ContentSecurityPolicy,
- PermissionsPolicy,
- SearchRobots,
- StrictTransportSecurity,
- Viewport,
-} from '@quilted/quilt/server';
-
-// This component sets details of the HTML page. If you need to customize
-// any of these details based on conditions like the active route, or some
-// state about the user, you can move these components to wherever in your
-// application you can read that state.
-//
-// @see https://github.com/lemonmade/quilt/blob/main/documentation/features/html.md
-export function HTML({children}: RenderableProps<{}>) {
- return (
- <>
-
-
- {children}
- >
- );
-}
-
-function Headers() {
- const {url} = useBrowserRequest();
- const isHttps = new URL(url).protocol === 'https:';
-
- return (
- <>
- {/*
- * Disables the cache for this page, which is generally the best option
- * when dealing with authenticated content. If your site doesn’t have
- * authentication, or you have a better cache policy that works for your
- * app or deployment, make sure to update this component accordingly!
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
- */}
-
-
- {/*
- * Sets a strict content security policy for this page. If you load
- * assets from other origins, or want to allow some more dangerous
- * resource loading techniques like `eval`, you can change the
- * `defaultSources` to be less restrictive, or add additional items
- * to the allowlist for more specific directives.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
- */}
-
-
- {/*
- * Sets a strict permissions policy for this page, which limits access
- * to some native browser features.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
- */}
-
-
- {/*
- * Instructs browsers to only load this page over HTTPS using the
- * `Strict-Transport-Security` header.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
- */}
- {isHttps && }
-
- {/*
- * Controls how much information about the current page is included in
- * requests (through the `Referer` header). The default value
- * (strict-origin-when-cross-origin) means that only the origin is included
- * for cross-origin requests, while the origin, path, and querystring
- * are included for same-origin requests.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
- */}
-
-
- {/*
- * Instructs browsers to respect the MIME type in the `Content-Type` header.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
- */}
-
- >
- );
-}
-
-function Head() {
- return (
- <>
- {/* Sets the default `
` for this application. */}
- App
-
- {/*
- * Sets the default favicon used by the application. You can
- * change this to a different emoji, make it `blank`, or pass
- * a URL with the `source` prop.
- */}
-
-
- {/* Adds a responsive-friendly `viewport` `` tag. */}
-
-
- {/*
- * Disables all search indexing for this application. If you are
- * building an unauthenticated app, you probably want to remove
- * this component, or update it to control how your site is indexed
- * by search engines.
- */}
-
- >
- );
-}
diff --git a/packages/create/templates/app-basic/foundation/html/Head.test.tsx b/packages/create/templates/app-basic/foundation/html/Head.test.tsx
new file mode 100644
index 000000000..a02800340
--- /dev/null
+++ b/packages/create/templates/app-basic/foundation/html/Head.test.tsx
@@ -0,0 +1,42 @@
+import {describe, it, expect} from 'vitest';
+import {Title, Favicon} from '@quilted/quilt/browser';
+import {SearchRobots, Viewport} from '@quilted/quilt/server';
+
+import {renderApp} from '~/tests/render.ts';
+
+import {Head} from './Head.tsx';
+
+describe('
', () => {
+ it('sets the default title', async () => {
+ const head = await renderApp(
` details of the HTML page. If you need
+// to customize any of these details based on conditions or application data, you
+// can render these same components deeper in the application to override these defaults.
+//
+// @see https://github.com/lemonmade/quilt/blob/main/documentation/features/html.md
+export function Head() {
+ return (
+ <>
+ {/* Sets the default `
` for this application. */}
+ App
+
+ {/*
+ * Sets the default favicon used by the application. You can
+ * change this to a different emoji, make it `blank`, or pass
+ * a URL with the `source` prop.
+ */}
+
+
+ {/* Adds a responsive-friendly `viewport` `` tag. */}
+
+
+ {/*
+ * Disables all search indexing for this application. If you are
+ * building an unauthenticated app, you probably want to remove
+ * this component, or update it to control how your site is indexed
+ * by search engines.
+ */}
+
+ >
+ );
+}
diff --git a/packages/create/templates/app-basic/server.tsx b/packages/create/templates/app-basic/server.tsx
index a4b9a4243..0ee78575c 100644
--- a/packages/create/templates/app-basic/server.tsx
+++ b/packages/create/templates/app-basic/server.tsx
@@ -1,7 +1,16 @@
import '@quilted/quilt/globals';
+
import {RequestRouter} from '@quilted/quilt/request-router';
-import {renderAppToHTMLResponse} from '@quilted/quilt/server';
import {Router} from '@quilted/quilt/navigation';
+import {
+ renderAppToHTMLResponse,
+ cacheControlHeader,
+ contentSecurityPolicyHeader,
+ permissionsPolicyHeader,
+ strictTransportSecurityHeader,
+} from '@quilted/quilt/server';
+
+import Env from 'quilt:module/env';
import {BrowserAssets} from 'quilt:module/assets';
import type {AppContext} from '~/shared/context.ts';
@@ -11,15 +20,92 @@ import {App} from './App.tsx';
const router = new RequestRouter();
const assets = new BrowserAssets();
-// For all GET requests, render our React application.
+// For all GET requests, render our Preact application.
router.get(async (request) => {
const context = {
router: new Router(request.url),
} satisfies AppContext;
+ const isHttps = request.url.startsWith('https://');
+
const response = await renderAppToHTMLResponse(, {
request,
assets,
+ headers: {
+ // Controls how much information about the current page is included in
+ // requests (through the `Referer` header). The default value
+ // (strict-origin-when-cross-origin) means that only the origin is included
+ // for cross-origin requests, while the origin, path, and querystring
+ // are included for same-origin requests.
+ //
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
+ 'Referrer-Policy': 'strict-origin-when-cross-origin',
+
+ // Disables the cache for this page, which is generally the best option
+ // when dealing with authenticated content. If your site doesn't have
+ // authentication, or you have a better cache policy that works for your
+ // app or deployment, make sure to update this component accordingly!
+ //
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
+ 'Cache-Control': cacheControlHeader({
+ cache: false,
+ }),
+
+ // Sets a strict content security policy for this page. If you load
+ // assets from other origins, or want to allow some more dangerous
+ // resource loading techniques like `eval`, you can change the
+ // `defaultSources` to be less restrictive, or add additional items
+ // to the allowlist for more specific directives.
+ //
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
+ 'Content-Security-Policy': contentSecurityPolicyHeader({
+ // By default, only allow sources from the page's origin.
+ defaultSources: ["'self'"],
+ // In development, allow connections to local websockets for hot reloading.
+ connectSources:
+ Env.MODE === 'development'
+ ? ["'self'", `${isHttps ? 'ws' : 'wss'}://localhost:*`]
+ : undefined,
+ // Includes `'unsafe-inline'` because CSS is often necessary in development,
+ // and can be difficult to avoid in production.
+ styleSources: ["'self'", "'unsafe-inline'"],
+ // Includes `data:` so that an inline image can be used for the favicon.
+ // If you do not use the `emoji` or `blank` favicons in your app, and you
+ // do not load any other images as data URIs, you can remove this directive.
+ imageSources: ["'self'", 'data:'],
+ // Don't allow this page to be rendered as a frame from a different origin.
+ frameAncestors: false,
+ // Ensure that all requests made by this page are made over https, unless
+ // it is being served over http (typically, during local development)
+ upgradeInsecureRequests: isHttps,
+ }),
+
+ // Sets a strict permissions policy for this page, which limits access
+ // to some native browser features.
+ //
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
+ 'Permissions-Policy': permissionsPolicyHeader({
+ // Disables Google's Federated Learning of Cohorts ("FLoC") tracking initiative.
+ // @see https://www.eff.org/deeplinks/2021/03/googles-floc-terrible-idea
+ interestCohort: false,
+ // Don't use synchronous XHRs!
+ // @see https://featurepolicy.info/policies/sync-xhr
+ syncXhr: false,
+ // Disables access to a few device APIs that are infrequently used
+ // and prone to abuse. If your application uses these APIs intentionally,
+ // feel free to remove the prop, or pass an array containing the origins
+ // that should be allowed to use this feature (e.g., `['self']` to allow
+ // only the main page's origin).
+ camera: false,
+ microphone: false,
+ geolocation: false,
+ }),
+
+ // Instructs browsers to only load this page over HTTPS.
+ //
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
+ 'Strict-Transport-Security': strictTransportSecurityHeader(),
+ },
});
return response;
diff --git a/packages/create/templates/app-empty/server.tsx b/packages/create/templates/app-empty/server.tsx
index 58c8dd2b6..bd4203884 100644
--- a/packages/create/templates/app-empty/server.tsx
+++ b/packages/create/templates/app-empty/server.tsx
@@ -1,4 +1,5 @@
import '@quilted/quilt/globals';
+
import {RequestRouter} from '@quilted/quilt/request-router';
import {renderAppToHTMLResponse} from '@quilted/quilt/server';
import {BrowserAssets} from 'quilt:module/assets';
@@ -8,7 +9,7 @@ import {App} from './App.tsx';
const router = new RequestRouter();
const assets = new BrowserAssets();
-// For all GET requests, render our React application.
+// For all GET requests, render our Preact application.
router.get(async (request) => {
const response = await renderAppToHTMLResponse(, {
request,
diff --git a/packages/create/templates/app-graphql/App.tsx b/packages/create/templates/app-graphql/App.tsx
index 1008cbbc5..36f271eb3 100644
--- a/packages/create/templates/app-graphql/App.tsx
+++ b/packages/create/templates/app-graphql/App.tsx
@@ -5,7 +5,7 @@ import {GraphQLContext} from '@quilted/quilt/graphql';
import {Navigation} from '@quilted/quilt/navigation';
import {Localization} from '@quilted/quilt/localize';
-import {HTML} from './foundation/html.ts';
+import {Head} from './foundation/html.ts';
import {Frame} from './foundation/frame.ts';
import {Home, homeQuery} from './features/home.ts';
@@ -42,9 +42,8 @@ const routes = [
export function App({context}: AppProps) {
return (
-
-
-
+
+
);
}
diff --git a/packages/create/templates/app-graphql/foundation/html.ts b/packages/create/templates/app-graphql/foundation/html.ts
index 4b307d8bc..af771dbeb 100644
--- a/packages/create/templates/app-graphql/foundation/html.ts
+++ b/packages/create/templates/app-graphql/foundation/html.ts
@@ -1 +1 @@
-export {HTML} from './html/HTML.tsx';
+export {Head} from './html/Head.tsx';
diff --git a/packages/create/templates/app-graphql/foundation/html/HTML.test.tsx b/packages/create/templates/app-graphql/foundation/html/HTML.test.tsx
deleted file mode 100644
index 575022a53..000000000
--- a/packages/create/templates/app-graphql/foundation/html/HTML.test.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import {describe, it, expect} from 'vitest';
-import {
- CacheControl,
- ContentSecurityPolicy,
- SearchRobots,
- Viewport,
-} from '@quilted/quilt/server';
-
-import {renderApp} from '~/tests/render.ts';
-
-import {HTML} from './HTML.tsx';
-
-describe('
', () => {
- it('includes a responsive viewport tag', async () => {
- const head = await renderApp(
);
-
- expect(headers).toContainPreactComponent(ContentSecurityPolicy, {
- defaultSources: ["'self'"],
- });
- });
-});
diff --git a/packages/create/templates/app-graphql/foundation/html/HTML.tsx b/packages/create/templates/app-graphql/foundation/html/HTML.tsx
deleted file mode 100644
index 383892ed1..000000000
--- a/packages/create/templates/app-graphql/foundation/html/HTML.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-import type {RenderableProps} from 'preact';
-import Env from 'quilt:module/env';
-import {Title, Favicon, useBrowserRequest} from '@quilted/quilt/browser';
-import {
- CacheControl,
- ResponseHeader,
- ContentSecurityPolicy,
- PermissionsPolicy,
- SearchRobots,
- StrictTransportSecurity,
- Viewport,
-} from '@quilted/quilt/server';
-
-// This component sets details of the HTML page. If you need to customize
-// any of these details based on conditions like the active route, or some
-// state about the user, you can move these components to wherever in your
-// application you can read that state.
-//
-// @see https://github.com/lemonmade/quilt/blob/main/documentation/features/html.md
-export function HTML({children}: RenderableProps<{}>) {
- return (
- <>
-
-
- {children}
- >
- );
-}
-
-function Headers() {
- const {url} = useBrowserRequest();
- const isHttps = new URL(url).protocol === 'https:';
-
- return (
- <>
- {/*
- * Disables the cache for this page, which is generally the best option
- * when dealing with authenticated content. If your site doesn’t have
- * authentication, or you have a better cache policy that works for your
- * app or deployment, make sure to update this component accordingly!
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
- */}
-
-
- {/*
- * Sets a strict content security policy for this page. If you load
- * assets from other origins, or want to allow some more dangerous
- * resource loading techniques like `eval`, you can change the
- * `defaultSources` to be less restrictive, or add additional items
- * to the allowlist for more specific directives.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
- */}
-
-
- {/*
- * Sets a strict permissions policy for this page, which limits access
- * to some native browser features.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
- */}
-
-
- {/*
- * Instructs browsers to only load this page over HTTPS using the
- * `Strict-Transport-Security` header.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
- */}
- {isHttps && }
-
- {/*
- * Controls how much information about the current page is included in
- * requests (through the `Referer` header). The default value
- * (strict-origin-when-cross-origin) means that only the origin is included
- * for cross-origin requests, while the origin, path, and querystring
- * are included for same-origin requests.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
- */}
-
-
- {/*
- * Instructs browsers to respect the MIME type in the `Content-Type` header.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
- */}
-
- >
- );
-}
-
-function Head() {
- return (
- <>
- {/* Sets the default `
` for this application. */}
- App
-
- {/*
- * Sets the default favicon used by the application. You can
- * change this to a different emoji, make it `blank`, or pass
- * a URL with the `source` prop.
- */}
-
-
- {/* Adds a responsive-friendly `viewport` `` tag. */}
-
-
- {/*
- * Disables all search indexing for this application. If you are
- * building an unauthenticated app, you probably want to remove
- * this component, or update it to control how your site is indexed
- * by search engines.
- */}
-
- >
- );
-}
diff --git a/packages/create/templates/app-graphql/foundation/html/Head.test.tsx b/packages/create/templates/app-graphql/foundation/html/Head.test.tsx
new file mode 100644
index 000000000..a02800340
--- /dev/null
+++ b/packages/create/templates/app-graphql/foundation/html/Head.test.tsx
@@ -0,0 +1,42 @@
+import {describe, it, expect} from 'vitest';
+import {Title, Favicon} from '@quilted/quilt/browser';
+import {SearchRobots, Viewport} from '@quilted/quilt/server';
+
+import {renderApp} from '~/tests/render.ts';
+
+import {Head} from './Head.tsx';
+
+describe('
', () => {
+ it('sets the default title', async () => {
+ const head = await renderApp(
` details of the HTML page. If you need
+// to customize any of these details based on conditions or application data, you
+// can render these same components deeper in the application to override these defaults.
+//
+// @see https://github.com/lemonmade/quilt/blob/main/documentation/features/html.md
+export function Head() {
+ return (
+ <>
+ {/* Sets the default `
` for this application. */}
+ App
+
+ {/*
+ * Sets the default favicon used by the application. You can
+ * change this to a different emoji, make it `blank`, or pass
+ * a URL with the `source` prop.
+ */}
+
+
+ {/* Adds a responsive-friendly `viewport` `` tag. */}
+
+
+ {/*
+ * Disables all search indexing for this application. If you are
+ * building an unauthenticated app, you probably want to remove
+ * this component, or update it to control how your site is indexed
+ * by search engines.
+ */}
+
+ >
+ );
+}
diff --git a/packages/create/templates/app-graphql/server.tsx b/packages/create/templates/app-graphql/server.tsx
index 9515aa720..65966b4c8 100644
--- a/packages/create/templates/app-graphql/server.tsx
+++ b/packages/create/templates/app-graphql/server.tsx
@@ -2,6 +2,15 @@ import '@quilted/quilt/globals';
import {RequestRouter, JSONResponse} from '@quilted/quilt/request-router';
import {Router} from '@quilted/quilt/navigation';
+import {
+ renderAppToHTMLResponse,
+ cacheControlHeader,
+ contentSecurityPolicyHeader,
+ permissionsPolicyHeader,
+ strictTransportSecurityHeader,
+} from '@quilted/quilt/server';
+
+import Env from 'quilt:module/env';
import {BrowserAssets} from 'quilt:module/assets';
import type {AppContext} from '~/shared/context.ts';
@@ -22,18 +31,12 @@ router.post('/api/graphql', async (request) => {
return new JSONResponse(result);
});
-// For all GET requests, render our React application.
+// For all GET requests, render our Preact application.
router.get(async (request) => {
- const [
- {App},
- {performGraphQLOperation},
- {GraphQLCache},
- {renderAppToHTMLResponse},
- ] = await Promise.all([
+ const [{App}, {performGraphQLOperation}, {GraphQLCache}] = await Promise.all([
import('./App.tsx'),
import('./server/graphql.ts'),
import('@quilted/quilt/graphql'),
- import('@quilted/quilt/server'),
]);
const context = {
@@ -44,9 +47,86 @@ router.get(async (request) => {
},
} satisfies AppContext;
+ const isHttps = request.url.startsWith('https://');
+
const response = await renderAppToHTMLResponse(, {
request,
assets,
+ headers: {
+ // Controls how much information about the current page is included in
+ // requests (through the `Referer` header). The default value
+ // (strict-origin-when-cross-origin) means that only the origin is included
+ // for cross-origin requests, while the origin, path, and querystring
+ // are included for same-origin requests.
+ //
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
+ 'Referrer-Policy': 'strict-origin-when-cross-origin',
+
+ // Disables the cache for this page, which is generally the best option
+ // when dealing with authenticated content. If your site doesn't have
+ // authentication, or you have a better cache policy that works for your
+ // app or deployment, make sure to update this component accordingly!
+ //
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
+ 'Cache-Control': cacheControlHeader({
+ cache: false,
+ }),
+
+ // Sets a strict content security policy for this page. If you load
+ // assets from other origins, or want to allow some more dangerous
+ // resource loading techniques like `eval`, you can change the
+ // `defaultSources` to be less restrictive, or add additional items
+ // to the allowlist for more specific directives.
+ //
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
+ 'Content-Security-Policy': contentSecurityPolicyHeader({
+ // By default, only allow sources from the page's origin.
+ defaultSources: ["'self'"],
+ // In development, allow connections to local websockets for hot reloading.
+ connectSources:
+ Env.MODE === 'development'
+ ? ["'self'", `${isHttps ? 'ws' : 'wss'}://localhost:*`]
+ : undefined,
+ // Includes `'unsafe-inline'` because CSS is often necessary in development,
+ // and can be difficult to avoid in production.
+ styleSources: ["'self'", "'unsafe-inline'"],
+ // Includes `data:` so that an inline image can be used for the favicon.
+ // If you do not use the `emoji` or `blank` favicons in your app, and you
+ // do not load any other images as data URIs, you can remove this directive.
+ imageSources: ["'self'", 'data:'],
+ // Don't allow this page to be rendered as a frame from a different origin.
+ frameAncestors: false,
+ // Ensure that all requests made by this page are made over https, unless
+ // it is being served over http (typically, during local development)
+ upgradeInsecureRequests: isHttps,
+ }),
+
+ // Sets a strict permissions policy for this page, which limits access
+ // to some native browser features.
+ //
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
+ 'Permissions-Policy': permissionsPolicyHeader({
+ // Disables Google's Federated Learning of Cohorts ("FLoC") tracking initiative.
+ // @see https://www.eff.org/deeplinks/2021/03/googles-floc-terrible-idea
+ interestCohort: false,
+ // Don't use synchronous XHRs!
+ // @see https://featurepolicy.info/policies/sync-xhr
+ syncXhr: false,
+ // Disables access to a few device APIs that are infrequently used
+ // and prone to abuse. If your application uses these APIs intentionally,
+ // feel free to remove the prop, or pass an array containing the origins
+ // that should be allowed to use this feature (e.g., `['self']` to allow
+ // only the main page's origin).
+ camera: false,
+ microphone: false,
+ geolocation: false,
+ }),
+
+ // Instructs browsers to only load this page over HTTPS.
+ //
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
+ 'Strict-Transport-Security': strictTransportSecurityHeader(),
+ },
});
return response;
diff --git a/packages/create/templates/app-trpc/App.tsx b/packages/create/templates/app-trpc/App.tsx
index 0de1dbf02..f0f42b3f6 100644
--- a/packages/create/templates/app-trpc/App.tsx
+++ b/packages/create/templates/app-trpc/App.tsx
@@ -6,7 +6,7 @@ import {Localization} from '@quilted/quilt/localize';
import {ReactQueryContext} from '@quilted/react-query';
-import {HTML} from './foundation/html.ts';
+import {Head} from './foundation/html.ts';
import {Frame} from './foundation/frame.ts';
import {Home} from './features/home.ts';
@@ -44,9 +44,8 @@ const routes = [
export function App({context}: AppProps) {
return (
-
-
-
+
+
);
}
diff --git a/packages/create/templates/app-trpc/foundation/html.ts b/packages/create/templates/app-trpc/foundation/html.ts
index 4b307d8bc..af771dbeb 100644
--- a/packages/create/templates/app-trpc/foundation/html.ts
+++ b/packages/create/templates/app-trpc/foundation/html.ts
@@ -1 +1 @@
-export {HTML} from './html/HTML.tsx';
+export {Head} from './html/Head.tsx';
diff --git a/packages/create/templates/app-trpc/foundation/html/HTML.test.tsx b/packages/create/templates/app-trpc/foundation/html/HTML.test.tsx
deleted file mode 100644
index 575022a53..000000000
--- a/packages/create/templates/app-trpc/foundation/html/HTML.test.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import {describe, it, expect} from 'vitest';
-import {
- CacheControl,
- ContentSecurityPolicy,
- SearchRobots,
- Viewport,
-} from '@quilted/quilt/server';
-
-import {renderApp} from '~/tests/render.ts';
-
-import {HTML} from './HTML.tsx';
-
-describe('
', () => {
- it('includes a responsive viewport tag', async () => {
- const head = await renderApp(
);
-
- expect(headers).toContainPreactComponent(ContentSecurityPolicy, {
- defaultSources: ["'self'"],
- });
- });
-});
diff --git a/packages/create/templates/app-trpc/foundation/html/HTML.tsx b/packages/create/templates/app-trpc/foundation/html/HTML.tsx
deleted file mode 100644
index 383892ed1..000000000
--- a/packages/create/templates/app-trpc/foundation/html/HTML.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-import type {RenderableProps} from 'preact';
-import Env from 'quilt:module/env';
-import {Title, Favicon, useBrowserRequest} from '@quilted/quilt/browser';
-import {
- CacheControl,
- ResponseHeader,
- ContentSecurityPolicy,
- PermissionsPolicy,
- SearchRobots,
- StrictTransportSecurity,
- Viewport,
-} from '@quilted/quilt/server';
-
-// This component sets details of the HTML page. If you need to customize
-// any of these details based on conditions like the active route, or some
-// state about the user, you can move these components to wherever in your
-// application you can read that state.
-//
-// @see https://github.com/lemonmade/quilt/blob/main/documentation/features/html.md
-export function HTML({children}: RenderableProps<{}>) {
- return (
- <>
-
-
- {children}
- >
- );
-}
-
-function Headers() {
- const {url} = useBrowserRequest();
- const isHttps = new URL(url).protocol === 'https:';
-
- return (
- <>
- {/*
- * Disables the cache for this page, which is generally the best option
- * when dealing with authenticated content. If your site doesn’t have
- * authentication, or you have a better cache policy that works for your
- * app or deployment, make sure to update this component accordingly!
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
- */}
-
-
- {/*
- * Sets a strict content security policy for this page. If you load
- * assets from other origins, or want to allow some more dangerous
- * resource loading techniques like `eval`, you can change the
- * `defaultSources` to be less restrictive, or add additional items
- * to the allowlist for more specific directives.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
- */}
-
-
- {/*
- * Sets a strict permissions policy for this page, which limits access
- * to some native browser features.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
- */}
-
-
- {/*
- * Instructs browsers to only load this page over HTTPS using the
- * `Strict-Transport-Security` header.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
- */}
- {isHttps && }
-
- {/*
- * Controls how much information about the current page is included in
- * requests (through the `Referer` header). The default value
- * (strict-origin-when-cross-origin) means that only the origin is included
- * for cross-origin requests, while the origin, path, and querystring
- * are included for same-origin requests.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
- */}
-
-
- {/*
- * Instructs browsers to respect the MIME type in the `Content-Type` header.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
- */}
-
- >
- );
-}
-
-function Head() {
- return (
- <>
- {/* Sets the default `
` for this application. */}
- App
-
- {/*
- * Sets the default favicon used by the application. You can
- * change this to a different emoji, make it `blank`, or pass
- * a URL with the `source` prop.
- */}
-
-
- {/* Adds a responsive-friendly `viewport` `` tag. */}
-
-
- {/*
- * Disables all search indexing for this application. If you are
- * building an unauthenticated app, you probably want to remove
- * this component, or update it to control how your site is indexed
- * by search engines.
- */}
-
- >
- );
-}
diff --git a/packages/create/templates/app-trpc/foundation/html/Head.test.tsx b/packages/create/templates/app-trpc/foundation/html/Head.test.tsx
new file mode 100644
index 000000000..a02800340
--- /dev/null
+++ b/packages/create/templates/app-trpc/foundation/html/Head.test.tsx
@@ -0,0 +1,42 @@
+import {describe, it, expect} from 'vitest';
+import {Title, Favicon} from '@quilted/quilt/browser';
+import {SearchRobots, Viewport} from '@quilted/quilt/server';
+
+import {renderApp} from '~/tests/render.ts';
+
+import {Head} from './Head.tsx';
+
+describe('
', () => {
+ it('sets the default title', async () => {
+ const head = await renderApp(
` details of the HTML page. If you need
+// to customize any of these details based on conditions or application data, you
+// can render these same components deeper in the application to override these defaults.
+//
+// @see https://github.com/lemonmade/quilt/blob/main/documentation/features/html.md
+export function Head() {
+ return (
+ <>
+ {/* Sets the default `
` for this application. */}
+ App
+
+ {/*
+ * Sets the default favicon used by the application. You can
+ * change this to a different emoji, make it `blank`, or pass
+ * a URL with the `source` prop.
+ */}
+
+
+ {/* Adds a responsive-friendly `viewport` `` tag. */}
+
+
+ {/*
+ * Disables all search indexing for this application. If you are
+ * building an unauthenticated app, you probably want to remove
+ * this component, or update it to control how your site is indexed
+ * by search engines.
+ */}
+
+ >
+ );
+}
diff --git a/packages/create/templates/app-trpc/server.tsx b/packages/create/templates/app-trpc/server.tsx
index c0a9ac477..76ed90563 100644
--- a/packages/create/templates/app-trpc/server.tsx
+++ b/packages/create/templates/app-trpc/server.tsx
@@ -1,5 +1,16 @@
import '@quilted/quilt/globals';
+
import {RequestRouter} from '@quilted/quilt/request-router';
+import {Router} from '@quilted/quilt/navigation';
+import {
+ renderAppToHTMLResponse,
+ cacheControlHeader,
+ contentSecurityPolicyHeader,
+ permissionsPolicyHeader,
+ strictTransportSecurityHeader,
+} from '@quilted/quilt/server';
+
+import Env from 'quilt:module/env';
import {BrowserAssets} from 'quilt:module/assets';
import {createDirectClient} from '@quilted/trpc/server';
@@ -27,13 +38,10 @@ router.any(
// For all GET requests, render our React application.
router.get(async (request) => {
- const [{App}, {renderAppToHTMLResponse}, {Router}, {QueryClient}] =
- await Promise.all([
- import('./App.tsx'),
- import('@quilted/quilt/server'),
- import('@quilted/quilt/navigation'),
- import('@tanstack/react-query'),
- ]);
+ const [{App}, {QueryClient}] = await Promise.all([
+ import('./App.tsx'),
+ import('@tanstack/react-query'),
+ ]);
const context = {
router: new Router(request.url),
@@ -41,9 +49,86 @@ router.get(async (request) => {
queryClient: new QueryClient(),
} satisfies AppContext;
+ const isHttps = request.url.startsWith('https://');
+
const response = await renderAppToHTMLResponse(, {
request,
assets,
+ headers: {
+ // Controls how much information about the current page is included in
+ // requests (through the `Referer` header). The default value
+ // (strict-origin-when-cross-origin) means that only the origin is included
+ // for cross-origin requests, while the origin, path, and querystring
+ // are included for same-origin requests.
+ //
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
+ 'Referrer-Policy': 'strict-origin-when-cross-origin',
+
+ // Disables the cache for this page, which is generally the best option
+ // when dealing with authenticated content. If your site doesn't have
+ // authentication, or you have a better cache policy that works for your
+ // app or deployment, make sure to update this component accordingly!
+ //
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
+ 'Cache-Control': cacheControlHeader({
+ cache: false,
+ }),
+
+ // Sets a strict content security policy for this page. If you load
+ // assets from other origins, or want to allow some more dangerous
+ // resource loading techniques like `eval`, you can change the
+ // `defaultSources` to be less restrictive, or add additional items
+ // to the allowlist for more specific directives.
+ //
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
+ 'Content-Security-Policy': contentSecurityPolicyHeader({
+ // By default, only allow sources from the page's origin.
+ defaultSources: ["'self'"],
+ // In development, allow connections to local websockets for hot reloading.
+ connectSources:
+ Env.MODE === 'development'
+ ? ["'self'", `${isHttps ? 'ws' : 'wss'}://localhost:*`]
+ : undefined,
+ // Includes `'unsafe-inline'` because CSS is often necessary in development,
+ // and can be difficult to avoid in production.
+ styleSources: ["'self'", "'unsafe-inline'"],
+ // Includes `data:` so that an inline image can be used for the favicon.
+ // If you do not use the `emoji` or `blank` favicons in your app, and you
+ // do not load any other images as data URIs, you can remove this directive.
+ imageSources: ["'self'", 'data:'],
+ // Don't allow this page to be rendered as a frame from a different origin.
+ frameAncestors: false,
+ // Ensure that all requests made by this page are made over https, unless
+ // it is being served over http (typically, during local development)
+ upgradeInsecureRequests: isHttps,
+ }),
+
+ // Sets a strict permissions policy for this page, which limits access
+ // to some native browser features.
+ //
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
+ 'Permissions-Policy': permissionsPolicyHeader({
+ // Disables Google's Federated Learning of Cohorts ("FLoC") tracking initiative.
+ // @see https://www.eff.org/deeplinks/2021/03/googles-floc-terrible-idea
+ interestCohort: false,
+ // Don't use synchronous XHRs!
+ // @see https://featurepolicy.info/policies/sync-xhr
+ syncXhr: false,
+ // Disables access to a few device APIs that are infrequently used
+ // and prone to abuse. If your application uses these APIs intentionally,
+ // feel free to remove the prop, or pass an array containing the origins
+ // that should be allowed to use this feature (e.g., `['self']` to allow
+ // only the main page's origin).
+ camera: false,
+ microphone: false,
+ geolocation: false,
+ }),
+
+ // Instructs browsers to only load this page over HTTPS.
+ //
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
+ 'Strict-Transport-Security': strictTransportSecurityHeader(),
+ },
});
return response;
diff --git a/packages/preact-browser/source/server/hooks/cache-control.ts b/packages/preact-browser/source/server/hooks/cache-control.ts
index 7863a5fb7..09516f0fe 100644
--- a/packages/preact-browser/source/server/hooks/cache-control.ts
+++ b/packages/preact-browser/source/server/hooks/cache-control.ts
@@ -1,81 +1,9 @@
+import {
+ cacheControlHeader,
+ type CacheControlOptions,
+} from '@quilted/browser/server';
import {useBrowserResponseAction} from './browser-response-action.ts';
-/**
- * Options for controlling the Cache-Control header of the current
- * response.
- *
- * @see https://csswizardry.com/2019/03/cache-control-for-civilians/
- */
-export type CacheControlOptions =
- | {
- /**
- * Whether this cache control directive applies only to “private”
- * caches, which is typically the end user’s browser cache for
- * applications using Quilt. When not set, the cache-control directive
- * will be set to `public`, which allows all caches (including CDNs,
- * proxies, and the like) to cache the content.
- */
- private?: true;
- /**
- * Completely disable caching of this response. Passing this option
- * sets the `Cache-Control` header to `no-store`.
- */
- cache: false;
- maxAge?: never;
- immutable?: never;
- revalidate?: never;
- }
- | {
- /**
- * Whether this cache control directive applies only to “private”
- * caches, which is typically the end user’s browser cache for
- * applications using Quilt. When not set, the cache-control directive
- * will be set to `public`, which allows all caches (including CDNs,
- * proxies, and the like) to cache the content.
- */
- private?: true;
- cache?: never;
- /**
- * The number of seconds, from the time of this request, that
- * the content is considered “fresh”.
- */
- maxAge?: number;
- immutable?: never;
- /**
- * Controls how clients should revalidate their cached content.
- * If this option is set to `true`, the resulting `Cache-Control`
- * header will have the `must-revalidate` directive, which asks
- * clients to revalidate their content after the `maxAge` period
- * has expired.
- *
- * If you instead pass `{allowStale: number}`, the resulting
- * `Cache-Control` header will have the `stale-while-revalidate`
- * directive, which allows caches that support it to use the stale
- * version from the cache while they perform revalidation in the
- * background.
- */
- revalidate?: boolean | {allowStale: number};
- }
- | {
- /**
- * Whether this cache control directive applies only to “private”
- * caches, which is typically the end user’s browser cache for
- * applications using Quilt. When not set, the cache-control directive
- * will be set to `public`, which allows all caches (including CDNs,
- * proxies, and the like) to cache the content.
- */
- private?: true;
- cache?: never;
- maxAge?: number;
- /**
- * Declares that this request is immutable. This means that it is
- * assumed to never change, and clients will never attempt to
- * revalidate the content. Be careful when using this option!
- */
- immutable?: boolean;
- revalidate?: never;
- };
-
/**
* A hook to set the `Cache-Control` header on the response. If you provide
* a string, this value will be used directly as the header value. Alternatively,
@@ -91,37 +19,7 @@ export function useCacheControl(value: string | CacheControlOptions) {
if (typeof value === 'string') {
normalizedValue = value;
} else {
- const {
- private: isPrivate,
- cache,
- immutable,
- maxAge = immutable ? 31536000 : undefined,
- revalidate,
- } = value;
-
- normalizedValue = isPrivate ? 'private' : 'public';
-
- const appendToHeader = (value: string) => {
- normalizedValue = `${normalizedValue}, ${value}`;
- };
-
- if (cache === false) {
- appendToHeader('no-store');
- }
-
- if (maxAge === 0 && revalidate === true) {
- appendToHeader('no-cache');
- } else if (typeof maxAge === 'number' || revalidate) {
- appendToHeader(`max-age=${maxAge ?? 0}`);
-
- if (revalidate === true) {
- appendToHeader('must-revalidate');
- } else if (typeof revalidate === 'object') {
- appendToHeader(`stale-while-revalidate=${revalidate.allowStale}`);
- }
- }
-
- if (immutable) appendToHeader('immutable');
+ normalizedValue = cacheControlHeader(value);
}
response.headers.append('Cache-Control', normalizedValue);
diff --git a/packages/preact-browser/source/server/hooks/content-security-policy.ts b/packages/preact-browser/source/server/hooks/content-security-policy.ts
index 5ab319ab4..48f3892b8 100644
--- a/packages/preact-browser/source/server/hooks/content-security-policy.ts
+++ b/packages/preact-browser/source/server/hooks/content-security-policy.ts
@@ -1,7 +1,7 @@
-import type {
- ContentSecurityPolicySandboxAllow,
- ContentSecurityPolicySpecialSource,
-} from '@quilted/http';
+import {
+ contentSecurityPolicyHeader,
+ type ContentSecurityPolicyOptions,
+} from '@quilted/browser/server';
import {useBrowserResponseAction} from './browser-response-action.ts';
@@ -10,320 +10,8 @@ import {useBrowserResponseAction} from './browser-response-action.ts';
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
*/
-export interface ContentSecurityPolicyOptions {
- /**
- * Sets the child-src content security policy directive, which determines what sources
- * are allowed for nested browsing contexts, including workers and iframes.
- *
- * Passing `false` or an empty array is equivalent to passing `["'none'"]`, disallowing
- * all sources.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/child-src
- */
- childSources?: false | (ContentSecurityPolicySpecialSource | string)[];
-
- /**
- * Sets the connect-src content security policy directive, which determines what sources
- * can be connected to from script sources (for example, when using `fetch()`).
- *
- * Passing `false` or an empty array is equivalent to passing `["'none'"]`, disallowing
- * all sources.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src
- */
- connectSources?: (ContentSecurityPolicySpecialSource | string)[];
-
- /**
- * Sets the default-src content security policy directive, which determines what sources
- * are allowed for resources where a more specific policy has not been set.
- *
- * Passing `false` or an empty array is equivalent to passing `["'none'"]`, disallowing
- * all sources.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src
- */
- defaultSources?: false | (ContentSecurityPolicySpecialSource | string)[];
-
- /**
- * Sets the font-src content security policy directive, which determines what sources
- * are allowed for fonts used on this page.
- *
- * Passing `false` or an empty array is equivalent to passing `["'none'"]`, disallowing
- * all sources.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/font-src
- */
- fontSources?: false | (ContentSecurityPolicySpecialSource | string)[];
-
- /**
- * Sets the frame-src content security policy directive, which determines what sources
- * are allowed for frames embedded in this page.
- *
- * Passing `false` or an empty array is equivalent to passing `["'none'"]`, disallowing
- * all sources.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src
- */
- frameSources?: false | (ContentSecurityPolicySpecialSource | string)[];
-
- /**
- * Sets the img-src content security policy directive, which determines what sources
- * are allowed for images.
- *
- * Passing `false` or an empty array is equivalent to passing `["'none'"]`, disallowing
- * all sources.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/img-src
- */
- imageSources?: false | (ContentSecurityPolicySpecialSource | string)[];
-
- /**
- * Sets the manifest-src content security policy directive, which determines what sources
- * are allowed for the app manifest for this page.
- *
- * Passing `false` or an empty array is equivalent to passing `["'none'"]`, disallowing
- * all sources.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/manifest-src
- */
- manifestSources?: false | (ContentSecurityPolicySpecialSource | string)[];
-
- /**
- * Sets the media-src content security policy directive, which determines what sources
- * are allowed for audio and video.
- *
- * Passing `false` or an empty array is equivalent to passing `["'none'"]`, disallowing
- * all sources.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/media-src
- */
- mediaSources?: false | (ContentSecurityPolicySpecialSource | string)[];
-
- /**
- * Sets the object-src content security policy directive, which determines what sources
- * are allowed for `