@jsenv/https-local
v4.0.14
Published
A programmatic way to generate locally trusted certificates
Readme
HTTPS Local 
Generate locally trusted HTTPS certificates for local development.
🔒 Certificates trusted by your operating system and browsers
🌐 Perfect for local HTTPS development
🖥️ Works on macOS, Linux, and Windows
📱 Trusted by iOS simulators too
⚡ Simple CLI and JavaScript API
Table of Contents
Quick Start
npx @jsenv/https-local init
npx @jsenv/https-local generateThen start your server reading the generated certificate files:
import { createServer } from "node:https";
import { readFileSync } from "node:fs";
const server = createServer(
{
cert: readFileSync("certificate.pem"),
key: readFileSync("private_key.pem"),
},
(request, response) => {
response.end("Hello HTTPS world!");
},
).listen(8443, () => {
console.log("HTTPS server running at https://localhost:8443");
});CLI
init
npx @jsenv/https-local initInstalls a root certificate authority, trusts it in your OS, your browsers and the iOS simulators currently booted, and ensures localhost is mapped to 127.0.0.1 in your hosts file. Safe to re-run — subsequent runs report the current status.
> npx @jsenv/https-local init
ℹ authority root certificate not found in filesystem
Generating authority root certificate with a validity of 20 years...
✔ authority root certificate written at /Users/you/https_local/https_local_root_certificate.crt
Adding certificate to mac keychain...
❯ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "/Users/you/https_local/https_local_root_certificate.crt"
Password:
✔ certificate added to mac keychain
Adding certificate to firefox...
✔ certificate added to Firefox
Check if certificate is in iOS simulator "iPhone 17"...
ℹ certificate not found in iOS simulator "iPhone 17"
Adding certificate to iOS simulator "iPhone 17"...
❯ xcrun simctl keychain 3353AABB-2A54-49FA-B69D-AA4454350523 add-root-cert "/Users/you/https_local/https_local_root_certificate.crt"
✔ certificate added to iOS simulator "iPhone 17"
Check hosts file content...
✔ all ip mappings found in hosts file> npx @jsenv/https-local init
✔ authority root certificate found in filesystem
Checking certificate validity...
✔ certificate still valid for 19 years
Detect if certificate attributes have changed...
✔ certificate attributes are the same
Check if certificate is in mac keychain...
✔ certificate found in mac keychain
Check if certificate is in Firefox...
✔ certificate found in Firefox
Check if certificate is in iOS simulator "iPhone 17"...
✔ certificate found in iOS simulator "iPhone 17"
Check hosts file content...
✔ all ip mappings found in hosts fileiOS simulator
An iOS simulator has a trust store of its own: a certificate trusted by the mac keychain is still refused by Safari inside the simulator, and a fetch towards another origin fails with TypeError: Load failed — WebKit only offers the "Visit website" exception for the page itself, not for cross-origin requests.
init adds the root certificate to every simulator booted at the time it runs, with full trust: there is nothing to enable in Settings › General › About › Certificate Trust Settings afterwards (that toggle is for certificates installed from a profile). Boot the simulator, then run init again; or add it by hand, booted standing for every running simulator:
xcrun simctl keychain booted add-root-cert "$HOME/Library/Application Support/https_local/https_local_root_certificate.crt"The certificate stays in the simulator across reboots. It cannot be removed on its own, so cleanup leaves it there; xcrun simctl keychain <udid> reset wipes the whole simulator keychain.
generate
npx @jsenv/https-local generateGenerates a server certificate signed by the local certificate authority and writes it to files. Requires init to have been run first.
Note: Certificate files are static — they are not renewed automatically. Re-run
generateafter one year to replace expired files.
Options:
| Option | Description | Default |
| --------------- | --------------------------------- | ----------------- |
| --certificate | Path for the certificate file | certificate.pem |
| --private-key | Path for the private key file | private_key.pem |
| --hostnames | Comma-separated list of hostnames | localhost |
Example:
npx @jsenv/https-local generate --certificate server.pem --private-key server.key --hostnames localhost,myapp.localThe default is localhost alone, unlike requestCertificate: a file keeps the network ips of the day it was written, and they change with the network.
cleanup
npx @jsenv/https-local cleanupUninstalls the root certificate and removes its trust from your OS and browsers.
Certificate Expiration
| Certificate | Expires after | How to renew? |
| ----------- | ------------- | ----------------- |
| server | 1 year | Re-run generate |
| authority | 20 years | Re-run init |
The server certificate expires after one year, which is the maximum duration allowed by web browsers.
The authority root certificate expires after 20 years. Re-running init after expiry will reinstall and re-trust a new one.
Other Devices
init trusts the authority on the machine it runs on, and in the iOS simulators booted there. Another device, a phone for instance, trusts it once the root certificate is installed on it. Until then, it shows a certificate warning; clicking through it is not enough for what needs a secure context: Chrome, for one, refuses to register a service worker on such an origin.
The jsenv dev server serves a page doing it from the phone itself (download, steps for Android and iOS, a check that it worked) when given the rootCertificate returned by requestCertificate.
By hand: copy https_local_root_certificate.crt to the device, never the .key next to it.
- Android: in Settings, search for "CA certificate", then install the file. Android requires a screen lock and then shows "Network may be monitored".
- iOS: send the file with AirDrop, or open a link to it in Safari, then Settings › Profile Downloaded › Install. Then Settings › General › About › Certificate Trust Settings, and turn on full trust for the certificate: without it the certificate is installed but not trusted.
The device then trusts anything signed by the authority's key, the trade-off already made on the machine. Remove it from Android's Settings › Encryption & credentials › Trusted credentials › User, or from iOS's Settings › General › VPN & Device Management.
JavaScript API
To use the JavaScript API, add the package to your dev dependencies:
npm install --save-dev @jsenv/https-localrequestCertificate
The requestCertificate function generates a fresh certificate each time it is called and returns it in memory. Because the certificate is generated on every server startup, it is always valid — as long as your server is restarted at least once a year.
Without altNames, the certificate is valid for every name of the machine: localhost, 127.0.0.1, ::1, the machine name, <name>.local and the network ips. That is what another device on the network types to reach it; a name missing from the certificate fails in the browser (ERR_CERT_COMMON_NAME_INVALID) even when the authority is trusted. The network ips are read at the time of the call: after joining another network, restart the server.
Besides certificate and privateKey, it returns the authority as rootCertificate (PEM), for a device that must trust it (see Other Devices), and rootCertificateFilePath.
import { createServer } from "node:https";
import { requestCertificate } from "@jsenv/https-local";
const { certificate, privateKey } = requestCertificate({
altNames: ["localhost", "local.example"],
});
const server = createServer(
{ cert: certificate, key: privateKey },
(request, response) => {
response.end("Hello HTTPS world!");
},
).listen(8443, () => {
console.log("HTTPS server running at https://localhost:8443");
});init (or installCertificateAuthority) must be called once before using this function.
It also makes the current process trust the certificate authority, as described in trustCertificateAuthority. Pass trustAuthority: false to keep the CA certificates of the process untouched:
const { certificate, privateKey } = requestCertificate({
trustAuthority: false,
});trustCertificateAuthority
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.
trustCertificateAuthority adds the authority root certificate to the CA list of the current process:
import { trustCertificateAuthority } from "@jsenv/https-local";
trustCertificateAuthority();
const response = await fetch("https://localhost:4000");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.
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.
init (or installCertificateAuthority) must be called once before using this function.
Note: it relies on
tls.setDefaultCACertificates, 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 theNODE_EXTRA_CA_CERTSenvironment variable, pointing to therootCertificateFilePathreturned by requestCertificate. It must be set before the process starts, which is why it does not replace this function:NODE_EXTRA_CA_CERTS="~/Library/Application Support/https_local/https_local_root_certificate.crt" node file.mjs
verifyHostsFile
Verifies that IP mappings important for your local server are present in the hosts file.
import { verifyHostsFile } from "@jsenv/https-local";
await verifyHostsFile({
ipMappings: {
"127.0.0.1": ["localhost", "local.example"],
},
});Auto Update Hosts
It's possible to update hosts file programmatically using tryToUpdateHostsFile:
import { verifyHostsFile } from "@jsenv/https-local";
await verifyHostsFile({
ipMappings: {
"127.0.0.1": ["localhost", "local.example"],
},
tryToUpdateHostsFile: true,
});installCertificateAuthority
The installCertificateAuthority function generates a certificate authority valid for 20 years.
This certificate authority is needed to generate local certificates that will be trusted by the operating system and web browsers.
import { installCertificateAuthority } from "@jsenv/https-local";
await installCertificateAuthority();By default, trusting the root certificate is a manual process. See BenMorel/dev-certificates for instructions. This can also be done programmatically as shown in Auto Trust.
Auto Trust
It's possible to trust root certificate programmatically using tryToTrust:
import { installCertificateAuthority } from "@jsenv/https-local";
await installCertificateAuthority({
tryToTrust: true,
});