Skip to content

fix(security): require a Node.js with the eMudhra CA fix (v2.9.2) - #216

Merged
paulkr merged 3 commits into
mainfrom
fix/require-patched-node
Oct 21, 2026
Merged

paulkr merged 3 commits into
mainfrom
fix/require-patched-node

Conversation

@paulkr

@paulkr paulkr commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Why

Eight eMudhra intermediate CAs were issued with 8-bit private keys, so anyone can compute them and sign a valid-looking certificate for any domain, including api.withone.ai. Node.js blocks them inside the TLS handshake from 22.23.4, 24.22.1 and 26.12.1 (nodejs/node#66871, shipped 2026-10-21). Node 18, 20, 23 and 25 are end-of-life and will never get the fix.

The CLI doesn't ship a runtime. It runs on whatever node the user has, so the Node release only protects users who upgrade. This PR makes that upgrade mandatory.

Alternative to #215. That PR filters CA roots in userland. It fails CI on Node 18, and on older Node it matches and removes AAA Certificate Services (the legacy Comodo/Sectigo root) by name. It also can't act on the old Node lines that need it most; there it only warns. Node's fix blocks the eight intermediates by key, which userland can't do, so we require it instead.

What

  • Startup check. src/node-check.ts is its own tsup entry, and its shared chunk has no other imports. bin/cli.js runs it before loading the CLI or opening any connection. On an unpatched Node it exits 1.
    • People see the reason and the command that updates Node for how it was installed: nvm, nvm-windows, Volta, fnm or Homebrew, otherwise nodejs.org. A line that got the fix (22, 24, 26) stays on its line; an end-of-life line moves to 24.
    • Agents (--agent) get JSON with code: "NODE_UPGRADE_REQUIRED", currentNode, requiredNode and upgrade.
  • Escape hatch. ONE_ALLOW_UNPATCHED_NODE=1 runs anyway, with a security warning on every run. The warning is silent in agent mode, where stderr would corrupt the JSON. It's documented in the README only, not in the agent error or the skill.
  • Library consumers are unaffected: the check runs only in the one binary. nodeHasTlsFix is exported so they can check for themselves.
  • engines.node: >=18 → ^22.23.4 || ^24.22.1 || >=26.12.1. CI matrix: 18/20/22 → 22/24/26. tsup target: node18 → node22.
  • Docs: README, skills/one/SKILL.md, guide-content.ts (show the user the upgrade command instead of retrying), CLAUDE.md, and the run-tests.mjs comment.
  • Version 2.9.2. This is a patch release, so ^2 dependents get the check too. Lockfile version fields edited by hand per CLAUDE.md.

Auto-update installs with --force, which ignores engines. So users on old Node still auto-update to 2.9.2, and then get the upgrade instructions instead of running exposed.

Verified

  • Homebrew Node 23.1.0:
    • The CLI refuses with To update: brew upgrade node, exit 1.
    • --agent list returns the JSON error with code and upgrade, exit 1.
    • With ONE_ALLOW_UNPATCHED_NODE=1, it prints the warning and then 2.9.2, exit 0. In agent mode, stdout is clean JSON.
  • On the official Node 26.12.1 binary (checksum verified) → prints 2.9.2, exit 0.
  • npm run build and npm run typecheck pass. The new node-check.test.ts covers version gating, each version manager, Homebrew formulas and the fallback, and it passes.
  • npm test locally: 680 passed, 6 failed. All 6 are the resolveConfig tests in config.test.ts (macOS /private/var vs /var tmpdir path), a file this PR doesn't touch; fix(security): stop trusting the compromised eMudhra roots (v2.9.2) #215 reports the same 6 failing on untouched main.

After merge

🤖 Generated with Claude Code

paulkr and others added 2 commits October 21, 2026 09:35
Eight eMudhra intermediate CAs were issued with 8-bit keys, so anyone can
sign a certificate for api.withone.ai. Node.js blocks them in the TLS
handshake from 22.23.4, 24.22.1 and 26.12.1. The CLI runs on the user's
Node, so on anything older it would hand a network attacker the API key.

bin/cli.js now runs src/node-check.ts (its own build entry) before loading
the CLI or opening any connection, and exits 1 on an unpatched Node, with
the usual {"error": ...} JSON in agent mode. engines.node becomes
^22.23.4 || ^24.22.1 || >=26.12.1, the CI matrix moves to 22/24/26, and the
tsup target to node22.

BREAKING CHANGE: Node.js 18, 20, 23, 25 and unpatched 22/24/26 are no
longer supported.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…de help

- Version 2.9.2 instead of 3.0.0, so ^2 dependents get the check too.
- The error prints the command that updates Node for how it was installed
  (nvm, nvm-windows, Volta, fnm, Homebrew, else nodejs.org), staying on a
  line that got the fix and moving an end-of-life line to 24.
- Agent mode adds code NODE_UPGRADE_REQUIRED, currentNode, requiredNode and
  upgrade to the JSON error.
- ONE_ALLOW_UNPATCHED_NODE=1 runs anyway with a warning on every run
  (silent in agent mode so stderr can't corrupt the JSON).
- nodeHasTlsFix is exported for library consumers.
- Versions read 22.23.4+, 24.22.1+ or 26.12.1+ everywhere.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@paulkr paulkr changed the title fix(security)!: require a Node.js with the eMudhra CA fix (v3.0.0) fix(security): require a Node.js with the eMudhra CA fix (v2.9.2) Oct 21, 2026
…g output

On Node 24+ the test runner reports through the same process.stdout the
mem tests patch to capture agent JSON. find-by-key captured one of those
binary chunks and failed to parse it; export-import swallowed them. Both
now capture only string writes and forward everything else, as
actions.preflight.test.ts already did.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@paulkr
paulkr merged commit a0e68a6 into main Oct 21, 2026
7 checks passed
@paulkr
paulkr deleted the fix/require-patched-node branch October 21, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant