@missing-elements/h5p-embed 0.6.2 → 0.7.0

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.
package/README.md CHANGED
@@ -47,14 +47,27 @@ npx @missing-elements/h5p-embed h5p-player \
47
47
 
48
48
  ## Hosting
49
49
 
50
- | Host | Policy and caching |
50
+ Any static host, over https, at a domain's root or under a path. The page carries its own
51
+ policy in a `<meta>` tag, so it is protected wherever it is served. The headers — the same
52
+ policy with `frame-ancestors`, and the caching — are written beside it in the form each kind of
53
+ host reads; use the one yours does and ignore the rest:
54
+
55
+ | Host | File |
56
+ |---|---|
57
+ | A server of your own — Hetzner, IONOS, OVH, Scaleway, a university's machine | `Caddyfile` (run it as it is; Caddy fetches the certificate), `nginx.conf` (include it in the server block), `.htaccess` (Apache with `mod_headers`) |
58
+ | Netlify, Cloudflare Pages | `_headers` |
59
+ | Vercel | `vercel.json` |
60
+ | GitHub Pages, S3 and other object storage | none can set headers: the page's `<meta>` policy applies, `--ancestors` does not, and `h5p-sw.js` is cached by the host's default |
61
+
62
+ The headers themselves, for a host not listed:
63
+
64
+ | Path | Headers |
51
65
  |---|---|
52
- | Netlify, Cloudflare Pages | `_headers`, written beside the page |
53
- | Vercel | `vercel.json`, written beside the page |
54
- | GitHub Pages, S3, nginx, others | a `<meta>` policy in the page; `--ancestors` needs a `Content-Security-Policy: frame-ancestors …` header set on the server |
66
+ | every path | `Content-Security-Policy` (the policy the command prints, with `frame-ancestors` from `--ancestors`), `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin` |
67
+ | `/h5p-sw.js` | `Cache-Control: no-cache`, so an update reaches learners on their next visit |
68
+ | `/frame-assets/*`, `/resizer.js`, `/libraries.h5p` | `Cache-Control: public, max-age=3600, stale-while-revalidate=86400` |
55
69
 
56
- It works at a domain's root or under a path, over https. Serve `h5p-sw.js` with
57
- `Cache-Control: no-cache`. To update, run the command again with `--force` and redeploy.
70
+ To update, run the command again with `--force` and redeploy.
58
71
 
59
72
  ## The address
60
73
 
package/bin/h5p-embed.mjs CHANGED
@@ -84,8 +84,10 @@ subdomain of your site — then embed a package:
84
84
  style="width: 100%; border: 0"></iframe>
85
85
  <script src="https://<player domain>/resizer.js"></script>
86
86
 
87
- _headers (Netlify, Cloudflare Pages) and vercel.json (Vercel) carry the policy and the caching.
88
- Other hosts, GitHub Pages among them, get the policy from the page itself${site.ancestors ? ', without --ancestors,\nwhich only a header can carry' : ''}.`)
87
+ The page carries its policy; the headers beside it are written for five kinds of host:
88
+ Caddyfile, nginx.conf and .htaccess for a server of your own, _headers for Netlify and
89
+ Cloudflare Pages, vercel.json for Vercel. Object storage and GitHub Pages serve the page's own
90
+ policy and no headers${site.ancestors ? ' — there --ancestors has no effect, since only a header\ncan carry it' : ''}.`)
89
91
  } catch (error) {
90
92
  if (error instanceof EmbedError) {
91
93
  console.error(`h5p-embed: ${error.message}`)
package/lib/build.mjs CHANGED
@@ -52,7 +52,7 @@ export function parseOrigin(value) {
52
52
  }
53
53
 
54
54
  /** Where the element's `libraries="pack"` fetches from when a site carries no copy of its own. */
55
- export const PACK_CDN_ORIGIN = 'https://cdn.jsdelivr.net'
55
+ const PACK_CDN_ORIGIN = 'https://cdn.jsdelivr.net'
56
56
 
57
57
  /**
58
58
  * A `libraries` value for addresses that name none, checked against the site it goes into: the
@@ -168,11 +168,87 @@ function vercelConfig(rules) {
168
168
  ) + '\n'
169
169
  }
170
170
 
171
+ /*
172
+ * The three server formats below are for a host of one's own — the European hosts a data
173
+ * protection officer accepts have no `_headers` convention, and a university's Apache or an
174
+ * nginx on a Hetzner box is where the embed page usually ends up. Each is written from the same
175
+ * rules as the two files above, so they cannot disagree. Values are quoted with double quotes
176
+ * in every format, which is safe because the policy carries only single quotes.
177
+ */
178
+
179
+ /** The rule for every path, and the ones for a single path or prefix. */
180
+ function splitRules(rules) {
181
+ const all = rules.find((rule) => rule.path === '/*')
182
+ return { all, specific: rules.filter((rule) => rule !== all) }
183
+ }
184
+
185
+ /** Caddy's `Caddyfile`: a site block to run as it is, or to paste into one. */
186
+ function caddyfile(rules) {
187
+ const { all, specific } = splitRules(rules)
188
+ const lines = [
189
+ '# Written by h5p-embed. Replace the domain and the root, then `caddy run` beside this file,',
190
+ '# or paste the block into your Caddyfile. Caddy fetches the certificate itself.',
191
+ 'h5p-player.example.net {',
192
+ '\troot * /srv/h5p-player',
193
+ '\tfile_server',
194
+ ...Object.entries(all.headers).map(([key, value]) => `\theader ${key} "${value}"`),
195
+ ...specific.flatMap(({ path, headers }) => Object.entries(headers).map(([key, value]) => `\theader ${path} ${key} "${value}"`)),
196
+ '}'
197
+ ]
198
+ return lines.join('\n') + '\n'
199
+ }
200
+
201
+ /**
202
+ * nginx: a file to `include` inside the `server` block that serves the folder. nginx's
203
+ * `add_header` in a `location` replaces every `add_header` inherited from the server level
204
+ * rather than adding to it, so each location repeats the headers that apply everywhere.
205
+ */
206
+ function nginxConf(rules) {
207
+ const { all, specific } = splitRules(rules)
208
+ const header = ([key, value]) => `add_header ${key} "${value}" always;`
209
+ const matcher = (path) => (path.endsWith('/*') ? `^~ ${path.slice(0, -1)}` : `= ${path}`)
210
+ const lines = [
211
+ '# Written by h5p-embed. Include this file inside the server block that serves the folder:',
212
+ '# server { listen 443 ssl; server_name h5p-player.example.net; root /srv/h5p-player; include /srv/h5p-player/nginx.conf; }',
213
+ '# A location repeats the headers set for every path: in nginx an add_header inside a location',
214
+ '# replaces the inherited ones instead of adding to them.',
215
+ ...Object.entries(all.headers).map(header),
216
+ ...specific.map(
217
+ ({ path, headers }) =>
218
+ `location ${matcher(path)} {\n${[...Object.entries(all.headers), ...Object.entries(headers)].map((entry) => `\t${header(entry)}`).join('\n')}\n}`
219
+ )
220
+ ]
221
+ return lines.join('\n') + '\n'
222
+ }
223
+
224
+ /** Apache's `.htaccess`, for a folder under a vhost that allows it. */
225
+ function htaccess(rules) {
226
+ const { all, specific } = splitRules(rules)
227
+ const header = ([key, value]) => `Header always set ${key} "${value}"`
228
+ const block = ({ path, headers }) => {
229
+ const body = Object.entries(headers).map((entry) => `\t${header(entry)}`).join('\n')
230
+ // <Files> matches a name in any directory; a prefix needs the request URI, with no anchor at
231
+ // the start, so that the site works under a path as well as at a root.
232
+ return path.endsWith('/*')
233
+ ? `<If "%{REQUEST_URI} =~ m#${path.slice(0, -1)}#">\n${body}\n</If>`
234
+ : `<Files "${path.slice(1)}">\n${body}\n</Files>`
235
+ }
236
+ const lines = [
237
+ '# Written by h5p-embed. Needs mod_headers, and AllowOverride FileInfo on the folder; or paste',
238
+ '# the directives into the virtual host.',
239
+ ...Object.entries(all.headers).map(header),
240
+ ...specific.map(block)
241
+ ]
242
+ return lines.join('\n') + '\n'
243
+ }
244
+
171
245
  function notice({ version, libraries }) {
172
246
  return `This folder is the H5P embed page, written by @missing-elements/h5p-embed.
173
247
 
174
248
  index.html, main.js, embed.js, embed.css, resizer.js, config.js
175
249
  the embed page and the sizing script: MIT
250
+ Caddyfile, nginx.conf, .htaccess, _headers, vercel.json
251
+ the same headers for five kinds of host; use the one yours reads
176
252
  h5p-player.js, h5p-sw.js, h5p-jobs.js
177
253
  @missing-elements/h5p-offline-player ${version}: MIT. The two workers also
178
254
  carry zip.js, BSD-3-Clause, its licence in each file's header.
@@ -259,6 +335,9 @@ export async function buildSite({ out, libraries = true, packages = null, ancest
259
335
  const rules = headerRules(csp)
260
336
  await writeFile(join(target, '_headers'), netlifyHeaders(rules))
261
337
  await writeFile(join(target, 'vercel.json'), vercelConfig(rules))
338
+ await writeFile(join(target, 'Caddyfile'), caddyfile(rules))
339
+ await writeFile(join(target, 'nginx.conf'), nginxConf(rules))
340
+ await writeFile(join(target, '.htaccess'), htaccess(rules))
262
341
  await writeFile(join(target, 'NOTICE.txt'), notice({ version, libraries: Boolean(pack) }))
263
342
 
264
343
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@missing-elements/h5p-embed",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Self-hosted H5P embeds: writes the H5P embed page as a static site for a player domain of your own, then any site frames it with an iframe and one script line. No H5P server, no third-party service; optional allowlists for package hosts and embedding sites.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -43,14 +43,14 @@
43
43
  "cli"
44
44
  ],
45
45
  "dependencies": {
46
- "@missing-elements/h5p-runtime": "^0.2.3",
47
46
  "@missing-elements/h5p-libraries": "^0.2.0",
48
- "@missing-elements/h5p-offline-player": "^0.9.3"
47
+ "@missing-elements/h5p-offline-player": "^0.9.5",
48
+ "@missing-elements/h5p-runtime": "^0.2.4"
49
49
  },
50
50
  "devDependencies": {
51
- "@types/node": "^26.6.4",
51
+ "@types/node": "^26.6.5",
52
52
  "@zip.js/zip.js": "^2.23.0",
53
- "playwright": "^1.50.0",
53
+ "playwright": "^1.64.0",
54
54
  "typescript": "^7.0.2",
55
55
  "vitest": "^5.0.3"
56
56
  },
package/site/embed.js CHANGED
@@ -75,7 +75,11 @@ export function startEmbed({ librariesPack = null, packages = null, defaultLibra
75
75
 
76
76
  /* ---------------------------------------------------------------- sizing, upward */
77
77
 
78
- /** The parent hears about the height in the shape h5p-resizer.js expects. Nothing in it is secret. */
78
+ /**
79
+ * A message to the page that frames this one: the height in the shape h5p-resizer.js expects,
80
+ * and `report`, `error` and `xapi` in this page's own. To any parent unless a target is named,
81
+ * since nothing in the height is secret; the xAPI relay names the origin that opted in.
82
+ */
79
83
  const post = (message, target = '*') => {
80
84
  if (framed) window.parent.postMessage(message, target)
81
85
  }