@missing-elements/h5p-embed 0.2.1 → 0.3.1

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
@@ -1,19 +1,13 @@
1
1
  # @missing-elements/h5p-embed
2
2
 
3
- Self-hosted H5P embeds. One command writes the H5P embed page as a static site; you deploy it to
4
- a player domain of your own, and any site — a page builder, a hosted CMS, an LMS page, a portal
5
- with signed-in users — embeds a package with an iframe and one script line. No H5P server, no
6
- backend, and no third-party service between your learners and your content.
3
+ Writes the H5P embed page as a static site for a player domain of your own. Any site then embeds
4
+ a package with an iframe and one script line. No H5P server, no backend, no third-party service.
7
5
 
8
6
  ```bash
9
- npx @missing-elements/h5p-embed h5p-player
7
+ npx @missing-elements/h5p-embed h5p-player # the folder defaults to ./h5p-player
10
8
  ```
11
9
 
12
- ```
13
- Wrote h5p-player/ (11.1 MB): the embed page, the player, the H5P runtime, the library pack.
14
- ```
15
-
16
- Deploy the folder to any static host, then embed:
10
+ Deploy it to any static host, then embed:
17
11
 
18
12
  ```html
19
13
  <iframe src="https://h5p-player.example.net/?src=https://cdn.example.org/course.h5p"
@@ -21,35 +15,28 @@ Deploy the folder to any static host, then embed:
21
15
  <script src="https://h5p-player.example.net/resizer.js"></script>
22
16
  ```
23
17
 
24
- The script line sizes the iframe to the content. A page that already has h5p.org's
25
- `h5p-resizer.js` needs no second one; it speaks the same protocol.
18
+ The script sizes the iframe to the content; a page that already loads h5p.org's `h5p-resizer.js`
19
+ needs no second one.
26
20
 
27
- ## Why a domain of its own
28
-
29
- An H5P package is JavaScript, and the player runs it on the origin that serves the page. On a
30
- domain that holds nothing else, a package cannot reach your site's page, cookies, storage or APIs.
31
- Use a separate registrable domain — `h5p-player.example.net`, not `h5p.example.com` — because a
32
- subdomain is the same site and receives cookies set for `.example.com`. Put no accounts, no
33
- cookies and nothing else on it.
21
+ **Use a separate registrable domain** — `h5p-player.example.net`, not `h5p.example.com` — and put
22
+ nothing else on it: no accounts, no cookies. A package is JavaScript and runs as that origin, and
23
+ a subdomain shares your site's cookies.
34
24
 
35
25
  ## Options
36
26
 
37
27
  | Option | Effect |
38
28
  |---|---|
39
- | `--packages <origins>` | Play packages, and fetch library bundles, only from these origins (this domain's own is always allowed). The page refuses anything else by name, and the policy's `connect-src` blocks it in the browser. Add `https://api.h5p.org` to allow `libraries=hub` |
40
- | `--ancestors <origins>` | Only these sites may frame the page: `frame-ancestors`, which only a header can carry |
41
- | `--default-libraries <sources>` | The `libraries` value for addresses that name none, so a snippet without `&libraries=` still plays an export that carries no libraries (h5p.com and h5p.org exports usually do not): `pack`, `hub`, URLs, as the parameter. Checked against `--packages` when the site is written. Default: none, and such exports are refused unless the address asks |
42
- | `--no-libraries` | Leave out the 9.5 MB library pack; `libraries=pack` then means the hub, where allowed |
43
- | `--force` | Write into a folder that is not empty, replacing only this tool's files |
44
-
45
- Origins are written `https://host.example`, comma or space separated, with no path or trailing
46
- slash. `http://localhost` is accepted for trying it locally.
47
-
48
- List every host a package URL passes through. The page checks the address it is given, but the
49
- browser's policy also applies to each redirect, so a listed host that redirects to a CDN off the
50
- list fails as an ordinary network error, with nothing naming the list as the cause.
51
-
52
- A player domain for one organisation is best locked to its own hosts:
29
+ | `--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 |
30
+ | `--ancestors <origins>` | Only these sites may frame the page (`frame-ancestors`, header only). Default: any site |
31
+ | `--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 |
32
+ | `--no-libraries` | Leave out the 9.8 MB library pack; `libraries=pack` then fetches it from jsDelivr, where allowed |
33
+ | `--force` | Write into a non-empty folder, replacing only this tool's files |
34
+ | `-h`, `--help` / `-v`, `--version` | |
35
+
36
+ Origins are `https://host.example`, comma or space separated, no path or trailing slash;
37
+ `http://localhost` is accepted for local trials. **List every host a package URL redirects
38
+ through:** the browser's policy applies to each hop, and a redirect off the list fails as a plain
39
+ network error.
53
40
 
54
41
  ```bash
55
42
  npx @missing-elements/h5p-embed h5p-player \
@@ -62,40 +49,31 @@ npx @missing-elements/h5p-embed h5p-player \
62
49
  | Host | Policy and caching |
63
50
  |---|---|
64
51
  | Netlify, Cloudflare Pages | `_headers`, written beside the page |
65
- | Vercel | `vercel.json`, written beside the page; deploy the folder as a project |
66
- | GitHub Pages, S3, nginx, anything else | The page carries the policy in a `<meta>` tag. `--ancestors` needs a header: configure `Content-Security-Policy: frame-ancestors …` on the server, or leave it out |
52
+ | Vercel | `vercel.json`, written beside the page |
53
+ | GitHub Pages, S3, nginx, others | a `<meta>` policy in the page; `--ancestors` needs a `Content-Security-Policy: frame-ancestors …` header set on the server |
67
54
 
68
- The site works at the domain's root or under a path (`https://example.github.io/player/`): every
69
- URL in it is relative, and the player's Service Worker takes the scope `<folder>/h5p/`. Serve it
70
- over https; a frame in a page on plain http has no Service Worker.
71
-
72
- `h5p-sw.js` should be served with `Cache-Control: no-cache`, as the header files say, so an update
73
- reaches learners on their next visit. To update, run the command again with `--force` and
74
- redeploy.
55
+ It works at a domain's root or under a path, over https. Serve `h5p-sw.js` with
56
+ `Cache-Control: no-cache`. To update, run the command again with `--force` and redeploy.
75
57
 
76
58
  ## The address
77
59
 
78
60
  | Parameter | Effect |
79
61
  |---|---|
80
- | `src=<url>` | The package, required. Encode `&`, `#`, `+`, `%` and spaces in it |
81
- | `libraries=pack`, `hub`, `<url>` or `none` | Libraries for an export that has none (h5p.com and h5p.org exports usually do not); `none` turns off the site's `--default-libraries` for this address. `pack` is the copy on this domain, with the hub behind it where allowed, or the hub alone on a site written with `--no-libraries`; several sources may be given, tried in order |
82
- | `frame`, `copyright`, `export`, `icon`, `reporting` | H5P's action bar under the content and its buttons |
62
+ | `src=<url>` | The package, required. Encode `&`, `#`, `+`, `%` and spaces |
63
+ | `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` |
64
+ | `frame`, `copyright`, `export`, `icon`, `reporting` | H5P's action bar and its buttons |
83
65
  | `fullscreen=off` | No fullscreen button |
84
66
  | `preload=auto` | Start fetching media at once |
85
- | `activity-id=<IRI>` | The object id every xAPI statement names, instead of the package URL |
86
- | `custom-css=<url>` | A stylesheet of yours, loaded into the content |
87
- | `xapi=<origin>` | Relay statements to the embedding page, see below |
88
-
89
- Not available on the address: a custom script, and a learner's name. A script is a capability on
90
- the player's origin that a link should not hand out, and a name has no place in a URL.
67
+ | `activity-id=<IRI>` | The xAPI object id, instead of the package URL |
68
+ | `custom-css=<url>` | A stylesheet loaded into the content |
69
+ | `xapi=<origin>` | Relay statements to the embedding page |
91
70
 
92
- Opened on its own (not framed), the page asks before playing a package from another origin
93
- unless `--packages` names it: there, every package shares the domain's storage.
71
+ A custom script and a learner's name cannot be set on the address. Opened unframed, the page asks
72
+ before playing a package from another origin unless `--packages` names it.
94
73
 
95
- ## Results
74
+ ## Messages
96
75
 
97
- Add `&xapi=<the embedding page's origin>` and the frame posts every statement to that origin
98
- and no other:
76
+ With `&xapi=<embedding page's origin>` the frame posts statements to that origin only:
99
77
 
100
78
  ```js
101
79
  const frame = document.querySelector('iframe')
@@ -107,48 +85,34 @@ addEventListener('message', ({ source, origin, data }) => {
107
85
  })
108
86
  ```
109
87
 
110
- The checks prove where a message came from, not what it says: a package can post any statement,
111
- so treat relayed results as the learner's report, not as proof for a grade. Each statement
112
- carries `context.revision`, a fingerprint of the package build.
88
+ A package can post any statement, so treat relayed results as the learner's report, not proof
89
+ for a grade. Each carries `context.revision`, the package build's fingerprint.
113
90
 
114
- ## Messages to the embedding page
115
-
116
- Besides the heights and the relayed statements, the frame posts two messages to its parent,
117
- whatever `xapi=` says. Neither carries anything the embedding page did not hand over itself.
91
+ Whatever `xapi=` says, the frame also posts to its parent:
118
92
 
119
93
  ```js
120
- // Once, when the content is up: what the player learnt about the package.
121
94
  { context: 'h5p-offline-player', action: 'report',
122
- source, // { type: 'range-http' | 'chunked' | 'file', size }: streamed, or downloaded whole
123
- metadata, // { title, license, licenseVersion, authors: [names], mainLibrary }, from h5p.json
124
- libraryBundle, // { url, origin, fromCache } when libraries came from a bundle, else null
125
- elapsedMs } // from setting the package to the content being up
126
-
127
- // Instead, when the load fails, or the page refuses the address (code: 'refused', or 'no-src').
128
- { context: 'h5p-offline-player', action: 'error', code, message }
95
+ source, // { type: 'range-http' | 'chunked' | 'file', size }
96
+ metadata, // { title, license, licenseVersion, authors, mainLibrary }, from h5p.json; show as text
97
+ libraryBundle, // { url, origin, fromCache }, or null
98
+ elapsedMs }
99
+ { context: 'h5p-offline-player', action: 'error', code, message } // code 'refused', 'no-src' or the load's
129
100
  ```
130
101
 
131
- The strings in `metadata` are the package's own; show them as text.
132
-
133
102
  ## As a library
134
103
 
135
104
  ```js
136
105
  import { buildSite } from '@missing-elements/h5p-embed'
137
-
138
106
  await buildSite({ out: 'public/player', packages: ['https://cdn.example.org'] })
139
107
  ```
140
108
 
141
- `@missing-elements/h5p-embed/embed.js` exports `startEmbed()`, the page's script, for a site
142
- that builds the page into its own pipeline. It reads the page's `<h5p-player>`, `#loader` and
143
- `#notice` (see `site/index.html`, and `site/embed.css` for their styles) and takes:
144
-
145
- | Option | |
146
- |---|---|
147
- | `librariesPack` | The URL of a copy of `@missing-elements/h5p-libraries`, which `libraries=pack` names |
148
- | `packages` | The origins packages may come from, besides the page's own; `null` for any |
149
- | `defaultLibraries` | The `libraries` value for addresses that name none |
150
- | `runtime` | The `runtime` export of `@missing-elements/h5p-runtime`, for a bundled element |
151
- | `askInOwnFrame` | `false` to skip the click a package from elsewhere otherwise waits for when a page of the same origin frames this one — for a site whose own preview frames it for a package the visitor just chose. Default `true` |
109
+ `buildSite` takes `out`, `libraries` (default `true`), `packages`, `ancestors`, `defaultLibraries`
110
+ and `force`, as the flags. `@missing-elements/h5p-embed/embed.js` exports `startEmbed()`, the
111
+ page's script, which reads the page's `<h5p-player>`, `#loader` and `#notice` (see
112
+ `site/index.html` and `site/embed.css`). Its options: `librariesPack` (the URL `libraries=pack`
113
+ names), `packages` (allowed origins, `null` for any), `defaultLibraries`, `runtime` (the
114
+ `@missing-elements/h5p-runtime` export, for a bundled element) and `askInOwnFrame` (default
115
+ `true`; `false` skips the click when a same-origin page frames it).
152
116
 
153
117
  ```js
154
118
  import '@missing-elements/h5p-offline-player'
@@ -160,10 +124,18 @@ import { startEmbed } from '@missing-elements/h5p-embed/embed.js'
160
124
  startEmbed({ runtime, librariesPack, defaultLibraries: 'pack' })
161
125
  ```
162
126
 
127
+ ## Privacy
128
+
129
+ The site sets no cookies and sends nothing anywhere of its own. Learners' browsers contact your
130
+ player domain, the package hosts and, only for `pack` on a site built with `--no-libraries`,
131
+ `cdn.jsdelivr.net`. What the player stores and
132
+ sends, for a privacy notice or a GDPR review:
133
+ [Privacy and data protection](https://github.com/missing-elements/h5p-offline-player/blob/main/docs/privacy.md).
134
+
163
135
  ## Licences
164
136
 
165
- This package is MIT. The folder it writes also carries
166
- [`@missing-elements/h5p-runtime`](https://www.npmjs.com/package/@missing-elements/h5p-runtime), the
167
- H5P core runtime, under the GPL-3.0, as `frame-assets/` with its `LICENSE.txt` and `NOTICE.txt`;
168
- and, unless `--no-libraries`, the H5P hub's libraries under their own licences, listed in
169
- `libraries.txt`. `NOTICE.txt` in the folder says which file is what.
137
+ MIT. The written folder also carries
138
+ [`@missing-elements/h5p-runtime`](https://www.npmjs.com/package/@missing-elements/h5p-runtime)
139
+ under the GPL-3.0, as `frame-assets/` with its `LICENSE.txt` and `NOTICE.txt`, and, unless
140
+ `--no-libraries`, the hub's libraries under their own licences, listed in `libraries.txt`. The
141
+ folder's `NOTICE.txt` says which file is what.
package/bin/h5p-embed.mjs CHANGED
@@ -12,15 +12,15 @@ The folder defaults to ./h5p-player.
12
12
  Options:
13
13
  --packages <origins> play packages, and fetch library bundles, only from these origins
14
14
  (comma or space separated; this site's own is always allowed). Add
15
- https://api.h5p.org to allow libraries=hub, and any host a package
16
- URL redirects to. Default: any https host.
15
+ any host a package URL redirects to, and https://cdn.jsdelivr.net for
16
+ libraries=pack on a site built with --no-libraries. Default: any https host.
17
17
  --ancestors <origins> only these sites may frame the page (frame-ancestors, sent as a header).
18
18
  Default: any site.
19
19
  --default-libraries <sources>
20
20
  the libraries= value for addresses that name none, so a snippet without
21
- it still plays an export with no libraries: pack, hub, URLs, as the
21
+ it still plays an export with no libraries: pack or URLs, as the
22
22
  parameter. Default: none, such exports are refused unless the address asks
23
- --no-libraries leave out the 9.5 MB library pack that libraries=pack names
23
+ --no-libraries leave out the 9.8 MB library pack; libraries=pack then fetches it from jsDelivr
24
24
  --force write into a folder that is not empty, replacing only this tool's files
25
25
  -h, --help show this
26
26
  -v, --version print the version`
package/lib/build.mjs CHANGED
@@ -51,6 +51,9 @@ export function parseOrigin(value) {
51
51
  return url.origin
52
52
  }
53
53
 
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'
56
+
54
57
  /**
55
58
  * A `libraries` value for addresses that name none, checked against the site it goes into: the
56
59
  * tokens the page understands (`pack`, `hub`, `none`, https URLs), and with a host list, nothing
@@ -69,13 +72,14 @@ export function parseDefaultLibraries(value, { packages = null, libraries = true
69
72
  if (tokens.length > 1) throw new EmbedError('--default-libraries none stands alone.')
70
73
  return null
71
74
  }
72
- // An exact match on a parsed origin, not a substring of a URL.
73
- const hubAllowed = !packages || packages.some((origin) => origin === 'https://api.h5p.org')
75
+ // An exact match on a parsed origin, not a substring of a URL. Without its own copy, `pack` is
76
+ // the copy on jsDelivr that the element names; `hub` has meant `pack` since player 0.6.
77
+ const cdnAllowed = !packages || packages.some((origin) => origin === PACK_CDN_ORIGIN)
74
78
  for (const token of tokens) {
75
- if (token === 'hub') {
76
- if (!hubAllowed) throw new EmbedError('--default-libraries names hub, but --packages does not list https://api.h5p.org.')
77
- } else if (token === 'pack') {
78
- if (!libraries && !hubAllowed) throw new EmbedError('--default-libraries names pack, but the site has no pack (--no-libraries) and no hub to fall back to.')
79
+ if (token === 'pack' || token === 'hub') {
80
+ if (!libraries && !cdnAllowed) {
81
+ throw new EmbedError(`--default-libraries names ${token}, but the site has no pack (--no-libraries) and --packages does not list ${PACK_CDN_ORIGIN}.`)
82
+ }
79
83
  } else {
80
84
  let url
81
85
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@missing-elements/h5p-embed",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
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,9 +43,9 @@
43
43
  "cli"
44
44
  ],
45
45
  "dependencies": {
46
- "@missing-elements/h5p-libraries": "^0.1.0",
47
- "@missing-elements/h5p-runtime": "^0.1.0",
48
- "@missing-elements/h5p-offline-player": "^0.5.2"
46
+ "@missing-elements/h5p-libraries": "^0.1.1",
47
+ "@missing-elements/h5p-offline-player": "^0.6.1",
48
+ "@missing-elements/h5p-runtime": "^0.1.1"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^26.6.4",
package/site/embed.js CHANGED
@@ -5,7 +5,7 @@
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|hub|<url> …|none][&preload=auto][&xapi=<parent origin>]
8
+ * ?src=<package url>[&libraries=pack|<url> …|none][&preload=auto][&xapi=<parent origin>]
9
9
  * [&frame][&copyright][&export][&icon][&reporting][&fullscreen=off]
10
10
  * [&activity-id=<IRI>][&custom-css=<stylesheet url>]
11
11
  *
@@ -18,17 +18,17 @@
18
18
  * package — Embed My's preview — shows.
19
19
  */
20
20
 
21
- /** Where `libraries=hub` fetches from: the one hub host that sends CORS headers (see the player). */
22
- const HUB_ORIGIN = 'https://api.h5p.org'
21
+ /** Where the element's `libraries="pack"` fetches the library pack from, for a site without its own copy. */
22
+ const PACK_CDN_ORIGIN = 'https://cdn.jsdelivr.net'
23
23
 
24
24
  /**
25
25
  * @param {object} [options]
26
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` means the hub where allowed
27
+ * on this site, which `libraries=pack` names; without one, `pack` is the element's copy on jsDelivr
28
28
  * @param {string[] | null} [options.packages] the origins packages and library bundles may come
29
29
  * from, besides this page's own; `null` plays any, asking first when the storage is this origin's
30
30
  * @param {string | null} [options.defaultLibraries] the `libraries` value for an address that has
31
- * none — `pack`, `hub`, URLs, as the parameter — so a snippet without `&libraries=` still plays
31
+ * none — `pack` or URLs, as the parameter — so a snippet without `&libraries=` still plays
32
32
  * an export that carries no libraries; `&libraries=none` turns it off for one address
33
33
  * @param {object | null} [options.runtime] the `runtime` export of `@missing-elements/h5p-runtime`,
34
34
  * for a page that bundles the element; without it the element looks for `frame-assets/` beside itself
@@ -215,24 +215,21 @@ export function startEmbed({ librariesPack = null, packages = null, defaultLibra
215
215
 
216
216
  /**
217
217
  * The `libraries` value with `pack` resolved to this site's copy, or an error to show. With a
218
- * list of hosts, every bundle's origin has to be on it, the hub's included; the CSP that
219
- * `h5p-embed` wrote says the same, this only says it in words.
218
+ * list of hosts, every bundle's origin has to be on it, jsDelivr's included when the pack comes
219
+ * from there; the CSP that `h5p-embed` wrote says the same, this only says it in words.
220
220
  */
221
221
  const librarySources = (value) => {
222
222
  const sources = []
223
223
  if (value === 'none') return { value: '' }
224
224
  for (const token of value.split(/\s+/).filter(Boolean)) {
225
- if (token === 'pack') {
226
- // The pack, with the hub behind it for what it lacks, where the hub may be reached: an
227
- // export without its libraries then plays with no request to h5p.org in the common case.
228
- // A site set up without the pack falls back to the hub alone, so a snippet written for
229
- // `pack` keeps playing after a rebuild with --no-libraries.
225
+ if (token === 'pack' || token === 'hub') {
226
+ // This site's copy of the pack; without one, the element's own `pack`, the copy on
227
+ // jsDelivr, where that may be reached, so a snippet written for `pack` keeps playing after
228
+ // a rebuild with --no-libraries. `hub` has meant `pack` since player 0.6: the one H5P hub
229
+ // host that answers a browser serves a catalogue older than current exports need.
230
230
  if (librariesPack) sources.push(librariesPack)
231
- if (permitted(HUB_ORIGIN)) sources.push('hub')
232
- else if (!librariesPack) return { error: 'This player was set up without the library pack, so libraries=pack is not available here.' }
233
- } else if (token === 'hub') {
234
- if (!permitted(HUB_ORIGIN)) return { error: 'This player does not fetch libraries from the H5P hub.' }
235
- sources.push(token)
231
+ else if (permitted(PACK_CDN_ORIGIN)) sources.push('pack')
232
+ else return { error: 'This player was set up without the library pack, so libraries=pack is not available here.' }
236
233
  } else {
237
234
  const origin = urlOrigin(token)
238
235
  if (!origin || !permitted(origin)) return { error: `This player does not fetch libraries from ${origin ?? token}.` }