Blame · Line-by-line history
route-params.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| fc623bf | 1 | /** |
| 2 | * URL parameter validation for route handlers. | |
| 3 | * | |
| 4 | * Handlers were passing raw path segments straight into database predicates. | |
| 5 | * `parseInt("abc", 10)` yields NaN, and a NaN or malformed-UUID comparison | |
| 6 | * makes Postgres raise, which surfaces as a 500. So /:owner/:repo/issues/abc | |
| 7 | * crashed while /:owner/:repo/issues/99999999 correctly 404'd — a hand-typed | |
| 8 | * or stale URL returned a server error instead of "not found". | |
| 9 | * | |
| 10 | * Found by the authz-matrix gate, which probes every repo-scoped route with a | |
| 11 | * placeholder id: eight routes answered 5xx (issues, pulls, milestones, | |
| 12 | * agents and claude sessions). | |
| 13 | */ | |
| 14 | ||
| 15 | /** | |
| 16 | * A positive integer path segment (issue/PR numbers), or null when the | |
| 17 | * segment is not one. Rejects NaN, zero, negatives, floats, and numeric | |
| 18 | * strings with trailing junk ("12abc") that parseInt would happily truncate. | |
| 19 | */ | |
| 20 | export function parseIdNumber(raw: string | undefined): number | null { | |
| 21 | if (!raw || !/^\d+$/.test(raw)) return null; | |
| 22 | const n = Number(raw); | |
| 23 | if (!Number.isSafeInteger(n) || n <= 0) return null; | |
| 24 | return n; | |
| 25 | } | |
| 26 | ||
| 27 | /** | |
| 28 | * A UUID path segment, or null. Postgres raises `invalid input syntax for | |
| 29 | * type uuid` on anything else, which the global error handler turns into a | |
| 30 | * 500 — so this must be checked before the value reaches a query. | |
| 31 | */ | |
| 32 | export function parseIdUuid(raw: string | undefined): string | null { | |
| 33 | if (!raw) return null; | |
| 34 | return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(raw) | |
| 35 | ? raw | |
| 36 | : null; | |
| 37 | } |