# Business rules — Click Foundation - LTI Integration Tool

Evidence date: 2026-10-19. Source: `master` `fbf20a6`.

Rules below are taken from docs or code. Nothing here is a proposed Click Foundation policy. Where the code behaves a certain way and no document states it, the rule is marked **Inferred**.

## Identity and access

### BR-001 — Operators are anonymous

The HTML UI does not require a user, role, or session login. Firewall `main` is `anonymous: lazy`. `access_control` entries are commented out.

Provenance: **Inferred** from `config/packages/security.yaml`. No document says the UI is public. **Requires Confirmation** if this host is reachable beyond a developer machine.

### BR-002 — API calls present a bearer key

Documented rule: every HTTP API request includes `Authorization: Bearer` and the value of `APP_API_KEY`.

Provenance: **Documented** (`doc/api.md`, OpenAPI security scheme).

Implemented check: if the header is present, the token is the substring after `Bearer `. An exact match with `application_api_key` marks the token authenticated. Any other value throws `Unauthorised api key`, which the listener returns as HTTP 401 JSON.

Provenance: **Implemented** (`ApiKeyListener`, `ApiKeyProvider`).

Gap: if the header is absent, `ApiKeyListener` returns without setting a response. No access-control rule requires a role on `^/api/`. Whether the request still runs was not executed. **Requires Confirmation**.

### BR-003 — LTI service scopes

A caller of a platform service must satisfy the firewall scopes:

- AGS routes: lineitem, lineitem.readonly, score, and result.readonly.
- NRPS: `contextmembership.readonly`.
- ACS: `control.all`.
- Basic Outcome: `basicoutcome`.

The same four AGS scopes and the NRPS, Basic Outcome, and ACS scopes are also the application's configured scope list in `lti1p3.yaml`.

Provenance: **Implemented**. The scope URIs are the IMS URIs. That they are the correct grant for a Click deployment is **Requires Confirmation**.

### BR-004 — Tool and platform message firewalls

`^/tool/launch` uses `lti1p3_message_tool`. `^/platform/message/return` uses `lti1p3_message_platform`. Both are stateless.

`/tool/deep-linking-response` and `/tool/proctoring-response` do not match `^/tool/launch`. They fall through to `main` (anonymous). Provenance: **Inferred** from the firewall patterns. **Requires Confirmation** that those POST endpoints are intentionally unauthenticated.

### BR-005 — Launch users are demo people or caller-supplied

Configured users exist only to be selected on a launch form or listed in the default membership. Roles are only IMS learner or instructor, as stored in `users.yaml`. The authenticator does not check a password.

A custom launch user is trusted from the login hint. An omitted API user becomes login hint type `anonymous`, and the authenticator returns success with no identity.

Provenance: **Implemented**. Treating these as real learners would be an inference this document does not make.

### BR-006 — One registration

Launches must name a registration that `RegistrationRepository` can find. This checkout defines only `local`, client id `common_client_id_value`, deployment `deploymentId1`.

Provenance: **Implemented**. The CLI document's example id `devkit` does not match. **Requires Confirmation** before anyone treats `common_client_id_value` as a real client id.

## Launches

### BR-007 — Supported message types

Only these four types can be generated: `LtiResourceLinkRequest`, `LtiDeepLinkingRequest`, `LtiStartProctoring`, `LtiSubmissionReviewRequest`. The API applies `ucfirst` to the path segment before the comparison. Any other type is HTTP 400.

Provenance: **Implemented**.

### BR-008 — Registration is mandatory on generated launches

Empty `registration` raises `Missing registration`. Unknown id raises `Invalid registration {id}`. On the API those become HTTP 500, because they are not HTTP exceptions.

Provenance: **Inferred** from `LtiMessageBuilder` and `ErrorHandlerSubscriber`. The OpenAPI lists 400 for bad requests and 500 for internal errors. A bad registration is implemented as 500, not 400.

### BR-009 — Resource link id

If the claims include an LTI resource-link id, that id is used. Otherwise the builder generates a UUID. Target link URI is optional and may be null, in which case the library's default tool launch URL applies (as the CLI doc states).

Provenance: **Implemented**, with the fallback sentence **Documented** in `doc/cli.md`.

### BR-010 — Deep linking defaults

When deep-linking settings are omitted on the API or CLI:

- accept types: `ltiResourceLink`
- presentation targets: `windows` (plural, as written in code)
- accept multiple: true
- auto create: false
- media types, title, and text: null

The return URL is always this app's route `/platform/message/return/deep-linking`.

The HTML form instead requires accepted types, accepted targets, accept-multiple, and auto-create, so the UI does not use those defaults unless the operator submits them.

Provenance: **Implemented**.

### BR-011 — Proctoring attempt

Attempt number defaults to 1 when the API or CLI omits it. The HTML form marks attempt number required. Start-assessment URL is required in both the form and the builder (the builder indexes it directly).

Provenance: **Implemented**. CLI doc agrees that the start-assessment URL is required and the attempt defaults to 1. **Documented** and **Implemented**.

### BR-012 — Submission review owner

AGS scopes and line item URL are required. If no submission owner is sent, the launch user is the owner. If there is no launch user either, the owner id is a new UUID and the role is learner. An explicit owner uses their id, name, email, and roles, defaulting the role list to learner when roles are omitted.

Provenance: **Implemented**. CLI doc marks submission owner optional and says it falls back to the launch user. The no-user UUID fallback is **Inferred** from the builder; the doc does not describe it.

### BR-013 — User id generation

For a custom user with no id, the builder sets a UUID v4 before authentication. Name, email, and locale are optional.

Provenance: **Implemented**. **Documented** in `doc/cli.md`.

## Records

### BR-014 — Redis is the store

These cache keys hold the records:

| Key | Contents |
|---|---|
| `lti1p3-nrps-memberships` | Custom memberships |
| `lti1p3-basic-outcomes` | Basic outcomes |
| `lti1p3-ags-line-items` | Line items |
| `lti1p3-ags-scores` | Scores grouped under line items |
| `lti1p3-ags-results` | Results grouped under line items |
| `lti1p3-proctoring-assessment` | ACS assessments |

There is no TTL in the repository classes. Lifetime is the cache adapter's. Redis is started with append-only enabled in Compose. Provenance: **Implemented**.

### BR-015 — Default membership is not a stored row

Identifier `default` is rebuilt from `users.yaml` on each request. Context id and the service context id must both be `default` or the NRPS server returns not found. Every configured user is an active member. Dashboard membership count is the number of cached memberships plus one.

Deleting `default` through the UI looks up Redis only, so a delete of an id that was never saved returns not found rather than removing the built-in membership.

Provenance: **Inferred** from `DefaultMembershipFactory`, `MembershipServiceServerBuilder`, `ListMembershipsAction`, and `DeleteMembershipAction`.

### BR-016 — NRPS context must match

For a stored membership, the context identifier in the URL must equal the membership's context identifier. Otherwise the response is not found, even if the membership id exists.

Members can be filtered by a `role` query parameter (exact role string). `limit` and `offset` page the filtered list. A `rel="next"` link is added when more members remain. The next offset adds `limit` to the current offset when `offset` is already in the query.

Provenance: **Implemented**.

### BR-017 — Membership form

Required: membership identifier, context identifier. Optional: context label, context title, JSON members.

Provenance: **Implemented** (`MembershipType`). What happens when `members` is not valid JSON was not fully traced. **Unknown** beyond the form accepting the textarea.

### BR-018 — Line item form

Required on create: identifier, context identifier, label, score maximum (integer). Optional: resource identifier, resource link identifier, tag, start date, end date. On edit the identifier is disabled and is the full URL.

Provenance: **Implemented** (`LineItemType`).

AGS list filtering in `LineItemRepository::findCollection` keeps a line item only when resource id, resource link id, and tag each match, if that filter was provided. Limit and offset slice the filtered list.

Provenance: **Implemented**.

### BR-019 — Assessment status

The status must be one of `none`, `running`, `paused`, `terminated`, `complete`. The form offers only those values. The library throws if another status is set.

Provenance: **Implemented** (`AssessmentType`, `AcsControlResultInterface::SUPPORTED_STATUSES`).

### BR-020 — UI delete is GET

NRPS, AGS, assessment, and basic-outcome deletes are HTTP GET and then redirect to the list. Success and failure are flash messages. Missing NRPS, AGS, or assessment ids throw not found before the redirect.

Provenance: **Implemented**. This is unsafe for a shared host (BR-001). **Requires Confirmation**.

### BR-021 — AJAX-only tool helpers

Tool service helpers under `/tool/ajax/` and `/platform/ajax/registration-url` are registered with `condition: request.isXmlHttpRequest()`. A normal browser navigation does not match the route.

Provenance: **Implemented**.

## Validation and errors

### BR-022 — API JSON

The message-launch API rejects a body that is not JSON with HTTP 400 and the PHP JSON error text.

Provenance: **Implemented**.

### BR-023 — API error shape

For `/api/` or a controller that implements `ApiActionInterface`, the body is `{"error": "<message>"}`. Status is the HTTP exception status, otherwise 500. The exception message is logged at error.

Other requests render the HTML error template.

Provenance: **Implemented** (`ErrorHandlerSubscriber`).

### BR-024 — CSRF

`csrf_protection` is commented out in `framework.yaml`. Forms were not checked for a manual CSRF token.

Provenance: **Inferred**. **Requires Confirmation** if the UI is exposed.

### BR-025 — Locale

Default locale and the only enabled locale are `en`.

Provenance: **Implemented**.

## Data that is not a business rule

The Star Wars display names, picture URLs, and emails in `users.yaml` are fixture content. They do not establish a naming policy.

Sample deep-linking resources do not establish approved content sources for Click Foundation.
