Wrap up cycle 3 across the autodev existing-code Phase B steps that follow Implement (Steps 12-15), plus the cross-workspace prerequisite ticket filed for AZ-512. Step 12 - Test-Spec Sync: - Un-quarantine FT-P-01 in traceability-matrix (closed by AZ-510) - Add AZ-510 chained /users/me failure-path test reference under AC-23 - Note AZ-512 deferral status under O9 (P12 Phase B target) Step 13 - Update Docs (task mode): - Refresh src__auth__AuthContext module doc with AZ-510 wire shape (POST refresh + chained /users/me + bootstrapInflight guard) - Add usersMe() to src__api__endpoints module doc + consumer note - Rename src__features__annotations__classColors module doc to src__class-colors__classColors (matches AZ-511 git mv); refresh header - Refresh src__components__DetectionClasses + src__features__annotations module group doc for the new class-colors barrel import path - Update components/11_class-colors Module Inventory to point at the renamed module doc filename - Rewrite system-flows.md Flow F2 (Bearer auto-refresh) with the AZ-510 POST + chained /users/me sequence; close Finding B3 references - Generate ripple_log_cycle3 documenting all changed source files, their reverse-dependency search results, and the docs touched Step 14 - Security Audit (cycle-3 delta): - Resume mode against cycle-2 baseline; cycle-2 artifacts untouched - Re-run bun audit on both roots: clean (cycle-2 inline fix held) - Re-rate OWASP A06: FAIL -> PASS; A07: PASS_WITH_KNOWN -> PASS (B3 closed by AZ-510) - New finding F-SAST-CY3-1 (LOW): __resetBootstrapInflightForTests exposed via src/auth public barrel; defer to hygiene cycle - Verdict: FAIL -> PASS_WITH_WARNINGS; one HIGH (F-SAST-1 mission-planner git-history key, unchanged) remains - Add amendment banner to cycle-2 security_report.md Step 15 - Performance Test: - Static profile NFT-PERF-01 PASS (290 575 B gzipped vs 2 MB budget; ~14% of budget; no regression from AZ-510 surface additions) - E2E profile SKIP (Playwright perf project still pending AZ-457..AZ-482); legitimate skip per test-run skill, gap acknowledged in report - AZ-510 200ms p95 chain NFR verified at spec level only - no CI gate yet (covered by future AZ-457..AZ-482 work) Cross-workspace prerequisite (AZ-513 just filed): - Updated _docs/_process_leftovers/2026-05-13_az-512-admin-classes-prereq.md to reflect AZ-513 filing on admin/ workspace (parent epic AZ-509, Blocks link to AZ-512). Companion task spec added in admin/ repo (separate commit there, owned by admin/ workspace). State file: advanced to Step 16 (Deploy) per autodev existing-code flow. Co-authored-by: Cursor <cursoragent@cursor.com>
9.4 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
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.
- 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,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— 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.