# Module: `src/api/endpoints.ts`

> **Source**: `src/api/endpoints.ts` (79 lines)
> **Topo batch**: B2 (leaf — no internal imports)
> **Introduced**: AZ-486 (2026-05-11, commit `8a461a2`), closing architecture baseline finding F7.

## Purpose

Single source of truth for every `/api/<service>/<path>` URL the UI talks to. Replaces the hardcoded string literals that previously lived at each `api.*` / `createSSE` call site (and at every `src={...}` URL for API-served images / videos). The `endpoints` object is the canonical wire-contract documentation: each builder produces a character-identical string to the literal it superseded, so MSW handlers + e2e stubs + the nginx routing table all keep matching.

Together with the `STC-ARCH-02` static gate (see [Configuration](#configuration)), this module enforces "no hardcoded API path literals in `src/`" as a build-time invariant rather than a code-review aspiration.

## Public interface

```ts
export const endpoints = {
  admin: {
    authRefresh: () => string
    authLogin:   () => string
    authLogout:  () => string
    users:       () => string
    user:        (id: string) => string
    usersMe:     () => string  // added 2026-05-13 by AZ-510 — chained read after POST refresh
    classes:     () => string
    class:       (id: string | number) => string
  },
  annotations: {
    classes:                  () => string
    settingsUser:             () => string
    settingsSystem:           () => string
    settingsDirectories:      () => string
    annotations:              () => string
    annotationsByMedia:       (mediaId: string, pageSize?: number) => string  // pageSize default = 1000
    annotationImage:          (annotationId: string) => string
    annotationThumbnail:      (annotationId: string) => string
    annotationEvents:         () => string
    media:                    (queryString: string) => string
    mediaFile:                (mediaId: string) => string
    mediaItem:                (mediaId: string) => string
    mediaBatch:               () => string
    dataset:                  (queryString: string) => string
    datasetItem:              (annotationId: string) => string
    datasetBulkStatus:        () => string
    datasetClassDistribution: () => string
  },
  flights: {
    collection:       (queryString?: string) => string  // GET ?pageSize=... lists; POST (no qs) creates
    aircrafts:        () => string
    aircraft:         (id: string) => string
    flight:           (id: string) => string
    flightWaypoints:  (id: string) => string
    flightWaypoint:   (flightId: string, waypointId: string) => string
    flightLiveGps:    (id: string) => string
  },
  detect: {
    media: (mediaId: string) => string  // POST → trigger detection for a media item
  },
} as const
```

The whole object is `as const`, so each leaf's return type is the narrow string literal where possible (e.g. `'/api/admin/auth/refresh'`) and the parameterised builders carry a `string` return.

## Internal logic

- **Pure data + template strings.** No side effects, no I/O, no caching. Every builder is a one-line `() => '...'` or arrow returning a template literal.
- **Function form (not constants)**, per direction at task-creation time:
  - Parameterised paths (e.g. `flight(id)`) need a function anyway. Keeping every entry as a function — even the constant ones — gives a single uniform call shape at every site (`endpoints.x.y()`) so reviewers don't have to remember which entries take parens and which don't.
  - Per-builder tree-shaking under Vite's production rollup remains intact.
- **Query strings owned by the caller for variable-shape paths.** Where the query is dynamic (`flights.collection`, `annotations.media`, `annotations.dataset`), the caller builds a `URLSearchParams.toString()` and the builder owns only the path + `?`. This keeps the wire contract identical to pre-refactor literals at every callsite.

## Public API (barrel re-export)

`src/api/index.ts` re-exports `endpoints` alongside `api`, `createSSE`, `setToken`, `getToken`, `getApiBase`, `setNavigateToLogin`. Consumers OUTSIDE the `01_api-transport` component MUST import from the barrel (`import { endpoints } from '@/api'` or `from '../api'`) — direct imports of `src/api/endpoints` from other components are blocked by `STC-ARCH-01` (F4 closure, see `src__api__client.md`).

## Dependencies

- **Internal**: none.
- **External**: none.

## Consumers (intra-repo)

After the AZ-486 migration, `endpoints` is imported by:

- `src/api/client.ts` — internal `refreshToken()` helper uses `endpoints.admin.authRefresh()`.
- `src/auth/AuthContext.tsx` — `authRefresh`, `authLogin`, `authLogout`, `usersMe` (added by AZ-510).
- `src/components/FlightContext.tsx` — `flights.collection`, `flights.flight`, `annotations.settingsUser`.
- `src/components/DetectionClasses.tsx` — `admin.classes`, `admin.class`.
- `src/features/admin/AdminPage.tsx` — `admin.users`, `admin.user`.
- `src/features/annotations/AnnotationsPage.tsx` — annotation CRUD endpoints, `detect.media`.
- `src/features/annotations/AnnotationsSidebar.tsx` — `annotations.annotationEvents` (SSE), bulk-status, dataset endpoints.
- `src/features/annotations/CanvasEditor.tsx` — `annotations.annotationImage`, `annotations.annotationThumbnail`.
- `src/features/annotations/MediaList.tsx` — `annotations.media`, `annotations.mediaFile`, `annotations.mediaItem`, `annotations.mediaBatch`.
- `src/features/annotations/VideoPlayer.tsx` — `annotations.mediaFile`.
- `src/features/dataset/DatasetPage.tsx` — `annotations.dataset*` family, `annotations.classes`, `annotations.annotationImage`.
- `src/features/flights/FlightsPage.tsx` — full `flights.*` surface + `annotations.settingsUser`.
- `src/features/settings/SettingsPage.tsx` — `annotations.settings*`, `flights.aircrafts`.

This is the full intra-repo consumer list — `STC-ARCH-02` guarantees no production-source caller falls outside it.

## Data models

None defined here. Path-string output only.

## Configuration

The module IS the API-path configuration. The only "config" is the nginx routing table that maps each `/api/<service>/...` prefix to a concrete backend service — see `src__api__client.md` → External integrations for the live table.

**Static enforcement (`STC-ARCH-02`)**:

- **Script**: `scripts/check-arch-imports.mjs --mode=api-literals`.
- **Wired into**: `scripts/run-tests.sh` (functional profile, static group) — runs before any unit test.
- **What it forbids**: any `/api/<service>/...` literal in `[`'"]` quoting under `src/`.
- **Exempt files**: this file (`src/api/endpoints.ts`) and `src/**/*.test.ts(x)` only.
- **Bypass policy**: none. Adding a new exempt path requires updating the exempt regex in the script AND a `module-layout.md` rule revision in the same commit.

## External integrations

This module integrates nothing directly. It documents — as TypeScript values — the wire contract for every external integration the SPA has, as routed by `nginx.conf`. See the routing table in `src__api__client.md` → External integrations for the per-prefix backend mapping.

## Security

- **No bearer plumbing here.** Token injection still happens in `client.ts` (`Authorization` header) and `sse.ts` (`access_token` query parameter). Builders return URLs **without** the token.
- **No URL-encoding** of interpolated `id` / `mediaId` / `queryString` parameters. All current callsites pass already-safe values (UUIDs, ints, pre-built `URLSearchParams.toString()` output). If any future caller passes user-controlled text, the builder must add `encodeURIComponent` (see open question below).
- **No CSRF surface change** — same posture as the pre-refactor literals.

## Tests

- **`src/api/endpoints.test.ts`** (36 Vitest assertions): pins every builder's output to its exact pre-refactor URL string. This is the contract documentation — any wire-contract change MUST update this test in the same commit as the backend / MSW / e2e stub change. Includes one barrel-re-export assertion (`endpoints` is reachable via `import { endpoints } from '../api'`).
- **`tests/architecture_imports.test.ts`** (AZ-486 / STC-ARCH-02 suite, 6 cases): verifies the static gate passes on the migrated codebase AND fails when a synthetic single-quoted / double-quoted / template-literal `/api/<service>/...` literal is introduced in `src/`. Also verifies the `*.test.ts` and `//` comment exemptions.

## Notes / open questions

- **`detect.media` only exposes the single-segment path** that the UI uses today (`POST /api/detect/<mediaId>`). The full `detect/` service has more endpoints (per the nginx table) but no UI callsite consumes them. Add new builders only when a real callsite needs them — don't pre-populate.
- **`flights.collection` overloads its return** on whether `queryString` is provided. Acceptable while the contract is "GET with `?pageSize`, POST without" — but if a third flights-collection verb (DELETE? PUT?) is ever added with its own query shape, split into named builders rather than threading more conditional logic through one.
- **No URL-encoding of interpolated params** (see Security). Add `encodeURIComponent` at the first callsite that needs it, plus a contract-test case in `endpoints.test.ts`. Currently safe across all 36 pinned URLs.
- **Wire-contract test coverage is exact-string, not shape.** This is deliberate: a "looks like a path" matcher would silently accept a hyphen-to-underscore change that breaks the backend. Updating these strings IS a wire-contract change — treat the test as a release-gate.
