Phase B cycle 1 was a structural refactor only: F4 (barrel imports + STC-ARCH-01) and F7 (endpoint builders + STC-ARCH-02). This commit brings docs in line with source after the cycle, no code changes. Module docs (12 consumers): swap every /api/<service>/... literal in code snippets and integration tables for the matching endpoints.* builder; note the barrel import migration in Dependencies. New module doc: src__api__endpoints.md (public surface, F4 barrel re-export note, STC-ARCH-02 enforcement, contract-test reference). Architecture compliance baseline: mark F4 + F7 CLOSED with commit hashes (23746ec,8a461a2). 01_api-transport component description: add endpoints.ts + barrel to Internal Interfaces, close the F7 caveat, extend Module Inventory. ripple_log_cycle1.md: Task Step 0.5 reverse-dep analysis records the import-graph closure (no extra docs needed beyond the direct set). Carry-over reports landed alongside the docs: - test_run_report_phase_b_cycle1.md (Step 11 outcome) - implementation_report_refactor_phase_b_cycle1.md (cycle summary) State file: trimmed to the autodev <30-line target; Steps 14 + 15 recorded as SKIPPED with rationale (no security or perf surface changed in this cycle); pointer moved to Step 16 (Deploy). Co-authored-by: Cursor <cursoragent@cursor.com>
9.3 KiB
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, commit8a461a2), 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), this module enforces "no hardcoded API path literals in src/" as a build-time invariant rather than a code-review aspiration.
Public interface
export const endpoints = {
admin: {
authRefresh: () => string
authLogin: () => string
authLogout: () => string
users: () => string
user: (id: string) => string
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.
- Parameterised paths (e.g.
- Query strings owned by the caller for variable-shape paths. Where the query is dynamic (
flights.collection,annotations.media,annotations.dataset), the caller builds aURLSearchParams.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— internalrefreshToken()helper usesendpoints.admin.authRefresh().src/auth/AuthContext.tsx—authRefresh,authLogin,authLogout.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— fullflights.*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 undersrc/`. - Exempt files: this file (
src/api/endpoints.ts) andsrc/**/*.test.ts(x)only. - Bypass policy: none. Adding a new exempt path requires updating the exempt regex in the script AND a
module-layout.mdrule 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(Authorizationheader) andsse.ts(access_tokenquery parameter). Builders return URLs without the token. - No URL-encoding of interpolated
id/mediaId/queryStringparameters. All current callsites pass already-safe values (UUIDs, ints, pre-builtURLSearchParams.toString()output). If any future caller passes user-controlled text, the builder must addencodeURIComponent(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 (endpointsis reachable viaimport { 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 insrc/. Also verifies the*.test.tsand//comment exemptions.
Notes / open questions
detect.mediaonly exposes the single-segment path that the UI uses today (POST /api/detect/<mediaId>). The fulldetect/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.collectionoverloads its return on whetherqueryStringis 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
encodeURIComponentat the first callsite that needs it, plus a contract-test case inendpoints.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.