Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commitfc623bf

fix(routes): malformed URL ids returned 500 instead of 404

fix(routes): malformed URL ids returned 500 instead of 404

Handlers passed raw path segments straight into database predicates.
parseInt("abc", 10) is NaN, and a NaN or malformed-UUID comparison makes
Postgres raise, which the global error handler renders as a 500.

Verified live before the fix:
  /ccantynz/Gluecron.com/issues/abc       -> 500
  /ccantynz/Gluecron.com/pulls/abc        -> 500
  /ccantynz/Gluecron.com/milestones/abc   -> 500
  /ccantynz/Gluecron.com/issues/99999999  -> 404   (valid id, absent row)

So a typo or a stale link produced a server error rather than "not found" —
and every one of those 500s is a logged exception and an error-page render.

Found by the new authz-matrix gate, which probes every repo-scoped route with
a placeholder id and reported eight 5xx routes. The gate paid for itself on
its first real run.

parseIdNumber returns null for anything that is not a positive safe integer —
including "12abc", which parseInt would silently truncate to 12 and load the
WRONG row. Call sites use `?? -1`: the sentinel matches no row, so each
handler falls through the not-found branch it already had. No new branches,
and no behaviour change for valid input. parseIdUuid does the same for uuid
segments, which Postgres rejects outright.

36 numeric sites across 7 files and 3 uuid sites in claude-web.tsx. A test
walks src/routes and fails if any handler goes back to bare parseInt on an
id param.

Also parallelises the authz-matrix probes (bounded at 6). Serially the gate
took over ten minutes, and a gate that slow does not get run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ccantynz-alt committed on July 27, 2026Parent: 45ac593
11 files changed+19840fc623bf54a97762e6f3c00a01677a1a37929a371
11 changed files+198−40
Modifiedscripts/production-readiness.mjs+16−1View fileUnifiedSplit
198198 }
199199 };
200200
201 for (const route of routes) {
201 // Bounded concurrency. Serially this is 50+ routes x 3 identities of
202 // round-trip latency — over ten minutes, which is long enough that nobody
203 // runs the gate, and a gate nobody runs is worth nothing. Capped low
204 // because hammering the server produces spurious slow-render timeouts in
205 // the perf gate below.
206 const LIMIT = 6;
207 let cursor = 0;
208 const worker = async () => {
209 for (;;) {
210 const i = cursor++;
211 if (i >= routes.length) return;
212 await checkRoute(routes[i]);
213 }
214 };
215
216 async function checkRoute(route) {
202217 // 1. Private repo must not render to an anonymous caller.
203218 const anon = await probe(materialize(route, po, pn), null);
204219 if (anon === 200) leaks.push(`anon ${route} -> 200`);
Addedsrc/__tests__/route-params.test.ts+95−0View fileUnifiedSplit
1/**
2 * Malformed URL ids must 404, not 500.
3 *
4 * Handlers passed raw path segments into database predicates.
5 * `parseInt("abc", 10)` is NaN, and NaN or a malformed UUID makes Postgres
6 * raise, which the global error handler turns into a 500. Live, before the
7 * fix: /:owner/:repo/issues/abc returned 500 while
8 * /:owner/:repo/issues/99999999 correctly returned 404 — so a typo or a stale
9 * link produced a server error rather than "not found".
10 *
11 * Found by the authz-matrix gate, which probes every repo-scoped route with a
12 * placeholder id; eight routes answered 5xx.
13 */
14
15import { describe, expect, it } from "bun:test";
16import { parseIdNumber, parseIdUuid } from "../lib/route-params";
17
18describe("parseIdNumber", () => {
19 it("accepts a positive integer", () => {
20 expect(parseIdNumber("1")).toBe(1);
21 expect(parseIdNumber("42")).toBe(42);
22 expect(parseIdNumber("99999999")).toBe(99999999);
23 });
24
25 for (const bad of ["abc", "", "0", "-1", "1.5", " 1", "1 ", "NaN", "Infinity"]) {
26 it(`rejects ${JSON.stringify(bad)}`, () => {
27 expect(parseIdNumber(bad)).toBeNull();
28 });
29 }
30
31 it("rejects numeric-prefixed junk that parseInt would truncate", () => {
32 // parseInt("12abc", 10) === 12, which would silently load the wrong row.
33 expect(parseIdNumber("12abc")).toBeNull();
34 });
35
36 it("rejects undefined", () => {
37 expect(parseIdNumber(undefined)).toBeNull();
38 });
39
40 it("rejects values beyond safe integer range", () => {
41 expect(parseIdNumber("999999999999999999999")).toBeNull();
42 });
43});
44
45describe("parseIdUuid", () => {
46 it("accepts a well-formed uuid in either case", () => {
47 const u = "df1c027c-a830-4c42-aafc-b3bc7155477c";
48 expect(parseIdUuid(u)).toBe(u);
49 expect(parseIdUuid(u.toUpperCase())).toBe(u.toUpperCase());
50 });
51
52 for (const bad of [
53 "abc",
54 "",
55 "df1c027c-a830-4c42-aafc",
56 "df1c027c_a830_4c42_aafc_b3bc7155477c",
57 "zzzzzzzz-a830-4c42-aafc-b3bc7155477c",
58 "df1c027c-a830-4c42-aafc-b3bc7155477cc",
59 ]) {
60 it(`rejects ${JSON.stringify(bad)}`, () => {
61 expect(parseIdUuid(bad)).toBeNull();
62 });
63 }
64});
65
66describe("the sentinel flows into existing not-found paths", () => {
67 it("callers use `?? -1`, which matches no row", () => {
68 // Deliberate: -1 can never equal a real issue/PR number, so each handler
69 // falls through the not-found branch it already had. No new branches, no
70 // behaviour change for valid input.
71 const { readFileSync } = require("fs") as typeof import("fs");
72 const issues = readFileSync("src/routes/issues.tsx", "utf8");
73 expect(issues).toContain("parseIdNumber(c.req.param(\"number\")) ?? -1");
74 expect(issues).not.toMatch(/parseInt\(c\.req\.param\("number"\), 10\)/);
75 });
76
77 it("no route still parses a number param with bare parseInt", () => {
78 const { readdirSync, readFileSync, statSync } = require("fs") as typeof import("fs");
79 const { join } = require("path") as typeof import("path");
80 const walk = (d: string): string[] =>
81 readdirSync(d).flatMap((e) => {
82 const p = join(d, e);
83 return statSync(p).isDirectory() ? walk(p) : [p];
84 });
85 const offenders: string[] = [];
86 for (const f of walk("src/routes")) {
87 if (!/\.tsx?$/.test(f)) continue;
88 const src = readFileSync(f, "utf8");
89 if (/parseInt\(c\.req\.param\("(number|prNumber|issueNumber)"\), 10\)/.test(src)) {
90 offenders.push(f.replace(/\\/g, "/"));
91 }
92 }
93 expect(offenders).toEqual([]);
94 });
95});
Addedsrc/lib/route-params.ts+37−0View fileUnifiedSplit
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 */
20export 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 */
32export 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}
Modifiedsrc/routes/ai-workspace.tsx+3−2View fileUnifiedSplit
66 */
77
88import { Hono } from "hono";
9import { parseIdNumber } from "../lib/route-params";
910import { eq, and } from "drizzle-orm";
1011import { db } from "../db";
1112import { issues, repositories, users } from "../db/schema";
466467 requireRepoAccess("read"),
467468 async (c) => {
468469 const { owner, repo } = c.req.param();
469 const issueNum = parseInt(c.req.param("number"), 10);
470 const issueNum = (parseIdNumber(c.req.param("number")) ?? -1);
470471 const user = c.get("user");
471472
472473 const resolved = await resolveIssueAndRepo(owner, repo, issueNum);
504505 requireRepoAccess("write"),
505506 async (c) => {
506507 const { owner, repo } = c.req.param();
507 const issueNum = parseInt(c.req.param("number"), 10);
508 const issueNum = (parseIdNumber(c.req.param("number")) ?? -1);
508509 const user = c.get("user")!;
509510
510511 if (!isAiAvailable()) {
Modifiedsrc/routes/api-v2.ts+6−5View fileUnifiedSplit
77 */
88
99import { Hono } from "hono";
10import { parseIdNumber } from "../lib/route-params";
1011import { join } from "path";
1112import { eq, and, desc, asc, sql, like, or, gte, lte, gt } from "drizzle-orm";
1213import { deflateRawSync } from "node:zlib";
11821183
11831184apiv2.get("/repos/:owner/:repo/issues/:number", async (c) => {
11841185 const { owner, repo } = c.req.param();
1185 const num = parseInt(c.req.param("number"), 10);
1186 const num = (parseIdNumber(c.req.param("number")) ?? -1);
11861187
11871188 const resolved = await resolveRepo(owner, repo);
11881189 if (!resolved) return c.json({ error: "Not found" }, 404);
12211222
12221223apiv2.patch("/repos/:owner/:repo/issues/:number", requireApiAuth, requireScope("repo"), async (c) => {
12231224 const { owner, repo } = c.req.param();
1224 const num = parseInt(c.req.param("number"), 10);
1225 const num = (parseIdNumber(c.req.param("number")) ?? -1);
12251226 const body = await c.req.json<{ title?: string; body?: string; state?: "open" | "closed" }>();
12261227
12271228 const resolved = await resolveRepo(owner, repo);
12481249
12491250apiv2.post("/repos/:owner/:repo/issues/:number/comments", requireApiAuth, requireScope("repo"), async (c) => {
12501251 const { owner, repo } = c.req.param();
1251 const num = parseInt(c.req.param("number"), 10);
1252 const num = (parseIdNumber(c.req.param("number")) ?? -1);
12521253 const user = c.get("user")!;
12531254 const body = await c.req.json<{ body: string }>();
12541255
13421343
13431344apiv2.get("/repos/:owner/:repo/pulls/:number", async (c) => {
13441345 const { owner, repo } = c.req.param();
1345 const num = parseInt(c.req.param("number"), 10);
1346 const num = (parseIdNumber(c.req.param("number")) ?? -1);
13461347
13471348 const resolved = await resolveRepo(owner, repo);
13481349 if (!resolved) return c.json({ error: "Not found" }, 404);
14681469 requireScope("repo"),
14691470 async (c) => {
14701471 const { owner, repo } = c.req.param();
1471 const num = parseInt(c.req.param("number"), 10);
1472 const num = (parseIdNumber(c.req.param("number")) ?? -1);
14721473 const user = c.get("user")!;
14731474
14741475 let body: { body?: string } = {};
Modifiedsrc/routes/claude-web.tsx+7−3View fileUnifiedSplit
1818 */
1919
2020import { Hono } from "hono";
21import { parseIdUuid } from "../lib/route-params";
2122import { and, eq } from "drizzle-orm";
2223import { db } from "../db";
2324import { repositories, users } from "../db/schema";
192193 const g = await gate(c);
193194 if (g instanceof Response) return g;
194195 const user = c.get("user")!;
195 const sessionId = c.req.param("sessionId");
196 const sessionId = parseIdUuid(c.req.param("sessionId"));
197 if (!sessionId) return c.notFound();
196198 const session = await getSession(sessionId, g.userId);
197199 if (!session || session.repositoryId !== g.repoId) return c.notFound();
198200
311313claudeWeb.get("/:owner/:repo/claude/:sessionId/stream", async (c) => {
312314 const g = await gate(c);
313315 if (g instanceof Response) return g;
314 const sessionId = c.req.param("sessionId");
316 const sessionId = parseIdUuid(c.req.param("sessionId"));
317 if (!sessionId) return c.notFound();
315318 const session = await getSession(sessionId, g.userId);
316319 if (!session || session.repositoryId !== g.repoId) return c.notFound();
317320 const prompt = (c.req.query("prompt") || "").slice(0, 16_000);
416419claudeWeb.post("/:owner/:repo/claude/:sessionId/delete", async (c) => {
417420 const g = await gate(c);
418421 if (g instanceof Response) return g;
419 const sessionId = c.req.param("sessionId");
422 const sessionId = parseIdUuid(c.req.param("sessionId"));
423 if (!sessionId) return c.notFound();
420424 const session = await getSession(sessionId, g.userId);
421425 if (!session || session.repositoryId !== g.repoId) return c.notFound();
422426 await deleteSession(sessionId);
Modifiedsrc/routes/issues.tsx+8−7View fileUnifiedSplit
33 */
44
55import { Hono } from "hono";
6import { parseIdNumber } from "../lib/route-params";
67import { eq, and, desc, asc, sql, ilike, inArray, or } from "drizzle-orm";
78import { db } from "../db";
89import { fireWebhooks } from "./webhooks";
14391440// View single issue
14401441issueRoutes.get("/:owner/:repo/issues/:number", softAuth, requireRepoAccess("read"), async (c) => {
14411442 const { owner: ownerName, repo: repoName } = c.req.param();
1442 const issueNum = parseInt(c.req.param("number"), 10);
1443 const issueNum = (parseIdNumber(c.req.param("number")) ?? -1);
14431444 const user = c.get("user");
14441445
14451446 const resolved = await resolveRepo(ownerName, repoName);
20582059 requireRepoAccess("read"),
20592060 async (c) => {
20602061 const { owner: ownerName, repo: repoName } = c.req.param();
2061 const issueNum = parseInt(c.req.param("number"), 10);
2062 const issueNum = (parseIdNumber(c.req.param("number")) ?? -1);
20622063 const user = c.get("user")!;
20632064 const body = await c.req.parseBody();
20642065 const commentBody = String(body.body || "").trim();
21772178 requireRepoAccess("write"),
21782179 async (c) => {
21792180 const { owner: ownerName, repo: repoName } = c.req.param();
2180 const issueNum = parseInt(c.req.param("number"), 10);
2181 const issueNum = (parseIdNumber(c.req.param("number")) ?? -1);
21812182
21822183 const resolved = await resolveRepo(ownerName, repoName);
21832184 if (!resolved) return c.redirect(`/${ownerName}/${repoName}`);
22102211 requireRepoAccess("write"),
22112212 async (c) => {
22122213 const { owner: ownerName, repo: repoName } = c.req.param();
2213 const issueNum = parseInt(c.req.param("number"), 10);
2214 const issueNum = (parseIdNumber(c.req.param("number")) ?? -1);
22142215
22152216 const resolved = await resolveRepo(ownerName, repoName);
22162217 if (!resolved) return c.redirect(`/${ownerName}/${repoName}`);
22612262 requireRepoAccess("write"),
22622263 async (c) => {
22632264 const { owner: ownerName, repo: repoName } = c.req.param();
2264 const issueNum = parseInt(c.req.param("number"), 10);
2265 const issueNum = (parseIdNumber(c.req.param("number")) ?? -1);
22652266 const user = c.get("user")!;
22662267 const resolved = await resolveRepo(ownerName, repoName);
22672268 if (!resolved) return c.redirect(`/${ownerName}/${repoName}`);
23192320 requireRepoAccess("write"),
23202321 async (c) => {
23212322 const { owner: ownerName, repo: repoName } = c.req.param();
2322 const issueNum = parseInt(c.req.param("number"), 10);
2323 const issueNum = (parseIdNumber(c.req.param("number")) ?? -1);
23232324
23242325 const resolved = await resolveRepo(ownerName, repoName);
23252326 if (!resolved) return c.redirect(`/${ownerName}/${repoName}/issues`);
23902391 requireRepoAccess("write"),
23912392 async (c) => {
23922393 const { owner: ownerName, repo: repoName } = c.req.param();
2393 const issueNum = parseInt(c.req.param("number"), 10);
2394 const issueNum = (parseIdNumber(c.req.param("number")) ?? -1);
23942395 const form = await c.req.formData();
23952396 const parentNum = form.get("parent_number")?.toString().trim() ?? "";
23962397
Modifiedsrc/routes/merge-queue.tsx+2−1View fileUnifiedSplit
1717 */
1818
1919import { Hono } from "hono";
20import { parseIdNumber } from "../lib/route-params";
2021import { and, eq } from "drizzle-orm";
2122import { db } from "../db";
2223import {
787788queue.post("/:owner/:repo/pulls/:number/enqueue", requireAuth, async (c) => {
788789 const user = c.get("user")!;
789790 const { owner, repo } = c.req.param();
790 const prNum = parseInt(c.req.param("number"), 10);
791 const prNum = (parseIdNumber(c.req.param("number")) ?? -1);
791792 const repoRow = await loadRepo(owner, repo);
792793 if (!repoRow) return c.notFound();
793794
Modifiedsrc/routes/pr-sandbox.ts+4−3View fileUnifiedSplit
2020 */
2121
2222import { Hono } from "hono";
23import { parseIdNumber } from "../lib/route-params";
2324import { and, eq } from "drizzle-orm";
2425import { db } from "../db";
2526import { pullRequests, repositories, users } from "../db/schema";
99100 requireRepoAccess("write"),
100101 async (c) => {
101102 const { owner: ownerName, repo: repoName } = c.req.param();
102 const n = parseInt(c.req.param("number"), 10);
103 const n = (parseIdNumber(c.req.param("number")) ?? -1);
103104 const resolved = await resolveRepoRow(ownerName, repoName);
104105 if (!resolved) return c.json({ ok: false, error: "Repo not found" }, 404);
105106 const pr = await resolvePr(resolved.repo.id, n);
126127 requireRepoAccess("write"),
127128 async (c) => {
128129 const { owner: ownerName, repo: repoName } = c.req.param();
129 const n = parseInt(c.req.param("number"), 10);
130 const n = (parseIdNumber(c.req.param("number")) ?? -1);
130131 const resolved = await resolveRepoRow(ownerName, repoName);
131132 if (!resolved) return c.json({ ok: false, error: "Repo not found" }, 404);
132133 const pr = await resolvePr(resolved.repo.id, n);
151152 requireRepoAccess("read"),
152153 async (c) => {
153154 const { owner: ownerName, repo: repoName } = c.req.param();
154 const n = parseInt(c.req.param("number"), 10);
155 const n = (parseIdNumber(c.req.param("number")) ?? -1);
155156 const resolved = await resolveRepoRow(ownerName, repoName);
156157 if (!resolved) return c.json({ ok: false, error: "Repo not found" }, 404);
157158 const pr = await resolvePr(resolved.repo.id, n);
Modifiedsrc/routes/pulls.tsx+18−17View fileUnifiedSplit
1414 */
1515
1616import { Hono } from "hono";
17import { parseIdNumber } from "../lib/route-params";
1718import { eq, and, desc, asc, sql, inArray, ilike, ne, isNotNull } from "drizzle-orm";
1819import { db } from "../db";
1920import {
39213922// View single PR
39223923pulls.get("/:owner/:repo/pulls/:number", softAuth, requireRepoAccess("read"), async (c) => {
39233924 const { owner: ownerName, repo: repoName } = c.req.param();
3924 const prNum = parseInt(c.req.param("number"), 10);
3925 const prNum = (parseIdNumber(c.req.param("number")) ?? -1);
39253926 const user = c.get("user");
39263927 const tab = c.req.query("tab") || "conversation";
39273928 const isSplit = c.req.query("diffview") === "split";
54075408 requireRepoAccess("write"),
54085409 async (c) => {
54095410 const { owner: ownerName, repo: repoName } = c.req.param();
5410 const prNum = parseInt(c.req.param("number"), 10);
5411 const prNum = (parseIdNumber(c.req.param("number")) ?? -1);
54115412 const user = c.get("user")!;
54125413 const resolved = await resolveRepo(ownerName, repoName);
54135414 if (!resolved) return c.redirect(`/${ownerName}/${repoName}`);
54835484 requireRepoAccess("write"),
54845485 async (c) => {
54855486 const { owner: ownerName, repo: repoName } = c.req.param();
5486 const prNum = parseInt(c.req.param("number"), 10);
5487 const prNum = (parseIdNumber(c.req.param("number")) ?? -1);
54875488 const user = c.get("user")!;
54885489 const resolved = await resolveRepo(ownerName, repoName);
54895490 if (!resolved) return c.redirect(`/${ownerName}/${repoName}`);
55355536 requireRepoAccess("read"),
55365537 async (c) => {
55375538 const { owner: ownerName, repo: repoName } = c.req.param();
5538 const prNum = parseInt(c.req.param("number"), 10);
5539 const prNum = (parseIdNumber(c.req.param("number")) ?? -1);
55395540 const user = c.get("user")!;
55405541 const body = await c.req.parseBody();
55415542 const commentBody = String(body.body || "").trim();
56995700 requireRepoAccess("read"),
57005701 async (c) => {
57015702 const { owner: ownerName, repo: repoName } = c.req.param();
5702 const prNum = parseInt(c.req.param("number"), 10);
5703 const prNum = (parseIdNumber(c.req.param("number")) ?? -1);
57035704 const user = c.get("user")!;
57045705
57055706 const resolved = await resolveRepo(ownerName, repoName);
57395740 requireRepoAccess("read"),
57405741 async (c) => {
57415742 const { owner: ownerName, repo: repoName } = c.req.param();
5742 const prNum = parseInt(c.req.param("number"), 10);
5743 const prNum = (parseIdNumber(c.req.param("number")) ?? -1);
57435744 const user = c.get("user")!;
57445745 const body = await c.req.parseBody();
57455746 const filePath = String(body.filePath || "").trim();
58115812 requireRepoAccess("read"),
58125813 async (c) => {
58135814 const { owner: ownerName, repo: repoName, commentId } = c.req.param();
5814 const prNum = parseInt(c.req.param("number"), 10);
5815 const prNum = (parseIdNumber(c.req.param("number")) ?? -1);
58155816 const user = c.get("user")!;
58165817
58175818 const resolved = await resolveRepo(ownerName, repoName);
58505851 requireRepoAccess("read"),
58515852 async (c) => {
58525853 const { owner: ownerName, repo: repoName } = c.req.param();
5853 const prNum = parseInt(c.req.param("number"), 10);
5854 const prNum = (parseIdNumber(c.req.param("number")) ?? -1);
58545855 const user = c.get("user")!;
58555856 const body = await c.req.parseBody();
58565857 const reviewBody = String(body.reviewBody || "").trim();
59245925 requireRepoAccess("read"),
59255926 async (c) => {
59265927 const { owner: ownerName, repo: repoName } = c.req.param();
5927 const prNum = parseInt(c.req.param("number"), 10);
5928 const prNum = (parseIdNumber(c.req.param("number")) ?? -1);
59285929 const commentId = c.req.param("commentId"); // UUID
59295930 const user = c.get("user")!;
59305931
60426043 requireRepoAccess("read"),
60436044 async (c) => {
60446045 const { owner: ownerName, repo: repoName } = c.req.param();
6045 const prNum = parseInt(c.req.param("number"), 10);
6046 const prNum = (parseIdNumber(c.req.param("number")) ?? -1);
60466047 const user = c.get("user")!;
60476048 const body = await c.req.parseBody();
60486049 const reviewBody = String(body.body || "").trim();
61066107 requireRepoAccess("write"),
61076108 async (c) => {
61086109 const { owner: ownerName, repo: repoName } = c.req.param();
6109 const prNum = parseInt(c.req.param("number"), 10);
6110 const prNum = (parseIdNumber(c.req.param("number")) ?? -1);
61106111 const user = c.get("user")!;
61116112
61126113 // Read merge strategy from form (default: merge commit)
64766477 requireRepoAccess("write"),
64776478 async (c) => {
64786479 const { owner: ownerName, repo: repoName } = c.req.param();
6479 const prNum = parseInt(c.req.param("number"), 10);
6480 const prNum = (parseIdNumber(c.req.param("number")) ?? -1);
64806481 const user = c.get("user")!;
64816482
64826483 const resolved = await resolveRepo(ownerName, repoName);
65306531 requireRepoAccess("write"),
65316532 async (c) => {
65326533 const { owner: ownerName, repo: repoName } = c.req.param();
6533 const prNum = parseInt(c.req.param("number"), 10);
6534 const prNum = (parseIdNumber(c.req.param("number")) ?? -1);
65346535 const user = c.get("user")!;
65356536
65366537 const resolved = await resolveRepo(ownerName, repoName);
65716572 requireRepoAccess("write"),
65726573 async (c) => {
65736574 const { owner: ownerName, repo: repoName } = c.req.param();
6574 const prNum = parseInt(c.req.param("number"), 10);
6575 const prNum = (parseIdNumber(c.req.param("number")) ?? -1);
65756576
65766577 const resolved = await resolveRepo(ownerName, repoName);
65776578 if (!resolved) return c.redirect(`/${ownerName}/${repoName}`);
66076608 requireRepoAccess("write"),
66086609 async (c) => {
66096610 const { owner: ownerName, repo: repoName } = c.req.param();
6610 const prNum = parseInt(c.req.param("number"), 10);
6611 const prNum = (parseIdNumber(c.req.param("number")) ?? -1);
66116612 const resolved = await resolveRepo(ownerName, repoName);
66126613 if (!resolved) return c.redirect(`/${ownerName}/${repoName}`);
66136614
66706671 requireRepoAccess("write"),
66716672 async (c) => {
66726673 const { owner: ownerName, repo: repoName } = c.req.param();
6673 const prNum = parseInt(c.req.param("number"), 10);
6674 const prNum = (parseIdNumber(c.req.param("number")) ?? -1);
66746675 const resolved = await resolveRepo(ownerName, repoName);
66756676 if (!resolved) return c.redirect(`/${ownerName}/${repoName}`);
66766677
67266727 requireRepoAccess("write"),
67276728 async (c) => {
67286729 const { owner: ownerName, repo: repoName } = c.req.param();
6729 const prNum = parseInt(c.req.param("number"), 10);
6730 const prNum = (parseIdNumber(c.req.param("number")) ?? -1);
67306731 const user = c.get("user")!;
67316732
67326733 const resolved = await resolveRepo(ownerName, repoName);
Modifiedsrc/routes/ship-agent.tsx+2−1View fileUnifiedSplit
77 */
88
99import { Hono } from "hono";
10import { parseIdNumber } from "../lib/route-params";
1011import { eq, and } from "drizzle-orm";
1112import { db } from "../db";
1213import { issues, repositories, users } from "../db/schema";
210211 }
211212
212213 const { owner: ownerName, repo: repoName } = c.req.param();
213 const issueNum = parseInt(c.req.param("issueNumber"), 10);
214 const issueNum = (parseIdNumber(c.req.param("issueNumber")) ?? -1);
214215 const user = c.get("user")!;
215216
216217 const resolved = await resolveIssue(ownerName, repoName, issueNum);
217218