---
status: walked + e2e-covered
coverage_depth: deep
last_walked: 2026-07-02
last_reconciled: 2026-07-18
specs:
  - apps/oshun/web/e2e/lms-connectors.spec.ts
  - apps/oshun/web/e2e/operator-lms.spec.ts
source: WALKTHROUGH/journeys/lms-connectors-operator.md
---

# Journey result: LMS connectors — operator visibility and validation

- **Walked**: 2026-07-02 against local real-BFF development infrastructure. The
  retained result names the suites but not an immutable commit or raw run
  artifact.
- **Reconciled**: 2026-07-18 against the current journey, operator page,
  connector runtime, OneRoster preview route/store, LTI launch path, and both
  focused Playwright specs. This was source review, not a fresh application run.
- **Verdict**: **pass / deep for the shipped operator flow** — current evidence
  deeply covers roster visibility, rejected boot descriptors, dry-run
  validation, runtime-local create/override/delete semantics, non-mutating
  OneRoster preview, and an LTI launch that fails closed on JWKS outage. The
  grade does not claim durable connector governance or successful third-party
  LMS interoperability.
- **Current authority**:
  [`WALKTHROUGH/journeys/lms-connectors-operator.md`](../journeys/lms-connectors-operator.md)
  defines the same runtime-local and preview-only boundaries.

## Result at a glance

| Evidence plane        | Shipped behavior                                                                                                            | Current-source proof                                                                                                                               | Authority limit                                                                                                                            |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Roster visibility     | `/operator/lms` reads the admin connector roster and renders valid rows plus every rejected environment descriptor          | Real server-component GET, seeded Canvas row, exact Moodle issue codes, scope denial, source badges, capability/LTI fields, and summary projection | A null server fetch becomes a locally timestamped zero-row state, so wrong scope, outage, and a genuinely empty roster look the same       |
| Descriptor validation | Parsed candidates reach `POST /v1/admin/lms/connectors/validate`; malformed JSON never leaves the browser                   | Missing ids return 400; protocol-less input returns ordered issues; valid input returns `valid: true`; 503 and pending states are visible          | Dry-run only: it neither registers nor persists the candidate                                                                              |
| Connector lifecycle   | Admin POST/DELETE creates, updates, overrides env state, restores env state, or tombstones an env-only row                  | Pure-BFF CRUD proves create→read→override→restore→delete; browser CRUD proves save, refreshed roster, and delete                                   | `Map` + `Set` state lives in one BFF process; no snapshot sink, mutation audit event, restart proof, or multi-instance convergence         |
| OneRoster preview     | The page compares normalized users/enrollments with the current committed roster and renders create/update/block rows       | Browser seeds a real apply, previews clean and conflicting payloads, then proves roster counts are unchanged                                       | The backing roster can be durable, but this surface exposes no apply control and preview writes nothing                                    |
| LTI boundary          | Login creates state/nonce and an authentication URL; launch rejects an unverifiable token when provider JWKS is unavailable | Real HTTP proves the seeded authorization URL and exact `502 lti_jwks_unavailable` response with no session or subject                             | State is process-local; no successful signed provider callback, SCORM journey, external registration, or provider conformance run is cited |

## Evidence map

Solid edges are exercised by the named suites. Dashed edges are the production
meaning that the result does not earn.

```mermaid
flowchart TB
    A[Admin-scoped operator] --> B[Inspect live and rejected descriptors]
    B --> C[Dry-run candidate]
    C --> D[Create, override, restore, or delete]
    D --> E[Preview OneRoster diff]
    E --> F[Exercise LTI login and launch boundary]
    C --> G[Connector runtime unchanged]
    D --> H[Process-local Maps and tombstones]
    E --> I[Committed roster unchanged]
    F --> J[JWKS outage rejected]
    H -. not implemented .-> K[Durable audited connector registry]
    J -. not demonstrated .-> L[Successful live-provider launch]
```

## Proven observations

### The operator surface is live, secret-safe, and intentionally fail-soft

- The server page calls `GET /v1/admin/lms/connectors` through `bffGet`. The
  admin route admits `admin:*` or `admin:workspace:tenant-console`, while no
  bearer, customer, and `admin:studio` callers are rejected by the BFF.
- Valid environment descriptors enter the live connector framework. Invalid JSON
  and invalid descriptor rows are retained separately with connector id, tenant
  id, code, path, severity, and message instead of disappearing at boot.
- `summarizeConnector` returns identity, source, provider, display/status,
  environment, capability and limited LTI metadata. It omits the authorization,
  token, and JWKS endpoints; the BFF spec also guards against `tokenUrl`
  leakage.
- The page deliberately turns any null `bffGet` result into empty connector and
  invalid arrays. It also synthesizes `generatedAt` from the web process. The
  wrong-scope browser case proves this fail-soft render, not an authorized
  observation that the tenant has zero connectors.

### Validation and runtime lifecycle are real HTTP, not fixture-only buttons

- The pure-BFF suite has four cases: roster gates/projection, dry-run
  validation, managed lifecycle, and LTI fail-closed behavior. The browser suite
  has eleven cases spanning the live roster, preview, CRUD, fail-soft state,
  navigation, validation branches, 503 handling, and pending-button semantics.
- Dry-run validation distinguishes transport validation from descriptor
  validation. Missing identity fields are a 400 request error; a structurally
  accepted descriptor with no protocol is a 200 result with `valid: false`; a
  complete LTI descriptor returns `valid: true`.
- Managed POST validates before mutation. The store returns `created`,
  `updated`, or `overrode_env`; DELETE returns `deleted`, `env_restored`, or
  `env_disabled`, and an unknown key returns `lms_connector_not_found`.
- Source precedence is deterministic within one process. A managed key shadows
  the matching environment row; deleting that override re-registers the env
  descriptor; deleting env-only state creates a tombstone until restart or
  reconfiguration.
- The browser CRUD and validation legs intercept their client requests and
  reissue them from Playwright with a server-generated tenant-console
  development bearer. They reach the real BFF and stores, but natural
  browser-token propagation is assisted in those cases.

### OneRoster preview reads durable authority without acquiring write authority

- The browser suite first commits a tenant roster through the real apply route,
  then sends two page-driven requests to `/v1/admin/lms/oneroster/preview`.
- The clean preview renders one user update, one user create, and one enrollment
  create with before/after values. The conflicting preview renders two blocking
  rows for a duplicate username and an unknown enrollment user.
- A final roster GET still reports the seeded two users and one enrollment.
  Preview calls `oneRosterApplyStore.preview`; it never calls `apply`.
- The apply store is wired to durable snapshots at BFF startup and its separate
  apply route emits audit events. Those stronger properties belong to the
  OneRoster sync journey, not to the preview button on this page.

### LTI evidence proves the safe failure branch

- Login resolves the seeded connector, creates 64-hex state and nonce values,
  stores them, and returns the expected Canvas authorization URL with client,
  state, nonce, redirect, and hint parameters.
- Launch consumes the state before fetching JWKS. The seeded unreachable
  provider produces `502 { error: "lti_jwks_unavailable" }`; the response has no
  session id or subject, so the unverified token is not accepted.
- `createTenantLmsRuntimeFromEnv` installs a real HTTP JWKS fetcher, but the
  named test never supplies a valid signed token from a reachable provider.
  SCORM routes exist in the same runtime and are not exercised by this result.

## Boundaries and gaps

- **Deep means the shipped operator contract, not production completion.** The
  inspected, validate, mutate, preview, and fail-closed branches are thorough
  for their declared scope.
- **A process-local override is not durable connector configuration.** Managed
  descriptors and env tombstones have no snapshot adapter; a restart can erase
  them or restore disabled environment rows.
- **A mutation response is not an audit trail.** Connector POST/DELETE does not
  record actor, before/after state, reason, or an admin audit event.
- **A fail-soft empty page is not a zero-connector fact.** Authorization
  failure, BFF outage, malformed response, and a genuine empty roster collapse
  to the same copy and a web-generated timestamp.
- **A preview is not an apply.** `canApply` describes a hypothetical commit;
  `/operator/lms` offers no commit id, conflict confirmation, or apply control.
- **Fail-closed is not provider interoperability.** The result proves rejection
  during a JWKS outage, not successful Canvas/Moodle login, NRPS/AGS behavior,
  SCORM launch, or certification against a live LMS.
- **Login state is process-local.** The default
  `InMemoryTenantLtiLoginStateStore` does not prove a login initiated on one BFF
  instance can complete on another.
- **External registration remains a precondition.** Oshun does not create the
  LMS-side client, deployment, redirect, keys, or scopes from this console.
- **The browser auth seam is assisted.** Playwright bearer injection proves the
  route and UI outcome while leaving the natural client-token path outside these
  interaction cases.
- **Responsive application coverage is not named.** The operator spec does not
  declare a 390px viewport case; this result page's own responsive verification
  does not upgrade the application evidence.
- **Source reconciliation is not execution.** Neither cited application suite
  was rerun for this documentation audit.

## Re-run evidence

Run the live BFF and production-like web app at one immutable commit, then run
the two focused suites sequentially with one worker:

```bash
pnpm --dir apps/oshun/web exec playwright test -c playwright.config.ts e2e/lms-connectors.spec.ts --project=chromium --workers=1
pnpm --dir apps/oshun/web exec playwright test -c playwright.config.ts e2e/operator-lms.spec.ts --project=chromium --workers=1
```

Retain the commit, exact `OSHUN_LMS_CONNECTORS` fixture, backing-store tier,
browser/BFF reports, and a restart result. A production-grade follow-up should
also retain connector mutation audit events, multi-instance state/nonce proof, a
natural browser-auth trace, and a signed launch against an approved LMS
conformance tenant.

## Source trail

- [Current journey contract](../journeys/lms-connectors-operator.md)
- [Current coverage registry](../journeys/coverage.md)
- [Operator LMS page](../../apps/oshun/web/src/app/operator/lms/page.tsx)
- [Connector CRUD panel](../../apps/oshun/web/src/components/lilith/LmsConnectorCrudPanel.tsx)
- [OneRoster preview form](../../apps/oshun/web/src/components/lilith/OneRosterDiffPreviewForm.tsx)
- [Connector admin and launch routes](../../apps/oshun/bff/src/tenant-console/lms-route.ts)
- [OneRoster preview/apply route](../../apps/oshun/bff/src/routes/admin-oneroster-apply.ts)
- [Durable OneRoster store](../../apps/oshun/bff/src/tenant-console/oneroster-apply-store.ts)
- [Pure-BFF connector spec](../../apps/oshun/web/e2e/lms-connectors.spec.ts)
- [Operator browser spec](../../apps/oshun/web/e2e/operator-lms.spec.ts)

## Cross-references

- [Operator LMS view evidence](../operator/operator-lms.md)
- [OneRoster sync journey](../journeys/tenant-oneroster-roster-sync.md)
- [OneRoster sync result](./tenant-oneroster-roster-sync.md)
- [Tenant SSO journey](../journeys/tenant-sso-config-saml-oidc-claim-mapping.md)
- [V1 deployment requirements](../../docs/audits/V1_DEPLOYMENT_REQUIREMENTS.md)

## Open questions

- Which tenant-scoped durable schema will replace the connector `Map`/tombstone
  set while keeping secret values behind references and supporting rotation?
- Which audit taxonomy records connector create, override, restore, disable, and
  delete with actor, reason, before/after hashes, and tenant scope?
- Should the roster distinguish unauthorised, unavailable, malformed, and truly
  empty states instead of presenting all four as “No LMS connectors configured”?
- Should `/operator/lms` expose the existing OneRoster apply route, including
  commit id, conflict confirmation, and audit read-back?
- Which shared state adapter will make LTI state/nonce consumption safe across
  multiple BFF instances without weakening replay protection?
- Which Canvas, Moodle, or certification environment will prove a valid signed
  launch and the enabled AGS/NRPS/SCORM capabilities end to end?
- When will the page interactions run through the natural browser auth path and
  a dedicated narrow-viewport application case?
