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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ dist/
.astro/
node_modules/
public/pagefind/
public/vendor/

# Generated binary/text assets
public/og/
Expand Down
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default [
".astro/",
"node_modules/",
"public/pagefind/",
"public/vendor/",
"mcp/",
".github/skills/",
".claude/",
Expand Down
65 changes: 17 additions & 48 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@astrojs/rss": "^4.0.18",
"@tailwindcss/vite": "^4.3.1",
"astro": "^6.4.8",
"dompurify": "^3.4.11",
"tailwindcss": "^4.3.0"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions public/_headers
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), interest-cohort=()
Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' 'sha256-N74AzU+1FxvXAWIxrP2zNCBUxV949ZHOXXqjTvbusx0=' 'sha256-UU9xsfeOKmx3D7Lk33alkWn1rIjk46pD684u4pupy4o=' https://plausible.io; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self' https://plausible.io; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'; report-to csp-endpoint; upgrade-insecure-requests
Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; trusted-types default dompurify; report-to csp-endpoint
Reporting-Endpoints: csp-endpoint="/reports", default="/reports"
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-site
Expand Down
52 changes: 52 additions & 0 deletions public/trusted-types-policy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Trusted Types default policy.
//
// Worked example for /spec/security/trusted-types/. The site's CSP can carry
// require-trusted-types-for 'script'; trusted-types default dompurify
// and, once enforcing, every string assigned to a DOM injection sink must be a
// trusted typed value or the browser throws.
//
// Our own scripts touch no sinks, but the Pagefind search bundle does, in two ways:
// - pagefind-ui.js builds its results list with innerHTML (TrustedHTML sink).
// - pagefind.js loads its own JS/WASM chunks by assigning a script URL
// (TrustedScriptURL sink).
// A *default* policy is the only thing that can cover Pagefind, because its
// bundled code assigns raw strings/URLs and cannot opt into a named policy itself.
// So the default policy implements both createHTML and createScriptURL.
//
// (DOMPurify registers its own policy named "dompurify"; the trusted-types
// allowlist in _headers names it too.)
(function () {
if (!window.trustedTypes || !window.trustedTypes.createPolicy) return; // unsupported browser: nothing to enforce
if (typeof window.DOMPurify === "undefined") {
// Fail closed and loud rather than registering an unsafe pass-through.
console.error(
"[trusted-types] DOMPurify not loaded; default policy not registered.",
);
return;
}
try {
window.trustedTypes.createPolicy("default", {
// HTML sinks (Pagefind results UI): sanitise, keeping its a/p/mark/list markup.
createHTML: function (input) {
return window.DOMPurify.sanitize(input);
},
// Script-URL sinks (Pagefind loading its own JS/WASM): allow same-origin
// URLs only. script-src 'self' is the backstop; this just stops a
// cross-origin URL ever reaching a script-loading sink.
createScriptURL: function (input) {
try {
if (new URL(input, document.baseURI).origin === location.origin) {
return input;
}
} catch {
/* malformed URL — fall through to the throw below */
}
throw new TypeError("[trusted-types] blocked script URL: " + input);
},
});
} catch (e) {
// createPolicy throws if a "default" policy already exists or the name is
// not in the trusted-types allowlist. Never break the page over it.
console.error("[trusted-types] default policy registration failed:", e);
}
})();
3 changes: 3 additions & 0 deletions public/vendor/purify.min.js

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions scripts/generate-assets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -618,4 +618,19 @@ console.log(
` wrote ${marketingPages.length} marketing OG images → /og/<slug>.png`,
);

// Vendor third-party runtime libraries we want self-hosted (script-src 'self').
// DOMPurify backs the Trusted Types default policy (see public/trusted-types-policy.js)
// so the strict CSP can require trusted values without pulling in a remote script.
const vendorOut = join(out, "vendor");
await mkdir(vendorOut, { recursive: true });
const dompurifySrc = join(
root,
"node_modules",
"dompurify",
"dist",
"purify.min.js",
);
await writeFile(join(vendorOut, "purify.min.js"), await readFile(dompurifySrc));
console.log(" vendored DOMPurify → /vendor/purify.min.js");

console.log("Done.");
8 changes: 8 additions & 0 deletions src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ const finalJsonLd = jsonLd
pattern documented in /spec/foundations/color-scheme/. */
}
<script is:inline>(function(){try{var t=localStorage.getItem("theme");if(t!=="light"&&t!=="dark")t=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";document.documentElement.setAttribute("data-theme",t);var m=document.createElement("meta");m.name="theme-color";m.id="theme-color-meta";m.content=t==="dark"?"#0e0e13":"#15803d";document.head.appendChild(m)}catch(e){}})();</script>
{
/* Trusted Types default policy, registered before any other script so it
exists before Pagefind's search UI mounts. DOMPurify (self-hosted, vendored
by scripts/generate-assets.mjs) backs it. Worked example for
/spec/security/trusted-types/. Both are script-src 'self'. */
}
<script is:inline src="/vendor/purify.min.js"></script>
<script is:inline src="/trusted-types-policy.js"></script>
<HeadMeta
title={title}
description={description}
Expand Down
Loading