# OT WebGL (Unity) integration

How the VisuoPrime OT Unity build (`E:\Git\Unity\VisuoPrimeOT`) talks to the
portal. The build is hosted at its own URL (a **different origin** from
`ot.neurapy.com`), so it can't use the portal session cookie — every server
call is authenticated by a short-lived **launch token**.

## 1. Where the build URL comes from
A Neurapy super admin / support sets the WebGL build URL (and version) in
**Admin → OT Provisioning → "WebGL game build"**. Update it on every release;
patients pick it up on their next launch. Stored as a global `org_settings` row
(`org_id = 0`, `category = 'webgl'`, keys `url` / `version`).

The build is hosted **same-origin** with the portal, at
`https://ot.neurapy.com/v<version>/index.html` (the admin sets this URL per
release in OT Provisioning). Same origin ⇒ no CORS needed for the API calls.

**Two authorities, kept separate:**
- **API** (games, scores, session validate) → this app, `https://ot.neurapy.com`.
- **Login / identity** → the IdP, `https://auth.neurapy.com`, reached via the
  portal's "Sign in with Neurapy" SSO. The WebGL never shows a local oth login.

## 2. Launch (portal → new tab)
The patient dashboard's **Launch games** button:
1. `POST /patient/webgl/launch` →
   `{ webgl_url, version, token, sig, expires_at, api_url, login_url, idp_url }`.
2. Opens `webgl_url` in a **new tab** with query params:
   - `t`   = launch token (base64url JSON claims)
   - `s`   = HMAC-SHA256(t, app key) signature
   - `api` = game/score API base (`https://ot.neurapy.com`)
   - `login` = SSO login URL (`/auth/neurapy/redirect` → auth.neurapy.com); the
     Unity build sends an unauthenticated/expired user here, **not** to a oth login
   - `idp` = IdP origin (`https://auth.neurapy.com`) for reference
   - `return_url` = the dashboard URL to return to
3. When that tab **closes**, the dashboard refreshes played-games/progress and
   refocuses. (Browsers return focus to the opener tab automatically on close.)

The token is valid for **15 minutes** and carries: `patient_id`, `org_id`,
`user_id`, `issued_at`, `expires_at`, `nonce`. Unity should read `t`/`s`/`api`
from its own URL and keep them for all API calls below.

## 3. Server API (Unity → portal)
Base = the `api` query param. Auth = send `t` and `s` on every call.
CORS is open for `api/*`.

### GET `/api/webgl/patient?t=<t>&s=<s>`
→ `200 { patient: { id, name }, org_id, games: [ { id, name, slug, game_type,
game_subtype, eye_side, launch_url } ] }`
→ `401 { message: "invalid_or_expired_token" }`

Use this to greet the patient and to know which `game_id`s are enabled.

### POST `/api/webgl/score`
Posted by the in-game performance panel when a play ends (timer expiry or manual
close). Body (JSON):
```
{ t, s, game_id, duration_seconds, started_at, ended_at, rating, skipped, response?, result? }
```
- `game_id` — one of the ids from `/patient` (must be enabled for the org).
- `duration_seconds` — play length.
- `started_at` / `ended_at` — ISO-8601 timestamps.
- `rating` — the patient's self-rating, **1–5 stars** (`null` when skipped).
- `skipped` — `true` if the patient skipped the rating.
- `response` — optional free-text note.
- `result` — optional extra per-game metrics (stored under `result.metrics`).

→ `200 { ok: true }` — records a completed play in `oth_game_sessions`
(`started_at`/`ended_at`, `status=ended`, and `result = { rating, skipped,
response, duration_seconds, metrics }`). Surfaces in the portal's Recent
Activity / Progress and in the practitioner's Daily Activity calendar.
→ `401 invalid_or_expired_token` · `404 game_not_available`

Unity side: `Assets/1_C/Scripts/OthScorePoster.cs` — `BeginGame(gameId)` on
launch, then wire star buttons → `SubmitRating(1..5)` and Skip → `SubmitSkip()`.

## 4. Standalone sign-in (no portal launch token)
When the build is opened **directly** (ot.neurapy.com/v<version>/ without `t`/`s`)
or as a **native app** (Windows/Mac/Android), it authenticates itself with the
IdP via OAuth 2.0 Authorization Code + PKCE — the public-client flow the IdP
already supports. Unity: `Assets/1_C/Scripts/NeurapyOAuth.cs`.

1. `NeurapyOAuth.StartLogin()` → PKCE pair, opens `{idp}/authorize?response_type=code&client_id=vp_ot_app&redirect_uri=…&code_challenge=…&code_challenge_method=S256&state=…&scope=…`.
   - WebGL → full-page redirect; on return the page reloads with `?code` and Unity resumes.
   - Desktop → loopback `http://127.0.0.1:<port>/` + system browser.
   - Android → custom scheme `vpoth://oauth` (add the intent-filter to the manifest).
2. Exchange at `POST {idp}/api/token` (`grant_type=authorization_code`, `code`,
   `code_verifier`, `redirect_uri`, `app`) → IdP **access token**.
3. Trade it for a launch token: **`POST {api}/api/webgl/session`** with
   `Authorization: Bearer <access_token>` → `{ token, sig, api_url, expires_at }`.
   The server verifies the IdP token (JWKS), maps the user → patient via
   `oth_user_patient_org_map`, and mints the same `t`/`s` the portal would.
4. From here it's identical to §3 — use `t`/`s` for `/api/webgl/patient` and `/score`.

### IdP client registration (run once on auth.neurapy.com)
```
php artisan idp:app-register vp_ot_app "VisuoPrime OT (app)" \
  --redirect-uri="https://ot.neurapy.com/webgl-oauth" \
  --redirect-uri="vpoth://oauth" \
  --loopback
```
(`key` and `name` are positional args — name in quotes since it has spaces.)
Public client (no secret): WebGL callback page + Android/desktop scheme + desktop
loopback. `vp_ot_app` is a separate client from the web app's `vp_ot` so their
redirect URIs don't collide.

## Notes / open items
- **Access gating:** `/patient/webgl/launch` refuses (402) if the patient has no
  active access window. In-game, the token simply expires after 15 min — long
  sessions should re-launch if needed (future: a token-refresh endpoint).
- **Tenant DB:** patient *name* is best-effort (patient records live on the
  tenant DB, which isn't resolved in the cookie-less API context). Identity is
  always trustworthy because it comes from the signed token. If per-org tenant
  DBs are used, add org→DB resolution in `WebglController` before reading tenant
  data.
- **Token signing** lives in `app/Services/WebglLaunchToken.php` (mint + verify).
