From 952b0fda8ef1eb7d2e62625fc9df523ca9a79ac6 Mon Sep 17 00:00:00 2001 From: "tyrion.lh" Date: Thu, 9 Jul 2026 15:51:26 -0700 Subject: [PATCH] fix(nodejs): narrow required route parameters --- rest/nodejs/src/api/checkout.ts | 17 +++++++++-------- rest/nodejs/src/api/order.ts | 10 +++++----- rest/nodejs/src/api/testing.ts | 7 +++---- rest/nodejs/src/utils/validation.ts | 11 ++++++++++- rest/nodejs/test/validation.test.ts | 19 +++++++++++++++++++ 5 files changed, 46 insertions(+), 18 deletions(-) create mode 100644 rest/nodejs/test/validation.test.ts diff --git a/rest/nodejs/src/api/checkout.ts b/rest/nodejs/src/api/checkout.ts index caa7aa85..cc1a9d23 100644 --- a/rest/nodejs/src/api/checkout.ts +++ b/rest/nodejs/src/api/checkout.ts @@ -37,6 +37,7 @@ import { PaymentDataSchema, type PostalAddress, } from "../models"; +import { type IdParamContext } from "../utils/validation"; /** * Schema for the request body when completing a checkout session. @@ -494,8 +495,8 @@ export class CheckoutService { } }; - getCheckout = async (c: Context) => { - const id = c.req.param("id"); + getCheckout = async (c: IdParamContext) => { + const { id } = c.req.valid("param"); // Log Request logRequest("GET", `/checkout-sessions/${id}`, id, {}); @@ -507,8 +508,8 @@ export class CheckoutService { return c.json(checkout, 200); }; - updateCheckout = async (c: Context) => { - const id = c.req.param("id"); + updateCheckout = async (c: IdParamContext) => { + const { id } = c.req.valid("param"); const idempotencyKey = c.req.header("Idempotency-Key"); const ucpAgent = c.req.header("UCP-Agent"); const updateRequest = await c.req.json(); @@ -627,8 +628,8 @@ export class CheckoutService { } }; - completeCheckout = async (c: Context) => { - const id = c.req.param("id"); + completeCheckout = async (c: IdParamContext) => { + const { id } = c.req.valid("param"); const idempotencyKey = c.req.header("Idempotency-Key"); const rawBody = await c.req.json(); let requestHash = ""; @@ -863,8 +864,8 @@ export class CheckoutService { } }; - cancelCheckout = async (c: Context) => { - const id = c.req.param("id"); + cancelCheckout = async (c: IdParamContext) => { + const { id } = c.req.valid("param"); const idempotencyKey = c.req.header("Idempotency-Key"); const rawBody = {}; // Empty body for cancel usually let requestHash = ""; diff --git a/rest/nodejs/src/api/order.ts b/rest/nodejs/src/api/order.ts index aa7b5b33..fe8cecd6 100644 --- a/rest/nodejs/src/api/order.ts +++ b/rest/nodejs/src/api/order.ts @@ -1,13 +1,13 @@ -import { type Context } from "hono"; import { getOrder, logRequest, saveOrder } from "../data"; import { type Order } from "../models"; +import { type IdParamContext } from "../utils/validation"; /** * Service for managing orders. */ export class OrderService { - getOrder = async (c: Context) => { - const id = c.req.param("id"); + getOrder = async (c: IdParamContext) => { + const { id } = c.req.valid("param"); // Log Request logRequest("GET", `/orders/${id}`, undefined, {}); @@ -19,8 +19,8 @@ export class OrderService { return c.json(order, 200); }; - updateOrder = async (c: Context) => { - const id = c.req.param("id"); + updateOrder = async (c: IdParamContext) => { + const { id } = c.req.valid("param"); const updateRequest = await c.req.json(); // Log Request diff --git a/rest/nodejs/src/api/testing.ts b/rest/nodejs/src/api/testing.ts index 00b21444..c08bb065 100644 --- a/rest/nodejs/src/api/testing.ts +++ b/rest/nodejs/src/api/testing.ts @@ -1,11 +1,10 @@ -import { type Context } from "hono"; - +import { type IdParamContext } from "../utils/validation"; import { CheckoutService } from "./checkout"; export class TestingService { constructor(private readonly checkoutService: CheckoutService) {} - shipOrder = async (c: Context) => { + shipOrder = async (c: IdParamContext) => { const secret = c.req.header("Simulation-Secret"); const expectedSecret = process.env.SIMULATION_SECRET || "super-secret-sim-key"; @@ -14,7 +13,7 @@ export class TestingService { return c.json({ detail: "Invalid Simulation Secret" }, 403); } - const id = c.req.param("id"); + const { id } = c.req.valid("param"); try { await this.checkoutService.shipOrder(id); return c.json({ status: "shipped" }, 200); diff --git a/rest/nodejs/src/utils/validation.ts b/rest/nodejs/src/utils/validation.ts index bd38dd0c..7a625b49 100644 --- a/rest/nodejs/src/utils/validation.ts +++ b/rest/nodejs/src/utils/validation.ts @@ -1,4 +1,4 @@ -import { type Context } from "hono"; +import { type Context, type Env } from "hono"; import * as z from "zod"; /** @@ -36,3 +36,12 @@ export function prettyValidation( export const IdParamSchema = z.object({ id: z.string(), }); + +export type IdParamContext = Context< + Env, + string, + { + in: { param: z.input }; + out: { param: z.output }; + } +>; diff --git a/rest/nodejs/test/validation.test.ts b/rest/nodejs/test/validation.test.ts new file mode 100644 index 00000000..be4cc020 --- /dev/null +++ b/rest/nodejs/test/validation.test.ts @@ -0,0 +1,19 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { IdParamSchema } from "../src/utils/validation"; + +test("accepts a required route ID", () => { + const result = IdParamSchema.safeParse({ id: "item-123" }); + + assert.equal(result.success, true); + if (result.success) { + assert.equal(result.data.id, "item-123"); + } +}); + +test("rejects a missing route ID", () => { + const result = IdParamSchema.safeParse({}); + + assert.equal(result.success, false); +});