Skip to content

cmi5 packages

Needs review

Needs review: cmi5 still has no admin screen; re-check this page when the cmi5 admin UI lands.

cmi5 is the xAPI profile for launching tracked learning content from an LMS. A cmi5 package is a zip with a cmi5.xml course structure and one or more assignable units (AUs). ulams can store cmi5 packages, has a cmi5 topic type and includes its own xAPI learning record store (LRS) for the statements the content sends.

Piece Where
Upload a cmi5 zip; it is checked by the upload guard (zip only, 512 MB by default, safe extraction) and its AUs are read from cmi5.xml POST /api/admin/cmi5 (field file)
List and delete uploaded packages GET /api/admin/cmi5, DELETE /api/admin/cmi5/{id} (deleting needs the cmi5_delete permission, admins only)
Topic type Cmi5Au whose value is the ID of one AU created through the topics API
Launch an AU (the player page, or with ?format=json the launch URL) GET /api/cmi5/player/{auId}; learners have the cmi5_read permission
Exchange the one-time launch token for the AU’s LRS session token POST /api/cmi5/fetch?token=... (public; the launch token is the credential)
Launch parameters for a course (endpoint, fetch URL, actor, registration) GET /api/cmi5/courses/{id}
Statements received for cmi5 content GET /api/admin/cmi5/statements
The xAPI LRS the content talks to /trax/api/<access>/xapi/std

The full endpoint list is in the API endpoints reference.

  1. Upload the cmi5 zip; ulams stores the package on the cmi5 disk (CMI5_DISK, which follows SCORM_DISK: the tenant bucket in production) and creates one record per AU.
  2. Add a topic of type cmi5 for an AU.
  3. When an enrolled learner opens the topic, the front asks the API for a launch and frames the AU on the tenant content origin (<slug>.content.<domain>/cmi5/...) in a sandboxed frame, so the package code never runs on the app or API origin.
  4. The launch URL carries the cmi5 endpoint, fetch, actor, registration and activityId (GET /api/cmi5/courses/{id} or the launch URL of GET /api/cmi5/player/{auId}). The fetch URL holds a one-time launch token, not the learner’s access token: 64 random characters, stored only as a hash, and valid for 10 minutes until the AU first uses it. The AU posts to the fetch URL (POST /api/cmi5/fetch?token=..., public, 60 requests a minute) and gets {"auth-token": ...}, an LRS-only session token, valid for CMI5_SESSION_MINUTES (default 120) counted from that first fetch. That token works only on the built-in LRS (/trax/api/<access>/xapi/std), only for that registration, and every other API endpoint rejects it. An AU that fetches again within the session gets the same token; a token that is unknown, malformed or expired gets a 401 with the cmi5 error body.
  5. The AU sends its xAPI statements to the LRS. A completed or passed statement completes the topic for the learner, and the usual lesson and course completion, certificates and events follow.

The AU runs on the content origin and calls fetch and the LRS from there, so the reverse proxy must answer those two paths with Access-Control-Allow-Origin: * and without cookies. The development Caddyfile and the production example have a @cmi5 block for it; if you write your own proxy configuration, copy that block (see Content origin). The design is in ADR 0046.

Older installs stored cmi5 packages on the API’s local disk. After switching CMI5_DISK to the bucket, copy them with php artisan cmi5:move-to-bucket (add --domain=<host> for one tenant). The command is safe to run again and leaves the originals in place.