Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ updates:
interval: weekly
open-pull-requests-limit: 5
groups:
dashboard-test-toolchain:
patterns:
- vite
- vitest
- "@vitest/*"
- "@vitejs/plugin-react"
- vite-tsconfig-paths
- "@testing-library/*"
react:
patterns:
- react
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ jobs:
- run: corepack enable
- run: yarn install --immutable
- name: Audit dependencies
run: yarn npm audit --all --recursive --no-deprecations --severity high
run: yarn audit
- run: yarn test:unit
- name: Check dependency compatibility
run: node --test scripts/test-esbuild-loader.mjs scripts/test-mail.mjs
- run: yarn workspace site lint
- name: Build public site
env:
Expand Down
10 changes: 5 additions & 5 deletions apps/dash/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"dotenv": "16.4.7",
"graphql": "^16.8.1",
"jsdom": "28.0.0",
"knex": "^3.1.0",
"knex": "^3.3.0",
"marked": "^17.0.5",
"mysql2": "^3.14.2",
"next": "16.3.5",
Expand All @@ -65,22 +65,22 @@
"devDependencies": {
"@playwright/test": "1.58.2",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "16.3.0",
"@testing-library/react": "16.3.3",
"@types/better-sqlite3": "^7.6.13",
"@types/jsdom": "^28.0.1",
"@types/node": "^24.0.0",
"@types/pg": "^8.23.1",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0",
"@vitejs/plugin-react": "4.5.2",
"@vitejs/plugin-react": "6.1.1",
"eslint": "^9.16.0",
"eslint-config-next": "16.3.5",
"prettier": "^3.4.2",
"tsx": "4.23.13",
"typescript": "5.9.2",
"vite": "^7.1.7",
"vite": "^8.3.0",
"vite-tsconfig-paths": "6.0.5",
"vitest": "^4.1.11"
"vitest": "^5.0.0"
},
"engines": {
"node": ">=24.21.0 <25",
Expand Down
2 changes: 1 addition & 1 deletion apps/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"dotenv": "^17.2.1",
"gray-matter": "^4.0.3",
"ioredis": "^5.6.1",
"knex": "^3.1.0",
"knex": "^3.3.0",
"lucide-react": "^0.487.0",
"moment": "^2.30.1",
"mysql2": "^3.14.2",
Expand Down
30 changes: 19 additions & 11 deletions docs/DependencyAudit.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
# Dependency audit

The public-source preparation audit on 2026-09-14 used Yarn 4.9.2 and Node.js 24.21.0:
The dependency review on 2026-09-15 used Yarn 4.9.2 and Node.js 24.21.0:

```bash
yarn install --immutable
yarn audit
yarn npm audit --all --recursive --no-deprecations --severity high
node --test scripts/test-esbuild-loader.mjs scripts/test-mail.mjs
```

The final JavaScript dependency tree reports **no high or critical advisories**. The full audit still reports the moderate advisory below and exits with status 1. CI rejects high and critical advisories; the full audit remains available without suppressions. Registry results can change after this review.
The full JavaScript dependency audit reports **no advisories**. CI runs the full audit without severity exclusions or advisory suppressions. Registry results can change after this review.

The dependency-update review on 2026-09-15 repeated the audit after updating Next.js, React, PostgreSQL client packages, cross-env, and Tailwind CSS. The remaining advisory is unchanged.
## esbuild configuration-loader override

## Remaining moderate advisory
The original audit found [GHSA-67mh-4wv8-2f99: development server cross-origin information exposure](https://github.com/advisories/GHSA-67mh-4wv8-2f99) in `esbuild@0.18.20`, pulled in through Payload's Drizzle configuration loader:

- Package: `esbuild@0.18.20`.
- Advisory: [GHSA-67mh-4wv8-2f99: development server cross-origin information exposure](https://github.com/advisories/GHSA-67mh-4wv8-2f99).
- Dependency path: `apps/dash` → `@payloadcms/db-postgres` / `@payloadcms/db-sqlite@3.89.0` → `drizzle-kit@0.31.7` → `@esbuild-kit/esm-loader@2.6.5` → `@esbuild-kit/core-utils@3.3.2` → `esbuild@0.18.20`.
- The advisory concerns esbuild's development HTTP server. Inspection of the installed configuration loader found transform calls, with no call to esbuild's `serve` or `context` APIs. The repository does not start this esbuild development server. This limits the observed exposure; it is not a claim that every possible use of these dependencies is safe.
- Do not expose an esbuild development server using this version. Remove this advisory by updating Payload/Drizzle to a dependency tree that uses a supported loader and patched esbuild. Re-run the full audit, dashboard type check, integration tests, and build after that change. Avoid forcing an incompatible esbuild version into the deprecated loader without testing its configuration-loading behavior.
`apps/dash` → `@payloadcms/db-postgres` / `@payloadcms/db-sqlite@3.89.0` → `drizzle-kit@0.31.7` → `@esbuild-kit/esm-loader@2.6.5` → `@esbuild-kit/core-utils@3.3.2` → `esbuild`.

Payload's current release still pins this Drizzle version, and the latest Drizzle Kit release also retains the legacy loader. The root `resolutions` entry therefore targets only `@esbuild-kit/core-utils/esbuild`, replacing the vulnerable version with patched `0.25.12`. Other esbuild consumers retain their own supported dependency ranges.

This override crosses the loader's declared esbuild range. CI tests the actual resolved dependency chain, synchronous CommonJS and asynchronous ESM TypeScript transforms, and loading a TypeScript configuration with a relative import through the legacy ESM loader. Dashboard integration tests and the production build also validate Payload/Drizzle behavior with the override.

Remove the resolution and its loader-specific regression tests when Payload/Drizzle drops the legacy loader or natively resolves patched esbuild. Repeat the full audit, dashboard type check, integration tests, and build when changing it.

## Test toolchain and mail compatibility

The dashboard uses Vite 8.3, Vitest 5, and `@vitejs/plugin-react` 6.1.1 together. The React plugin requires Vite 8; upgrading it alone on Vite 7 prevents the test configuration from loading. Dependabot groups these tools and Testing Library for future updates.

Nodemailer 10 supports the repository's Node.js 24 baseline. A regression test compiles the real OTP template through its SES transport with the AWS SDK send method stubbed, without sending email or using credentials. This does not validate delivery through a live SES account.

## Next.js compatibility

Both Next.js applications use `next@16.3.5` with the matching ESLint configuration. The site uses the native flat ESLint configuration supported by Next.js 16. The earlier PostCSS override for `next@15.5.25` has been removed because that dependency is no longer present; site lint/build and the high/critical dependency audit pass without that override.
Both Next.js applications use `next@16.3.5` with the matching ESLint configuration. The site uses Next.js 16's native flat ESLint configuration. The obsolete Next.js 15 PostCSS override is no longer needed.

## PHP integration

Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,8 @@
"bugs": {
"url": "https://github.com/suciudan/gatekeepr/issues"
},
"homepage": "https://github.com/suciudan/gatekeepr#readme"
"homepage": "https://github.com/suciudan/gatekeepr#readme",
"resolutions": {
"@esbuild-kit/core-utils/esbuild": "0.25.12"
}
}
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"ioredis": "^5.6.1",
"ipaddr.js": "^2.5.0",
"moment": "^2.30.1",
"nodemailer": "^9.1.0",
"nodemailer": "^10.0.8",
"whoiser": "2.0.0-beta.9"
},
"private": true,
Expand Down
2 changes: 1 addition & 1 deletion packages/db/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"dependencies": {
"@repo/config": "*",
"dotenv": "^17.2.1",
"knex": "^3.1.0",
"knex": "^3.3.0",
"mysql2": "^3.14.2"
},
"private": true,
Expand Down
59 changes: 59 additions & 0 deletions scripts/test-esbuild-loader.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import assert from "node:assert/strict"
import { execFileSync } from "node:child_process"
import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
import { createRequire } from "node:module"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { test } from "node:test"
import { pathToFileURL } from "node:url"
import { runInNewContext } from "node:vm"

// Resolve through Payload's actual dependency chain, rather than a separately
// installed esbuild. This guards the scoped security override in package.json.
const dashRequire = createRequire(new URL("../apps/dash/package.json", import.meta.url))
const payloadRequire = createRequire(dashRequire.resolve("@payloadcms/db-sqlite"))
const drizzleRequire = createRequire(payloadRequire.resolve("drizzle-kit/api"))
const loaderPath = drizzleRequire.resolve("@esbuild-kit/esm-loader")
const loaderRequire = createRequire(loaderPath)
const corePath = loaderRequire.resolve("@esbuild-kit/core-utils")
const { transform, transformSync } = loaderRequire("@esbuild-kit/core-utils")
const coreRequire = createRequire(corePath)

const source = 'const value: number = 42; export default { value }'

test("the legacy configuration loader resolves patched esbuild", () => {
const [major, minor] = coreRequire("esbuild").version.split(".").map(Number)
assert.ok(major > 0 || minor >= 25)
})

test("the loader synchronously transforms TypeScript configuration to CommonJS", () => {
const output = transformSync(source, "/tmp/gatekeepr-config.cts")
const module = { exports: {} }
runInNewContext(output.code, { module, exports: module.exports })
assert.equal(module.exports.default.value, 42)
assert.ok(output.map)
})

test("the loader asynchronously transforms TypeScript configuration to ESM", async () => {
const output = await transform(source, "/tmp/gatekeepr-config.mts")
const config = await import(`data:text/javascript;base64,${Buffer.from(output.code).toString("base64")}`)
assert.equal(config.default.value, 42)
assert.ok(output.map)
})

test("the legacy ESM loader loads a TypeScript config with a relative import", () => {
const directory = mkdtempSync(join(tmpdir(), "gatekeepr-loader-test-"))
try {
writeFileSync(join(directory, "package.json"), '{"type":"module"}')
writeFileSync(join(directory, "value.ts"), 'export const value: number = 42')
writeFileSync(join(directory, "config.ts"), 'import { value } from "./value.ts"; export default { value }')
writeFileSync(join(directory, "entry.mjs"), 'import config from "./config.ts"; console.log(JSON.stringify(config))')
const result = execFileSync(process.execPath, ["--no-warnings", "--loader", pathToFileURL(loaderPath).href, join(directory, "entry.mjs")], {
encoding: "utf8",
timeout: 15_000,
})
assert.equal(JSON.parse(result).value, 42)
} finally {
rmSync(directory, { recursive: true, force: true })
}
})
31 changes: 31 additions & 0 deletions scripts/test-mail.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import assert from "node:assert/strict"
import { createRequire } from "node:module"
import { test } from "node:test"
import { pathToFileURL } from "node:url"

const coreRequire = createRequire(new URL("../packages/core/package.json", import.meta.url))
const { SESv2Client, SendEmailCommand } = await import(pathToFileURL(coreRequire.resolve("@aws-sdk/client-sesv2")))

test("the mail helper compiles the OTP template through Nodemailer's SES transport", async (t) => {
let request
// Replace the SDK boundary: this test must never send mail or use AWS credentials.
t.mock.method(SESv2Client.prototype, "send", async (command) => {
assert.ok(command instanceof SendEmailCommand)
request = command.input
return { MessageId: "synthetic-message-id" }
})
t.mock.method(console, "log", () => {})
const { sendEmail } = await import("../packages/core/src/mail.js")
const sent = await sendEmail({
from: "sender@example.test",
to: "recipient@example.test",
subject: "Synthetic OTP test",
template: "otp-email",
templateProps: { otp: "123456" },
})
assert.equal(sent, true)
assert.deepEqual(request.Destination.ToAddresses, ["recipient@example.test"])
const message = Buffer.from(request.Content.Raw.Data).toString("utf8")
assert.match(message, /Subject: Synthetic OTP test/)
assert.match(message, /123456/)
})
Loading