@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 +19 -6
- package/bin/h5p-embed.mjs +4 -2
- package/lib/build.mjs +80 -1
- package/package.json +5 -5
- package/site/embed.js +5 -1
package/README.md
CHANGED
|
@@ -47,14 +47,27 @@ npx @missing-elements/h5p-embed h5p-player \
|
|
|
47
47
|
|
|
48
48
|
## Hosting
|
|
49
49
|
|
|
50
|
-
|
|
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
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
51
|
+
"@types/node": "^26.6.5",
|
|
52
52
|
"@zip.js/zip.js": "^2.23.0",
|
|
53
|
-
"playwright": "^1.
|
|
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
|
-
/**
|
|
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
|
}
|