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 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 teams.
3
+ One-command local HTTPS for local development.
4
4
 
5
5
  ## Install
6
6
 
7
+ ### One-time run
7
8
  ```bash
8
- npm i -D localssl
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
- - `localssl` / `localssl use` - initialize + create project cert + configure framework
15
- - `localssl init` - initialize machine CA and trust stores
16
- - `localssl status` - check expiry and team sync status
17
- - `localssl renew` - renew project cert
18
- - `localssl trust` - import team public CAs from `localssl.json`
19
- - `localssl qr` - serve CA certificate + print QR for phone install
20
- - `localssl ci` - ephemeral cert setup for CI (`CI=true`)
21
- - `localssl remove` - uninstall trust and local files
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
- ## Team sharing
88
+ ### `localssl-cli trust`
89
+ Imports teammate public CAs from `localssl.json` into local trust stores.
24
90
 
25
- `localssl.json` is safe to commit. It stores only public CA certificates.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "localssl-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "description": "One-command local HTTPS setup for teams",
5
5
  "type": "commonjs",
6
6
  "main": "src/index.js",
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 'Windows Root store';
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', '(already configured)');
155
+ step(1, 1, 'Machine CA setup', 'skip', `(${repairSummary})`);
143
156
  }
144
157
  return { mkcertPath, initialized: false };
145
158
  }
146
159
 
147
- await execMkcert(mkcertPath, ['-install']);
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 };
@@ -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
- execFile('certutil', ['-addstore', '-f', 'ROOT', certPath], { windowsHide: true }, (error) => {
6
- if (error) {
7
- reject(new Error('Admin privileges required to install CA on Windows. Re-run terminal as Administrator.'));
8
- return;
9
- }
10
- resolve();
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 }, () => resolve());
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