@missing-elements/h5p-embed 0.6.2 → 0.8.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
@@ -30,7 +30,7 @@ a subdomain shares your site's cookies.
30
30
  | `--packages <origins>` | Play packages and fetch library bundles only from these origins (the site's own is always allowed). Add `https://cdn.jsdelivr.net` for `libraries=pack` on a site built with `--no-libraries`. Default: any https host |
31
31
  | `--ancestors <origins>` | Only these sites may frame the page (`frame-ancestors`, header only). Default: any site |
32
32
  | `--default-libraries <sources>` | The `libraries` value for addresses that name none: `pack` or URLs. Checked against `--packages`. Default: none, so exports without libraries are refused unless the address asks |
33
- | `--no-libraries` | Leave out the 13.7 MB library pack; `libraries=pack` then fetches it from jsDelivr, where allowed |
33
+ | `--no-libraries` | Leave out the library pack, 13.8 MB in `libraries/`, one archive per library, of which a learner's browser fetches only those a package needs; `libraries=pack` then fetches them from jsDelivr, where allowed |
34
34
  | `--force` | Write into a non-empty folder, replacing only this tool's files |
35
35
  | `-h`, `--help` / `-v`, `--version` | |
36
36
 
@@ -47,21 +47,35 @@ 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/*` | `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
 
61
74
  | Parameter | Effect |
62
75
  |---|---|
63
76
  | `src=<url>` | The package, required. Encode `&`, `#`, `+`, `%` and spaces |
64
- | `libraries=pack`, `<url>` or `none` | Libraries for an export without them; several are tried in order. `pack` is this domain's copy, or jsDelivr's without one; `hub` means `pack`; `none` turns off `--default-libraries` |
77
+ | `libraries=pack`, `<url>` or `none` | Where libraries come from; several are tried in order. `pack` is this domain's copy, or jsDelivr's without one: a package gets the libraries it lacks from it, and those it carries at the pack's newer versions. `hub` means `pack`; `none` turns off `--default-libraries` |
78
+ | `refresh-libraries=off` | Play the libraries a package carries as exported; `pack` then only fills gaps |
65
79
  | `frame`, `copyright`, `export`, `icon`, `reporting` | H5P's action bar and its buttons |
66
80
  | `fullscreen=off` | No fullscreen button |
67
81
  | `preload=auto` | Start fetching media at once |
@@ -99,7 +113,7 @@ Whatever `xapi=` says, the frame also posts to its parent:
99
113
  { context: 'h5p-offline-player', action: 'report',
100
114
  source, // { type: 'range-http' | 'chunked' | 'file', size }
101
115
  metadata, // { title, license, licenseVersion, authors, mainLibrary }, from h5p.json; show as text
102
- libraryBundle, // { url, origin, fromCache }, or null
116
+ libraryBundle, // { url, origin, fromCache, refreshed? }, or null; refreshed: [{ library, from, to }]
103
117
  elapsedMs }
104
118
  { context: 'h5p-offline-player', action: 'error', code, message } // code 'refused', 'no-src' or the load's
105
119
  ```
@@ -114,19 +128,21 @@ await buildSite({ out: 'public/player', packages: ['https://cdn.example.org'] })
114
128
  `buildSite` takes `out`, `libraries` (default `true`), `packages`, `ancestors`, `defaultLibraries`
115
129
  and `force`, as the flags. `@missing-elements/h5p-embed/embed.js` exports `startEmbed()`, the
116
130
  page's script, which reads the page's `<h5p-player>`, `#loader` and `#notice` (see
117
- `site/index.html` and `site/embed.css`). Its options: `librariesPack` (the URL `libraries=pack`
118
- names), `packages` (allowed origins, `null` for any), `defaultLibraries`, `runtime` (the
131
+ `site/index.html` and `site/embed.css`). Its options: `pack` (the `@missing-elements/h5p-libraries`
132
+ export, which `libraries=pack` then plays from) or `packBase` (the folder of a copy of its
133
+ `parts/`), `packages` (allowed origins, `null` for any), `defaultLibraries`, `runtime` (the
119
134
  `@missing-elements/h5p-runtime` export, for a bundled element) and `askInOwnFrame` (default
120
135
  `true`; `false` plays a package from another origin as it is when a same-origin page frames it).
136
+ `librariesPack`, the URL of a whole `libraries.h5p`, still works, as a bundle that only fills gaps.
121
137
 
122
138
  ```js
123
139
  import '@missing-elements/h5p-offline-player'
124
140
  import { runtime } from '@missing-elements/h5p-runtime'
125
- import librariesPack from '@missing-elements/h5p-libraries/libraries.h5p?url'
141
+ import { pack } from '@missing-elements/h5p-libraries'
126
142
  import '@missing-elements/h5p-embed/embed.css'
127
143
  import { startEmbed } from '@missing-elements/h5p-embed/embed.js'
128
144
 
129
- startEmbed({ runtime, librariesPack, defaultLibraries: 'pack' })
145
+ startEmbed({ runtime, pack, defaultLibraries: 'pack' })
130
146
  ```
131
147
 
132
148
  ## Privacy
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
@@ -7,8 +7,9 @@ import { fileURLToPath } from 'node:url'
7
7
  * Writes the embed page as a static site: one folder, deployable to any static host, that a site
8
8
  * frames from a player domain of its own. Everything comes from installed packages — the element
9
9
  * and its workers from the player's `dist/`, the H5P runtime from its own package as
10
- * `frame-assets/`, the library pack from `@missing-elements/h5p-libraries` — so the versions are
11
- * the ones this package was published with, and nothing is fetched.
10
+ * `frame-assets/`, the library pack's parts from `@missing-elements/h5p-libraries` as
11
+ * `libraries/` — so the versions are the ones this package was published with, and nothing is
12
+ * fetched.
12
13
  */
13
14
 
14
15
  export class EmbedError extends Error {}
@@ -52,7 +53,7 @@ export function parseOrigin(value) {
52
53
  }
53
54
 
54
55
  /** 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'
56
+ const PACK_CDN_ORIGIN = 'https://cdn.jsdelivr.net'
56
57
 
57
58
  /**
58
59
  * A `libraries` value for addresses that name none, checked against the site it goes into: the
@@ -149,7 +150,7 @@ function headerRules(csp) {
149
150
  { path: '/h5p-sw.js', headers: { 'Cache-Control': 'no-cache' } },
150
151
  { path: '/frame-assets/*', headers: { 'Cache-Control': revalidate } },
151
152
  { path: '/resizer.js', headers: { 'Cache-Control': revalidate } },
152
- { path: '/libraries.h5p', headers: { 'Cache-Control': revalidate } }
153
+ { path: '/libraries/*', headers: { 'Cache-Control': revalidate } }
153
154
  ]
154
155
  }
155
156
 
@@ -168,19 +169,96 @@ function vercelConfig(rules) {
168
169
  ) + '\n'
169
170
  }
170
171
 
172
+ /*
173
+ * The three server formats below are for a host of one's own — the European hosts a data
174
+ * protection officer accepts have no `_headers` convention, and a university's Apache or an
175
+ * nginx on a Hetzner box is where the embed page usually ends up. Each is written from the same
176
+ * rules as the two files above, so they cannot disagree. Values are quoted with double quotes
177
+ * in every format, which is safe because the policy carries only single quotes.
178
+ */
179
+
180
+ /** The rule for every path, and the ones for a single path or prefix. */
181
+ function splitRules(rules) {
182
+ const all = rules.find((rule) => rule.path === '/*')
183
+ return { all, specific: rules.filter((rule) => rule !== all) }
184
+ }
185
+
186
+ /** Caddy's `Caddyfile`: a site block to run as it is, or to paste into one. */
187
+ function caddyfile(rules) {
188
+ const { all, specific } = splitRules(rules)
189
+ const lines = [
190
+ '# Written by h5p-embed. Replace the domain and the root, then `caddy run` beside this file,',
191
+ '# or paste the block into your Caddyfile. Caddy fetches the certificate itself.',
192
+ 'h5p-player.example.net {',
193
+ '\troot * /srv/h5p-player',
194
+ '\tfile_server',
195
+ ...Object.entries(all.headers).map(([key, value]) => `\theader ${key} "${value}"`),
196
+ ...specific.flatMap(({ path, headers }) => Object.entries(headers).map(([key, value]) => `\theader ${path} ${key} "${value}"`)),
197
+ '}'
198
+ ]
199
+ return lines.join('\n') + '\n'
200
+ }
201
+
202
+ /**
203
+ * nginx: a file to `include` inside the `server` block that serves the folder. nginx's
204
+ * `add_header` in a `location` replaces every `add_header` inherited from the server level
205
+ * rather than adding to it, so each location repeats the headers that apply everywhere.
206
+ */
207
+ function nginxConf(rules) {
208
+ const { all, specific } = splitRules(rules)
209
+ const header = ([key, value]) => `add_header ${key} "${value}" always;`
210
+ const matcher = (path) => (path.endsWith('/*') ? `^~ ${path.slice(0, -1)}` : `= ${path}`)
211
+ const lines = [
212
+ '# Written by h5p-embed. Include this file inside the server block that serves the folder:',
213
+ '# server { listen 443 ssl; server_name h5p-player.example.net; root /srv/h5p-player; include /srv/h5p-player/nginx.conf; }',
214
+ '# A location repeats the headers set for every path: in nginx an add_header inside a location',
215
+ '# replaces the inherited ones instead of adding to them.',
216
+ ...Object.entries(all.headers).map(header),
217
+ ...specific.map(
218
+ ({ path, headers }) =>
219
+ `location ${matcher(path)} {\n${[...Object.entries(all.headers), ...Object.entries(headers)].map((entry) => `\t${header(entry)}`).join('\n')}\n}`
220
+ )
221
+ ]
222
+ return lines.join('\n') + '\n'
223
+ }
224
+
225
+ /** Apache's `.htaccess`, for a folder under a vhost that allows it. */
226
+ function htaccess(rules) {
227
+ const { all, specific } = splitRules(rules)
228
+ const header = ([key, value]) => `Header always set ${key} "${value}"`
229
+ const block = ({ path, headers }) => {
230
+ const body = Object.entries(headers).map((entry) => `\t${header(entry)}`).join('\n')
231
+ // <Files> matches a name in any directory; a prefix needs the request URI, with no anchor at
232
+ // the start, so that the site works under a path as well as at a root.
233
+ return path.endsWith('/*')
234
+ ? `<If "%{REQUEST_URI} =~ m#${path.slice(0, -1)}#">\n${body}\n</If>`
235
+ : `<Files "${path.slice(1)}">\n${body}\n</Files>`
236
+ }
237
+ const lines = [
238
+ '# Written by h5p-embed. Needs mod_headers, and AllowOverride FileInfo on the folder; or paste',
239
+ '# the directives into the virtual host.',
240
+ ...Object.entries(all.headers).map(header),
241
+ ...specific.map(block)
242
+ ]
243
+ return lines.join('\n') + '\n'
244
+ }
245
+
171
246
  function notice({ version, libraries }) {
172
247
  return `This folder is the H5P embed page, written by @missing-elements/h5p-embed.
173
248
 
174
249
  index.html, main.js, embed.js, embed.css, resizer.js, config.js
175
250
  the embed page and the sizing script: MIT
251
+ Caddyfile, nginx.conf, .htaccess, _headers, vercel.json
252
+ the same headers for five kinds of host; use the one yours reads
176
253
  h5p-player.js, h5p-sw.js, h5p-jobs.js
177
254
  @missing-elements/h5p-offline-player ${version}: MIT. The two workers also
178
255
  carry zip.js, BSD-3-Clause, its licence in each file's header.
179
256
  frame-assets/ @missing-elements/h5p-runtime, the H5P core runtime: GPL-3.0-only. Its
180
257
  LICENSE.txt and NOTICE.txt say what it is and where its source is; keep them
181
258
  with it.
182
- ${libraries ? ` libraries.h5p @missing-elements/h5p-libraries, the H5P hub's libraries, each under its
183
- own licence, listed in libraries.txt.
259
+ ${libraries ? ` libraries/ @missing-elements/h5p-libraries, the H5P hub's libraries, one archive per
260
+ library and index.json, each under its own licence, listed in libraries.txt.
261
+ The player downloads only the ones a package needs.
184
262
  ` : ''}
185
263
  Serve it from a domain that holds nothing else, and frame it:
186
264
 
@@ -211,7 +289,7 @@ async function occupied(dir) {
211
289
  *
212
290
  * @param {object} options
213
291
  * @param {string} options.out
214
- * @param {boolean} [options.libraries] include the library pack, for `libraries=pack` (default true)
292
+ * @param {boolean} [options.libraries] include the library pack's parts, for `libraries=pack` (default true)
215
293
  * @param {string[] | string | null} [options.packages] the only origins packages may come from
216
294
  * @param {string[] | string | null} [options.ancestors] the only origins that may frame the page
217
295
  * @param {string | null} [options.defaultLibraries] the `libraries` value for addresses that name none
@@ -232,7 +310,7 @@ export async function buildSite({ out, libraries = true, packages = null, ancest
232
310
 
233
311
  const player = dirname(installed('@missing-elements/h5p-offline-player/dist/h5p-player.js', 'Run `pnpm build` in the workspace, or reinstall this package.'))
234
312
  const runtime = dirname(installed('@missing-elements/h5p-runtime/dist/h5p.css', 'Run `pnpm build` in the workspace, or reinstall this package.'))
235
- const pack = libraries ? installed('@missing-elements/h5p-libraries/libraries.h5p', 'Reinstall this package, or pass --no-libraries.') : null
313
+ const pack = libraries ? dirname(installed('@missing-elements/h5p-libraries/parts/index.json', 'Reinstall this package, or pass --no-libraries.')) : null
236
314
  const version = (await readFile(join(player, 'VERSION'), 'utf8').catch(() => 'unknown')).trim()
237
315
 
238
316
  await mkdir(target, { recursive: true })
@@ -240,11 +318,13 @@ export async function buildSite({ out, libraries = true, packages = null, ancest
240
318
  for (const name of PLAYER_FILES) await cp(join(player, name), join(target, name))
241
319
  await rm(join(target, 'frame-assets'), { recursive: true, force: true })
242
320
  await cp(runtime, join(target, 'frame-assets'), { recursive: true })
321
+ // A site written before the pack was split carried it whole; that file is gone either way.
322
+ await rm(join(target, 'libraries.h5p'), { force: true })
323
+ await rm(join(target, 'libraries'), { recursive: true, force: true })
243
324
  if (pack) {
244
- await cp(pack, join(target, 'libraries.h5p'))
325
+ await cp(pack, join(target, 'libraries'), { recursive: true })
245
326
  await cp(join(dirname(pack), 'libraries.txt'), join(target, 'libraries.txt'))
246
327
  } else {
247
- await rm(join(target, 'libraries.h5p'), { force: true })
248
328
  await rm(join(target, 'libraries.txt'), { force: true })
249
329
  }
250
330
 
@@ -259,6 +339,9 @@ export async function buildSite({ out, libraries = true, packages = null, ancest
259
339
  const rules = headerRules(csp)
260
340
  await writeFile(join(target, '_headers'), netlifyHeaders(rules))
261
341
  await writeFile(join(target, 'vercel.json'), vercelConfig(rules))
342
+ await writeFile(join(target, 'Caddyfile'), caddyfile(rules))
343
+ await writeFile(join(target, 'nginx.conf'), nginxConf(rules))
344
+ await writeFile(join(target, '.htaccess'), htaccess(rules))
262
345
  await writeFile(join(target, 'NOTICE.txt'), notice({ version, libraries: Boolean(pack) }))
263
346
 
264
347
  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.8.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,19 +43,19 @@
43
43
  "cli"
44
44
  ],
45
45
  "dependencies": {
46
- "@missing-elements/h5p-runtime": "^0.2.3",
47
- "@missing-elements/h5p-libraries": "^0.2.0",
48
- "@missing-elements/h5p-offline-player": "^0.9.3"
46
+ "@missing-elements/h5p-libraries": "^0.3.0",
47
+ "@missing-elements/h5p-runtime": "^0.2.4",
48
+ "@missing-elements/h5p-offline-player": "^0.10.0"
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
  },
57
57
  "engines": {
58
- "node": ">=20"
58
+ "node": ">=22"
59
59
  },
60
60
  "scripts": {
61
61
  "test": "pnpm --filter @missing-elements/h5p-offline-player build && vitest run",
package/site/embed.js CHANGED
@@ -5,8 +5,8 @@
5
5
  * `h5p-embed` was told when it wrote the site, and the demo's `/embed` with the demo's copy of the
6
6
  * library pack.
7
7
  *
8
- * ?src=<package url>[&libraries=pack|<url> …|none][&preload=auto][&xapi=<parent origin>]
9
- * [&frame][&copyright][&export][&icon][&reporting][&fullscreen=off]
8
+ * ?src=<package url>[&libraries=pack|<url> …|none][&refresh-libraries=off][&preload=auto]
9
+ * [&xapi=<parent origin>][&frame][&copyright][&export][&icon][&reporting][&fullscreen=off]
10
10
  * [&activity-id=<IRI>][&custom-css=<stylesheet url>][&lang=<language tag>]
11
11
  *
12
12
  * Upward it speaks H5P's own resizer protocol — the `hello` / `resize` exchange that h5p.org's
@@ -23,8 +23,12 @@ const PACK_CDN_ORIGIN = 'https://cdn.jsdelivr.net'
23
23
 
24
24
  /**
25
25
  * @param {object} [options]
26
- * @param {string | null} [options.librariesPack] the URL of a copy of `@missing-elements/h5p-libraries`
27
- * on this site, which `libraries=pack` names; without one, `pack` is the element's copy on jsDelivr
26
+ * @param {string | null} [options.packBase] the folder of this site's copy of the library pack's
27
+ * parts (`parts/` of `@missing-elements/h5p-libraries`), which `libraries=pack` plays from
28
+ * @param {{ libraries: object[] } | null} [options.pack] the same, as the `pack` export a bundler
29
+ * resolves; it wins over `packBase`. Without either, `pack` is the element's copy on jsDelivr
30
+ * @param {string | null} [options.librariesPack] the URL of a whole `libraries.h5p` on this site,
31
+ * for a page written before the pack was split: played as any bundle, with no refresh
28
32
  * @param {string[] | null} [options.packages] the origins packages and library bundles may come
29
33
  * from, besides this page's own; `null` plays any, one from another origin `untrusted` when the
30
34
  * storage is this origin's
@@ -38,11 +42,15 @@ const PACK_CDN_ORIGIN = 'https://cdn.jsdelivr.net'
38
42
  * page of its own origin, whose storage it shares. A site whose own preview frames the page for
39
43
  * a package the visitor just chose passes `false`
40
44
  */
41
- export function startEmbed({ librariesPack = null, packages = null, defaultLibraries = null, runtime = null, askInOwnFrame = true } = {}) {
45
+ export function startEmbed({ packBase = null, pack = null, librariesPack = null, packages = null, defaultLibraries = null, runtime = null, askInOwnFrame = true } = {}) {
42
46
  const params = new URLSearchParams(location.search)
43
47
  const player = document.querySelector('h5p-player')
44
- // Before `src`: the element resolves the runtime's files when a package is set.
48
+ // Before `src`: the element resolves the runtime's files, and reads where the pack is, when a
49
+ // package is set.
45
50
  if (runtime) player.runtime = runtime
51
+ if (pack) player.pack = pack
52
+ else if (packBase) player.setAttribute('pack-base', packBase)
53
+ const ownPack = Boolean(pack || packBase)
46
54
  const notice = document.querySelector('#notice')
47
55
  const loader = document.querySelector('#loader')
48
56
  const framed = window.parent !== window
@@ -75,7 +83,11 @@ export function startEmbed({ librariesPack = null, packages = null, defaultLibra
75
83
 
76
84
  /* ---------------------------------------------------------------- sizing, upward */
77
85
 
78
- /** The parent hears about the height in the shape h5p-resizer.js expects. Nothing in it is secret. */
86
+ /**
87
+ * A message to the page that frames this one: the height in the shape h5p-resizer.js expects,
88
+ * and `report`, `error` and `xapi` in this page's own. To any parent unless a target is named,
89
+ * since nothing in the height is secret; the xAPI relay names the origin that opted in.
90
+ */
79
91
  const post = (message, target = '*') => {
80
92
  if (framed) window.parent.postMessage(message, target)
81
93
  }
@@ -233,12 +245,13 @@ export function startEmbed({ librariesPack = null, packages = null, defaultLibra
233
245
  if (value === 'none') return { value: '' }
234
246
  for (const token of value.split(/\s+/).filter(Boolean)) {
235
247
  if (token === 'pack' || token === 'hub') {
236
- // This site's copy of the pack; without one, the element's own `pack`, the copy on
237
- // jsDelivr, where that may be reached, so a snippet written for `pack` keeps playing after
238
- // a rebuild with --no-libraries. `hub` has meant `pack` since player 0.6: the one H5P hub
239
- // host that answers a browser serves a catalogue older than current exports need.
240
- if (librariesPack) sources.push(librariesPack)
241
- else if (permitted(PACK_CDN_ORIGIN)) sources.push('pack')
248
+ // This site's copy of the pack, which the element was pointed at above; without one, the
249
+ // element's own `pack`, the copy on jsDelivr, where that may be reached, so a snippet
250
+ // written for `pack` keeps playing after a rebuild with --no-libraries. `hub` has meant
251
+ // `pack` since player 0.6: the one H5P hub host that answers a browser serves a
252
+ // catalogue older than current exports need.
253
+ if (ownPack || (!librariesPack && permitted(PACK_CDN_ORIGIN))) sources.push('pack')
254
+ else if (librariesPack) sources.push(librariesPack)
242
255
  else return { error: 'This player was set up without the library pack, so libraries=pack is not available here.' }
243
256
  } else {
244
257
  const origin = urlOrigin(token)
@@ -252,7 +265,8 @@ export function startEmbed({ librariesPack = null, packages = null, defaultLibra
252
265
  /**
253
266
  * The element's display options, by their attribute names, for the embedding page to ask for:
254
267
  * `&frame&copyright&export` shows H5P's action bar with those buttons, `&fullscreen=off` takes
255
- * that one away, `&activity-id=` names the statements' object and `&custom-css=` restyles the
268
+ * that one away, `&refresh-libraries=off` plays a package's own libraries even where the pack
269
+ * has newer ones, `&activity-id=` names the statements' object and `&custom-css=` restyles the
256
270
  * content to the embedding site's taste. `&lang=` is the embedding page's language, which this
257
271
  * page cannot read across origins: H5P's own buttons and dialogs follow it. Not `custom-js`,
258
272
  * `embed-code` or `user`: a script is a capability on this origin that a link should not hand
@@ -263,6 +277,9 @@ export function startEmbed({ librariesPack = null, packages = null, defaultLibra
263
277
  if (params.has(name) && params.get(name) !== 'off') player.setAttribute(name, '')
264
278
  }
265
279
  if (params.get('fullscreen') === 'off') player.setAttribute('fullscreen', 'off')
280
+ // The pack's newer versions of the libraries a package carries, unless the embedding page
281
+ // needs the package exactly as exported.
282
+ if (params.get('refresh-libraries') === 'off') player.setAttribute('refresh-libraries', 'off')
266
283
  for (const name of ['activity-id', 'custom-css']) {
267
284
  const value = params.get(name)?.trim()
268
285
  if (value) player.setAttribute(name, value)
@@ -354,11 +371,11 @@ export function startEmbed({ librariesPack = null, packages = null, defaultLibra
354
371
  // as the host's own: an untrusted package plays with this site's sources alone, the page's
355
372
  // default or its pack, and the address's only once the click has trusted it.
356
373
  const own = defaultLibraries?.trim() ? librarySources(defaultLibraries.trim()).value : ''
357
- const pack = own || librarySources('pack').value
358
- if (!pack) {
374
+ const sitesSources = own || librarySources('pack').value
375
+ if (!sitesSources) {
359
376
  askToTrust()
360
377
  return
361
378
  }
362
379
  player.untrusted = true
363
- start(src, pack)
380
+ start(src, sitesSources)
364
381
  }
package/site/main.js CHANGED
@@ -1,11 +1,11 @@
1
1
  /*! @missing-elements/h5p-embed. MIT. */
2
2
  // The page's entry: `config.js` is what `h5p-embed` was told when it wrote this site — whether
3
- // the library pack is here, and which hosts packages may come from.
3
+ // the library pack's parts are here, and which hosts packages may come from.
4
4
  import config from './config.js'
5
5
  import { startEmbed } from './embed.js'
6
6
 
7
7
  startEmbed({
8
- librariesPack: config.libraries ? new URL('./libraries.h5p', import.meta.url).href : null,
8
+ packBase: config.libraries ? new URL('./libraries/', import.meta.url).href : null,
9
9
  packages: config.packages,
10
10
  defaultLibraries: config.defaultLibraries
11
11
  })