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.
What exists today
Section titled “What exists today”| 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.
How a learner launch works
Section titled “How a learner launch works”- Upload the cmi5 zip; ulams stores the package on the cmi5 disk (
CMI5_DISK, which followsSCORM_DISK: the tenant bucket in production) and creates one record per AU. - Add a topic of type cmi5 for an AU.
- 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. - The launch URL carries the cmi5
endpoint,fetch,actor,registrationandactivityId(GET /api/cmi5/courses/{id}or the launch URL ofGET /api/cmi5/player/{auId}). ThefetchURL 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 thefetchURL (POST /api/cmi5/fetch?token=..., public, 60 requests a minute) and gets{"auth-token": ...}, an LRS-only session token, valid forCMI5_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. - The AU sends its xAPI statements to the LRS. A
completedorpassedstatement 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.
Packages uploaded before this change
Section titled “Packages uploaded before this change”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.