@driggsby/deploy 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,6 +62,16 @@ deploy limits locally.
62
62
  uploads only content Driggsby doesn't already have, and finalizes the
63
63
  version.
64
64
 
65
+ Call `distrustCompromisedRoots()` once at startup, before any request. Some
66
+ Node.js releases still trust the roots behind the eMudhra certificate
67
+ compromise of October 2026. On official Node.js builds, 22.19.0 and later
68
+ 22.x or 24.5.0 and later, it removes them from your process's default CA
69
+ list. Older releases can't change that list. Builds or settings that verify
70
+ against the system's OpenSSL store (such as Homebrew's builds, or
71
+ `--use-openssl-ca`) rely on that store's own update. In those cases, update
72
+ Node.js or your system's CA certificates to a current release. The
73
+ `driggsby` and `driggsby-deploy` commands already call it.
74
+
65
75
  Zero runtime dependencies; Node.js 18 or newer; macOS, Linux, and Windows.
66
76
 
67
77
  ## License
package/dist/cli.js CHANGED
@@ -2,7 +2,9 @@
2
2
  // entrypoint.ts so tests exercise runDeployEntrypoint without triggering a
3
3
  // real deploy on import.
4
4
  import process from "node:process";
5
+ import { distrustCompromisedRoots } from "./distrusted-roots.js";
5
6
  import { runDeployEntrypoint } from "./entrypoint.js";
7
+ distrustCompromisedRoots();
6
8
  process.exitCode = await runDeployEntrypoint({
7
9
  argv: process.argv.slice(2),
8
10
  cwd: process.cwd(),
@@ -0,0 +1,16 @@
1
+ export declare function isDistrustedRoot(pem: string): boolean;
2
+ export interface CaListApi {
3
+ getCACertificates?: (type: "default" | "bundled" | "extra") => string[];
4
+ setDefaultCACertificates?: (certs: string[]) => void;
5
+ }
6
+ export interface BuildConfig {
7
+ variables: {
8
+ node_shared_openssl?: unknown;
9
+ };
10
+ target_defaults: {
11
+ defines?: unknown;
12
+ };
13
+ }
14
+ export declare function mayUseOpenSslStore(config?: BuildConfig): boolean;
15
+ export declare function removeDistrustedRoots(api: CaListApi, opensslStore: () => boolean): void;
16
+ export declare function distrustCompromisedRoots(): void;
@@ -0,0 +1,83 @@
1
+ // Roots over the eMudhra intermediate CAs that turned out, in October 2026,
2
+ // to have trivially weak private keys. The intermediates chain to emSign ECC
3
+ // Root CA - G3, and through Sectigo cross-signs to AAA Certificate Services,
4
+ // so anyone can issue a certificate for any host under either root. Current
5
+ // Node.js releases trust neither; older ones trust one or both. Both bins
6
+ // remove them from the process's default CA list before making any request,
7
+ // where Node.js lets them.
8
+ import { createHash } from "node:crypto";
9
+ import process from "node:process";
10
+ import tls from "node:tls";
11
+ // SHA-256 fingerprints of the two roots' DER encodings.
12
+ const DISTRUSTED_ROOT_FINGERPRINTS = new Set([
13
+ // emSign ECC Root CA - G3
14
+ "86a1ecba089c4a8d3bbe2734c612ba341d813e043cf9e8a862cd5c57a36bbe6b",
15
+ // AAA Certificate Services
16
+ "d7a7a0fb5d7e2731d771e9484ebcdef71d5f0c3e0a2948782bc83ee0ea699ef4",
17
+ ]);
18
+ const PEM_BODY = /-----BEGIN CERTIFICATE-----([\s\S]*?)-----END CERTIFICATE-----/;
19
+ // The SHA-256 of a PEM certificate's DER. Node.js re-serializes the list
20
+ // once it has been set, so certificates compare by content, not by text.
21
+ function fingerprint(pem) {
22
+ const body = PEM_BODY.exec(pem)?.[1];
23
+ if (body === undefined) {
24
+ return undefined;
25
+ }
26
+ const der = Buffer.from(body.replace(/\s+/g, ""), "base64");
27
+ return createHash("sha256").update(der).digest("hex");
28
+ }
29
+ export function isDistrustedRoot(pem) {
30
+ const print = fingerprint(pem);
31
+ return print !== undefined && DISTRUSTED_ROOT_FINGERPRINTS.has(print);
32
+ }
33
+ // Builds made to verify against OpenSSL's own store (the build flag that
34
+ // defines NODE_OPENSSL_CERT_STORE, as Homebrew's and many Linux
35
+ // distributions' builds do) still report Node.js's bundled list, so
36
+ // replacing that list would swap their real store, company CAs included,
37
+ // for the bundled roots; their fix comes from that store's own update.
38
+ // Builds linked against a shared OpenSSL are skipped too, to stay on the
39
+ // safe side, as is any runtime whose build configuration doesn't say.
40
+ // Exported for tests.
41
+ export function mayUseOpenSslStore(config = process.config) {
42
+ const defines = config.target_defaults.defines;
43
+ return (config.variables.node_shared_openssl !== false ||
44
+ !Array.isArray(defines) ||
45
+ defines.includes("NODE_OPENSSL_CERT_STORE"));
46
+ }
47
+ // Removes the distrusted roots from the default CA list, where that list is
48
+ // the one Node.js verifies against: it holds every bundled root, and is at
49
+ // least as long as the bundled roots plus the extra CAs, as it is unless
50
+ // --use-openssl-ca is set. It never sets an empty list, and it does nothing
51
+ // where Node.js can't change the list or the list doesn't hold the roots.
52
+ // Exported for tests; callers use distrustCompromisedRoots.
53
+ export function removeDistrustedRoots(api, opensslStore) {
54
+ if (api.getCACertificates === undefined || api.setDefaultCACertificates === undefined) {
55
+ return;
56
+ }
57
+ try {
58
+ if (opensslStore()) {
59
+ return;
60
+ }
61
+ const roots = api.getCACertificates("default");
62
+ const bundled = api.getCACertificates("bundled");
63
+ const listed = new Set(roots.map(fingerprint));
64
+ const holdsBundled = bundled.every((pem) => listed.has(fingerprint(pem)));
65
+ if (!holdsBundled || roots.length < bundled.length + api.getCACertificates("extra").length) {
66
+ return;
67
+ }
68
+ const kept = roots.filter((pem) => !isDistrustedRoot(pem));
69
+ if (kept.length !== roots.length && kept.length > 0) {
70
+ api.setDefaultCACertificates(kept);
71
+ }
72
+ }
73
+ catch {
74
+ // Hardening only: if Node.js can't read or replace its list, the CLI
75
+ // carries on with the list Node.js already uses.
76
+ }
77
+ }
78
+ // Silently removes the distrusted roots from this process's default CA list,
79
+ // where Node.js lets it. Call it once at startup, before the first HTTPS
80
+ // request: the list applies to TLS connections made after it is set.
81
+ export function distrustCompromisedRoots() {
82
+ removeDistrustedRoots(tls, () => mayUseOpenSslStore());
83
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { DeployApiError, DeployError } from "./errors.ts";
2
+ export { distrustCompromisedRoots } from "./distrusted-roots.ts";
2
3
  export { PUBLIC_BASE_URL, consoleUrlOnOrigin, isLoopbackBaseUrl, resolveBaseUrl } from "./base-url.ts";
3
4
  export { MAX_FILE_BYTES, MAX_FILE_COUNT, MAX_PATH_LENGTH, MAX_TOTAL_BYTES, slugProblem, } from "./limits.ts";
4
5
  export { type ProjectConfig, readProjectConfig, writeAssignedSlug } from "./project-config.ts";
package/dist/index.js CHANGED
@@ -2,6 +2,9 @@
2
2
  // The driggsby CLI's deploy/rollback/versions commands build on these
3
3
  // exports; Node-only sandboxes can use them directly.
4
4
  export { DeployApiError, DeployError } from "./errors.js";
5
+ // Drops the roots over the compromised eMudhra intermediates from the
6
+ // process's default CA list. Both bins call it before any request.
7
+ export { distrustCompromisedRoots } from "./distrusted-roots.js";
5
8
  // The base-URL policy: which origin the token travels to, and what
6
9
  // DRIGGSBY_BASE_URL may override it with. One implementation for both this
7
10
  // package's bin and the driggsby CLI.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driggsby/deploy",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "description": "Deploy static Driggsby apps: the deploy protocol client used by the driggsby CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {