POST /api/v1/uploads minted an upload key from caller input with no
ownership check, no content-type allowlist, and the raw filename
interpolated into the S3 key.
- Ownership: a caller may only mint upload URLs for a route/activity
they own, enforced via the branded loadOwnedRoute/loadOwnedActivity
(404 on miss, no existence leak). Closes writing into another
user's resource key-space.
- Content type: the request schema now constrains contentType to an
image/gpx allowlist, so active content (HTML/SVG/JS) that would
execute if served inline is rejected at the request boundary.
- Filename: sanitizeUploadFilename() reduces the client filename to a
safe basename (charset-restricted, no path components, no leading
dots, length-bounded) before it becomes part of the key.
Tests: schema allow/deny + filename sanitization in @trails-cool/api;
handler tests covering owned-success, not-owner 404, disallowed
content-type, and the route-vs-activity ownership branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five journal API route handlers had the same inline lambda:
parsed.error.issues.map((i) => ({
field: i.path.join("."),
message: i.message,
}))
Pull it into `zodIssuesToFieldErrors(error)` next to FieldError in
`@trails-cool/api/errors.ts` and re-export it so all five handlers
share the same implementation. The path-flattening rule now lives in
one place, and new routes get it for free.
Public error-response shape is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add all REST API endpoints for mobile app consumption:
Routes:
- GET /api/v1/routes — paginated list with cursor, Zod validation
- GET /api/v1/routes/:id — full detail with GPX, versions
- POST /api/v1/routes — create with Zod-validated body
- PUT /api/v1/routes/:id — update, creates new version
- DELETE /api/v1/routes/:id — returns 204
Activities:
- GET /api/v1/activities — paginated list with cursor
- GET /api/v1/activities/:id — full detail
- POST /api/v1/activities — create with GPX stat extraction
- DELETE /api/v1/activities/:id — returns 204
Supporting:
- POST /api/v1/routes/compute — BRouter proxy
- POST /api/v1/uploads — presigned upload URL generation
Device management:
- GET /api/v1/auth/devices — list connected devices with isCurrent
- DELETE /api/v1/auth/devices/:id — revoke device token
- Store device_name on token exchange
Infrastructure:
- requireApiUser() guard — returns 401 with structured error
- apiError() helper for consistent error responses
- All endpoints use Zod schemas from @trails-cool/api for validation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>