Skip to content

emergent as an LTI 1.3 Platform

Status: accepted

emergent today is a fully conformant LTI 1.3 tool/provider (LTI 1.3 26/27 on the conformance board; apps/api/src/features/lti-tool/* on the vendored @lti-tool/core + @lti-tool/postgresql adapter). This ADR records the decision to build the inverse role — emergent as an LTI 1.3 platform, the LMS side that launches into external tools. Verifying becomes signing; consuming NRPS/AGS becomes serving them; receiving a Deep Linking response becomes issuing the request. It is a major LMS product capability, adds a brand-new Platform conformance profile to the board (distinct from Tool certification), and unlocks the Common Cartridge CC-LTI-1 rider (a cartridge’s Basic LTI Link becomes a launchable link instead of quarantine). The decisions below come from a grilling/design session; the glossary terms are in CONTEXT.md → External tools (LTI platform).

“Platform” throughout is the LTI role term (the LMS that launches tools), not the everyday “the platform” (emergent itself).

Scope — full breadth, all six profiles inverted, plus Dynamic Registration

Section titled “Scope — full breadth, all six profiles inverted, plus Dynamic Registration”

The v1 platform role targets full LTI Advantage parity with the tool board, inverted: core launch, security, NRPS (serving), AGS (receiving), Deep Linking (issuing), and the platform-side Proctoring role, plus LTI Dynamic Registration. (A product call — max standards coverage now, refine on customer pull; these are industry standards, not novel invention.) Build is sliced smallest-blast-radius-first (see Slice plan), each slice conformance-proven before the next.

Identity & registration — one issuer, two-level (global catalog + per-org entitlement), lazy deployment

Section titled “Identity & registration — one issuer, two-level (global catalog + per-org entitlement), lazy deployment”
  • One Platform Issuer = the emergent instance’s stable iss, across every tool.
  • Two-level registration. A platform-admin registers an External Tool once in a global catalog (the client_id emergent assigns, OIDC login-initiation URL, redirect URIs, JWKS/keyset URL, default target + custom params). An org is then entitled to use it (Tool Entitlement) — the governance point of the two-level model: registration is global, use is granted per-org.
  • Lazy per-org deployment. A Tool Deployment (carrying the LTI deployment_id) is minted on the first Resource Link placement within an entitled org — not up front. The deployment table is 1→N per tool, so per-org deployments are intrinsic, not a future reshape.
  • Both manual and Dynamic Registration. Manual configuration is the foundation (the data model and every flow work off it); LTI Dynamic Registration (the OpenID-based handshake — emergent exposes an openid-configuration + registration endpoint, distinct from the RFC 7591 generic reg the Badge Connect host already does) is a fast-follow slice with its own conformance reqs.
Section titled “Placement — Resource Link = activity unit (tree) or course-level, sidecar-backed”

A Resource Link is a placement of a deployed tool, backed by a sidecar lti_platform_resource_link row. It is launchable in two shapes (same record, nullable learning_unit_id):

  • Content-tree placement — a leaf activity Learning Unit, sequenced and release-rule-aware exactly like external links / packages / assessments today.
  • Course-level placement — an offering “External Tools” area, for tools meant to launch outside the linear content sequence.

Deep-linked content items a tool returns become Resource Link activities under a chosen parent unit. The launch context claim maps to the Offering.

Launch & subject identity — OIDC + RS256 id_token, pairwise subject, per-deployment PII

Section titled “Launch & subject identity — OIDC + RS256 id_token, pairwise subject, per-deployment PII”
  • A Launch is OIDC third-party-initiated login from emergent → tool, completed by a platform-signed id_token (RS256) carrying the LTI claims (the inverse of the tool’s verifyLaunch). emergent hosts the platform JWKS + the OIDC auth endpoint.
  • Pairwise subject. sub is minted stably per (Person, External Tool) — realised as an Identity Binding scoped to the tool, the same pairwise discipline credentials use — and reused as the NRPS member user_id so a tool can correlate the launching user to the roster without being able to correlate that learner across tools.
  • PII release is per-deployment-flagged (name / email / picture), default conservative (send name, withhold email unless enabled).

Token endpoint — standalone, NOT routed through the @jmondi AS

Section titled “Token endpoint — standalone, NOT routed through the @jmondi AS”

LTI’s token endpoint is client_credentials grant with private_key_jwt client authentication (the tool presents a signed client_assertion verified against its registered JWKS). The existing Badge Connect AS core (@jmondi/oauth2-server@4.3.5) has a client_credentials grant but zero support for private_key_jwt / client_assertion (verified: no occurrence anywhere in its dist — it authenticates clients by client_secret only). So the LTI token endpoint is net-new and standalone: verify the tool’s client_assertion JWT against its registered JWKS (via jose), check requested scopes ⊆ granted, and mint a scoped access token by reusing the stateless HS256 access-token.ts codec already shared by the OneRoster provider and Badge Connect host. emergent is both AS (mints at the token endpoint) and RS (verifies the same token at NRPS/AGS) → one secret, one codec, no token table.

emergent’s org → offering → membership model is the authoritative roster. The platform serves a Membership container (with Link: rel=next paging — mirroring the contract the tool side consumes) directly from native membership for any offering with a placed tool. There is no separate “LTI-origin” roster source; the platform always serves its own.

AGS — first-class line items, fully integrated into the gradebook (NOT Assessments)

Section titled “AGS — first-class line items, fully integrated into the gradebook (NOT Assessments)”

An incoming AGS line item is a gradebook column, but it is not an Assessment — it has no Item, Attempt, or test structure, and assessment_type is a pedagogical purpose (diagnostic/formative/summative), not a “kind of gradable thing”. Coercing it into assessment_definition / gradebook_entry (whose assessment_definition_id is NOT NULL) would be a domain category error. Instead:

  • AGS line items/scores land in first-class lti_platform_line_item + lti_platform_line_item_result tables tied to the resource link / context (mirroring the tool side’s separate lti_tool_ags_line_item_mapping).
  • They are fully integrated: surfaced as real columns in the main gradebook grid, weightable in a Grade Category, and exported through the OneRoster gradebook provider. (This is the larger blast radius into the gradebook read-model + OneRoster provider — accepted.)
  • The receiving surface is complete: accept line-item create/update/delete, accept scores, serve results + the line-item container back — every AGS req interop-provable.

Deep Linking — issue a signed request, validate the returned content_items

Section titled “Deep Linking — issue a signed request, validate the returned content_items”

The platform signs an LtiDeepLinkingRequest (id_token with deep_linking_settings), launches the tool’s DL flow, and validates the returned content_items JWT (signed by the tool, verified against its registered JWKS) before placing the items as Resource Links. Canonical offering images (ADR-0035) are reused where a content item wants a thumbnail/icon.

emergent-as-platform drives an external proctoring service: it sends the StartProctoring message and receives/validates the tool’s StartAssessment/EndAssessment messages — the inverse of the tool-side proctoring emergent already ships.

Key custody — env keypair, mirroring the tool side

Section titled “Key custody — env keypair, mirroring the tool side”

The platform’s RS256 id_token signing key lives in env (LTI_PLATFORM_PRIVATE_JWK_JSON / LTI_PLATFORM_PUBLIC_JWK_JSON); the platform JWKS endpoint serves the public half; rotation publishes new+old via an env JWK array while tools re-cache. This is the exact symmetric inverse of the existing tool key custody (LTI_TOOL_*) — same security posture, no Vault role and no DB-batch step for keys. (Vault-backed custody, mirroring credential issuer keys, was considered and rejected for v1: the id_token key is operationally a service key like the tool’s, not a per-tenant credential-issuer key.)

CC-LTI-1 rider — match to a registered tool, else a visible pending placement

Section titled “CC-LTI-1 rider — match to a registered tool, else a visible pending placement”

On cartridge import, an imsbasiclti Basic LTI Link resolves its launch URL/domain to a registered External Tool. If matched → a launchable Resource Link placement (auto-deploy within the entitled org). If unmatched → a visible pending-registration placement / import- report entry with an actionable “register this tool” prompt — never silent quarantine. A cartridge cannot bootstrap a full 1.3 registration (it carries no OIDC login URL/JWKS), and auto-registering a tool from untrusted cartridge data would be a security hole — both rejected. This flips cc:1.3:conf:lti-link/CC-LTI-1 from not-applicable to done.

  • Reuse (verify + wire, do not rebuild): RS256 signing via jose (the tool already signs this way); the stateless HS256 access-token.ts codec for the access token; conform-ed’s bidirectional LTI contracts (CoreLaunchClaimsSchema, MembershipContainerSchema, LineItem/Score/ResultSchema, DL response schemas) — the platform’s serving endpoints validate output against the same schemas the tool consumes; the native roster (NRPS source); the gradebook + OneRoster provider (AGS sink); ADR-0035 canonical images (DL).
  • Build new: the lti_platform_* data model; the OIDC login-initiation + RS256 id_token launch; the platform JWKS + OIDC auth endpoint; the standalone token endpoint; NRPS/AGS serving/receiving endpoints; the DL issue+validate flow; the platform Proctoring flow; Dynamic Registration; teacher placement/DL UI + admin registration/entitlement UI.

Conformance — platform catalogue on the same map, inverted interop lane

Section titled “Conformance — platform catalogue on the same map, inverted interop lane”
  • The LTI Coverage Map (docs/conformance/maps/lti-v1.3.json, conform-ed packages/coverage/maps/lti-v1.3.json) is 100% tool-role (27 reqs, no role field). conform-ed curates a platform-role catalogue (ADR-0017 curated denominators — LTI publishes no schema for most of this; the message schemas already model both directions) as new platform-* profiles on the same map (platform-core, platform-security, platform-nrps, platform-ags, platform-deep-linking, platform-proctoring, platform-dynamic-registration) with PLAT-* reqIds. New profile strings render as a distinct Platform board section automatically — no map/overlay schema change.
  • Proof vehicle = the inverted interop lane (“interop proof = done”, ADR-0033). A new tests/conformance/lti-platform-interop.conformance.test.ts boots an in-process mock TOOL that drives emergent’s REAL platform: initiates login, receives the signed id_token and verifies it against emergent’s platform JWKS, calls the token endpoint with private_key_jwt, reads NRPS, posts AGS line items/scores, returns a DL response. DB- backed via run-integration-suite.ts; added to prove.ts so the conformance-required CI job enforces it. Every platform req goes done only when exercised end-to-end.
  • The board flip (overlay → done) is gated on the cross-repo flow: conform-ed curates + releases the catalogue → FF conform-ed main@dev publish → bun update (no args) in emergent → re-pin the emergent map (refresh coverageMapSha256). The code + interop lane are not gated on it.

Slice plan (smallest blast radius first, each conformance-proven)

Section titled “Slice plan (smallest blast radius first, each conformance-proven)”
  1. conform-ed: curate the platform-role catalogue (platform-* profiles). (gates the board flip, not the code)
  2. Foundation + Core launch: lti_platform_* schema (one migration); manual tool registration; OIDC login-init + RS256 id_token launch + platform JWKS/OIDC auth endpoint. Interop lane: mock tool logs in, verifies the id_token. → core + security.
  3. Token endpoint + NRPS serving: standalone client_credentials+private_key_jwt token endpoint; paged NRPS from native roster; pairwise subjects + PII flags. → nrps.
  4. AGS receiving + gradebook integration: line-item/score endpoints; first-class tables; gradebook-grid + Grade Category + OneRoster-export integration. → ags.
  5. Deep Linking: issue signed DL request; validate returned content_items; place. → dl.
  6. Proctoring (platform role). → proctoring.
  7. Dynamic Registration: openid-configuration + registration endpoint. → dynamic-reg.
  8. Teacher/admin UX: teacher-portal placement + course-level Tools area + DL selection; admin-console global registration + entitlement.
  9. CC-LTI-1 rider: wire the cartridge importer (match-to-registered-tool). → CC-LTI-1.
  • Reverses the CONTEXT.md “LTI links have no launch path” note — Basic LTI Links now have a home (updated in the glossary).
  • Larger blast radius into the gradebook read-model + OneRoster provider (AGS columns) — accepted as the cost of full integration.
  • Anton-only follow-ups: FF conform-ed main (catalogue) → re-pin emergent map; FF emergent main; apply the lti_platform_* migration (generated per the runbook, never hand-edited; all related tables in one migration). No new Vault role (env keys).
  • Does not regress the 26 tool-role reqs, touch the QTI model-coverage work, or touch the open follow-ups (proctoring/CC-AUTH commits, the upstream lti-tool PR).