> ## Documentation Index
> Fetch the complete documentation index at: https://docs.safeonlineexam.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Canvas SEB Detector, Theme Loader, and Diagnostics Routes

> Stable detector URL, Canvas theme loader, compatibility alias, debug trace endpoint, and the server-owned YouTube player page for Safe Online Exam.

Safe Online Exam injects a JavaScript detector into Canvas quiz pages that reads the SEB Config Key hash, verifies it against the server, and fills the Canvas access-code prompt only when SEB is running the current configuration. This page documents the routes that deliver the detector and its associated assets — the stable script URL, the hosted Canvas theme loader, the compatibility alias, the debug trace endpoint, and the server-owned YouTube player — together with the behaviour and security constraints of each. The approved-tools endpoint (`GET /api/seb/tools/:courseId/:quizId`) is documented on the [SEB Student Routes](/api/seb-student-routes) page.

## Detector Script Delivery

### GET /js/canvas-seb-detector.js

The stable, canonical URL for the SEB detector script. Canvas theme loaders and `js_overrides` configurations should always point to this path.

The server injects deployment-specific values into the script at response time: the application base URL, the LTI client ID, the LTI deployment ID list, and the debug/diagnostics flags. In production mode with a configured base URL the injected response is cached in-process for identical configuration tuples, and the same cached bytes are served to every subsequent request with the same parameters. Debug and diagnostics modes serve a non-cached, human-readable version of the script so issues can be diagnosed without a deployment rebuild.

**Authentication:** None. This is a public, unauthenticated endpoint.

**Cache behaviour**

The `cache-control` response header depends on the deployment mode:

| Mode                         | `cache-control` value                 |
| ---------------------------- | ------------------------------------- |
| Debug or diagnostics enabled | `no-cache, no-store, must-revalidate` |
| Normal production            | `no-cache, must-revalidate`           |

The response also sets `pragma: no-cache`, `expires: 0`, and `vary: Origin, X-Forwarded-Host, X-Forwarded-Proto, X-Forwarded-Port` so reverse proxies and CDNs do not cache the injected deployment constants across different origins.

**Response**

```text theme={null}
Content-Type: application/javascript; charset=utf-8
```

The response body is the assembled, deployment-configured JavaScript detector.

***

### GET /api/seb/canvas-detector.js

A stable compatibility alias for `/js/canvas-seb-detector.js`. Canvas theme loaders that were configured before the primary URL was established continue to work without reconfiguration.

This route is handled by the same controller method and produces identical output to `/js/canvas-seb-detector.js` — same injection, same cache headers, same `vary` directive. No request is ever redirected; the alias is a true second route on the same handler.

<Note>
  New Canvas theme loader configurations should use `/js/canvas-seb-detector.js`. The compatibility alias at `/api/seb/canvas-detector.js` is maintained for existing deployments.
</Note>

***

### GET /js/canvas-seb-theme-loader.js

A minimal Canvas theme loader hosted by the application rather than uploaded to Canvas Files. Some self-hosted Canvas deployments reject locally stored theme JavaScript with Rails' `InvalidCrossOriginRequest` protection. Pointing Canvas's `js_overrides` theme setting at this endpoint avoids that rejection while retaining the quiz-page-only detector loading behaviour.

**Authentication:** None. Public endpoint.

**What the loader does**

The loader is a self-invoking function that runs on every Canvas page load:

1. Matches the current path against Classic Quiz take routes (`/courses/:courseId/quizzes/:quizId/take`) and New Quiz assignment routes (`/courses/:courseId/assignments/:assignmentId`). It exits immediately on non-quiz pages and on New Quiz authoring paths (build, settings, moderate, reports, exports).
2. Constructs the canonical content ID — `classicquiz_{quizId}` for Classic Quizzes or `newquiz:{courseId}:{assignmentId}` for New Quizzes.
3. Calls `GET /api/seb/requirement/:courseId/:contentId` with `credentials: omit` and `cache: no-store`. If the response is not `{ success: true, sebRequired: true }`, it exits without loading the detector.
4. If the requirement check confirms SEB is required and no detector script tag is already present, it dynamically appends a `<script>` element pointing to `/js/canvas-seb-detector.js`.

This two-stage approach means the full detector script is downloaded only on assessed quiz pages where SEB enforcement is active.

**Cache behaviour**

Same as the detector script: `no-cache, must-revalidate` in normal mode; `no-cache, no-store, must-revalidate` when debug is enabled.

***

## What the Detector Does on Canvas Quiz Pages

Once loaded, the detector:

1. Reads `SafeExamBrowser.security.configKeyHash` and the current `window.location.href` from the SEB JavaScript API.
2. Calls `POST /api/seb/access-proof/:courseId/:quizId` with the Config Key hash and URL as the request body.
3. If the proof request succeeds, immediately calls `POST /api/seb/access-code/:courseId/:quizId` with the `proofToken` in the `x-seb-proof-token` request header.
4. Fills the Canvas access-code input when — and only when — Canvas presents an unambiguous access-code prompt, and the server returned a valid access code. The detector does not treat DOM content as authorisation; it fills a field only when the server has already validated SEB's identity.
5. Displays approved exam tools from the `tools` array in the access-code response.
6. Waits for Canvas-authored completion evidence (Classic Quiz: successful final submission with a matching Canvas result structure; New Quiz: the authoritative result UI) before using the exit grant to display the settings-bound quit link.

In an ordinary browser (no SEB JavaScript API), `SafeExamBrowser.security.configKeyHash` is absent. The detector calls `GET /api/seb/requirement/:courseId/:quizId` first and shows the SEB-required prompt only when the server confirms SEB is needed — it never shows the prompt based on DOM inspection alone.

<Warning>
  The detector sidebar and its buttons are **affordances only**. The SEB URL filter in the generated `.seb` configuration is the control that determines what can load inside SEB. Changing any selected exam tool or URL policy rule changes the configuration fingerprint; students must download a fresh `.seb` file before those changes take effect.
</Warning>

***

## Why the Detector Is One Script

The public detector is served as a single JavaScript response. Its maintainable source is broken into modules assembled in `manifest.json` order during development, tests, and the build. At runtime:

* The Canvas theme loader already limits detector loading to supported assessment routes where the requirement check returns `sebRequired: true`. Splitting the detector into multiple browser requests would lose cross-fragment HTTP compression and introduce startup-ordering risk if a later fragment depends on state set by an earlier one.
* The in-process cache for production responses means all concurrent students on the same page receive the same compressed bytes without additional database or filesystem reads.
* Diagnostics mode serves the readable, unminified assembly so issues can be traced in the browser console without a source map.

***

## Requirement Check Caching

`GET /api/seb/requirement/:courseId/:quizId` performs one exact primary-key lookup against the `assessments` table and coalesces concurrent requests for the same assessment through a short, bounded in-process promise cache. The cache is stored per `(courseId, canonicalContentId)` tuple. All concurrent checks for the same assessment join the same pending promise instead of issuing parallel database reads.

The response always carries `cache-control: private, no-store, max-age=0`. Browsers and proxies must not cache the requirement result; staleness could cause the detector to show or hide the SEB-required prompt incorrectly after an instructor changes the assessment policy.

***

## Debug Trace Endpoint

### POST /api/debug/canvas-detector-trace

Accepts sanitised diagnostic events from the detector when the deployment has debug or diagnostics mode enabled.

<Note>
  This endpoint is **disabled in all normal production deployments**. It returns `{ "enabled": false }` immediately — without logging anything — unless both `SEB_DEBUG_ENABLED` or `SEB_DETECTOR_DIAGNOSTICS_ENABLED` is set **and** the `TESTBED_ENABLED` flag is active. Never enable the testbed flag in production; it is intended only for controlled non-production test environments.
</Note>

**Authentication:** None. However, the server enforces three independent guards:

1. Both `debugEnabled` (or `detectorDiagnosticsEnabled`) and `testbed.enabled` must be `true` in the application configuration.
2. The `Origin` header must exactly match the configured Canvas domain origin. Requests from any other origin return `{ "enabled": false }` without recording.
3. A per-origin rolling window rate limit of 120 requests per 60 seconds is enforced. Exceeding it returns `429`.

**Request body**

The body is any JSON-serialisable value. The `DetectorTraceService` sanitises and records the payload. When `detectorDiagnosticsEnabled` is `true`, additional detail fields are recorded; when only `debugEnabled` is `true`, the recorded payload is reduced.

**Success response — 200**

```json theme={null}
{ "enabled": true }
```

**Disabled or rejected response — 200**

```json theme={null}
{ "enabled": false }
```

The endpoint always returns HTTP 200 when the feature is disabled to avoid leaking configuration information to unauthenticated callers. A `429` is the only non-200 response, and only when the rate limit is exceeded on an otherwise-enabled endpoint.

***

## Approved Tools Under Proof/Session Boundary

The detector calls `GET /api/seb/tools/:courseId/:quizId` to refresh the approved exam tool list after an access-code redemption, without re-running the full proof cycle. This endpoint is documented in full on the [SEB Student Routes](/api/seb-student-routes) page, which covers all student-facing SEB endpoints including the tools route.

***

## Server-Owned YouTube Player

### GET /seb/tool/youtube/:videoId

Renders a server-owned single-video YouTube player page. Instructors can create a dedicated YouTube video tool that accepts a watch, share, Shorts, or embed link; the server converts the accepted formats into this endpoint with the canonical YouTube video ID.

YouTube requires a valid embedding identity (a stable HTTPS origin in the embed URL's `origin` parameter). Because SEB opens tools in a separate window, the player page is the embedding document, and the server supplies that stable origin rather than relying on the SEB window's origin.

**Authentication:** None. Public endpoint.

**Path parameters**

<ParamField path="videoId" type="string" required>
  An 11-character YouTube video ID. Must match `[A-Za-z0-9_-]{11}`. Any other value returns `404`.
</ParamField>

**Accepted source URL forms (resolved at tool-creation time, not at this endpoint)**

<Accordion title="YouTube URL formats the tool creation flow accepts">
  | Format             | Example                                     |
  | ------------------ | ------------------------------------------- |
  | Standard watch URL | `https://www.youtube.com/watch?v=<videoId>` |
  | Share URL          | `https://youtu.be/<videoId>`                |
  | Shorts URL         | `https://www.youtube.com/shorts/<videoId>`  |
  | Embed URL          | `https://www.youtube.com/embed/<videoId>`   |
</Accordion>

**What the player deliberately excludes**

The server-owned player page uses a strict Content Security Policy that allows only the YouTube embed frame and no other content. Specifically:

* YouTube browsing (`www.youtube.com` navigation, channel pages, search) is not available from this page.
* Google sign-in flows are blocked; the page does not include `accounts.google.com` in any allow-list.
* No scripts, images, or styles other than inline `<style>` are permitted.
* No `<form>` submissions and no `<base>` element are allowed.

This bounded media policy means the approved tool gives students access to exactly one video, not to YouTube as a platform. The SEB URL filter in the generated configuration controls what domains can load at the network level; the player page restricts what the embedding document itself does.

**Response — 200**

```text theme={null}
Content-Type: text/html
Cache-Control: no-store
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'none'; frame-src https://www.youtube.com; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'
```

The response body is a minimal HTML page containing a single full-viewport `<iframe>` pointing to `https://www.youtube.com/embed/<videoId>?rel=0&origin=<applicationOrigin>`.

**Error response**

| Status | Meaning                                                             |
| ------ | ------------------------------------------------------------------- |
| `404`  | The `videoId` path segment does not match the 11-character pattern. |
