From 1375577c860f1ae9af5caf1c488d47ec1cf52b6f Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 22 Jan 2026 05:45:51 -0500 Subject: [PATCH 1/3] fix: typo'd arg name for `d1 insights --time-period` (#12039) * fix typo'd arg name * Fixed `time-period` flag string formatting * Added changeset --------- Co-authored-by: dimitropoulos Co-authored-by: Ben <4991309+NuroDev@users.noreply.github.com> Co-authored-by: Ben Dixon --- .changeset/good-needles-strive.md | 5 +++++ packages/wrangler/src/d1/insights.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/good-needles-strive.md diff --git a/.changeset/good-needles-strive.md b/.changeset/good-needles-strive.md new file mode 100644 index 000000000000..ff8dd2dd4329 --- /dev/null +++ b/.changeset/good-needles-strive.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +Fixed the flag casing for the time period flag for the `d1 insights` command. diff --git a/packages/wrangler/src/d1/insights.ts b/packages/wrangler/src/d1/insights.ts index e2fecf980d5a..e810abf7a709 100644 --- a/packages/wrangler/src/d1/insights.ts +++ b/packages/wrangler/src/d1/insights.ts @@ -70,7 +70,7 @@ export const d1InsightsCommand = createCommand({ demandOption: true, description: "The name of the DB", }, - timePeriod: { + "time-period": { type: "string", description: "Fetch data from now to the provided time period", default: "1d", From 2aa769c8730a0ef99f315f9a1dd5d25d52c51974 Mon Sep 17 00:00:00 2001 From: emily-shen <69125074+emily-shen@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:55:27 +0000 Subject: [PATCH 2/3] Add a new linting rule to catch unsafe exec usage (#11902) * add lint rule for command injection * add agents.local.md to gitignore * PR feedback * Fix any linting errors * changeset * fix tooling tests --- .changeset/slick-pens-stare.md | 5 + .gitignore | 2 + packages/eslint-config-shared/index.js | 25 ++ packages/eslint-config-shared/package.json | 5 + .../no-unsafe-command-execution.test.mjs | 194 +++++++++++++ .../rules/no-unsafe-command-execution.mjs | 268 ++++++++++++++++++ packages/miniflare/src/workers/d1/dumpSql.ts | 1 + packages/mock-npm-registry/src/index.ts | 1 + .../__tests__/deploy-non-npm-packages.test.ts | 15 +- .../__tests__/validate-changesets.test.ts | 1 + tools/deployments/deploy-non-npm-packages.ts | 4 +- 11 files changed, 512 insertions(+), 9 deletions(-) create mode 100644 .changeset/slick-pens-stare.md create mode 100644 packages/eslint-config-shared/rules/__tests__/no-unsafe-command-execution.test.mjs create mode 100644 packages/eslint-config-shared/rules/no-unsafe-command-execution.mjs diff --git a/.changeset/slick-pens-stare.md b/.changeset/slick-pens-stare.md new file mode 100644 index 000000000000..9e823d17b288 --- /dev/null +++ b/.changeset/slick-pens-stare.md @@ -0,0 +1,5 @@ +--- +"@cloudflare/eslint-config-shared": minor +--- + +Add a custom eslint rule that checks for unsafe command execution diff --git a/.gitignore b/.gitignore index d9ee7c5c8d9c..c6d2fbc22689 100644 --- a/.gitignore +++ b/.gitignore @@ -230,3 +230,5 @@ dist/** .env* !.env.example .node-cache/ + +AGENTS.local.md \ No newline at end of file diff --git a/packages/eslint-config-shared/index.js b/packages/eslint-config-shared/index.js index ef3862db8b7f..7f0ba855b584 100644 --- a/packages/eslint-config-shared/index.js +++ b/packages/eslint-config-shared/index.js @@ -6,6 +6,7 @@ import turbo from "eslint-plugin-turbo"; import unusedImports from "eslint-plugin-unused-imports"; import { defineConfig, globalIgnores } from "eslint/config"; import tseslint from "typescript-eslint"; +import noUnsafeCommandExecution from "./rules/no-unsafe-command-execution.mjs"; export default defineConfig( globalIgnores([ @@ -30,6 +31,11 @@ export default defineConfig( plugins: { "unused-imports": unusedImports, "no-only-tests": noOnlyTests, + "workers-sdk": { + rules: { + "no-unsafe-command-execution": noUnsafeCommandExecution, + }, + }, }, }, @@ -79,6 +85,25 @@ export default defineConfig( argsIgnorePattern: "^_", }, ], + "workers-sdk/no-unsafe-command-execution": "error", + }, + }, + { + files: [ + "**/*.test.ts", + "**/*.test.mts", + "**/*.test.tsx", + "**/test/**/*.ts", + "**/__tests__/**/*.ts", + "**/__tests__/**/*.mts", + "**/__tests__/**/*.tsx", + "**/e2e/**/*.ts", + "**/e2e/**/*.mts", + "**/fixtures/**/*.ts", + "**/fixtures/**/*.mts", + ], + rules: { + "workers-sdk/no-unsafe-command-execution": "off", }, } ); diff --git a/packages/eslint-config-shared/package.json b/packages/eslint-config-shared/package.json index 947b35853d25..8cbc7184a1de 100644 --- a/packages/eslint-config-shared/package.json +++ b/packages/eslint-config-shared/package.json @@ -8,6 +8,11 @@ ".": "./index.js", "./react": "./react.js" }, + "scripts": { + "deploy": "echo 'no deploy'", + "test": "node rules/__tests__/no-unsafe-command-execution.test.mjs", + "test:ci": "node rules/__tests__/no-unsafe-command-execution.test.mjs" + }, "devDependencies": { "@cloudflare/workers-tsconfig": "workspace:*", "@eslint/js": "^9.39.1", diff --git a/packages/eslint-config-shared/rules/__tests__/no-unsafe-command-execution.test.mjs b/packages/eslint-config-shared/rules/__tests__/no-unsafe-command-execution.test.mjs new file mode 100644 index 000000000000..358bf3f79b0a --- /dev/null +++ b/packages/eslint-config-shared/rules/__tests__/no-unsafe-command-execution.test.mjs @@ -0,0 +1,194 @@ +import { RuleTester } from "eslint"; +import rule from "../no-unsafe-command-execution.mjs"; + +const ruleTester = new RuleTester({ + languageOptions: { ecmaVersion: 2022, sourceType: "module" }, +}); + +ruleTester.run("no-unsafe-command-execution", rule, { + valid: [ + // Static strings are safe + { + code: 'const { execSync } = require("child_process"); execSync("ls -la");', + }, + { + code: 'require("child_process").execSync("git status");', + }, + // execFileSync with argument array is safe + { + code: 'const { execFileSync } = require("child_process"); execFileSync("git", ["show", "-s", commitHash]);', + }, + // spawn with argument array and no shell is safe + { + code: 'const { spawn } = require("child_process"); spawn("git", ["status"]);', + }, + // Template literals without expressions are safe + { + code: 'const { execSync } = require("child_process"); execSync(`ls -la`);', + }, + // Non-child_process functions are ignored + { + code: "const result = exec(`command ${variable}`);", + }, + // node:child_process also works + { + code: 'const { execSync } = require("node:child_process"); execSync("ls");', + }, + // ES module imports are safe with static strings + { + code: 'import { execSync } from "node:child_process"; execSync("git status");', + }, + { + code: 'import { execFileSync } from "node:child_process"; execFileSync("git", ["show", commitHash]);', + }, + // Aliased imports with static strings are safe + { + code: 'import { execSync as run } from "node:child_process"; run("git status");', + }, + // Namespaced imports with static strings are safe + { + code: 'import * as cp from "node:child_process"; cp.execSync("git status");', + }, + // Aliased CommonJS imports with static strings are safe + { + code: 'const { execSync: run } = require("node:child_process"); run("git status");', + }, + ], + + invalid: [ + // Template literal with expression in execSync + { + code: 'const { execSync } = require("child_process"); execSync(`git show ${commitHash}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // String concatenation in execSync + { + code: 'const { execSync } = require("child_process"); execSync("git show " + commitHash);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // Direct require pattern + { + code: 'require("child_process").execSync(`command ${variable}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // exec (not execSync) + { + code: 'const { exec } = require("child_process"); exec(`ls ${dir}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // spawn with shell: true and template literal + { + code: 'const { spawn } = require("child_process"); spawn(`ls ${dir}`, [], { shell: true });', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // execFile with template literal + { + code: 'const { execFile } = require("child_process"); execFile(`command ${arg}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // node:child_process variant + { + code: 'const { execSync } = require("node:child_process"); execSync(`git ${command}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // ES module import with template literal + { + code: 'import { execSync } from "node:child_process"; execSync(`git show ${commitHash}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // ES module import with string concatenation + { + code: 'import { execSync } from "node:child_process"; execSync("git show " + commitHash);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // The actual vulnerability we found + { + code: ` +const { execSync } = require("child_process"); +if (!commitMessage) { + commitMessage = execSync(\`git show -s --format=%B \${commitHash}\`) + .toString() + .trim(); +}`, + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // Aliased ES module import with template literal + { + code: 'import { execSync as run } from "node:child_process"; run(`git show ${hash}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // Namespaced ES module import with template literal + { + code: 'import * as cp from "node:child_process"; cp.execSync(`git show ${hash}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // Aliased CommonJS import with template literal + { + code: 'const { execSync: run } = require("node:child_process"); run(`git ${cmd}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // Namespaced CommonJS import with template literal + { + code: 'const cp = require("node:child_process"); cp.execSync(`git ${cmd}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + ], +}); + +console.log("✅ All tests passed!"); diff --git a/packages/eslint-config-shared/rules/no-unsafe-command-execution.mjs b/packages/eslint-config-shared/rules/no-unsafe-command-execution.mjs new file mode 100644 index 000000000000..92e1bd3393b2 --- /dev/null +++ b/packages/eslint-config-shared/rules/no-unsafe-command-execution.mjs @@ -0,0 +1,268 @@ +const UNSAFE_FUNCTIONS = [ + "exec", + "execSync", + "spawn", + "spawnSync", + "execFile", + "execFileSync", +]; + +/** + * Package names for child_process module + */ +const CHILD_PROCESS_MODULES = ["child_process", "node:child_process"]; + +/** + * Check if a node is a template literal with expressions + */ +function isTemplateLiteralWithExpressions(node) { + return node.type === "TemplateLiteral" && node.expressions.length > 0; +} + +/** + * Check if a node is a binary expression (string concatenation) + */ +function isBinaryExpression(node) { + return node.type === "BinaryExpression" && node.operator === "+"; +} + +/** + * Check if a call is to one of the unsafe functions from child_process + * Supports both CommonJS (require) and ES module (import) syntax + */ +function isUnsafeChildProcessCall(node, context) { + const sourceCode = context.sourceCode || context.getSourceCode(); + const scope = sourceCode.getScope + ? sourceCode.getScope(node) + : context.getScope(); + + // Check for direct require('child_process').exec(...) pattern + if ( + node.callee.type === "MemberExpression" && + node.callee.object.type === "CallExpression" && + node.callee.object.callee.name === "require" && + node.callee.object.arguments[0]?.type === "Literal" && + CHILD_PROCESS_MODULES.includes(node.callee.object.arguments[0].value) && + node.callee.property.type === "Identifier" && + UNSAFE_FUNCTIONS.includes(node.callee.property.name) + ) { + return node.callee.property.name; + } + + // Check for imported functions: const { exec } = require('child_process') or import { exec } from 'child_process' + if (node.callee.type === "Identifier") { + // Walk up the scope chain to find if this identifier is from child_process + let currentScope = scope; + while (currentScope) { + const variable = currentScope.set.get(node.callee.name); + if (variable && variable.defs.length > 0) { + const def = variable.defs[0]; + + // Check for ES module import: import { execSync } from 'node:child_process' + // or import { execSync as run } from 'node:child_process' + if ( + def.type === "ImportBinding" && + def.parent?.type === "ImportDeclaration" + ) { + const importSource = def.parent.source?.value; + if (CHILD_PROCESS_MODULES.includes(importSource)) { + // Check if it's one of the unsafe functions + // For aliased imports: import { execSync as run } - def.node.imported.name = "execSync" + // For regular imports: import { execSync } - def.node.local.name = "execSync" + const importedName = + def.node.imported?.name || def.node.local?.name; + if (importedName && UNSAFE_FUNCTIONS.includes(importedName)) { + return importedName; + } + } + } + + // Check for CommonJS require() + if ( + def.type === "Variable" && + def.parent?.type === "VariableDeclaration" && + def.node?.init + ) { + const init = def.node.init; + // Check if it's from require('child_process') + if ( + init.type === "CallExpression" && + init.callee.name === "require" && + init.arguments[0]?.type === "Literal" && + CHILD_PROCESS_MODULES.includes(init.arguments[0].value) + ) { + // For destructured requires, check the property name + // const { execSync: run } = require('child_process') + if (def.node.id?.type === "ObjectPattern") { + const prop = def.node.id.properties.find( + (p) => p.value?.name === node.callee.name + ); + if (prop && UNSAFE_FUNCTIONS.includes(prop.key?.name)) { + return prop.key.name; + } + } + // For direct import: const execSync = require('child_process').execSync + if (UNSAFE_FUNCTIONS.includes(node.callee.name)) { + return node.callee.name; + } + } + // Check if it's from destructuring: const { exec } = require('child_process') + if ( + init.type === "MemberExpression" && + init.object.type === "CallExpression" && + init.object.callee.name === "require" && + init.object.arguments[0]?.type === "Literal" && + CHILD_PROCESS_MODULES.includes(init.object.arguments[0].value) + ) { + if (UNSAFE_FUNCTIONS.includes(node.callee.name)) { + return node.callee.name; + } + } + } + } + currentScope = currentScope.upper; + } + } + + // Check for MemberExpression: childProcess.exec(...) + if ( + node.callee.type === "MemberExpression" && + node.callee.property.type === "Identifier" && + UNSAFE_FUNCTIONS.includes(node.callee.property.name) + ) { + // This is a heuristic - we check if the object might be child_process + // We return the function name to trigger checking, even if we're not 100% sure + return node.callee.property.name; + } + + return null; +} + +/** + * Check if the options object has shell: true + */ +function hasShellOption(node) { + if (!node || node.type !== "ObjectExpression") { + return false; + } + + return node.properties.some( + (prop) => + prop.type === "Property" && + prop.key.type === "Identifier" && + prop.key.name === "shell" && + prop.value.type === "Literal" && + prop.value.value === true + ); +} + +/** + * Get a helpful suggestion message based on the function being used + */ +function getSuggestion(functionName, hasTemplate, hasConcatenation) { + const patterns = []; + if (hasTemplate) { + patterns.push("template literal with expressions"); + } + if (hasConcatenation) { + patterns.push("string concatenation"); + } + + const patternStr = patterns.join(" and "); + + if (functionName.includes("exec")) { + return `Detected ${patternStr} in ${functionName}(). Use execFileSync() with an argument array instead to prevent command injection: execFileSync("command", ["arg1", "arg2"])`; + } + + if (functionName.includes("spawn")) { + return `Detected ${patternStr} in ${functionName}(). Ensure you're using an argument array and avoid shell: true with user input.`; + } + + return `Detected ${patternStr} in ${functionName}(). This may allow command injection. Use argument arrays instead of string interpolation.`; +} + +export default { + meta: { + type: "error", + docs: { + description: + "Disallow template literals and string concatenation in shell command execution", + category: "Possible Security Vulnerability", + recommended: true, + url: "https://cwe.mitre.org/data/definitions/78.html", + }, + messages: { + unsafeCommandExecution: + "Potential command injection vulnerability: {{message}}", + }, + schema: [], // no options + }, + + create(context) { + return { + CallExpression(node) { + // Check if this is a call to a child_process function + const functionName = isUnsafeChildProcessCall(node, context); + if (!functionName) { + return; + } + + // Check first argument (command string) + if (node.arguments.length === 0) { + return; + } + + const commandArg = node.arguments[0]; + let hasTemplate = false; + let hasConcatenation = false; + + // Check for template literal with expressions + if (isTemplateLiteralWithExpressions(commandArg)) { + hasTemplate = true; + } + + // Check for string concatenation + if (isBinaryExpression(commandArg)) { + hasConcatenation = true; + } + + // For spawn/spawnSync, also check if shell: true is in options + if ( + (functionName === "spawn" || functionName === "spawnSync") && + node.arguments.length >= 3 + ) { + const options = node.arguments[2]; + if (hasShellOption(options) && (hasTemplate || hasConcatenation)) { + context.report({ + node, + messageId: "unsafeCommandExecution", + data: { + message: getSuggestion( + functionName, + hasTemplate, + hasConcatenation + ), + }, + }); + return; + } + } + + // Report if we found unsafe patterns + if (hasTemplate || hasConcatenation) { + context.report({ + node, + messageId: "unsafeCommandExecution", + data: { + message: getSuggestion( + functionName, + hasTemplate, + hasConcatenation + ), + }, + }); + } + }, + }; + }, +}; diff --git a/packages/miniflare/src/workers/d1/dumpSql.ts b/packages/miniflare/src/workers/d1/dumpSql.ts index b63fedcfc3b7..5e49af8302cf 100644 --- a/packages/miniflare/src/workers/d1/dumpSql.ts +++ b/packages/miniflare/src/workers/d1/dumpSql.ts @@ -65,6 +65,7 @@ export function* dumpSql( } if (noData) continue; + // eslint-disable-next-line workers-sdk/no-unsafe-command-execution const columns_cursor = db.exec(`PRAGMA table_info=${escapeId(table)}`); const columns = Array.from(columns_cursor) as { diff --git a/packages/mock-npm-registry/src/index.ts b/packages/mock-npm-registry/src/index.ts index dd453f69705f..6329d7c03895 100644 --- a/packages/mock-npm-registry/src/index.ts +++ b/packages/mock-npm-registry/src/index.ts @@ -135,6 +135,7 @@ async function getPackagesToPublish(names: string[]) { `{ package(name: "${name}") { path, allDependencies { items { name, path } } } }` ); const results = JSON.parse( + // eslint-disable-next-line workers-sdk/no-unsafe-command-execution execSync("pnpm exec turbo query " + turboQueryPath, { encoding: "utf8", stdio: "pipe", diff --git a/tools/deployments/__tests__/deploy-non-npm-packages.test.ts b/tools/deployments/__tests__/deploy-non-npm-packages.test.ts index 7b138a38a231..c22f140b8b6c 100644 --- a/tools/deployments/__tests__/deploy-non-npm-packages.test.ts +++ b/tools/deployments/__tests__/deploy-non-npm-packages.test.ts @@ -1,4 +1,4 @@ -import { execSync } from "node:child_process"; +import { spawnSync } from "node:child_process"; import { afterEach, describe, expect, it, vitest } from "vitest"; import { deployNonNpmPackages, @@ -11,7 +11,7 @@ import type { Mock } from "vitest"; vitest.mock("node:child_process", async () => { return { - execSync: vitest.fn(), + spawnSync: vitest.fn(), }; }); @@ -108,16 +108,17 @@ describe("findDeployablePackageNames()", () => { }); describe("deployPackage", () => { - it("should run `pnpm deploy` for the given package via `execSync`", () => { + it("should run `pnpm deploy` for the given package via `spawnSync`", () => { deployPackage("foo", new Map()); - expect(execSync).toHaveBeenCalledWith( - "pnpm -F foo run deploy", + expect(spawnSync).toHaveBeenCalledWith( + "pnpm", + ["-F", "foo", "run", "deploy"], expect.any(Object) ); }); - it("should ignore failures in `execSync`", () => { - (execSync as Mock).mockImplementationOnce(() => { + it("should ignore failures in `spawnSync`", () => { + (spawnSync as Mock).mockImplementationOnce(() => { throw new Error("Bad deployment"); }); const logs: string[] = []; diff --git a/tools/deployments/__tests__/validate-changesets.test.ts b/tools/deployments/__tests__/validate-changesets.test.ts index bd8c2559cd67..aee3404fab06 100644 --- a/tools/deployments/__tests__/validate-changesets.test.ts +++ b/tools/deployments/__tests__/validate-changesets.test.ts @@ -31,6 +31,7 @@ describe("findPackageNames()", () => { "@cloudflare/workers-shared", "@cloudflare/workers-utils", "@cloudflare/workflows-shared", + "@cloudflare/eslint-config-shared", "@cloudflare/containers-shared", "@cloudflare/vite-plugin", "create-cloudflare", diff --git a/tools/deployments/deploy-non-npm-packages.ts b/tools/deployments/deploy-non-npm-packages.ts index fabcd190618b..0e59cd106a52 100644 --- a/tools/deployments/deploy-non-npm-packages.ts +++ b/tools/deployments/deploy-non-npm-packages.ts @@ -1,5 +1,5 @@ import assert from "node:assert"; -import { execSync } from "node:child_process"; +import { spawnSync } from "node:child_process"; import { readdirSync, readFileSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; @@ -120,7 +120,7 @@ export function deployPackage( deploymentErrors: Map ) { try { - execSync(`pnpm -F ${pkgName} run deploy`, { + spawnSync(`pnpm`, [`-F`, pkgName, `run`, `deploy`], { env: process.env, stdio: "inherit", }); From fb0e1010d6abe4e1c830f8945023bb1f820fa54f Mon Sep 17 00:00:00 2001 From: ANT Bot <116369605+workers-devprod@users.noreply.github.com> Date: Thu, 22 Jan 2026 05:50:01 -0600 Subject: [PATCH 3/3] Version Packages (#12021) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/clever-cloths-push.md | 5 - .changeset/dependabot-update-11993.md | 12 --- .changeset/good-needles-strive.md | 5 - .changeset/hip-spies-wash.md | 7 -- .changeset/rare-seals-exist.md | 9 -- .changeset/rude-cows-cheat.md | 24 ----- .changeset/salty-ways-call.md | 7 -- .changeset/slick-pens-stare.md | 5 - .changeset/tall-hairs-send.md | 56 ---------- .changeset/witty-taxes-itch.md | 7 -- packages/eslint-config-shared/CHANGELOG.md | 6 ++ packages/eslint-config-shared/package.json | 2 +- packages/miniflare/CHANGELOG.md | 12 +++ packages/miniflare/package.json | 2 +- packages/pages-shared/CHANGELOG.md | 7 ++ packages/pages-shared/package.json | 2 +- packages/quick-edit/CHANGELOG.md | 10 ++ packages/quick-edit/package.json | 2 +- packages/unenv-preset/CHANGELOG.md | 8 ++ packages/unenv-preset/package.json | 2 +- packages/vite-plugin-cloudflare/CHANGELOG.md | 13 +++ packages/vite-plugin-cloudflare/package.json | 2 +- packages/vitest-pool-workers/CHANGELOG.md | 8 ++ packages/vitest-pool-workers/package.json | 2 +- packages/wrangler/CHANGELOG.md | 101 +++++++++++++++++++ packages/wrangler/package.json | 2 +- 26 files changed, 173 insertions(+), 145 deletions(-) delete mode 100644 .changeset/clever-cloths-push.md delete mode 100644 .changeset/dependabot-update-11993.md delete mode 100644 .changeset/good-needles-strive.md delete mode 100644 .changeset/hip-spies-wash.md delete mode 100644 .changeset/rare-seals-exist.md delete mode 100644 .changeset/rude-cows-cheat.md delete mode 100644 .changeset/salty-ways-call.md delete mode 100644 .changeset/slick-pens-stare.md delete mode 100644 .changeset/tall-hairs-send.md delete mode 100644 .changeset/witty-taxes-itch.md diff --git a/.changeset/clever-cloths-push.md b/.changeset/clever-cloths-push.md deleted file mode 100644 index 4cdf1de1d95d..000000000000 --- a/.changeset/clever-cloths-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"wrangler": patch ---- - -Fix `wrangler pages project validate` to respect file count limits from `CF_PAGES_UPLOAD_JWT` diff --git a/.changeset/dependabot-update-11993.md b/.changeset/dependabot-update-11993.md deleted file mode 100644 index a413f13cddfd..000000000000 --- a/.changeset/dependabot-update-11993.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"miniflare": patch -"wrangler": patch ---- - -chore: update dependencies of "miniflare", "wrangler" - -The following dependency versions have been updated: - -| Dependency | From | To | -| ---------- | ------------ | ------------ | -| workerd | 1.20260116.0 | 1.20260120.0 | diff --git a/.changeset/good-needles-strive.md b/.changeset/good-needles-strive.md deleted file mode 100644 index ff8dd2dd4329..000000000000 --- a/.changeset/good-needles-strive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"wrangler": patch ---- - -Fixed the flag casing for the time period flag for the `d1 insights` command. diff --git a/.changeset/hip-spies-wash.md b/.changeset/hip-spies-wash.md deleted file mode 100644 index 3514993b539b..000000000000 --- a/.changeset/hip-spies-wash.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@cloudflare/unenv-preset": minor ---- - -Remove the experimental flag from `node:_stream_wrap`, `node:dgram`, `node:inspector`, and `node:sqlite` - -Those modules are no more experimental since workerd 1.20260115.0 diff --git a/.changeset/rare-seals-exist.md b/.changeset/rare-seals-exist.md deleted file mode 100644 index 650b1c100b73..000000000000 --- a/.changeset/rare-seals-exist.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@cloudflare/quick-edit": patch ---- - -Fix relative path computation when the root folder name appears multiple times in a path - -Previously, the logic assumed the root folder appeared exactly once in the path. When the root folder name appeared more than once, file modifications were not correctly detected. - -For example, if the root folder is `my-worker`, a path like `/my-worker/my-worker/util.js` would incorrectly return `/` instead of `/my-worker/util.js`. diff --git a/.changeset/rude-cows-cheat.md b/.changeset/rude-cows-cheat.md deleted file mode 100644 index 9db2766b77b6..000000000000 --- a/.changeset/rude-cows-cheat.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -"wrangler": minor ---- - -Add `wrangler complete` command for shell completion scripts (bash, zsh, powershell) - -Usage: - -```bash -# Bash -wrangler complete bash >> ~/.bashrc - -# Zsh -wrangler complete zsh >> ~/.zshrc - -# Fish -wrangler complete fish >> ~/.config/fish/completions/wrangler.fish - -# PowerShell -wrangler complete powershell > $PROFILE -``` - -- Uses `@bomb.sh/tab` library for cross-shell compatibility -- Completions are dynamically generated from `experimental_getWranglerCommands()` API diff --git a/.changeset/salty-ways-call.md b/.changeset/salty-ways-call.md deleted file mode 100644 index a75ce603569a..000000000000 --- a/.changeset/salty-ways-call.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"wrangler": patch ---- - -Fix `wrangler setup` not automatically selecting `workers` as the target for new SvelteKit apps - -The Sveltekit `adapter:cloudflare` adapter now accepts two different targets `workers` or `pages`. Since the wrangler auto configuration only targets workers, wrangler should instruct the adapter to use the `workers` variant. (The auto configuration process would in any case not work if the user were to target `pages`.) diff --git a/.changeset/slick-pens-stare.md b/.changeset/slick-pens-stare.md deleted file mode 100644 index 9e823d17b288..000000000000 --- a/.changeset/slick-pens-stare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@cloudflare/eslint-config-shared": minor ---- - -Add a custom eslint rule that checks for unsafe command execution diff --git a/.changeset/tall-hairs-send.md b/.changeset/tall-hairs-send.md deleted file mode 100644 index c4e8d519d2bc..000000000000 --- a/.changeset/tall-hairs-send.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -"wrangler": minor ---- - -`wrangler types` now generates per-environment TypeScript interfaces when named environments exist in your configuration. - -When your configuration has named environments (an `env` object), `wrangler types` now generates both: - -- **Per-environment interfaces** (e.g., `StagingEnv`, `ProductionEnv`) containing only the bindings explicitly declared in each environment, plus inherited secrets -- **An aggregated `Env` interface** with all bindings from all environments (top-level + named environments), where: - - Bindings present in **all** environments are required - - Bindings not present in all environments are optional - - Secrets are always required (since they're inherited everywhere) - - Conflicting binding types across environments produce union types (e.g., `KVNamespace | R2Bucket`) - -However, if your config does not contain any environments, or you manually specify an environment via `--env`, `wrangler types` will continue to generate a single interface as before. - -**Example:** - -Given the following `wrangler.jsonc`: - -```jsonc -{ - "name": "my-worker", - "kv_namespaces": [ - { - "binding": "SHARED_KV", - "id": "abc123", - }, - ], - "env": { - "staging": { - "kv_namespaces": [ - { "binding": "SHARED_KV", "id": "staging-kv" }, - { "binding": "STAGING_CACHE", "id": "staging-cache" }, - ], - }, - }, -} -``` - -Running `wrangler types` will generate: - -```ts -declare namespace Cloudflare { - interface StagingEnv { - SHARED_KV: KVNamespace; - STAGING_CACHE: KVNamespace; - } - interface Env { - SHARED_KV: KVNamespace; // Required: in all environments - STAGING_CACHE?: KVNamespace; // Optional: only in staging - } -} -interface Env extends Cloudflare.Env {} -``` diff --git a/.changeset/witty-taxes-itch.md b/.changeset/witty-taxes-itch.md deleted file mode 100644 index eefad81df411..000000000000 --- a/.changeset/witty-taxes-itch.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@cloudflare/vite-plugin": patch ---- - -Skip shortcut registration in non-TTY environments - -Previously, registering keyboard shortcuts in non-TTY environments (e.g., Turborepo) caused Miniflare `ERR_DISPOSED` errors during prerendering. Shortcuts are now only registered when running in an interactive terminal. diff --git a/packages/eslint-config-shared/CHANGELOG.md b/packages/eslint-config-shared/CHANGELOG.md index 2ef848e4e49f..389767f7316e 100644 --- a/packages/eslint-config-shared/CHANGELOG.md +++ b/packages/eslint-config-shared/CHANGELOG.md @@ -1,5 +1,11 @@ # @cloudflare/eslint-config-shared +## 1.2.0 + +### Minor Changes + +- [#11902](https://github.com/cloudflare/workers-sdk/pull/11902) [`2aa769c`](https://github.com/cloudflare/workers-sdk/commit/2aa769c8730a0ef99f315f9a1dd5d25d52c51974) Thanks [@emily-shen](https://github.com/emily-shen)! - Add a custom eslint rule that checks for unsafe command execution + ## 1.1.0 ### Minor Changes diff --git a/packages/eslint-config-shared/package.json b/packages/eslint-config-shared/package.json index 8cbc7184a1de..8f263c42c5aa 100644 --- a/packages/eslint-config-shared/package.json +++ b/packages/eslint-config-shared/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/eslint-config-shared", - "version": "1.1.0", + "version": "1.2.0", "private": true, "description": "ESLint config for workers-sdk", "type": "module", diff --git a/packages/miniflare/CHANGELOG.md b/packages/miniflare/CHANGELOG.md index 83690d02c1ee..3e732d700580 100644 --- a/packages/miniflare/CHANGELOG.md +++ b/packages/miniflare/CHANGELOG.md @@ -1,5 +1,17 @@ # miniflare +## 4.20260120.0 + +### Patch Changes + +- [#11993](https://github.com/cloudflare/workers-sdk/pull/11993) [`788bf78`](https://github.com/cloudflare/workers-sdk/commit/788bf786b4c5cb8e1bdd6464d3f88b4125cebc75) Thanks [@dependabot](https://github.com/apps/dependabot)! - chore: update dependencies of "miniflare", "wrangler" + + The following dependency versions have been updated: + + | Dependency | From | To | + | ---------- | ------------ | ------------ | + | workerd | 1.20260116.0 | 1.20260120.0 | + ## 4.20260116.0 ### Minor Changes diff --git a/packages/miniflare/package.json b/packages/miniflare/package.json index c915fd24a45f..20f44e957c8b 100644 --- a/packages/miniflare/package.json +++ b/packages/miniflare/package.json @@ -1,6 +1,6 @@ { "name": "miniflare", - "version": "4.20260116.0", + "version": "4.20260120.0", "description": "Fun, full-featured, fully-local simulator for Cloudflare Workers", "keywords": [ "cloudflare", diff --git a/packages/pages-shared/CHANGELOG.md b/packages/pages-shared/CHANGELOG.md index e4f2064306c1..353a4eabf9dd 100644 --- a/packages/pages-shared/CHANGELOG.md +++ b/packages/pages-shared/CHANGELOG.md @@ -1,5 +1,12 @@ # @cloudflare/pages-shared +## 0.13.101 + +### Patch Changes + +- Updated dependencies [[`788bf78`](https://github.com/cloudflare/workers-sdk/commit/788bf786b4c5cb8e1bdd6464d3f88b4125cebc75)]: + - miniflare@4.20260120.0 + ## 0.13.100 ### Patch Changes diff --git a/packages/pages-shared/package.json b/packages/pages-shared/package.json index ca31ce76eb5b..f7701b9076e1 100644 --- a/packages/pages-shared/package.json +++ b/packages/pages-shared/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/pages-shared", - "version": "0.13.100", + "version": "0.13.101", "repository": { "type": "git", "url": "https://github.com/cloudflare/workers-sdk.git", diff --git a/packages/quick-edit/CHANGELOG.md b/packages/quick-edit/CHANGELOG.md index 9a5737f18470..fc782acc50ca 100644 --- a/packages/quick-edit/CHANGELOG.md +++ b/packages/quick-edit/CHANGELOG.md @@ -1,5 +1,15 @@ # @cloudflare/quick-edit +## 0.4.4 + +### Patch Changes + +- [#11878](https://github.com/cloudflare/workers-sdk/pull/11878) [`e84e8fa`](https://github.com/cloudflare/workers-sdk/commit/e84e8fab79f17ffcc6a4c29c92c6924ceb351f94) Thanks [@dario-piotrowicz](https://github.com/dario-piotrowicz)! - Fix relative path computation when the root folder name appears multiple times in a path + + Previously, the logic assumed the root folder appeared exactly once in the path. When the root folder name appeared more than once, file modifications were not correctly detected. + + For example, if the root folder is `my-worker`, a path like `/my-worker/my-worker/util.js` would incorrectly return `/` instead of `/my-worker/util.js`. + ## 0.4.3 ### Patch Changes diff --git a/packages/quick-edit/package.json b/packages/quick-edit/package.json index a93ab003e028..ad1302380b36 100644 --- a/packages/quick-edit/package.json +++ b/packages/quick-edit/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/quick-edit", - "version": "0.4.3", + "version": "0.4.4", "private": true, "description": "VSCode for Web hosted for use in Cloudflare's Quick Editor", "homepage": "https://github.com/cloudflare/workers-sdk#readme", diff --git a/packages/unenv-preset/CHANGELOG.md b/packages/unenv-preset/CHANGELOG.md index c92b3990fd3c..79c37b253c6f 100644 --- a/packages/unenv-preset/CHANGELOG.md +++ b/packages/unenv-preset/CHANGELOG.md @@ -1,5 +1,13 @@ # @cloudflare/unenv-preset +## 2.11.0 + +### Minor Changes + +- [#12024](https://github.com/cloudflare/workers-sdk/pull/12024) [`ae108f0`](https://github.com/cloudflare/workers-sdk/commit/ae108f090532765751c3996ba4c863a9fe858ddf) Thanks [@vicb](https://github.com/vicb)! - Remove the experimental flag from `node:_stream_wrap`, `node:dgram`, `node:inspector`, and `node:sqlite` + + Those modules are no more experimental since workerd 1.20260115.0 + ## 2.10.0 ### Minor Changes diff --git a/packages/unenv-preset/package.json b/packages/unenv-preset/package.json index ca030c14f609..48c071df7221 100644 --- a/packages/unenv-preset/package.json +++ b/packages/unenv-preset/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/unenv-preset", - "version": "2.10.0", + "version": "2.11.0", "description": "cloudflare preset for unenv", "keywords": [ "cloudflare", diff --git a/packages/vite-plugin-cloudflare/CHANGELOG.md b/packages/vite-plugin-cloudflare/CHANGELOG.md index 751614c24680..9307936abbef 100644 --- a/packages/vite-plugin-cloudflare/CHANGELOG.md +++ b/packages/vite-plugin-cloudflare/CHANGELOG.md @@ -1,5 +1,18 @@ # @cloudflare/vite-plugin +## 1.21.2 + +### Patch Changes + +- [#11875](https://github.com/cloudflare/workers-sdk/pull/11875) [`ae2459c`](https://github.com/cloudflare/workers-sdk/commit/ae2459c6ef0dc2d5419bc692dea4a936c1859c21) Thanks [@bxff](https://github.com/bxff)! - Skip shortcut registration in non-TTY environments + + Previously, registering keyboard shortcuts in non-TTY environments (e.g., Turborepo) caused Miniflare `ERR_DISPOSED` errors during prerendering. Shortcuts are now only registered when running in an interactive terminal. + +- Updated dependencies [[`614bbd7`](https://github.com/cloudflare/workers-sdk/commit/614bbd709529191bbae6aa92790bbfe00a37e3d9), [`788bf78`](https://github.com/cloudflare/workers-sdk/commit/788bf786b4c5cb8e1bdd6464d3f88b4125cebc75), [`1375577`](https://github.com/cloudflare/workers-sdk/commit/1375577c860f1ae9af5caf1c488d47ec1cf52b6f), [`ae108f0`](https://github.com/cloudflare/workers-sdk/commit/ae108f090532765751c3996ba4c863a9fe858ddf), [`bba0968`](https://github.com/cloudflare/workers-sdk/commit/bba09689ca258b6da36b21b7300845ce031eaca6), [`c3407ad`](https://github.com/cloudflare/workers-sdk/commit/c3407ada8cff1170ef2a3bbc4d3137dcf3998461), [`f9e8a45`](https://github.com/cloudflare/workers-sdk/commit/f9e8a452fb299e6cb1a0ff2985347bfc277deac8)]: + - wrangler@4.60.0 + - miniflare@4.20260120.0 + - @cloudflare/unenv-preset@2.11.0 + ## 1.21.1 ### Patch Changes diff --git a/packages/vite-plugin-cloudflare/package.json b/packages/vite-plugin-cloudflare/package.json index 0239ee4d7445..4a8232d3c377 100644 --- a/packages/vite-plugin-cloudflare/package.json +++ b/packages/vite-plugin-cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/vite-plugin", - "version": "1.21.1", + "version": "1.21.2", "description": "Cloudflare plugin for Vite", "keywords": [ "cloudflare", diff --git a/packages/vitest-pool-workers/CHANGELOG.md b/packages/vitest-pool-workers/CHANGELOG.md index f02155e3f324..93efe99970c0 100644 --- a/packages/vitest-pool-workers/CHANGELOG.md +++ b/packages/vitest-pool-workers/CHANGELOG.md @@ -1,5 +1,13 @@ # @cloudflare/vitest-pool-workers +## 0.12.6 + +### Patch Changes + +- Updated dependencies [[`614bbd7`](https://github.com/cloudflare/workers-sdk/commit/614bbd709529191bbae6aa92790bbfe00a37e3d9), [`788bf78`](https://github.com/cloudflare/workers-sdk/commit/788bf786b4c5cb8e1bdd6464d3f88b4125cebc75), [`1375577`](https://github.com/cloudflare/workers-sdk/commit/1375577c860f1ae9af5caf1c488d47ec1cf52b6f), [`bba0968`](https://github.com/cloudflare/workers-sdk/commit/bba09689ca258b6da36b21b7300845ce031eaca6), [`c3407ad`](https://github.com/cloudflare/workers-sdk/commit/c3407ada8cff1170ef2a3bbc4d3137dcf3998461), [`f9e8a45`](https://github.com/cloudflare/workers-sdk/commit/f9e8a452fb299e6cb1a0ff2985347bfc277deac8)]: + - wrangler@4.60.0 + - miniflare@4.20260120.0 + ## 0.12.5 ### Patch Changes diff --git a/packages/vitest-pool-workers/package.json b/packages/vitest-pool-workers/package.json index 1d8040de2794..f48ef3fd3972 100644 --- a/packages/vitest-pool-workers/package.json +++ b/packages/vitest-pool-workers/package.json @@ -1,6 +1,6 @@ { "name": "@cloudflare/vitest-pool-workers", - "version": "0.12.5", + "version": "0.12.6", "description": "Workers Vitest integration for writing Vitest unit and integration tests that run inside the Workers runtime", "keywords": [ "cloudflare", diff --git a/packages/wrangler/CHANGELOG.md b/packages/wrangler/CHANGELOG.md index 3f153a92519b..a3115e5af33f 100644 --- a/packages/wrangler/CHANGELOG.md +++ b/packages/wrangler/CHANGELOG.md @@ -1,5 +1,106 @@ # wrangler +## 4.60.0 + +### Minor Changes + +- [#11113](https://github.com/cloudflare/workers-sdk/pull/11113) [`bba0968`](https://github.com/cloudflare/workers-sdk/commit/bba09689ca258b6da36b21b7300845ce031eaca6) Thanks [@AmirSa12](https://github.com/AmirSa12)! - Add `wrangler complete` command for shell completion scripts (bash, zsh, powershell) + + Usage: + + ```bash + # Bash + wrangler complete bash >> ~/.bashrc + + # Zsh + wrangler complete zsh >> ~/.zshrc + + # Fish + wrangler complete fish >> ~/.config/fish/completions/wrangler.fish + + # PowerShell + wrangler complete powershell > $PROFILE + ``` + + - Uses `@bomb.sh/tab` library for cross-shell compatibility + - Completions are dynamically generated from `experimental_getWranglerCommands()` API + +- [#11893](https://github.com/cloudflare/workers-sdk/pull/11893) [`f9e8a45`](https://github.com/cloudflare/workers-sdk/commit/f9e8a452fb299e6cb1a0ff2985347bfc277deac8) Thanks [@NuroDev](https://github.com/NuroDev)! - `wrangler types` now generates per-environment TypeScript interfaces when named environments exist in your configuration. + + When your configuration has named environments (an `env` object), `wrangler types` now generates both: + + - **Per-environment interfaces** (e.g., `StagingEnv`, `ProductionEnv`) containing only the bindings explicitly declared in each environment, plus inherited secrets + - **An aggregated `Env` interface** with all bindings from all environments (top-level + named environments), where: + - Bindings present in **all** environments are required + - Bindings not present in all environments are optional + - Secrets are always required (since they're inherited everywhere) + - Conflicting binding types across environments produce union types (e.g., `KVNamespace | R2Bucket`) + + However, if your config does not contain any environments, or you manually specify an environment via `--env`, `wrangler types` will continue to generate a single interface as before. + + **Example:** + + Given the following `wrangler.jsonc`: + + ```jsonc + { + "name": "my-worker", + "kv_namespaces": [ + { + "binding": "SHARED_KV", + "id": "abc123", + }, + ], + "env": { + "staging": { + "kv_namespaces": [ + { "binding": "SHARED_KV", "id": "staging-kv" }, + { "binding": "STAGING_CACHE", "id": "staging-cache" }, + ], + }, + }, + } + ``` + + Running `wrangler types` will generate: + + ```ts + declare namespace Cloudflare { + interface StagingEnv { + SHARED_KV: KVNamespace; + STAGING_CACHE: KVNamespace; + } + interface Env { + SHARED_KV: KVNamespace; // Required: in all environments + STAGING_CACHE?: KVNamespace; // Optional: only in staging + } + } + interface Env extends Cloudflare.Env {} + ``` + +### Patch Changes + +- [#12030](https://github.com/cloudflare/workers-sdk/pull/12030) [`614bbd7`](https://github.com/cloudflare/workers-sdk/commit/614bbd709529191bbae6aa92790bbfe00a37e3d9) Thanks [@jbwcloudflare](https://github.com/jbwcloudflare)! - Fix `wrangler pages project validate` to respect file count limits from `CF_PAGES_UPLOAD_JWT` + +- [#11993](https://github.com/cloudflare/workers-sdk/pull/11993) [`788bf78`](https://github.com/cloudflare/workers-sdk/commit/788bf786b4c5cb8e1bdd6464d3f88b4125cebc75) Thanks [@dependabot](https://github.com/apps/dependabot)! - chore: update dependencies of "miniflare", "wrangler" + + The following dependency versions have been updated: + + | Dependency | From | To | + | ---------- | ------------ | ------------ | + | workerd | 1.20260116.0 | 1.20260120.0 | + +- [#12039](https://github.com/cloudflare/workers-sdk/pull/12039) [`1375577`](https://github.com/cloudflare/workers-sdk/commit/1375577c860f1ae9af5caf1c488d47ec1cf52b6f) Thanks [@dimitropoulos](https://github.com/dimitropoulos)! - Fixed the flag casing for the time period flag for the `d1 insights` command. + +- [#12026](https://github.com/cloudflare/workers-sdk/pull/12026) [`c3407ad`](https://github.com/cloudflare/workers-sdk/commit/c3407ada8cff1170ef2a3bbc4d3137dcf3998461) Thanks [@dario-piotrowicz](https://github.com/dario-piotrowicz)! - Fix `wrangler setup` not automatically selecting `workers` as the target for new SvelteKit apps + + The Sveltekit `adapter:cloudflare` adapter now accepts two different targets `workers` or `pages`. Since the wrangler auto configuration only targets workers, wrangler should instruct the adapter to use the `workers` variant. (The auto configuration process would in any case not work if the user were to target `pages`.) + +- Updated dependencies [[`788bf78`](https://github.com/cloudflare/workers-sdk/commit/788bf786b4c5cb8e1bdd6464d3f88b4125cebc75), [`ae108f0`](https://github.com/cloudflare/workers-sdk/commit/ae108f090532765751c3996ba4c863a9fe858ddf)]: + - miniflare@4.20260120.0 + - @cloudflare/unenv-preset@2.11.0 + - @cloudflare/kv-asset-handler@0.4.2 + ## 4.59.3 ### Patch Changes diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index ab04d53d92df..7217619dc8dc 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -1,6 +1,6 @@ { "name": "wrangler", - "version": "4.59.3", + "version": "4.60.0", "description": "Command-line interface for all things Cloudflare Workers", "keywords": [ "wrangler",