@jsenv/https-local 3.6.2 → 4.0.0

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
@@ -19,6 +19,7 @@ Generate locally trusted HTTPS certificates for local development.
19
19
  - [Certificate Expiration](#certificate-expiration)
20
20
  - [JavaScript API](#javascript-api)
21
21
  - [requestCertificate](#requestcertificate)
22
+ - [trustCertificateAuthority](#trustcertificateauthority)
22
23
  - [verifyHostsFile](#verifyhostsfile)
23
24
  - [Auto Update Hosts](#auto-update-hosts)
24
25
  - [installCertificateAuthority](#installcertificateauthority)
@@ -176,6 +177,40 @@ const server = createServer(
176
177
 
177
178
  [`init`](#init) (or `installCertificateAuthority`) must be called once before using this function.
178
179
 
180
+ It also makes the current process trust the certificate authority, as described in [trustCertificateAuthority](#trustcertificateauthority). Pass `trustAuthority: false` to keep the CA certificates of the process untouched:
181
+
182
+ ```js
183
+ const { certificate, privateKey } = requestCertificate({
184
+ trustAuthority: false,
185
+ });
186
+ ```
187
+
188
+ ### trustCertificateAuthority
189
+
190
+ The root certificate authority is installed in the system keychain, which browsers read — but node does not: it uses its own CA list. A node process requesting a local HTTPS server signed by the authority therefore fails with `unable to verify the first certificate`, even on the machine that created the authority.
191
+
192
+ `trustCertificateAuthority` adds the authority root certificate to the CA list of the current process:
193
+
194
+ ```js
195
+ import { trustCertificateAuthority } from "@jsenv/https-local";
196
+
197
+ trustCertificateAuthority();
198
+
199
+ const response = await fetch("https://localhost:4000");
200
+ ```
201
+
202
+ Only the authority root certificate is added; the CA certificates node trusts by default are preserved and the rest of the system keychain is left out. Calling it twice does nothing the second time.
203
+
204
+ [requestCertificate](#requestcertificate) does this on its own, so this function is for processes acting only as a client — an integration test requesting a local HTTPS server started elsewhere, for instance.
205
+
206
+ [`init`](#init) (or `installCertificateAuthority`) must be called once before using this function.
207
+
208
+ > **Note:** it relies on [`tls.setDefaultCACertificates`](https://nodejs.org/api/tls.html#tlssetdefaultcacertificatescerts), available starting from node 22.19.0 and 24.5.0. On older versions the call logs a warning and does nothing; the alternative there is the `NODE_EXTRA_CA_CERTS` environment variable, pointing to the `rootCertificateFilePath` returned by [requestCertificate](#requestcertificate). It must be set before the process starts, which is why it does not replace this function:
209
+ >
210
+ > ```console
211
+ > NODE_EXTRA_CA_CERTS="~/Library/Application Support/https_local/https_local_root_certificate.crt" node file.mjs
212
+ > ```
213
+
179
214
  ### verifyHostsFile
180
215
 
181
216
  Verifies that IP mappings important for your local server are present in the hosts file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/https-local",
3
- "version": "3.6.2",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "description": "A programmatic way to generate locally trusted certificates",
6
6
  "repository": {
@@ -50,7 +50,7 @@
50
50
  "@jsenv/https-local": "./",
51
51
  "@jsenv/performance-impact": "../performance-impact",
52
52
  "@jsenv/test": "../../related/test",
53
- "playwright": "1.59.1"
53
+ "playwright": "1.61.1"
54
54
  },
55
55
  "publishConfig": {
56
56
  "access": "public"
@@ -3,6 +3,7 @@ import { UNICODE, createDetailedMessage, createLogger } from "@jsenv/humanize";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { getAuthorityFileInfos } from "./internal/authority_file_infos.js";
5
5
  import { requestCertificateFromAuthority } from "./internal/certificate_generator.js";
6
+ import { addCertificateToProcessCACertificates } from "./internal/process_ca_certificates.js";
6
7
  import { forge } from "./internal/forge.js";
7
8
  import { formatDuration } from "./internal/validity_formatting.js";
8
9
  import {
@@ -17,6 +18,7 @@ export const requestCertificate = ({
17
18
  altNames = ["localhost"],
18
19
  commonName = "https local server certificate",
19
20
  validityDurationInMs = createValidityDurationOfXDays(396),
21
+ trustAuthority = true,
20
22
  } = {}) => {
21
23
  if (typeof validityDurationInMs !== "number") {
22
24
  throw new TypeError(
@@ -105,6 +107,14 @@ npx @jsenv/https-local init`,
105
107
  )}`,
106
108
  );
107
109
 
110
+ if (trustAuthority) {
111
+ addCertificateToProcessCACertificates({
112
+ logger,
113
+ certificate: rootCertificate,
114
+ certificateFilePath: rootCertificateFileInfo.path,
115
+ });
116
+ }
117
+
108
118
  return {
109
119
  certificate: serverCertificate,
110
120
  privateKey: serverCertificatePrivateKey,
@@ -0,0 +1,63 @@
1
+ /*
2
+ * Browsers trust the authority root certificate because it is installed in the
3
+ * system keychain; node ignores that keychain and uses its own CA list, so a
4
+ * node process fetching a local https server signed by the authority fails with
5
+ * "unable to verify the first certificate".
6
+ *
7
+ * Only the authority root certificate is added: reading the whole system
8
+ * keychain (tls.getCACertificates("system")) would also pull in whatever a
9
+ * corporate proxy installed there.
10
+ */
11
+
12
+ import { UNICODE, createDetailedMessage } from "@jsenv/humanize";
13
+ import tls from "node:tls";
14
+
15
+ export const addCertificateToProcessCACertificates = ({
16
+ logger,
17
+ certificate,
18
+ certificateFilePath,
19
+ }) => {
20
+ // named imports would make the whole package fail to link on node versions
21
+ // where these functions do not exist yet
22
+ const { getCACertificates, setDefaultCACertificates } = tls;
23
+ if (!getCACertificates || !setDefaultCACertificates) {
24
+ logger.warn(
25
+ createDetailedMessage(
26
+ `Cannot trust certificate authority: "tls.setDefaultCACertificates" is not available in node ${process.version} (available starting from node 22.19.0 and 24.5.0).`,
27
+ {
28
+ "suggested workaround": `start the process with NODE_EXTRA_CA_CERTS, it must be set before the process starts`,
29
+ "suggested command to run": `NODE_EXTRA_CA_CERTS="${certificateFilePath}" node file.mjs`,
30
+ },
31
+ ),
32
+ );
33
+ return false;
34
+ }
35
+
36
+ const defaultCertificates = getCACertificates("default");
37
+ const alreadyTrusted = defaultCertificates.some((defaultCertificate) => {
38
+ return isSameCertificate(defaultCertificate, certificate);
39
+ });
40
+ if (alreadyTrusted) {
41
+ logger.debug(
42
+ `${UNICODE.OK} authority root certificate already trusted by this process`,
43
+ );
44
+ return false;
45
+ }
46
+ setDefaultCACertificates([...defaultCertificates, certificate]);
47
+ logger.debug(
48
+ `${UNICODE.OK} authority root certificate trusted by this process`,
49
+ );
50
+ return true;
51
+ };
52
+
53
+ const isSameCertificate = (a, b) => {
54
+ return toCertificateBody(a) === toCertificateBody(b);
55
+ };
56
+
57
+ // PEM files describing the very same certificate differ by their line wrapping
58
+ // and trailing newlines
59
+ const toCertificateBody = (certificate) => {
60
+ return certificate
61
+ .replace(/-----(BEGIN|END) CERTIFICATE-----/g, "")
62
+ .replace(/\s/g, "");
63
+ };
package/src/main.js CHANGED
@@ -14,6 +14,7 @@ export {
14
14
  } from "./certificate_authority.js";
15
15
  export { requestCertificate } from "./certificate_request.js";
16
16
  export { verifyHostsFile } from "./hosts_file_verif.js";
17
+ export { trustCertificateAuthority } from "./trust_certificate_authority.js";
17
18
  export {
18
19
  createValidityDurationOfXDays,
19
20
  createValidityDurationOfXYears,
@@ -0,0 +1,28 @@
1
+ import { createLogger } from "@jsenv/humanize";
2
+ import { readFileSync } from "node:fs";
3
+ import { getAuthorityFileInfos } from "./internal/authority_file_infos.js";
4
+ import { addCertificateToProcessCACertificates } from "./internal/process_ca_certificates.js";
5
+
6
+ export const trustCertificateAuthority = ({
7
+ logLevel,
8
+ logger = createLogger({ logLevel }), // to be able to catch logs during unit tests
9
+ } = {}) => {
10
+ const { rootCertificateFileInfo } = getAuthorityFileInfos();
11
+ if (!rootCertificateFileInfo.exists) {
12
+ throw new Error(
13
+ `Certificate authority not found, "installCertificateAuthority" must be called before "trustCertificateAuthority".
14
+ --- Suggested command to run ---
15
+ npx @jsenv/https-local init`,
16
+ );
17
+ }
18
+
19
+ const rootCertificate = readFileSync(
20
+ new URL(rootCertificateFileInfo.url),
21
+ "utf8",
22
+ );
23
+ return addCertificateToProcessCACertificates({
24
+ logger,
25
+ certificate: rootCertificate,
26
+ certificateFilePath: rootCertificateFileInfo.path,
27
+ });
28
+ };