Skip to content
LongsightGroupPublic

Latest commit

 

History

276 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Lantern

Lantern runs untrusted and semi-trusted learning apps through one governed LMS integration boundary.

It handles LTI login and launch, Deep Linking, reviewed runtime delivery, grade publishing, evidence, and admin tools so app packages never need raw LMS credentials, direct database access, or direct grade-write power.

Try A Browser Autograder In 2 Minutes

If you want to evaluate Lantern as a JS/TS teaching tool, start here instead of with the full platform install:

  1. Install Node.js 26 LTS and run npm ci.
  2. Run npm run app:preview -- examples/apps/typescript-ladder-game.
  3. Open the localhost URL printed by the command.

This path does not need LMS setup or a Cloudflare account. It runs the same browser authoring seam Lantern uses for local package preview, so you can judge the app model first.

Run Lantern Locally

When you want the full local admin and review flow, use the built-in local bootstrap path:

npm run local:init
npm run local:bootstrap
npm run local:start

Then open the localhost URL printed by the Cloudflare Vite dev server.

For arbitrary reviewed packages, use /admin/packages/import. Reference apps remain available at /admin/packages/reference.

What these commands do:

  • local:init writes .env.local, generates a local development signing key, and creates Lantern's local artifact directories
  • local:bootstrap applies local D1 migrations through cf
  • local:start runs Lantern through cf with cloudflare.config.ts

JS/TS Authoring Path

Lantern app authoring is intentionally narrow:

  1. Scaffold one reviewed package from a curated starter.
  2. Edit HTML, CSS, JavaScript, and JSON content.
  3. Validate the package.
  4. Execute preview assertions through Lantern's local browser seam.
  5. Open a live preview URL only when that helps.
  6. Import the same package into Lantern for review and inventory.

Start with:

Common path:

npm run app:new -- /tmp/my-lantern-app --starter=simple-activity --app-id=my-app --title="My App"
npm run app:dev -- /tmp/my-lantern-app

app:dev starts one local loop with a stable preview URL. It reruns package validation and preview assertions after file saves, shows blocked package diagnostics clearly, and reloads the blocked preview page after the next valid save.

Use --starter=browser-autograder when the reviewed package needs grading/specs/*.js plus evidence/example-output.json.

What Lantern Is Good For

  • running untrusted or semi-trusted learner-facing apps through one LMS boundary
  • packaging browser-first learning activities as signed, versioned app packages
  • keeping LMS credentials and service calls in Lantern instead of in app code
  • serving reviewed browser assets through a read-only Dynamic Worker envelope
  • supporting Deep Linking without ad hoc placement setup
  • publishing grades and roster checks from the server
  • giving admins one place to inspect deployments, retries, and audit records

For the plain-language trust promise behind this model, see TRUST_MODEL.md.

For the exact boundary App Writer-generated packages must stay inside, see GENERATED_APP_CONTRACT.md.

What Lantern Is Not

  • a general app hosting platform
  • an LMS replacement
  • a pile of compatibility shims for every LMS quirk
  • a way to hand raw LMS tokens or D1 database access to generated apps
  • a way to let generated apps write grades or call arbitrary outbound services
  • a backend framework for arbitrary server code inside app packages

Security Model

Lantern's answer to institution-built and AI-built learning tools is not to give every tool its own LMS integration. App code runs as reviewed, browser-first packages behind Lantern's gateway. Lantern owns the LTI boundary, runtime session, storage bridge, grading flow, evidence trail, and audit record.

On Cloudflare, that model maps to one narrow platform path:

  • Cloudflare Workers validate launches, create runtime sessions, broker capabilities, and publish grades.
  • Cloudflare D1 stores trusted product state such as reviewed packages, placements, attempts, runtime sessions, and audit records.
  • Cloudflare R2 stores immutable reviewed package artifacts and evidence bytes.
  • Worker Loader / Dynamic Workers serve approved browser assets from a reviewed package identity without receiving LMS tokens, a D1 binding, or generic outbound capability.

This keeps the blast radius of app code small: reviewed packages can render the learner experience and request only the capabilities Lantern exposes, while credentials, persistence, grade writes, and durable audit evidence stay inside the trusted platform boundary.

How It Works

At a high level:

  1. An app package is imported, reviewed, and versioned.
  2. Lantern stores one exact LMS setup record for each supported deployment.
  3. The LMS launches Lantern over LTI 1.3.
  4. Lantern validates the launch, creates a runtime session, and loads the app package.
  5. Lantern, not the app, owns Deep Linking return, placement creation, grade publication, and audit records.

This keeps the design simple:

  • apps get launch context, content, and the features Lantern exposes to them
  • apps do not get raw LMS access tokens
  • apps do not get arbitrary outbound HTTP
  • apps do not get direct grade-write power
  • apps do not get direct D1 database access

LTI In Reality

Lantern implements LTI against real LMS behavior, not just the clean examples from the spec.

Examples of realities Lantern already accounts for include:

  • exact launch verification against the saved LMS setup
  • one JWKS refetch during key rotation races
  • Deep Linking replay protection
  • sparse or partial AGS line item responses
  • one token refresh retry on LMS service 401s
  • proxy-aware public origin resolution for Cloudflare Workers and other reverse proxy deployments
  • audit events when Lantern has to take a recovery path

The goal is still one correct path. Lantern only adds recovery behavior where real LMS behavior requires it and where the behavior stays clear.

Launch State Without Third-Party Cookies

Lantern does not need a browser cookie to track LTI launch state.

Instead, Lantern treats state as a one-time server-side record:

  1. On login initiation, Lantern generates a random state and nonce.
  2. Lantern stores them with the expected LMS identity and launch facts: issuer, client_id, deployment_id, target_link_uri, expiry, and used_at.
  3. Lantern sends the random state and nonce to the LMS authorization flow.
  4. On the signed launch back, Lantern loads the saved record by state.
  5. Lantern verifies that the launch JWT matches the saved record, especially nonce, deployment_id, and target_link_uri.
  6. Lantern atomically marks the state as used so it cannot replay.

This means the browser does not need to hold the real launch state in a third-party cookie. If the LMS is embedding Lantern in an iframe and the browser blocks third-party cookies, the launch can still be correlated because the real state lives in Lantern's D1 database.

Cloudflare Deployment

Lantern's canonical full-app runtime is Cloudflare Workers with D1, R2, and Worker Loader bindings.

For public LTI URLs, Lantern resolves its public origin in this order:

  1. Forwarded / X-Forwarded-* request headers
  2. APP_ORIGIN
  3. the raw request origin

This keeps LTI config, login redirects, dynamic registration callbacks, and Deep Linking responses consistent behind Cloudflare domains, routes, and previews.

The repo treats the Workers entrypoint at src/worker.ts as the canonical runtime path. The local app wrapper in src/test_helpers/app.ts exists for injected tests and package preview tooling; full persistence runs through the Worker DB binding. The typed deployment configuration lives in cloudflare.config.ts, with the browser service defined in cloudflare.platform.ts.

Current Workers boundaries:

  • Lantern still owns the top-level reviewed runtime session document, signed bootstrap injection, JSON content bridge, local-state/event/evidence/score mutations, and audit boundary
  • on Workers, immutable reviewed entrypoint HTML, reviewed static assets, and reviewed browser-grader files launch through a reviewed-identity-keyed Dynamic Worker envelope created from a Worker Loader binding named LOADER
  • that Dynamic Worker envelope is browser-first and read-only: it serves only reviewed immutable bytes, receives no D1 database, LMS, or generic outbound capability, and sets globalOutbound: null
  • reviewed package snapshots are also written to and reloaded from that same PACKAGE_ARTIFACTS bucket on Workers
  • arbitrary reviewed package imports and curated reference app imports follow the same governed snapshot flow and land under var/packages/<app-id>/<version>/...
  • repository-backed Worker routes use D1 through a binding named DB
  • curated reference package imports on Workers read source files from PACKAGE_ARTIFACTS under reference-packages/<app-id>/source
  • the Cloudflare configuration enables nodejs_compat and Smart Placement because Lantern makes multiple D1 queries per request
  • the gateway configuration declares the LOADER binding used to cache Dynamic Workers by reviewed runtime identity instead of by session id

The R2 bucket should contain two key layouts:

  • reviewed snapshots under var/packages/<app-id>/<version>/..., for example var/packages/<app-id>/<version>/dist/index.html
  • curated reference package sources under reference-packages/<app-id>/source/..., for example reference-packages/chapter-4-asteroids/source/manifest.json

At import time Lantern persists the immutable snapshot, derives the reviewed artifact digest from stored files, and signs the reviewed runtime contract that runtime launch later verifies. On Workers, Lantern uses that reviewed identity to key the Dynamic Worker envelope, so the same approved package bytes can be reused across sessions without turning reviewed packages into arbitrary server-side apps. The Worker app path no longer depends on local disk for demo package import or reviewed runtime delivery. Provision the curated reference package source files into PACKAGE_ARTIFACTS during Cloudflare bootstrap or release with:

npm run reference:sync -- --bucket=replace-with-package-artifact-bucket

This command uses the installed cf r2 objects put CLI. Authenticate with cf auth login or provide a Cloudflare API token before syncing remotely. Local syncs use .cloudflare/state, the same directory as the dev server.

npm run build uses cf build and the Cloudflare Vite plugin to build the gateway, the browser service, and the Python container. Build Output is written under .cloudflare/output/v0/ and is not checked in. npm run check:worker verifies the build and required bindings, and rejects Deno-only references in Worker modules. npm run check:deploy validates both deployable Workers without API requests. Use npm run deploy when ready to publish: it builds once, deploys the browser service explicitly, then deploys the gateway and its Python container.

To run the Worker locally:

  • set Worker vars such as APP_ORIGIN, BETTER_AUTH_SECRET, and LTI_TOOL_PRIVATE_JWK in .env.local
  • keep the LOADER Worker Loader binding present; Lantern uses it for the governed Dynamic Worker envelope around immutable reviewed runtime delivery
  • keep the DB D1 binding present; it is the Worker persistence binding
  • apply D1 migrations from src/db/d1_migrations before using repository-backed Worker routes

Google admin sign-in is optional. To enable it, create a Google OAuth web client with these redirect URIs:

  • https://lantern.appboundary.com/api/auth/callback/google
  • http://localhost:8417/api/auth/callback/google

Then configure both GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET as Worker secrets, or put both values in local .env.local. Setting only one value fails startup clearly.

  • if you want the demo import path to work on Workers, provision the curated reference sources into the same bucket first with npm run reference:sync -- --bucket=<bucket-name> or npm run reference:sync -- --bucket=<bucket-name> --local --persist-to=<dir> before calling /admin/packages/import-reference
  • then run npm run local:start

The reviewed runtime contract remains the same across local Node.js preview and the Cloudflare-native Worker path. The browser still sees one Lantern-owned session document plus the same GatewayBootstrap and GatewayApp bridge. What changes on Workers is only the immutable delivery substrate for reviewed browser-first package bytes. This is a governed reviewed-runtime envelope, not a generic code-hosting path for arbitrary uploaded server logic.

Public Entry Points

Stack

  • Node.js 26 LTS and TypeScript 7
  • Hono
  • Cloudflare Workers
  • Cloudflare D1
  • Cloudflare R2
  • Cloudflare Worker Loader / Dynamic Workers
  • hand-written SQL
  • server-rendered HTML
  • plain CSS tokens

Local Development

Use Node.js 26.13.0 and npm 12.2.1, then install the locked dependencies with npm ci. The Node version is recorded in .node-version.

Vite+ runs formatting, linting, TypeScript checks, and tests. App Writer uses the installed TypeScript 7 compiler for local source compilation; it does not fetch compiler libraries at runtime. Package and rubric validators are precompiled from schemas/ so Workers never compile schemas with new Function. Run npm run schemas:generate after changing a schema; validation checks for stale output. Cloudflare's Vite and Vitest plugins use the same pinned Miniflare 5 runtime. Node tests run in the node Vite+ project; D1, RPC, and Browser Rendering tests run inside workerd in the workers project. Cloudflare's test pool owns their runtime and storage lifecycle.

Cloudflare Puppeteer is pinned to 1.5.0-rc.0 because it removes the vulnerable legacy browser-download dependency tree. CI runs the dependency audit to catch new advisories. Miniflare also embeds an older downloader that npm's audit cannot see. The install step replaces that bundled copy with pinned @puppeteer/browsers 3.2.6, removes its unused legacy dependencies, uses current Chrome, and keeps the Chrome sandbox enabled in CI. The patch verifies the upstream and patched file hashes and fails on unexpected changes; review it when upgrading Miniflare. The current cf beta also imports two removed Miniflare helpers. A second hash-checked install patch redirects those imports to Cloudflare's maintained workers-utils entrypoints. Review that patch when upgrading cf.

Run npm run browser:install after installing dependencies to download the pinned Chrome version. Local preview assertions execute generated JavaScript in Chrome's sandboxed renderer, with no Node objects or inherited host environment. Lantern handles preview requests and blocks external requests. No DOM emulator executes app code in the host process.

CI installs Chrome into a fresh cache and runs both local preview regression tests and npm run check:browser, which exercises the production App Writer preview handler in the Workers test pool with a real Browser Rendering binding. Linux hosts must allow Chrome's sandbox user namespaces; CI enables them explicitly instead of disabling the browser sandbox.

Fast path:

npm run local:init
npm run local:bootstrap
npm run local:start

Open the localhost URL printed by the Cloudflare Vite dev server.

Generated local environment:

  • .env.local: written by npm run local:init
  • the Vite dev server listens on port 8417
  • APP_ORIGIN: defaults to http://localhost:8417
  • APP_RUNTIME_ORIGIN: defaults to http://localhost:8417 so local preview and reviewed runtime stay on one origin
  • BETTER_AUTH_SECRET: generated automatically for local admin sessions
  • LTI_STATE_SECRET: generated automatically for local development
  • LTI_TOOL_PRIVATE_JWK: generated automatically for local development

Manual minimum environment if you do not use local:init:

  • LTI_STATE_SECRET: one stable secret with at least 32 UTF-8 bytes used for LTI OIDC state signing
  • LTI_TOOL_PRIVATE_JWK: one RSA private JWK used to publish Lantern's public JWKS and sign LTI assertions
  • APP_ORIGIN: recommended for stable local config and required anywhere Lantern must publish fixed public URLs without proxy headers
  • APP_RUNTIME_ORIGIN: required anywhere Lantern must publish runtime URLs for attempts and app sessions
  • BETTER_AUTH_SECRET: one stable secret with at least 32 UTF-8 bytes used for admin auth cookies and sessions

Common commands:

  • npm run local:init
  • npm run local:migrate
  • npm run local:bootstrap
  • npm run local:start
  • npm run local:dev
  • npm run app:new -- --list-starters
  • npm run app:new -- <output-root> --starter=<id> --app-id=<app-id> --title="<title>"
  • npm run app:dev -- <package-root>
  • npm run app:validate -- <package-root>
  • npm run app:test-preview -- <package-root>
  • npm run app:preview -- <package-root>
  • npm run reference:sync -- --bucket=<bucket-name>
  • npm run fmt
  • npm run fmt:check
  • npm run lint
  • npm run check
  • npm run check:worker
  • npm run test
  • npm run validate

Default local port: 8417

Repo Map

  • src/lti/: LTI login, launch validation, Deep Linking, service calls, and config publishing
  • src/package_review/: package import, review persistence, placements, runtime session persistence, and audit events
  • src/runtime/: runtime and grade publishing code
  • src/admin/: server-rendered admin pages
  • sdk/: app-facing contract
  • examples/: sample app packages
  • schemas/: public package schema

Current Scope

This repo is the public code and docs. It intentionally focuses on:

  • runtime interfaces
  • schemas
  • SDKs
  • sample apps
  • public implementation code

Private strategy, security notes, infra specifics, pricing, and other internal operational material stay out of this repo.

Releases

Packages

Contributors

Languages