localssl-cli 0.1.3 → 0.1.6
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/LICENSE +21 -0
- package/README.md +165 -14
- package/package.json +1 -1
- package/src/bootstrap.js +28 -6
- package/src/prompt.js +22 -0
- package/src/trust/windows.js +46 -9
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sumit Sheokand
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,25 +1,176 @@
|
|
|
1
|
-
# localssl
|
|
1
|
+
# localssl-cli
|
|
2
2
|
|
|
3
|
-
One-command local HTTPS for development
|
|
3
|
+
One-command local HTTPS for local development.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
+
### One-time run
|
|
7
8
|
```bash
|
|
8
|
-
|
|
9
|
-
npx localssl
|
|
9
|
+
npx localssl-cli
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
### Project install (recommended)
|
|
13
|
+
```bash
|
|
14
|
+
npm i -D localssl-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Global install
|
|
18
|
+
```bash
|
|
19
|
+
npm i -g localssl-cli
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
> Binary names exposed: `localssl-cli` and `localssl`.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx localssl-cli
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This does:
|
|
33
|
+
1. Installs/uses mkcert in `~/.localssl`
|
|
34
|
+
2. Creates machine CA and trusts it in OS store
|
|
35
|
+
3. Tries trust import for Firefox + Chrome/Edge NSS stores
|
|
36
|
+
4. Generates project cert/key in `.localssl/`
|
|
37
|
+
5. Configures supported framework HTTPS settings
|
|
38
|
+
6. Updates `.gitignore` to avoid key commits
|
|
39
|
+
7. Syncs team public cert metadata in `localssl.json`
|
|
40
|
+
|
|
41
|
+
Then run your app as usual (`npm run dev` / `npm start`).
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Auto-setup on install
|
|
46
|
+
|
|
47
|
+
`localssl-cli@0.1.3+` adds setup hooks at install time:
|
|
48
|
+
|
|
49
|
+
- Adds `predev: "localssl-cli use"` if `dev` exists
|
|
50
|
+
- Adds `prestart: "localssl-cli use"` if `start` exists
|
|
51
|
+
- If `predev`/`prestart` already exists, prepends `localssl-cli use && ...`
|
|
52
|
+
- Skips if already configured
|
|
53
|
+
|
|
54
|
+
Disable this behavior:
|
|
55
|
+
```bash
|
|
56
|
+
LOCALSSL_SKIP_POSTINSTALL=1 npm i -D localssl-cli
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
12
61
|
## Commands
|
|
13
62
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
63
|
+
### `localssl-cli` (default)
|
|
64
|
+
Runs project setup flow (`use`).
|
|
65
|
+
|
|
66
|
+
### `localssl-cli init`
|
|
67
|
+
Machine bootstrap only:
|
|
68
|
+
- mkcert setup
|
|
69
|
+
- machine CA install
|
|
70
|
+
- trust stores (OS + Firefox + Chrome/Edge NSS)
|
|
71
|
+
|
|
72
|
+
### `localssl-cli use [hosts...]`
|
|
73
|
+
Project setup only:
|
|
74
|
+
- detects hosts from `package.json` + `.env`/`.env.local`
|
|
75
|
+
- defaults: `localhost`, `127.0.0.1`, `::1`
|
|
76
|
+
- generates `.localssl/cert.pem` and `.localssl/key.pem`
|
|
77
|
+
- injects framework HTTPS config if supported
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
```bash
|
|
81
|
+
localssl-cli use
|
|
82
|
+
localssl-cli use myapp.local api.myapp.local
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `localssl-cli use --open` or `localssl-cli --open`
|
|
86
|
+
Same as setup, then opens a guessed HTTPS URL in default browser.
|
|
22
87
|
|
|
23
|
-
|
|
88
|
+
### `localssl-cli trust`
|
|
89
|
+
Imports teammate public CAs from `localssl.json` into local trust stores.
|
|
24
90
|
|
|
25
|
-
`localssl
|
|
91
|
+
### `localssl-cli status`
|
|
92
|
+
Shows:
|
|
93
|
+
- machine CA validity
|
|
94
|
+
- project cert validity
|
|
95
|
+
- detected framework
|
|
96
|
+
- hosts/team summary
|
|
97
|
+
- warning when cert expires in <=30 days
|
|
98
|
+
|
|
99
|
+
### `localssl-cli renew`
|
|
100
|
+
Regenerates project cert/key (keeps machine CA).
|
|
101
|
+
|
|
102
|
+
### `localssl-cli qr`
|
|
103
|
+
Starts temporary HTTP server to download CA cert and prints QR code for mobile install.
|
|
104
|
+
|
|
105
|
+
### `localssl-cli ci`
|
|
106
|
+
CI-only mode (`CI=true`):
|
|
107
|
+
- ephemeral CA/cert generation
|
|
108
|
+
- exports `NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`
|
|
109
|
+
- also exports `LOCALSSL_CERT_FILE`, `LOCALSSL_KEY_FILE`
|
|
110
|
+
|
|
111
|
+
### `localssl-cli remove`
|
|
112
|
+
Best-effort cleanup:
|
|
113
|
+
- removes trust from OS/Firefox/Chrome/Edge NSS
|
|
114
|
+
- deletes `~/.localssl`
|
|
115
|
+
- deletes project `.localssl`
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Framework support
|
|
120
|
+
|
|
121
|
+
- **Vite**: injects HTTPS cert/key in `vite.config.*`
|
|
122
|
+
- **Next.js**: updates `dev` script with `--experimental-https` flags
|
|
123
|
+
- **Create React App**: writes HTTPS vars to `.env.local`
|
|
124
|
+
- **Express**: creates `localssl.js` helper exporting HTTPS options
|
|
125
|
+
- **Webpack Dev Server**: injects `devServer.https`
|
|
126
|
+
- **Generic**: prints manual cert/key usage hint
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Team sharing (`localssl.json`)
|
|
131
|
+
|
|
132
|
+
`localssl.json` is safe to commit.
|
|
133
|
+
|
|
134
|
+
It stores only:
|
|
135
|
+
- project hosts
|
|
136
|
+
- teammate machine metadata
|
|
137
|
+
- teammate **public** CA certificates
|
|
138
|
+
|
|
139
|
+
It never stores CA private keys.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Security notes
|
|
144
|
+
|
|
145
|
+
- Private keys are written to project `.localssl/` and ignored by `.gitignore`
|
|
146
|
+
- Team file validation blocks private-key content in `localssl.json`
|
|
147
|
+
- Never share root CA private key files
|
|
148
|
+
|
|
149
|
+
## Windows permissions behavior
|
|
150
|
+
|
|
151
|
+
- localssl-cli first trusts certs in `CurrentUser\\Root` (no admin expected)
|
|
152
|
+
- if needed, it prompts: `Admin access needed for machine-wide trust. Continue? (y/N)`
|
|
153
|
+
- choosing `No` keeps safe mode and skips machine-wide trust
|
|
154
|
+
- rerunning `localssl-cli init` repairs trust if CA already exists
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Troubleshooting
|
|
159
|
+
|
|
160
|
+
### Windows `EPERM` when running `npx` inside this package source folder
|
|
161
|
+
Run from another directory, for example:
|
|
162
|
+
```powershell
|
|
163
|
+
cd $env:TEMP
|
|
164
|
+
npx --yes localssl-cli --help
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Firefox/Chrome/Edge trust skipped
|
|
168
|
+
Install `certutil` (NSS tools), then rerun:
|
|
169
|
+
```bash
|
|
170
|
+
localssl-cli init
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Rebuild certs
|
|
174
|
+
```bash
|
|
175
|
+
localssl-cli renew
|
|
176
|
+
```
|
package/package.json
CHANGED
package/src/bootstrap.js
CHANGED
|
@@ -80,9 +80,9 @@ async function ensureMkcert() {
|
|
|
80
80
|
return mkcertPath;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
function execMkcert(mkcertPath, args) {
|
|
83
|
+
function execMkcert(mkcertPath, args, extraEnv = {}) {
|
|
84
84
|
return new Promise((resolve, reject) => {
|
|
85
|
-
execFile(mkcertPath, args, { env: { ...process.env, CAROOT: LOCALSSL_HOME } }, (error, stdout, stderr) => {
|
|
85
|
+
execFile(mkcertPath, args, { env: { ...process.env, CAROOT: LOCALSSL_HOME, ...extraEnv } }, (error, stdout, stderr) => {
|
|
86
86
|
if (error) {
|
|
87
87
|
reject(new Error(stderr || error.message));
|
|
88
88
|
return;
|
|
@@ -99,8 +99,8 @@ async function trustSystem(certPath) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
if (process.platform === 'win32') {
|
|
102
|
-
await trustWindows(certPath);
|
|
103
|
-
return
|
|
102
|
+
const scope = await trustWindows(certPath);
|
|
103
|
+
return scope;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
await trustLinux(certPath);
|
|
@@ -138,13 +138,35 @@ async function initMachine({ quiet = false } = {}) {
|
|
|
138
138
|
|
|
139
139
|
const hasCA = await fs.pathExists(LOCALSSL_CA_PUBLIC);
|
|
140
140
|
if (hasCA) {
|
|
141
|
+
let repairSummary = 'already configured';
|
|
142
|
+
try {
|
|
143
|
+
const systemResult = await trustSystem(LOCALSSL_CA_PUBLIC);
|
|
144
|
+
const firefoxResult = await trustInFirefox(LOCALSSL_CA_PUBLIC);
|
|
145
|
+
const chromiumResult = await trustInChromium(LOCALSSL_CA_PUBLIC);
|
|
146
|
+
const nodeResult = await configureNodeExtraCACerts();
|
|
147
|
+
repairSummary = `${systemResult}; ${nodeResult}; Firefox ${firefoxResult.trusted ? 'ok' : 'skipped'}; Chrome/Edge ${chromiumResult.trusted ? 'ok' : 'skipped'}`;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (!quiet) {
|
|
150
|
+
warn(`Trust repair skipped: ${error.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
141
154
|
if (!quiet) {
|
|
142
|
-
step(1, 1, 'Machine CA setup', 'skip',
|
|
155
|
+
step(1, 1, 'Machine CA setup', 'skip', `(${repairSummary})`);
|
|
143
156
|
}
|
|
144
157
|
return { mkcertPath, initialized: false };
|
|
145
158
|
}
|
|
146
159
|
|
|
147
|
-
|
|
160
|
+
try {
|
|
161
|
+
await execMkcert(mkcertPath, ['-install'], { TRUST_STORES: 'system,nss' });
|
|
162
|
+
} catch (error) {
|
|
163
|
+
const message = error.message || '';
|
|
164
|
+
const javaTrustError = /keytool|cacerts|access is denied/i.test(message);
|
|
165
|
+
if (!javaTrustError) {
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
warn('Java trust store update skipped (no admin access). System/browser trust still configured.');
|
|
169
|
+
}
|
|
148
170
|
const systemResult = await trustSystem(LOCALSSL_CA_PUBLIC);
|
|
149
171
|
const firefoxResult = await trustInFirefox(LOCALSSL_CA_PUBLIC);
|
|
150
172
|
const chromiumResult = await trustInChromium(LOCALSSL_CA_PUBLIC);
|
package/src/prompt.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
|
|
3
|
+
async function askYesNo(question, defaultNo = true) {
|
|
4
|
+
if (process.env.LOCALSSL_ASSUME_YES === '1') {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
9
|
+
return !defaultNo;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
14
|
+
rl.question(question, (answer) => {
|
|
15
|
+
rl.close();
|
|
16
|
+
const normalized = String(answer || '').trim().toLowerCase();
|
|
17
|
+
resolve(normalized === 'y' || normalized === 'yes');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { askYesNo };
|
package/src/trust/windows.js
CHANGED
|
@@ -1,20 +1,57 @@
|
|
|
1
1
|
const { execFile } = require('child_process');
|
|
2
|
+
const { askYesNo } = require('../prompt');
|
|
3
|
+
|
|
4
|
+
function run(file, args) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
execFile(file, args, { windowsHide: true }, (error) => resolve(!error));
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function trustCurrentUserWithPowerShell(certPath) {
|
|
11
|
+
const command = `Import-Certificate -FilePath \"${certPath}\" -CertStoreLocation Cert:\\CurrentUser\\Root | Out-Null`;
|
|
12
|
+
return run('powershell', ['-NoProfile', '-NonInteractive', '-Command', command]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function trustMachineWithElevation(certPath) {
|
|
16
|
+
const command = `$p = Start-Process certutil -ArgumentList '-addstore','-f','ROOT','${certPath}' -Verb RunAs -Wait -PassThru; if ($p.ExitCode -eq 0) { exit 0 } else { exit 1 }`;
|
|
17
|
+
return run('powershell', ['-NoProfile', '-NonInteractive', '-Command', command]);
|
|
18
|
+
}
|
|
2
19
|
|
|
3
20
|
function trustCertificate(certPath) {
|
|
4
|
-
return new Promise((resolve, reject) => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
21
|
+
return new Promise(async (resolve, reject) => {
|
|
22
|
+
const userViaCertutil = await run('certutil', ['-user', '-addstore', '-f', 'ROOT', certPath]);
|
|
23
|
+
if (userViaCertutil) {
|
|
24
|
+
resolve('Windows CurrentUser Root');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const userViaPowerShell = await trustCurrentUserWithPowerShell(certPath);
|
|
29
|
+
if (userViaPowerShell) {
|
|
30
|
+
resolve('Windows CurrentUser Root (PowerShell)');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const consent = await askYesNo(' Admin access needed for machine-wide trust. Continue? (y/N): ');
|
|
35
|
+
if (!consent) {
|
|
36
|
+
resolve('Windows trust skipped (user declined admin prompt)');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const machineStore = await trustMachineWithElevation(certPath);
|
|
41
|
+
if (machineStore) {
|
|
42
|
+
resolve('Windows LocalMachine Root (elevated)');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
reject(new Error('Could not install CA in Windows trust stores. Safe mode preserved (no insecure fallback used).'));
|
|
12
47
|
});
|
|
13
48
|
}
|
|
14
49
|
|
|
15
50
|
function untrustCertificate(certPath) {
|
|
16
51
|
return new Promise((resolve) => {
|
|
17
|
-
execFile('certutil', ['-delstore', 'ROOT', certPath], { windowsHide: true }, () =>
|
|
52
|
+
execFile('certutil', ['-user', '-delstore', 'ROOT', certPath], { windowsHide: true }, () => {
|
|
53
|
+
execFile('certutil', ['-delstore', 'ROOT', certPath], { windowsHide: true }, () => resolve());
|
|
54
|
+
});
|
|
18
55
|
});
|
|
19
56
|
}
|
|
20
57
|
|