Skip to content

Commit 5f4902d

Browse files
committed
Put the build page's policy first, and fix the review's other findings
- base.njk writes a page's Content-Security-Policy first in <head>, before the inline color-scheme script, and an Eleventy transform adds each inline script's hash to script-src, so the policy covers every script on the page. - The test PNG's IDAT failed its CRC and zlib check; it is a valid 2x2 red PNG now, and the test checks the picture reaches the content. - The README describes embed-my's /build redirect as a step of its own, and says the bundles test runs unzip, as the skill's steps do.
1 parent 78d32c9 commit 5f4902d

5 files changed

Lines changed: 27 additions & 9 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pnpm e2e # the build page and the bundles in a browser, against _site/: bu
2424
# per machine: pnpm exec playwright install chromium
2525
```
2626

27-
`pnpm dev` serves the pages as Eleventy writes them; the build page's script is there only after a `pnpm build`.
27+
`pnpm dev` serves the pages as Eleventy writes them; the build page's script is there only after a `pnpm build`. The bundles test unzips a bundle with `unzip`, as the h5p-author skill's steps do: macOS and Linux have it, and so does CI's Ubuntu runner; on Windows, run the tests under WSL.
2828

2929
Pushes to `main` deploy through [.github/workflows/pages.yml](.github/workflows/pages.yml). Pull requests get a build check from [.github/workflows/build.yml](.github/workflows/build.yml), which runs the browser tests too.
3030

@@ -66,7 +66,7 @@ Nothing leaves the browser. A link can carry the source in its fragment, which n
6666

6767
The player's Service Worker is in `/h5p/assets/` too, so its scope is `/h5p/assets/h5p/`: no other page of the site is under it. The address names who runs the page, as H5P Group's trademark policy asks of a name with "H5P" in it, and the page uses the word descriptively.
6868

69-
The page and the bundles were embed-my.github.io's `/build` and `/bundles/` until 2026-11-10. `/build` there now redirects here with its fragment, so the links chats wrote before still build.
69+
The page and the bundles were embed-my.github.io's `/build` and `/bundles/` until 2026-11-10. Its `/build` becomes a redirect here that keeps the fragment, so the links chats wrote before still build: [embed-my/embed-my.github.io#18](https://github.com/embed-my/embed-my.github.io/pull/18), a step of its own, deployed once this page is live.
7070

7171
## The bundles
7272

‎e2e/build.spec.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const RIVERS = {
1919
}
2020

2121
/** A 2×2 PNG, red. */
22-
const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAFklEQVR4nGP4z8DAwMDAxMDAwMDAAAANHQEDasKb6QAAAABJRU5ErkJggg==', 'base64')
22+
const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAEElEQVR42mP4z8AARAwQCgAf7gP9Y167WwAAAABJRU5ErkJggg==', 'base64')
2323

2424
/** Collects console errors, CSP reports, page errors and requests that leave the origin. */
2525
function watch(page: Page, baseURL: string | undefined): { problems: string[], elsewhere: string[] } {
@@ -170,5 +170,7 @@ test('a source that names a file waits for it, then builds with it', async ({ pa
170170
await page.locator('#build').click()
171171
await expectPlays(page)
172172
await expect(page.locator('#status')).toContainText('1 media file')
173+
// The picture reaches the content and decodes: 2×2, as chosen.
174+
await expect.poll(() => page.frameLocator('h5p-player iframe').first().locator('img').first().evaluate((img: HTMLImageElement) => img.naturalWidth)).toBe(2)
173175
expect(problems, problems.join('\n')).toEqual([])
174176
})

‎eleventy.config.js‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createHash } from "node:crypto";
12
import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
23
import attributes from "./src/_data/pdfjsAttributes.js";
34

@@ -16,6 +17,19 @@ export default function (eleventyConfig) {
1617
// The docs run the exact package version they describe, served from this site.
1718
eleventyConfig.addPassthroughCopy({ "node_modules/@missing-elements/pdfjs-viewer/dist": "pdfjs-viewer/vendor" });
1819

20+
// A page with a Content-Security-Policy (`csp` in its front matter, written first in <head> by
21+
// base.njk, and holding no double quote) allows its inline scripts by hash, as they are written:
22+
// the color-scheme script has to run before the page paints, so it stays inline.
23+
eleventyConfig.addTransform("csp-inline-script-hashes", function (content) {
24+
if (!(this.page.outputPath || "").endsWith(".html")) return content;
25+
const scriptSrc = /<meta http-equiv="Content-Security-Policy" content="[^"]*?script-src [^;"]*/;
26+
if (!scriptSrc.test(content)) return content;
27+
const hashes = [...content.matchAll(/<script(?![^>]*\ssrc=)[^>]*>([\s\S]*?)<\/script>/g)]
28+
.map(([, source]) => ` 'sha256-${createHash("sha256").update(source).digest("base64")}'`)
29+
.join("");
30+
return content.replace(scriptSrc, (directive) => directive + hashes);
31+
});
32+
1933
let markdown;
2034
eleventyConfig.amendLibrary("md", (md) => {
2135
markdown = md;

‎src/_includes/base.njk‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
<html lang="en">
33
<head>
44
<meta charset="utf-8">
5+
{%- if csp %}
6+
<meta http-equiv="Content-Security-Policy" content="{{ csp | safe }}">
7+
{%- endif %}
58
<meta name="viewport" content="width=device-width, initial-scale=1">
69
<meta name="color-scheme" content="light dark">
710
<script>
811
try { document.querySelector('meta[name="color-scheme"]').content = localStorage.getItem("color-scheme") ?? "light dark"; } catch {}
912
</script>
10-
{%- if csp %}
11-
<meta http-equiv="Content-Security-Policy" content="{{ csp }}">
12-
{%- endif %}
1313
<title>{{ title }}</title>
1414
<meta name="description" content="{{ description }}">
1515
<link rel="canonical" href="{{ site.url }}{{ page.url }}">

‎src/h5p/build/index.njk‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
layout: base.njk
33
title: Build an H5P activity · Missing Elements
44
description: Turn the source an AI chat wrote with the h5p-author skill into a .h5p file, in your browser. Nothing is uploaded.
5-
# The page's only policy: GitHub Pages sends no headers. Packages it built play in the player's frame,
6-
# under the policy the player's Service Worker gives each response there. A file a source names by
7-
# web address is fetched by the build, so any host may be connected to.
5+
# The page's only policy: GitHub Pages sends no headers. base.njk writes it first in <head>, and the
6+
# site's inline color-scheme script is added to script-src by its hash at build (eleventy.config.js).
7+
# Packages it built play in the player's frame, under the policy the player's Service Worker gives
8+
# each response there. A file a source names by web address is fetched by the build, so any host may
9+
# be connected to.
810
csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: blob:; font-src 'self'; connect-src * data: blob:; worker-src 'self' blob:; frame-src 'self' blob:; object-src 'none'; base-uri 'self'"
911
---
1012
{#- The markup build-page.ts wires up, built into /h5p/build/build-page.js by Vite (vite.config.ts). -#}

0 commit comments

Comments
 (0)