fix(server): plan 4 — guard-level single parse: verify + correct the overclaiming verifyTripAccess docstring (L-2)

TripAccessGuard/TripOwnerGuard already parse :tripId exactly once
(Number(), guarded by Number.isFinite) and hand the resolved TripAccess
row to the handler via request[TRIP_REQUEST_KEY], TripOwnerGuard reusing
TripAccessGuard's own lookup when both are on the same route rather than
re-querying — verified by reading both guards directly, no change needed
there.

PlacesService.verifyTripAccess's own docstring separately claimed its
returned `tid` gets "returned... for every downstream call to reuse
(never re-parsing)" (3c's Task 9 review, L-2, carried to Plan 4). Reading
every call site (not assumed) shows this is false: `.tid` has exactly one
reader (PlacesController.requireTrip itself), which discards it — none of
requireTrip's 13 callers read `.tid` off the trip it returns; each passes
the original raw tripId string on to the write/read method it calls next,
which toRowId-parses it again itself. That second parse cannot be
eliminated by this gate alone: the same service methods are called
directly by places.mcp.ts too, an entry point with no requireTrip gate at
all (its own numeric tripId is re-stringified, String(tripId), to call
them), so each method owns its own id validation regardless of entry
point. Threading `tid` all the way through would mean giving every one of
~15 methods a second, number-only call shape for the REST path alone to
use — a real refactor, out of this docstring fix's own scope. Corrected
the docstring to say so plainly instead of repeating the false claim.
This commit is contained in:
jubnl
2026-09-24 06:26:45 +02:00
parent dba42c3aab
commit 23c292f2f2
+20 -4
View File
@@ -189,10 +189,26 @@ export class PlacesService {
* writes behind this gate (`toRowId(tripId) ?? -1`) then refused, or sent
* a bare `NaN` into `TripsRepository.findAccessible`'s raw bind and 500'd
* (fixed program-wide at the platform too — see `NulSafeSqlitePlatform`).
* Parsing ONCE, here, and returning the parsed `tid` for every downstream
* call to reuse (never re-parsing) closes both: a non-canonical trip id
* now answers this gate's own 404 before any read or write runs, and every
* write behind it shares the exact id the gate authorised.
* Parsing here answers this gate's own 404 before any read or write runs.
*
* **The returned `tid` is NOT threaded to every downstream call, despite
* an earlier version of this docstring claiming it was** (Plan 4 Task 8a,
* L-2 — confirmed false by reading every call site, not assumed): `.tid`
* has exactly one reader today, `PlacesController.requireTrip` itself
* (`places.controller.ts:122`), which discards it — `requireTrip` returns
* the WHOLE `trip` object to its 13 callers, none of which read `.tid`
* off it; every one instead passes the ORIGINAL raw `tripId` string on to
* the write/read method it calls next (`create(tripId, …)`,
* `importGpx(tripId, …)`, …), which `toRowId`-parses it AGAIN itself.
* That second parse is not a bug this gate can close by itself: those
* same methods are also called directly by `places.mcp.ts` (an entry
* point with no `requireTrip` gate at all — its own `tripId: number`
* input is re-stringified, `String(tripId)`, to call them), so each
* method owns its own id validation regardless of which entry point
* reached it, and `tid` alone cannot remove that. Threading it all the
* way through would mean giving every one of those methods a second,
* number-only call shape for the REST path to use instead of its shared
* string-taking one — a real refactor, out of this gate's own scope.
*/
async verifyTripAccess(tripId: string, userId: number): Promise<(TripAccess & { tid: number }) | undefined> {
const tid = toRowId(tripId);