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).
Decision
Section titled “Decision”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_idemergent 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 is1→Nper 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.
Placement — Resource Link = activity unit (tree) or course-level, sidecar-backed
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
activityLearning 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’sverifyLaunch). emergent hosts the platform JWKS + the OIDC auth endpoint. - Pairwise subject.
subis 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 memberuser_idso 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.
NRPS — serve emergent’s own roster
Section titled “NRPS — serve emergent’s own roster”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_resulttables tied to the resource link / context (mirroring the tool side’s separatelti_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.
Proctoring (platform role)
Section titled “Proctoring (platform role)”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 vs build
Section titled “Reuse vs build”- Reuse (verify + wire, do not rebuild): RS256 signing via
jose(the tool already signs this way); the stateless HS256access-token.tscodec 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-edpackages/coverage/maps/lti-v1.3.json) is 100% tool-role (27 reqs, norolefield). 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 newplatform-*profiles on the same map (platform-core,platform-security,platform-nrps,platform-ags,platform-deep-linking,platform-proctoring,platform-dynamic-registration) withPLAT-*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.tsboots 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 withprivate_key_jwt, reads NRPS, posts AGS line items/scores, returns a DL response. DB- backed viarun-integration-suite.ts; added toprove.tsso theconformance-requiredCI job enforces it. Every platform req goesdoneonly 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→@devpublish →bun update(no args) in emergent → re-pin the emergent map (refreshcoverageMapSha256). 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)”- conform-ed: curate the platform-role catalogue (
platform-*profiles). (gates the board flip, not the code) - 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. - Token endpoint + NRPS serving: standalone
client_credentials+private_key_jwttoken endpoint; paged NRPS from native roster; pairwise subjects + PII flags. → nrps. - AGS receiving + gradebook integration: line-item/score endpoints; first-class tables; gradebook-grid + Grade Category + OneRoster-export integration. → ags.
- Deep Linking: issue signed DL request; validate returned content_items; place. → dl.
- Proctoring (platform role). → proctoring.
- Dynamic Registration: openid-configuration + registration endpoint. → dynamic-reg.
- Teacher/admin UX: teacher-portal placement + course-level Tools area + DL selection; admin-console global registration + entitlement.
- CC-LTI-1 rider: wire the cartridge importer (match-to-registered-tool). → CC-LTI-1.
Consequences
Section titled “Consequences”- 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 emergentmain; apply thelti_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-toolPR).