From 4ad4c0722f150f20de26191fe65690c87e894be5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:28:16 +0000 Subject: [PATCH 1/3] Initial plan From 11faf51457291842a132309a2aa4f84426ad2c8a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:33:17 +0000 Subject: [PATCH 2/3] fix: harden authz checks for game result endpoints Agent-Logs-Url: https://github.com/ut-code/hitori-mahjong/sessions/57392065-5323-4fba-a63b-7722d9200a64 Co-authored-by: tknkaa <145080781+tknkaa@users.noreply.github.com> --- app/routes/api.agari.ts | 27 +++++++++++---------------- app/routes/api.ryukyoku.ts | 3 +++ app/routes/api.seed.ts | 14 +++++--------- app/routes/api.tedashi.ts | 2 +- 4 files changed, 20 insertions(+), 26 deletions(-) diff --git a/app/routes/api.agari.ts b/app/routes/api.agari.ts index cd8c3b2..fb23776 100644 --- a/app/routes/api.agari.ts +++ b/app/routes/api.agari.ts @@ -1,14 +1,11 @@ import { redirect } from "react-router"; -import { z } from "zod"; import { getAuth } from "~/lib/auth"; import { getDB } from "~/lib/db"; import { getGameState, recordKyoku, restartGame } from "~/lib/game-service"; +import judgeAgari from "~/lib/hai/agari"; +import { sortTehai } from "~/lib/hai/types"; import type { Route } from "./+types/api.agari"; -const agariSchema = z.object({ - junme: z.coerce.number().optional(), -}); - export async function action({ context, request }: Route.ActionArgs) { const env = context.cloudflare.env; const auth = getAuth(env); @@ -17,15 +14,6 @@ export async function action({ context, request }: Route.ActionArgs) { return new Response("Unauthorized", { status: 401 }); } - const formData = await request.formData(); - const junme = formData.get("junme"); - const parsedData = agariSchema.safeParse({ - junme: junme === null || junme === "" ? undefined : junme, - }); - if (!parsedData.success) { - return new Response("Invalid form data", { status: 400 }); - } - const db = getDB(env); const userId = session.user.id; const gameStateRecord = await getGameState(db, userId); @@ -33,12 +21,19 @@ export async function action({ context, request }: Route.ActionArgs) { return new Response("Game state not found", { status: 404 }); } - const agariJunme = parsedData.data.junme ?? gameStateRecord.junme; + const tsumohai = gameStateRecord.tsumohai[0]; + if (!tsumohai) { + return new Response("Invalid agari request", { status: 400 }); + } + const canAgari = judgeAgari(sortTehai([...gameStateRecord.tehai, tsumohai])); + if (!canAgari) { + return new Response("Invalid agari request", { status: 400 }); + } // Record win with +8000 points await recordKyoku(db, userId, { didAgari: true, - agariJunme, + agariJunme: gameStateRecord.junme, shanten: 0, scoreDelta: 8000, }); diff --git a/app/routes/api.ryukyoku.ts b/app/routes/api.ryukyoku.ts index 9cc67da..9e20cef 100644 --- a/app/routes/api.ryukyoku.ts +++ b/app/routes/api.ryukyoku.ts @@ -19,6 +19,9 @@ export async function action({ context, request }: Route.ActionArgs) { if (!gameStateRecord) { return new Response("Game state not found", { status: 404 }); } + if (gameStateRecord.remainTsumo > 0) { + return new Response("Ryukyoku is not allowed yet", { status: 400 }); + } // Calculate shanten for current hand const shantenResult = calculateShanten(gameStateRecord.tehai); diff --git a/app/routes/api.seed.ts b/app/routes/api.seed.ts index a4b600e..78fbb7d 100644 --- a/app/routes/api.seed.ts +++ b/app/routes/api.seed.ts @@ -39,15 +39,11 @@ async function seedAndRespond(env: Env, countInput: unknown) { }); } -export async function loader({ context, request }: Route.LoaderArgs) { - const env = context.cloudflare.env; - const session = await ensureSession(request, env); - if (!session) { - return new Response("Unauthorized", { status: 401 }); - } - - const count = new URL(request.url).searchParams.get("count") ?? undefined; - return seedAndRespond(env, count); +export async function loader(_: Route.LoaderArgs) { + return new Response("Method Not Allowed", { + status: 405, + headers: { Allow: "POST" }, + }); } export async function action({ context, request }: Route.ActionArgs) { diff --git a/app/routes/api.tedashi.ts b/app/routes/api.tedashi.ts index 521ab00..443d8cf 100644 --- a/app/routes/api.tedashi.ts +++ b/app/routes/api.tedashi.ts @@ -6,7 +6,7 @@ import { getGameState, tedashi } from "~/lib/game-service"; import type { Route } from "./+types/api.tedashi"; const tedashiSchema = z.object({ - index: z.coerce.number(), + index: z.coerce.number().int().min(0), }); export async function action({ context, request }: Route.ActionArgs) { From 7e1cca5f31e1fd05f3d32b8709741b1c2a61665c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:36:40 +0000 Subject: [PATCH 3/3] chore: clarify agari validation error responses Agent-Logs-Url: https://github.com/ut-code/hitori-mahjong/sessions/57392065-5323-4fba-a63b-7722d9200a64 Co-authored-by: tknkaa <145080781+tknkaa@users.noreply.github.com> --- app/routes/api.agari.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/routes/api.agari.ts b/app/routes/api.agari.ts index fb23776..ab843a8 100644 --- a/app/routes/api.agari.ts +++ b/app/routes/api.agari.ts @@ -23,11 +23,13 @@ export async function action({ context, request }: Route.ActionArgs) { const tsumohai = gameStateRecord.tsumohai[0]; if (!tsumohai) { - return new Response("Invalid agari request", { status: 400 }); + return new Response("No tile drawn - cannot declare win", { status: 400 }); } const canAgari = judgeAgari(sortTehai([...gameStateRecord.tehai, tsumohai])); if (!canAgari) { - return new Response("Invalid agari request", { status: 400 }); + return new Response("Hand does not form a valid winning combination", { + status: 400, + }); } // Record win with +8000 points