Commit Graph

2 Commits

Author SHA1 Message Date
Maurice 8c0280e287 Replace the copied requireTrip helpers with a TripAccessGuard
Every trip-scoped controller carried its own three-line requireTrip, and a
requireEdit next to it: eighteen copies of a security check, each of which had to
remember that no access is a 404 'Trip not found' and never a 403 — the
distinction that stops a stranger learning which trip ids exist. One of them
getting it wrong would have looked like nothing.

TripAccessGuard resolves :tripId once, refuses what the user cannot reach and
hands the row to the handler through a @Trip() param decorator (a param
decorator rather than a req.trip cast, because the request type that used to be
widened for that is gone). @RequirePermission carries the same action string the
domain services pass to checkPermission, so the HTTP and MCP paths cannot drift
apart on which right they demand. Status codes and bodies are unchanged.

Piloted on days, day-notes, todo, packing and files. Two things worth knowing:

Files applies the guard PER HANDLER and leaves the upload route on its own
check. Guards run before interceptors, so a class-level guard would answer 404
before multer had read the multipart body, and a response sent while the client
is still streaming gets the socket destroyed — the caller sees ECONNRESET
instead of the 404. Refusing after the body has been read costs the bandwidth
and keeps the answer readable.

The services keep verifyTripAccess and canEdit: the large majority of their
callers are *.mcp.ts tools, which never pass through an HTTP guard. canEdit lost
its incidental controller-test coverage in the move, so it now has direct tests
asserting the action and the shared flag — the same pair the guard reproduces.

Also fixes the backup zip-slip test that failed only on CI: it asserted a
drive-letter path, which is absolute on win32 and an ordinary relative segment on
Linux, so the branch it was guarding never fired there.
2026-08-09 19:13:21 +02:00
Maurice fef338091c Fold backupService into the backup domain
src/services/backupService.ts moves to src/nest/backup/backup.impl.ts, imported
only by its own domain now that the controller reaches it through BackupService.
src/services/ is down to airtrail/.

It stays a module of free functions rather than becoming methods on the service.
The restore path closes and reinitializes the core DB handle, which is the single
most dangerous sequence in the server, and rewriting its shape in the same step
as the move would make a regression there impossible to bisect. The db-lifecycle
question is the last step of this fold, not part of it.

The move also pulled 512 lines out of ungated src/services/ into a gated domain,
which is how the per-domain ratchet caught that the per-entry extraction loop had
no tests at all: the zip-slip refusal, the running decompressed-byte cap (the
declared size in the central directory is attacker-controlled, so only the
running total catches a lying archive), a corrupt stream not being dressed up as
a size refusal, and the reopen failure that has to report restart-required rather
than success. Six cases now cover it, and backup goes from 84% to 96% statements.
2026-08-09 18:44:59 +02:00