From 1dd142b806dc36cd6af3b38cf879e79fa84a65d6 Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Fri, 25 Sep 2026 09:56:14 -0700 Subject: [PATCH] hardening(docs): pepper rule as plain JS so the mint script runs on node 20 lib/auth/mcp-pepper.ts becomes lib/auth/mcp-pepper.mjs (no imports), with types in mcp-pepper.d.mts. The resolver, scripts/mint-mcp-key.mjs and check:mcp-keys all load the .mjs, so the documented mint command works on any supported node, not only node >= 22.18. A test asserts that the mint script and the shared rule import no TypeScript file. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012cD3fDq5vhh2YWZPU2SV6H --- lib/auth/mcp-keys.ts | 8 ++++---- lib/auth/mcp-pepper.d.mts | 5 +++++ lib/auth/{mcp-pepper.ts => mcp-pepper.mjs} | 13 +++++-------- scripts/check-mcp-keys.mjs | 3 +-- scripts/mint-mcp-key.mjs | 2 +- test/mcp-key-floor.test.ts | 13 +++++++++++++ 6 files changed, 29 insertions(+), 15 deletions(-) create mode 100644 lib/auth/mcp-pepper.d.mts rename lib/auth/{mcp-pepper.ts => mcp-pepper.mjs} (65%) diff --git a/lib/auth/mcp-keys.ts b/lib/auth/mcp-keys.ts index dfd8facd..a552f1bf 100644 --- a/lib/auth/mcp-keys.ts +++ b/lib/auth/mcp-keys.ts @@ -1,6 +1,6 @@ import "server-only"; import { createHmac } from "node:crypto"; -import { mcpPepperStatus, type McpPepperStatus } from "./mcp-pepper"; +import { mcpPepperStatus, type McpPepperStatus } from "./mcp-pepper.mjs"; import { Tier, normalizeTier } from "@/prototype/fixtures"; /** @@ -45,10 +45,10 @@ interface RawEntry { */ export const MCP_KEY_RE = /^cdk_[A-Za-z0-9_-]{43}$/; -// The pepper rule lives in ./mcp-pepper (plain TS, no server-only / alias imports) so the mint +// The pepper rule lives in ./mcp-pepper (plain JS, no imports; node 20 can load it) so the mint // script applies exactly the same check. -export { MIN_MCP_KEY_PEPPER_LENGTH, MIN_MCP_KEY_PEPPER_DISTINCT, mcpPepperStatus, pepperStatus } from "./mcp-pepper"; -export type { McpPepperStatus } from "./mcp-pepper"; +export { MIN_MCP_KEY_PEPPER_LENGTH, MIN_MCP_KEY_PEPPER_DISTINCT, mcpPepperStatus, pepperStatus } from "./mcp-pepper.mjs"; +export type { McpPepperStatus } from "./mcp-pepper.mjs"; /** True when MCP_API_KEYS holds at least one entry. */ export function mcpStoreConfigured(env: NodeJS.ProcessEnv = process.env): boolean { diff --git a/lib/auth/mcp-pepper.d.mts b/lib/auth/mcp-pepper.d.mts new file mode 100644 index 00000000..ebf31e79 --- /dev/null +++ b/lib/auth/mcp-pepper.d.mts @@ -0,0 +1,5 @@ +export declare const MIN_MCP_KEY_PEPPER_LENGTH: number; +export declare const MIN_MCP_KEY_PEPPER_DISTINCT: number; +export type McpPepperStatus = "ok" | "missing" | "weak"; +export declare function pepperStatus(raw: string | undefined | null): McpPepperStatus; +export declare function mcpPepperStatus(env?: NodeJS.ProcessEnv): McpPepperStatus; diff --git a/lib/auth/mcp-pepper.ts b/lib/auth/mcp-pepper.mjs similarity index 65% rename from lib/auth/mcp-pepper.ts rename to lib/auth/mcp-pepper.mjs index a2728df3..1446cd4e 100644 --- a/lib/auth/mcp-pepper.ts +++ b/lib/auth/mcp-pepper.mjs @@ -1,17 +1,14 @@ -/** - * MCP key pepper rule, shared by the resolver (lib/auth/mcp-keys.ts) and the mint script - * (scripts/mint-mcp-key.mjs). No server-only or path-alias imports, so plain node can load it. - */ +// MCP key pepper rule, shared by the resolver (lib/auth/mcp-keys.ts) and the mint script +// (scripts/mint-mcp-key.mjs). Plain JavaScript with no imports, so any supported node version +// (including 20) can load it; types live in mcp-pepper.d.mts. /** Minimum length of the server-side pepper (`MCP_KEY_PEPPER`), after trimming. */ export const MIN_MCP_KEY_PEPPER_LENGTH = 32; /** Minimum distinct characters in the pepper (rejects "aaaa…" / whitespace padding). */ export const MIN_MCP_KEY_PEPPER_DISTINCT = 8; -export type McpPepperStatus = "ok" | "missing" | "weak"; - /** Status of a pepper value: set, >= 32 chars, >= 8 distinct characters, no surrounding whitespace. */ -export function pepperStatus(raw: string | undefined | null): McpPepperStatus { +export function pepperStatus(raw) { const v = typeof raw === "string" ? raw : ""; if (!v.trim()) return "missing"; const p = v.trim(); @@ -20,6 +17,6 @@ export function pepperStatus(raw: string | undefined | null): McpPepperStatus { } /** Whether `MCP_KEY_PEPPER` in `env` is usable. */ -export function mcpPepperStatus(env: NodeJS.ProcessEnv = process.env): McpPepperStatus { +export function mcpPepperStatus(env = process.env) { return pepperStatus(env.MCP_KEY_PEPPER); } diff --git a/scripts/check-mcp-keys.mjs b/scripts/check-mcp-keys.mjs index 46cd0bd0..9514f838 100644 --- a/scripts/check-mcp-keys.mjs +++ b/scripts/check-mcp-keys.mjs @@ -65,8 +65,7 @@ async function loadResolver() { // mcp-keys.ts's only runtime import from the barrel is normalizeTier (Tier is type-only). m = m.replace(/^import\s+\{[^}]*\}\s+from\s+["']@\/prototype\/fixtures["'];?\s*$/m, 'import { normalizeTier } from "./types.ts";'); - m = m.replace(/from\s+["']\.\/mcp-pepper["']/g, 'from "./mcp-pepper.ts"'); - fs.copyFileSync(path.join(ROOT, "lib/auth/mcp-pepper.ts"), path.join(tmp, "mcp-pepper.ts")); + fs.copyFileSync(path.join(ROOT, "lib/auth/mcp-pepper.mjs"), path.join(tmp, "mcp-pepper.mjs")); fs.writeFileSync(path.join(tmp, "mcp-keys.ts"), m); return import(path.join(tmp, "mcp-keys.ts")); } diff --git a/scripts/mint-mcp-key.mjs b/scripts/mint-mcp-key.mjs index 62ec1a9f..910f7ece 100644 --- a/scripts/mint-mcp-key.mjs +++ b/scripts/mint-mcp-key.mjs @@ -8,7 +8,7 @@ // keyed by HMAC-SHA256(key, MCP_KEY_PEPPER), matching lib/auth/mcp-keys.ts. import { createHmac, randomBytes } from "node:crypto"; import { pathToFileURL } from "node:url"; -import { pepperStatus } from "../lib/auth/mcp-pepper.ts"; +import { pepperStatus } from "../lib/auth/mcp-pepper.mjs"; export const MCP_KEY_PREFIX = "cdk_"; const TIERS = new Set(["public", "commercial", "academic", "confidential"]); diff --git a/test/mcp-key-floor.test.ts b/test/mcp-key-floor.test.ts index 40d7db56..f3fa59b6 100644 --- a/test/mcp-key-floor.test.ts +++ b/test/mcp-key-floor.test.ts @@ -135,3 +135,16 @@ describe("mint script applies the resolver's pepper rule", () => { } }); }); + +describe("mint script runs on node 20 (no TypeScript imports)", () => { + it("scripts/mint-mcp-key.mjs and lib/auth/mcp-pepper.mjs import no .ts file", async () => { + const { readFileSync } = await import("node:fs"); + const { join } = await import("node:path"); + const root = join(__dirname, ".."); + const mint = readFileSync(join(root, "scripts", "mint-mcp-key.mjs"), "utf8"); + expect(mint).not.toMatch(/from\s+["'][^"']+\.(c|m)?ts["']/); + expect(mint).toMatch(/from "\.\.\/lib\/auth\/mcp-pepper\.mjs"/); + const rule = readFileSync(join(root, "lib", "auth", "mcp-pepper.mjs"), "utf8"); + expect(rule).not.toMatch(/^\s*import\s/m); + }); +});