Normalize every packages/* package for public npm publish: - version: pre-1.0 (0.x) → 1.0.0; already-≥1.0 kept (mcp 2.4.1, cli-tools 1.8.0, dxt 1.8.1, browser/hanzo-ide/office/outlook 1.9.31, gworkspace 1.9.30, aci/auth 1.0.0) - remove "private" from all 22 gated packages/* (apps/site + repo root stay private — site is a deployed website, not an npm library) - add publishConfig.access=public everywhere (scoped @hanzo/* need it) - add files[] + valid main pointing at built output so no empty tarballs; no-build packages ship src+README CI npm job now publishes the whole workspace: - build @hanzo/cards first (slack/teams import its dist), then pnpm -r --if-present run build || true - pnpm -r publish --access public --no-git-checks --ignore-scripts --ignore-scripts prevents raycast/vscode `publish` lifecycle scripts (Raycast Store / VS Marketplace) from hijacking and aborting the run. workspace:* (auth, cards) is rewritten to the real version at publish.
Hanzo AI for Canvas LMS
AI over your Canvas course — summarize a course, draft assignment feedback,
generate quiz/discussion questions, summarize student submissions, and
draft an announcement, plus a freeform "ask about the course" box. An
embedded-app panel you host at canvas.hanzo.ai, backed by a small OAuth +
API-proxy service that keeps the Canvas client secret and access tokens
server-side.
Built on the published Hanzo SDK:
@hanzo/ai— the headless client.createAiClient({ baseUrl, token }).chat.completions.create({ model, messages })and.models.list(). All model calls go tohttps://api.hanzo.aion/v1/...(never an/api/prefix). We import it; we do not reimplement the transport.@hanzo/iam— Hanzo identity / thehk-…API key the panel uses as its gateway bearer.
Canvas is the dominant higher-ed / K12 LMS: courses, syllabi, modules, assignments, pages, discussions, and student submissions. This app reads those records through the Canvas REST API and grounds every AI draft in them. Everything it produces — feedback, questions, announcements, summaries — is a draft for an educator to review, not an authoritative grade or decision.
Package name:
@hanzo/canvas-lms(directorypackages/canvas-lms) — named to avoid colliding with the design-tool packagepackages/canva.
Architecture
Browser panel (dist/index.html, app.js) Server service (dist/server.js)
───────────────────────────────────── ──────────────────────────────
reads Canvas via ──► /proxy/* ──────────► injects OAuth Bearer + refresh,
same-origin proxy (cookie) forwards to {canvas-host}/api/v1
(never sees a Canvas token) with the user's access token
│
└── calls api.hanzo.ai /v1 directly with the pasted hk-… Hanzo key
(@hanzo/ai headless client)
- The panel never holds a Canvas token or the client secret. It reaches Canvas
only through the same-origin
/proxy/*endpoint (with an HttpOnly session cookie). It talks toapi.hanzo.aidirectly with the user's pasted Hanzo key. - The service is the only place
CANVAS_CLIENT_SECRETand the OAuth tokens exist. It runs the install flow, holds the session, refreshes the token transparently, and proxies REST calls.
Source layout (all pure logic is unit-tested)
| File | Responsibility |
|---|---|
src/config.ts |
Canvas host normalization, REST version, gateway URL, server-secret config (readServerConfig fails fast). |
src/canvas-oauth.ts |
OAuth2 Authorization-Code shaping: authorizeUrl, tokenExchange, refreshExchange, token parsing, non-rotating-refresh carry + expiry math. Pure. |
src/canvas-api.ts |
REST v1 request wrappers (listCourses, getCourse, listAssignments, listSubmissions, postSubmissionComment, createAnnouncement, …) with pagination + Link-header parsing, and response parsers. Pure. |
src/course.ts |
Course/assignment/submission/page/module JSON → windowed context text, HTML→text, honest truncation. Pure. |
src/hanzo.ts |
Thin over @hanzo/ai: prompt assembly + the single ask path + listModels. |
src/actions.ts |
The five AI actions (id → prompt) over ask. One code path to the model. |
src/panel.ts |
Browser proxy-client request shaping + launch-context parsing. Pure. |
src/app.ts |
The DOM glue for the panel (the one impure browser entry). |
src/server.ts |
Node http service: OAuth install/callback + /proxy/*. |
1. Create a Canvas Developer Key
Canvas OAuth apps authenticate with a Developer Key (an OAuth2 client-id/secret). An account admin creates it in Admin → Developer Keys → + Developer Key → API Key:
-
Key Name:
Hanzo AI for Canvas(any label). -
Redirect URIs — exactly the URL your service serves the callback at:
https://canvas.hanzo.ai/oauth/callback(For local development, add
http://localhost:8791/oauth/callbacktoo.) -
Scopes — leave Enforce Scopes off for the simplest setup (permissions then follow the authorizing user's Canvas role). If you enable enforcement, add the granular URL-scopes exported as
RECOMMENDED_SCOPESinsrc/config.ts(the read surface plusPUT …/submissions/:user_idandPOST …/discussion_topicsfor the two write-backs). -
Save, then ON the key's state, and copy the Client ID (the numeric key id) and Client Secret (the "Key" / secret value).
-
Note your institution's Canvas host (e.g.
school.instructure.comor your custom Canvas domain) — the app builds every OAuth + API URL from it.
Because Canvas is self-hosted per institution, the host is a required deployment
value (CANVAS_HOST); there is no fixed host list.
2. OAuth flow
Standard Authorization Code grant (server-side secret):
GET /oauth/install→ 302 tohttps://{canvas-host}/login/oauth2/auth?client_id=…&response_type=code&redirect_uri=…&state=….- The user consents; Canvas redirects back to
GET /oauth/callback?code=…&state=…. - The service verifies
state, then POSTs to/login/oauth2/tokenwithgrant_type=authorization_code, thecode, theclient_id+client_secret(in the body, server-side only), and the sameredirect_uri. - It opens a session, sets an HttpOnly cookie, and the panel is authenticated.
- Access tokens are short-lived (~1h). The service refreshes with
grant_type=refresh_tokenbefore each proxied call when the token has expired. Canvas does not rotate the refresh token (the refresh response omits it), so the original refresh token is carried forward.
In-memory sessions here are for a single instance. For a multi-instance deployment behind
hanzoai/ingress, persist sessions + the OAuthstateset tohanzoai/kv(Valkey), and readCANVAS_CLIENT_SECRETfrom KMS (kms.hanzo.ai) — never from a committed env file.
3. Canvas REST API
All reads/writes go through https://{canvas-host}/api/v1/... with the user's
Authorization: Bearer {token}. Canvas's own path is /api/v1 — this is unrelated
to the Hanzo gateway (which is always api.hanzo.ai/v1/..., never /api/).
| Resource | Endpoint (relative to /api/v1) |
|---|---|
| Courses | GET /courses, GET /courses/{id}?include[]=syllabus_body |
| Assignments | GET /courses/{id}/assignments, GET /courses/{id}/assignments/{id} |
| Submissions | GET /courses/{id}/assignments/{aid}/submissions, GET …/submissions/{user_id} |
| Submission comment (write) | PUT /courses/{id}/assignments/{aid}/submissions/{user_id} — body { "comment": { "text_comment": "…" } } |
| Discussions | GET /courses/{id}/discussion_topics |
| Announcement (write) | POST /courses/{id}/discussion_topics — body { "title", "message", "is_announcement": true } |
| Pages | GET /courses/{id}/pages, GET /courses/{id}/pages/{url_or_id} |
| Modules | GET /courses/{id}/modules?include[]=items |
| Enrollments | GET /courses/{id}/enrollments |
Canvas models a submission comment as an update to the submission (a
PUTwithcomment[text_comment]), not a separate comments collection — there is noPOST …/submissions/{id}/comments. This app uses the correctPUTshape.
List endpoints paginate with page (1-based) + per_page (max 100) and return the
page relations in the RFC 5988 Link response header
(<…?page=2…>; rel="next"), parsed by parseLinkHeader / nextPage.
4. The summarize-course / draft-feedback flow
- Connect the app (
/oauth/install) so the service holds a Canvas token. - Open the panel and Load a course (the picker lists the courses the connected user can see). The panel pulls the course's syllabus, modules, assignments, pages, and discussions through the proxy and assembles a windowed, truncation-honest context (Canvas HTML is stripped to plain text).
- Summarize course — the model writes a course overview from the syllabus and module structure, grounded only in the records.
- Draft feedback — pick an assignment and Load submissions; the model drafts specific, constructive feedback for a student submission, grounded in what the student actually wrote and the assignment's requirements. The result lands in the Result box.
- Optional write-backs (gated, behind a confirm):
- Post as submission comment posts the Result back via
PUT …/submissions/{user_id}withcomment[text_comment], targeting the first submitted student on the loaded assignment. - Post as announcement posts the Result as a course announcement via
POST …/discussion_topics(is_announcement: true), first line as the subject.
- Post as submission comment posts the Result back via
The other actions — Generate questions (quiz + discussion prompts from a page/module), Summarize submissions (submitted/missing/late, score spread, common strengths and problems), and Draft announcement — run the same way.
Build & run
pnpm install
pnpm --filter @hanzo/canvas-lms build # → dist/ (panel + server)
pnpm --filter @hanzo/canvas-lms test # vitest
pnpm --filter @hanzo/canvas-lms typecheck # tsc --noEmit
# run the service
CANVAS_HOST=school.instructure.com \
CANVAS_CLIENT_ID=… \
CANVAS_CLIENT_SECRET=… \
CANVAS_REDIRECT_URI=https://canvas.hanzo.ai/oauth/callback \
node packages/canvas-lms/dist/server.js
Required environment (service)
| Var | Notes |
|---|---|
CANVAS_HOST |
Institution Canvas host, e.g. school.instructure.com. Normalized (scheme/path stripped). |
CANVAS_CLIENT_ID |
Developer Key client id (public). |
CANVAS_CLIENT_SECRET |
Server only. From KMS in production. |
CANVAS_REDIRECT_URI |
Must match the key's registered redirect. |
PORT |
Listen port (default 8791). |
readServerConfig throws on a missing value — the service refuses to start rather
than pretend it can complete an OAuth exchange or reach an unknown Canvas host.
Deploy over hanzoai/ingress
Host the static panel (dist/index.html, app.js, styles.css) with the
hanzoai/static plugin and run dist/server.js as a small service, both behind
hanzoai/ingress at canvas.hanzo.ai (no nginx, no caddy):
/and the static assets → the panel./oauth/*,/proxy/*,/healthz→ the service.
Secrets come from KMS as KMSSecret-synced env; sessions from Valkey
(hanzoai/kv) for multi-instance. Image published to
ghcr.io/hanzoai/canvas-lms:<tag> by CI/CD (platform.hanzo.ai / Tekton) — never
built locally.
Alternative embed path: LTI 1.3
This package ships the OAuth2 + REST API app (an external app the instructor
connects, ideal for a standalone panel at canvas.hanzo.ai). Canvas also supports
LTI 1.3 / Advantage as an in-Canvas launch: Canvas issues a signed OIDC launch
JWT and the tool calls the same REST API (or LTI Advantage services — Names & Roles,
Assignment & Grade Services). LTI is the better fit for a deep in-course placement
(course navigation, assignment menu) and passes the course context (custom_canvas_course_id)
in the launch — which parseLaunchContext already reads. The AI + context modules
(course.ts, hanzo.ts, actions.ts, canvas-api.ts) are transport-agnostic and
would be reused unchanged behind an LTI launch; only the auth front-door differs.
This build ships the OAuth+API app.
Routed through api.hanzo.ai. Answers are grounded in your Canvas course records —
a draft for an educator to review, not an authoritative grade, decision, or the
final word to a student.