@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 +35 -0
- package/package.json +2 -2
- package/src/certificate_request.js +10 -0
- package/src/internal/process_ca_certificates.js +63 -0
- package/src/main.js +1 -0
- package/src/trust_certificate_authority.js +28 -0
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
|
+
"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.
|
|
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
|
+
};
|