Phase 6: Canvas integration — connect, link, file import, roster sync - #82
Open
sroygit wants to merge 10 commits into
Open
Phase 6: Canvas integration — connect, link, file import, roster sync#82sroygit wants to merge 10 commits into
sroygit wants to merge 10 commits into
Conversation
Canvas LMS integration package from GitHub Packages. The project .npmrc carries only the @ubc registry line; the read:packages PAT lives in each developer's ~/.npmrc and is never committed.
…, Saurav's plan Read-only Canvas integration: connect via OAuth, link a Canvas course the instructor teaches, import Canvas Files into materials, sync the roster matched on integration_id (= PUID). Synced entries add to the CSV roster. No gradebook write-back, no Moodle.
Binds @ubc/ubc-genai-toolkit-lms-integration to FinanceBot identity (getUserKey = req.user.puid) and a Mongo token store on lmsCanvasTokens. Mounts the package OAuth router and GET /api/lms/canvas/status under /api only when all four CANVAS_* variables are set. Verified by hand against local Canvas: connect stores one token keyed by PUID, disconnect removes it.
GET /courses lists only courses the connected Canvas identity teaches; PUT link refuses any id outside that list (403 not-teacher) and stores name/code from Canvas's own row. DELETE link also clears the course's synced roster entries. Adds Course.canvas, the LmsRosterEntry type and its collection accessor.
GET files lists Canvas Files in an upload-accepted format under the shared size limit, flagging ones already imported. POST files/import downloads each with maxBytes, writes it under UPLOAD_DIR like a multer upload, and hands it to createMaterials; already-imported ids are skipped, one failure does not stop the rest. Material.origin plus the fixed-name partial unique index make a re-import a skip, never a duplicate. The upload batch/size constants move to materials.service so the policy has one definition.
POST roster/sync reads the linked course's Canvas roster, builds a match report against students enrolled in this course (keyed by PUID), explains the unmatched, and replaces the course's lmsRosterEntries with every Canvas user carrying an integration_id. A roster exposing no integration_id at all is refused (409 roster-coverage) and nothing is written. Two unique indexes make a duplicate Canvas identity per PUID a loud failure.
The registration-code gate now passes when the student is on the CSV roster OR on the course's synced Canvas roster (lmsRosterEntries, keyed by PUID). The Canvas lookup runs only when the CSV roster did not match, so existing behaviour is byte-identical. PRD updated: Canvas linking, file import and roster sync are in scope; gradebook write-back stays a stretch goal.
…aterials Settings gains a Canvas card — connect, choose the Canvas course you teach, linked — with Sync roster, Unlink, and Disconnect, and a sync report that shows matched / on-Canvas-only / in-FinanceBot-only counts next to the student-ID coverage that makes them trustworthy. The roster heading reads 'N from CSV · M from Canvas'. Materials gains an Import-from-Canvas dialog, shown only for a linked course, that disables already-imported files and reads the selection back before importing.
npm 11 on macOS prunes the optional @emnapi/* entries that npm 10 on the runner still expects, so npm ci failed with 'Missing: @emnapi/core'. The lock is now origin/main's plus only the lms-integration entry. CI also needs a token for the @ubc scope: setup-node writes the registry auth line from NODE_AUTH_TOKEN, which prefers a GH_PACKAGES_READ_TOKEN secret and falls back to GITHUB_TOKEN.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 6 — Canvas integration (read-only)
An instructor connects their own Canvas account via OAuth, links a Canvas course they teach, imports Canvas Files into the course's materials, and syncs the Canvas roster so a student on it can enroll with the registration code alone.
Design:
docs/superpowers/specs/2026-08-27-canvas-integration-design.md· Plan:docs/superpowers/plans/phase-6/Saurav/· Contract:docs/api-contract.md→ "Canvas LMS".What's in it
@ubc/ubc-genai-toolkit-lms-integration@^1.2.0(GitHub Packages). The committed.npmrccarries only the@ubcregistry line; each developer needs aread:packagesPAT in~/.npmrc.components/lms/binds the package to FinanceBot identity:getUserKey = req.user.puid, tokens inlmsCanvasTokens. Mounted under/api/lms/canvasonly when all fourCANVAS_*variables are set.course.canvassub-document.PUT linkrefuses any Canvas course not in the connected identity's teacher list. Every Canvas read derives its external course id from the stored link, never a request body.maxBytes, written underuploads/like a multer upload, handed to the existingcreateMaterials → ingestMaterialpipeline.Material.origin+ the fixed-name partial unique indexmaterials_origin_uniquemake a re-import a skip, never a duplicate. Per-file independent.integration_id(= PUID at UBC) only, no fallback key. Stored in a newlmsRosterEntriescollection with two unique indexes; each sync replaces the course's set. A roster exposing nointegration_idis refused (409 roster-coverage) and nothing is written.integration_id).Not in it, on purpose
No writes to Canvas (grades, feedback). No Moodle. No submission import.
Verification
../local-lms-dev/, verified 2026-08-27) as a plain non-admin teacher — every step matched the prediction, with each stage confirmed in the database. Table indocs/superpowers/plans/phase-6/Saurav/STATUS.md. Highlights: a student with no CSV entry enrolled via the Canvas roster alone; a Canvas user withoutintegration_idwas reported and not stored; unlink left enrollments and imported materials intact.Reviewer notes
materials.routes.test.tsmocks the wholematerials.service; the upload limits now come from that module, so the mock supplies them. Anyone else mocking it wholesale will need the same.MongoDbLiketype doesn't satisfy mongodb v7'sDbat the type level (createIndexparam variance); one cast at the boundary incomponents/lms/index.ts, runtime-compatible. Worth an upstream note.integration_idwith the PUID and exposes it to Teacher-role tokens. Locally it does; hosted is a separate account-level configuration.