From 9a4307da67debc9c8458b79099ddc3ac281d190d Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Wed, 15 Apr 2026 12:05:20 +1000 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20target=20Node=2024=20LTS=20?= =?UTF-8?q?=E2=80=94=20bump=20.nvmrc,=20add=20engines=20field,=20CI=20on?= =?UTF-8?q?=20Node=2024,=20drop=20node-fetch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 21 ++++++++++++++++ .nvmrc | 2 +- package.json | 4 +++- pnpm-lock.yaml | 49 -------------------------------------- 4 files changed, 25 insertions(+), 51 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..061dd848 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,21 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + - run: pnpm install + - run: pnpm build + - run: pnpm test diff --git a/.nvmrc b/.nvmrc index 209e3ef4..a45fd52c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 +24 diff --git a/package.json b/package.json index 0de507e4..1c57e077 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,9 @@ "author": "Marcos Caceres ", "type": "module", "license": "MIT", + "engines": { + "node": ">=24" + }, "dependencies": { "chalk": "^5.6.2", "compression": "^1.8.1", @@ -16,7 +19,6 @@ "helmet": "^8.0.0", "morgan": "^1.10.1", "nanoid": "^5.0.8", - "node-fetch": "^3.3.2", "serialize-error": "^11.0.3", "split2": "^4.2.0", "ucontent": "^2.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6b96d07..20aba9a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,9 +32,6 @@ importers: nanoid: specifier: ^5.0.8 version: 5.0.8 - node-fetch: - specifier: ^3.3.2 - version: 3.3.2 serialize-error: specifier: ^11.0.3 version: 11.0.3 @@ -213,10 +210,6 @@ packages: resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} engines: {node: '>=8.0.0'} - data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -276,18 +269,10 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} - fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} - formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -428,15 +413,6 @@ packages: no-case@2.3.2: resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - deprecated: Use your platform's native DOMException instead - - node-fetch@3.3.2: - resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -610,10 +586,6 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -779,8 +751,6 @@ snapshots: dependencies: css-tree: 1.1.3 - data-uri-to-buffer@4.0.1: {} - debug@2.6.9: dependencies: ms: 2.0.0 @@ -848,11 +818,6 @@ snapshots: transitivePeerDependencies: - supports-color - fetch-blob@3.2.0: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 - finalhandler@2.1.1: dependencies: debug: 4.4.3 @@ -864,10 +829,6 @@ snapshots: transitivePeerDependencies: - supports-color - formdata-polyfill@4.0.10: - dependencies: - fetch-blob: 3.2.0 - forwarded@0.2.0: {} fresh@2.0.0: {} @@ -996,14 +957,6 @@ snapshots: dependencies: lower-case: 1.1.4 - node-domexception@1.0.0: {} - - node-fetch@3.3.2: - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 - object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -1188,6 +1141,4 @@ snapshots: vary@1.1.2: {} - web-streams-polyfill@3.3.3: {} - wrappy@1.0.2: {} From 5f5cff85d42f77682269c8d2d9f885d6a33b944d Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Wed, 15 Apr 2026 12:06:01 +1000 Subject: [PATCH 2/8] refactor: replace node-fetch with native fetch, legacyDirname/Filename with import.meta, fix Express 5 param types --- app.ts | 3 +-- routes/caniuse/update.ts | 3 +-- routes/docs/update.ts | 1 - routes/github/commits.ts | 4 ++-- routes/github/contributors.ts | 2 +- routes/github/lib/utils/graphql.ts | 1 - routes/github/lib/utils/rest.ts | 3 +-- routes/w3c/group.ts | 3 +-- routes/well-known/pay.ts | 1 - routes/xref/update.ts | 3 +-- scripts/update-w3c-groups-list.ts | 8 +------- tests/node24-globals.test.js | 13 +++++++++++++ tests/utils/constants.test.js | 9 +++++++++ utils/background-task-queue.ts | 4 ++-- utils/constants.ts | 3 +-- utils/misc.ts | 13 ------------- 16 files changed, 34 insertions(+), 40 deletions(-) create mode 100644 tests/node24-globals.test.js create mode 100644 tests/utils/constants.test.js diff --git a/app.ts b/app.ts index 27228cd1..ba36c024 100644 --- a/app.ts +++ b/app.ts @@ -7,7 +7,6 @@ import helmet from "helmet"; import * as logging from "./utils/logging.js"; import { register as registerViewEngine } from "./utils/view-engine.js"; -import { legacyDirname } from "./utils/misc.js"; import { PROJECT_ROOT } from "./utils/constants.js"; import xrefRouter from "./routes/xref/index.js"; @@ -28,7 +27,7 @@ app.use(logging.stderr()); app.use(express.static(path.join(PROJECT_ROOT, "/static"))); -app.set("views", path.join(legacyDirname(import.meta), "views")); +app.set("views", path.join(import.meta.dirname, "views")); registerViewEngine(app); // Security diff --git a/routes/caniuse/update.ts b/routes/caniuse/update.ts index 7954d903..85472cce 100644 --- a/routes/caniuse/update.ts +++ b/routes/caniuse/update.ts @@ -1,11 +1,10 @@ import path from "path"; -import { legacyDirname } from "../../utils/misc.js"; import { BackgroundTaskQueue } from "../../utils/background-task-queue.js"; import { cache } from "./lib/index.js"; import { Request, Response } from "express"; -const workerFile = path.join(legacyDirname(import.meta), "update.worker.js"); +const workerFile = path.join(import.meta.dirname, "update.worker.js"); const taskQueue = new BackgroundTaskQueue( workerFile, "caniuse_update", diff --git a/routes/docs/update.ts b/routes/docs/update.ts index 2958b644..b9ede8ac 100644 --- a/routes/docs/update.ts +++ b/routes/docs/update.ts @@ -1,7 +1,6 @@ import path from "path"; import { writeFile } from "fs/promises"; -import fetch from "node-fetch"; import { Request, Response } from "express"; import { HTTPError } from "../../utils/misc.js"; diff --git a/routes/github/commits.ts b/routes/github/commits.ts index 125ca04f..bb163c17 100644 --- a/routes/github/commits.ts +++ b/routes/github/commits.ts @@ -3,7 +3,7 @@ import { seconds } from "../../utils/misc.js"; import { getCommits } from "./lib/commits.js"; export default async function route(req: Request, res: Response) { - const { org, repo } = req.params; + const { org, repo } = req.params as Record; const { from, to , path } = req.query; if (!from || typeof from !== "string") { res.set("Content-Type", "text/plain"); @@ -24,7 +24,7 @@ export default async function route(req: Request, res: Response) { try { const commits: { hash: string; message: string }[] = []; - for await (const commit of getCommits(org, repo, from, to, path)) { + for await (const commit of getCommits(org, repo, from, to, path as string | undefined)) { commits.push({ hash: commit.abbreviatedOid, message: commit.messageHeadline, diff --git a/routes/github/contributors.ts b/routes/github/contributors.ts index eadcc423..8492caa2 100644 --- a/routes/github/contributors.ts +++ b/routes/github/contributors.ts @@ -13,7 +13,7 @@ const cache = new DiskCache({ }); export default async function route(req: Request, res: Response) { - const { org, repo } = req.params; + const { org, repo } = req.params as Record; const cacheKey = `${org}/${repo}`; res.set("Cache-Control", `max-age=${seconds("24h")}`); diff --git a/routes/github/lib/utils/graphql.ts b/routes/github/lib/utils/graphql.ts index 57893fe4..3fd03617 100644 --- a/routes/github/lib/utils/graphql.ts +++ b/routes/github/lib/utils/graphql.ts @@ -1,4 +1,3 @@ -import fetch from "node-fetch"; import { getToken, updateRateLimit, RateLimit } from "./tokens.js"; const ENDPOINT = "https://api.github.com/graphql"; diff --git a/routes/github/lib/utils/rest.ts b/routes/github/lib/utils/rest.ts index 207ec9cc..390cadfa 100644 --- a/routes/github/lib/utils/rest.ts +++ b/routes/github/lib/utils/rest.ts @@ -1,4 +1,3 @@ -import fetch, { Response } from "node-fetch"; import { getToken, updateRateLimit, RateLimit } from "./tokens.js"; export async function* requestData(endpoint: string, pages = 30) { @@ -38,7 +37,7 @@ function nextPage(link: string) { return m ? m[1] : null; } -function getRateLimit(headers: Response["headers"]): RateLimit { +function getRateLimit(headers: Headers): RateLimit { return { remaining: parseInt(headers.get("x-ratelimit-remaining") as string, 10), resetAt: new Date(parseInt(headers.get("x-ratelimit-reset") as string, 10)), diff --git a/routes/w3c/group.ts b/routes/w3c/group.ts index 677ae957..0be9960b 100644 --- a/routes/w3c/group.ts +++ b/routes/w3c/group.ts @@ -1,7 +1,6 @@ import path from "path"; import { readFileSync } from "fs"; -import fetch from "node-fetch"; import { Request, Response } from "express"; import { MemCache } from "../../utils/mem-cache.js"; @@ -40,7 +39,7 @@ const LEGACY_SHORTNAMES = new Map([ ]); export default async function route(req: Request, res: Response) { - const { shortname, type } = req.params; + const { shortname, type } = req.params as Record; if (!shortname) { if (req.headers.accept?.includes("text/html")) { return res.render("w3c/groups.js", { groups }); diff --git a/routes/well-known/pay.ts b/routes/well-known/pay.ts index 418e79a8..be754aa8 100644 --- a/routes/well-known/pay.ts +++ b/routes/well-known/pay.ts @@ -1,5 +1,4 @@ import { Request, Response } from "express"; -import fetch from "node-fetch"; const PAYMENT_POINTERS = [ // "$ilp.uphold.com/DwJmxPHHi8K3", // Marcos diff --git a/routes/xref/update.ts b/routes/xref/update.ts index 8fb16298..07c086ec 100644 --- a/routes/xref/update.ts +++ b/routes/xref/update.ts @@ -2,14 +2,13 @@ import path from "path"; import { Request, Response } from "express"; -import { legacyDirname } from "../../utils/misc.js"; import { BackgroundTaskQueue } from "../../utils/background-task-queue.js"; import { ms } from "../../utils/misc.js"; import { cache as searchCache } from "./lib/search.js"; import { store } from "./lib/store-init.js"; -const workerFile = path.join(legacyDirname(import.meta), "update.worker.js"); +const workerFile = path.join(import.meta.dirname, "update.worker.js"); const taskQueue = new BackgroundTaskQueue( workerFile, "xref_update", diff --git a/scripts/update-w3c-groups-list.ts b/scripts/update-w3c-groups-list.ts index 7ace808e..a7a310c9 100644 --- a/scripts/update-w3c-groups-list.ts +++ b/scripts/update-w3c-groups-list.ts @@ -4,11 +4,9 @@ */ import path from "path"; -import { fileURLToPath } from "url"; import { writeFile, mkdir } from "fs/promises"; import "dotenv/config"; -import fetch from "node-fetch"; import { env } from "../utils/misc.js"; @@ -122,11 +120,7 @@ export default async function update() { await writeFile(OUTPUT_FILE, JSON.stringify(data, null, 2), "utf-8"); } -const runAsScript = (() => { - const modulePath = fileURLToPath(import.meta.url); - const scriptPath = process.argv[1]; - return modulePath === scriptPath; -})(); +const runAsScript = import.meta.filename === process.argv[1]; if (runAsScript) { await update(); diff --git a/tests/node24-globals.test.js b/tests/node24-globals.test.js new file mode 100644 index 00000000..64731ae0 --- /dev/null +++ b/tests/node24-globals.test.js @@ -0,0 +1,13 @@ +describe("Node 24 globals", () => { + it("fetch is available as a global", () => { + expect(typeof fetch).toBe("function"); + }); + + it("import.meta.dirname is a string", () => { + expect(typeof import.meta.dirname).toBe("string"); + }); + + it("import.meta.filename is a string", () => { + expect(typeof import.meta.filename).toBe("string"); + }); +}); diff --git a/tests/utils/constants.test.js b/tests/utils/constants.test.js new file mode 100644 index 00000000..36e65b96 --- /dev/null +++ b/tests/utils/constants.test.js @@ -0,0 +1,9 @@ +import { PROJECT_ROOT } from "../../build/utils/constants.js"; +import { existsSync } from "fs"; +import { join } from "path"; + +describe("utils/constants", () => { + it("PROJECT_ROOT resolves to the repo root", () => { + expect(existsSync(join(PROJECT_ROOT, "package.json"))).toBeTrue(); + }); +}); diff --git a/utils/background-task-queue.ts b/utils/background-task-queue.ts index 7b5843fe..ad4d6e7a 100644 --- a/utils/background-task-queue.ts +++ b/utils/background-task-queue.ts @@ -7,7 +7,7 @@ import { nanoid } from "nanoid"; import split2 from "split2"; import { serializeError, deserializeError } from "serialize-error"; -import { legacyFilename, env } from "./misc.js"; +import { env } from "./misc.js"; type Message = { id: string }; @@ -151,7 +151,7 @@ export class BackgroundTaskQueue { constructor(jobDescriptionModulePath: string, queueName: string) { this.modulePath = jobDescriptionModulePath; this.name = queueName; - const __filename = legacyFilename(import.meta); + const __filename = import.meta.filename; this.worker = new Worker(__filename, { stderr: true, stdout: true }); } diff --git a/utils/constants.ts b/utils/constants.ts index 779d56e6..268a35e1 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -1,11 +1,10 @@ import { join } from "path"; import { readdirSync } from "fs"; -import { legacyDirname } from "./misc.js"; export const isDevEnv = process.env.NODE_ENV !== "production"; export const PROJECT_ROOT = (() => { - let currentDir = legacyDirname(import.meta); + let currentDir = import.meta.dirname; while (!readdirSync(currentDir).includes("package.json")) { const newCurrentDir = join(currentDir, ".."); if (currentDir === newCurrentDir) { diff --git a/utils/misc.ts b/utils/misc.ts index df8650ca..2564c701 100644 --- a/utils/misc.ts +++ b/utils/misc.ts @@ -1,6 +1,3 @@ -import path from "path"; -import { fileURLToPath } from "url"; - /** * Ensure env variable exists and get its value. * @param name name of env variable @@ -58,13 +55,3 @@ export class HTTPError extends Error { super(message); } } - -// __dirname -export function legacyDirname(meta: ImportMeta) { - return path.dirname(fileURLToPath(meta.url)); -} - -// __filename -export function legacyFilename(meta: ImportMeta) { - return fileURLToPath(meta.url); -} From 7f5c397c1a6d30b7ec75a546e33a689e45d1eb0a Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Wed, 15 Apr 2026 12:15:26 +1000 Subject: [PATCH 3/8] fix: apply XSS and SSRF security fixes from #480 --- routes/github/commits.ts | 1 + routes/github/lib/utils/rest.ts | 11 ++++++++++- routes/w3c/group.ts | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/routes/github/commits.ts b/routes/github/commits.ts index bb163c17..97cc2b80 100644 --- a/routes/github/commits.ts +++ b/routes/github/commits.ts @@ -32,6 +32,7 @@ export default async function route(req: Request, res: Response) { } res.json(commits); } catch (error) { + res.set("Content-Type", "text/plain"); res.status(404).send(error.message); } } diff --git a/routes/github/lib/utils/rest.ts b/routes/github/lib/utils/rest.ts index 390cadfa..1774140f 100644 --- a/routes/github/lib/utils/rest.ts +++ b/routes/github/lib/utils/rest.ts @@ -1,6 +1,11 @@ import { getToken, updateRateLimit, RateLimit } from "./tokens.js"; +const GITHUB_API_PREFIX = "https://api.github.com/"; + export async function* requestData(endpoint: string, pages = 30) { + if (!endpoint.startsWith(GITHUB_API_PREFIX)) { + throw new Error(`requestData: endpoint must start with ${GITHUB_API_PREFIX}`); + } let url: string | null = endpoint; do { const token = getToken(); @@ -19,7 +24,11 @@ export async function* requestData(endpoint: string, pages = 30) { const result = await response.json(); yield { url, result }; - url = nextPage(response.headers.get("link") || ""); + const next = nextPage(response.headers.get("link") || ""); + if (next !== null && !next.startsWith(GITHUB_API_PREFIX)) { + throw new Error(`requestData: pagination URL must start with ${GITHUB_API_PREFIX}`); + } + url = next; updateRateLimit(token, getRateLimit(response.headers)); } while (url !== null && --pages > 0); diff --git a/routes/w3c/group.ts b/routes/w3c/group.ts index 0be9960b..97956e48 100644 --- a/routes/w3c/group.ts +++ b/routes/w3c/group.ts @@ -52,6 +52,7 @@ export default async function route(req: Request, res: Response) { } if (type && !groups.hasOwnProperty(type)) { + res.set("Content-Type", "text/plain"); return res.status(404).send(`Invalid group type: "${type}".`); } From 73896caccff1bab9a040de2179630cc45eba14e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 02:42:40 +0000 Subject: [PATCH 4/8] fix: source CI Node version from .nvmrc Agent-Logs-Url: https://github.com/speced/respec-web-services/sessions/5e94febe-bb8b-4d99-86e6-050ffa3f7cb0 Co-authored-by: marcoscaceres <870154+marcoscaceres@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 061dd848..565dea13 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 24 + node-version-file: .nvmrc cache: pnpm - run: pnpm install - run: pnpm build From 20fe0f9e984b2a413e1c55c5fc8660dec63f42c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 02:42:46 +0000 Subject: [PATCH 5/8] fix: address review feedback for node version source and commits route typing Agent-Logs-Url: https://github.com/speced/respec-web-services/sessions/dde0f0f2-efe1-43fc-b5df-d6bcdf6e4d53 --- routes/github/commits.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/routes/github/commits.ts b/routes/github/commits.ts index 97cc2b80..907d0f0c 100644 --- a/routes/github/commits.ts +++ b/routes/github/commits.ts @@ -2,9 +2,13 @@ import { Request, Response } from "express"; import { seconds } from "../../utils/misc.js"; import { getCommits } from "./lib/commits.js"; -export default async function route(req: Request, res: Response) { - const { org, repo } = req.params as Record; - const { from, to , path } = req.query; +type Params = { org: string; repo: string }; +type Query = { from?: string; to?: string; path?: string }; +type IRequest = Request; + +export default async function route(req: IRequest, res: Response) { + const { org, repo } = req.params; + const { from, to, path } = req.query; if (!from || typeof from !== "string") { res.set("Content-Type", "text/plain"); return res.status(400).send("query parameter 'from' is required"); @@ -24,7 +28,7 @@ export default async function route(req: Request, res: Response) { try { const commits: { hash: string; message: string }[] = []; - for await (const commit of getCommits(org, repo, from, to, path as string | undefined)) { + for await (const commit of getCommits(org, repo, from, to, path)) { commits.push({ hash: commit.abbreviatedOid, message: commit.messageHeadline, From bd6e4f1be587b27994ffbb722e616637280a8f93 Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Wed, 15 Apr 2026 12:46:26 +1000 Subject: [PATCH 6/8] refactor: use typed Request in contributors and group routes --- routes/github/contributors.ts | 7 +++++-- routes/w3c/group.ts | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/routes/github/contributors.ts b/routes/github/contributors.ts index 8492caa2..aa53e8c7 100644 --- a/routes/github/contributors.ts +++ b/routes/github/contributors.ts @@ -12,8 +12,11 @@ const cache = new DiskCache({ path: "github/contributors", }); -export default async function route(req: Request, res: Response) { - const { org, repo } = req.params as Record; +type Params = { org: string; repo: string }; +type IRequest = Request; + +export default async function route(req: IRequest, res: Response) { + const { org, repo } = req.params; const cacheKey = `${org}/${repo}`; res.set("Cache-Control", `max-age=${seconds("24h")}`); diff --git a/routes/w3c/group.ts b/routes/w3c/group.ts index 97956e48..98d0c609 100644 --- a/routes/w3c/group.ts +++ b/routes/w3c/group.ts @@ -38,8 +38,11 @@ const LEGACY_SHORTNAMES = new Map([ ["i18n", "i18n-core"], // more than 10 instances ]); -export default async function route(req: Request, res: Response) { - const { shortname, type } = req.params as Record; +type Params = { shortname?: string; type?: string }; +type IRequest = Request; + +export default async function route(req: IRequest, res: Response) { + const { shortname, type } = req.params; if (!shortname) { if (req.headers.accept?.includes("text/html")) { return res.render("w3c/groups.js", { groups }); From eb69200106024cee65ddc1b82ab105b4d22ce821 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 08:54:19 +0000 Subject: [PATCH 7/8] chore: apply reviewer suggestions for workflow actions and runtime checks Agent-Logs-Url: https://github.com/speced/respec-web-services/sessions/ccaf8248-161f-4e1c-be5b-c38f4c43bf24 --- .github/workflows/test.yml | 6 +++--- routes/github/commits.ts | 2 +- scripts/update-w3c-groups-list.ts | 4 +--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 565dea13..dd21a4e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,9 +10,9 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v5 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: pnpm diff --git a/routes/github/commits.ts b/routes/github/commits.ts index 907d0f0c..8099421e 100644 --- a/routes/github/commits.ts +++ b/routes/github/commits.ts @@ -3,7 +3,7 @@ import { seconds } from "../../utils/misc.js"; import { getCommits } from "./lib/commits.js"; type Params = { org: string; repo: string }; -type Query = { from?: string; to?: string; path?: string }; +type Query = { from: string; to?: string; path?: string }; type IRequest = Request; export default async function route(req: IRequest, res: Response) { diff --git a/scripts/update-w3c-groups-list.ts b/scripts/update-w3c-groups-list.ts index a7a310c9..474e7969 100644 --- a/scripts/update-w3c-groups-list.ts +++ b/scripts/update-w3c-groups-list.ts @@ -120,8 +120,6 @@ export default async function update() { await writeFile(OUTPUT_FILE, JSON.stringify(data, null, 2), "utf-8"); } -const runAsScript = import.meta.filename === process.argv[1]; - -if (runAsScript) { +if (import.meta.main) { await update(); } From 6d8802667fe28e8ea4fa4cba0493f4177b65beb3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 09:37:27 +0000 Subject: [PATCH 8/8] fix: add ImportMeta.main typing for CI TypeScript build Agent-Logs-Url: https://github.com/speced/respec-web-services/sessions/ddca0bfa-90d9-4424-a983-bdadec983e7d --- @types/import-meta.d.ts | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 @types/import-meta.d.ts diff --git a/@types/import-meta.d.ts b/@types/import-meta.d.ts new file mode 100644 index 00000000..ff6bd4e5 --- /dev/null +++ b/@types/import-meta.d.ts @@ -0,0 +1,3 @@ +interface ImportMeta { + main: boolean; +}