@lasso-ai/cli 1.4.0 → 1.4.2

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.
@@ -1,12 +1,13 @@
1
1
  export declare const TLS_KEY_FILE: string;
2
2
  export declare const TLS_CERT_FILE: string;
3
- export declare function ensureHostCertificate(): {
3
+ export declare function ensureHostCertificate(onProgress?: (message: string) => void): Promise<{
4
4
  ok: boolean;
5
5
  trusted: boolean;
6
6
  error?: string;
7
- };
7
+ }>;
8
8
  export declare function hostCertificateStatus(): {
9
9
  present: boolean;
10
10
  certificate: string;
11
11
  key: string;
12
+ trusted: boolean;
12
13
  };
@@ -10,27 +10,238 @@ const node_fs_1 = __importDefault(require("node:fs"));
10
10
  const node_os_1 = __importDefault(require("node:os"));
11
11
  const node_path_1 = __importDefault(require("node:path"));
12
12
  const node_child_process_1 = require("node:child_process");
13
+ const node_https_1 = __importDefault(require("node:https"));
14
+ const node_http_1 = __importDefault(require("node:http"));
13
15
  const paths_1 = require("./paths");
14
16
  exports.TLS_KEY_FILE = node_path_1.default.join(paths_1.HOST_DIR, "host.key");
15
17
  exports.TLS_CERT_FILE = node_path_1.default.join(paths_1.HOST_DIR, "host.crt");
16
18
  function hasCertificate() {
17
19
  return node_fs_1.default.existsSync(exports.TLS_KEY_FILE) && node_fs_1.default.existsSync(exports.TLS_CERT_FILE);
18
20
  }
19
- function ensureHostCertificate() {
20
- node_fs_1.default.mkdirSync(paths_1.HOST_DIR, { recursive: true, mode: 0o700 });
21
- if (hasCertificate())
22
- return { ok: true, trusted: false };
21
+ function isMkcertInstalled() {
22
+ // Check system PATH first
23
23
  try {
24
- (0, node_child_process_1.execFileSync)("mkcert", ["-install"], { stdio: "ignore" });
25
- (0, node_child_process_1.execFileSync)("mkcert", ["-cert-file", exports.TLS_CERT_FILE, "-key-file", exports.TLS_KEY_FILE, "localhost", "127.0.0.1", "::1", "*.lasso"], { stdio: "ignore" });
26
- node_fs_1.default.chmodSync(exports.TLS_KEY_FILE, 0o600);
27
- return { ok: true, trusted: true };
24
+ (0, node_child_process_1.execFileSync)("mkcert", ["-version"], { stdio: "ignore" });
25
+ return true;
28
26
  }
29
27
  catch {
30
- // mkcert is optional; OpenSSL still provides a usable local certificate.
28
+ // Check local installation
29
+ try {
30
+ const localMkcert = node_path_1.default.join(node_os_1.default.homedir(), ".lasso", "bin", "mkcert");
31
+ if (node_fs_1.default.existsSync(localMkcert)) {
32
+ (0, node_child_process_1.execFileSync)(localMkcert, ["-version"], { stdio: "ignore" });
33
+ return true;
34
+ }
35
+ }
36
+ catch { }
37
+ return false;
38
+ }
39
+ }
40
+ function getMkcertPath() {
41
+ // Check local installation first
42
+ const localMkcert = node_path_1.default.join(node_os_1.default.homedir(), ".lasso", "bin", "mkcert");
43
+ if (node_fs_1.default.existsSync(localMkcert)) {
44
+ return localMkcert;
45
+ }
46
+ return "mkcert";
47
+ }
48
+ async function downloadMkcert() {
49
+ const platform = node_os_1.default.platform();
50
+ const arch = node_os_1.default.arch();
51
+ let downloadUrl;
52
+ let binaryName;
53
+ // Determine the correct binary for the platform
54
+ if (platform === "darwin") {
55
+ if (arch === "x64") {
56
+ downloadUrl = "https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-darwin-amd64";
57
+ binaryName = "mkcert-darwin-amd64";
58
+ }
59
+ else if (arch === "arm64") {
60
+ downloadUrl = "https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-darwin-arm64";
61
+ binaryName = "mkcert-darwin-arm64";
62
+ }
63
+ else {
64
+ return { ok: false, error: `Unsupported architecture: ${arch}` };
65
+ }
66
+ }
67
+ else if (platform === "linux") {
68
+ if (arch === "x64") {
69
+ downloadUrl = "https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64";
70
+ binaryName = "mkcert-linux-amd64";
71
+ }
72
+ else if (arch === "arm64") {
73
+ downloadUrl = "https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-arm64";
74
+ binaryName = "mkcert-linux-arm64";
75
+ }
76
+ else {
77
+ return { ok: false, error: `Unsupported architecture: ${arch}` };
78
+ }
79
+ }
80
+ else {
81
+ return { ok: false, error: `Unsupported platform: ${platform}` };
82
+ }
83
+ return new Promise((resolve) => {
84
+ const protocol = downloadUrl.startsWith("https") ? node_https_1.default : node_http_1.default;
85
+ const tempDir = node_os_1.default.tmpdir();
86
+ const tempPath = node_path_1.default.join(tempDir, binaryName);
87
+ console.log(`[Lasso] Downloading mkcert from ${downloadUrl}...`);
88
+ const file = node_fs_1.default.createWriteStream(tempPath);
89
+ protocol.get(downloadUrl, (response) => {
90
+ if (response.statusCode !== 200) {
91
+ node_fs_1.default.unlinkSync(tempPath);
92
+ return resolve({ ok: false, error: `Failed to download mkcert: ${response.statusCode}` });
93
+ }
94
+ response.pipe(file);
95
+ file.on("finish", () => {
96
+ file.close();
97
+ node_fs_1.default.chmodSync(tempPath, 0o755);
98
+ console.log(`[Lasso] ✓ mkcert downloaded to ${tempPath}`);
99
+ resolve({ ok: true, path: tempPath });
100
+ });
101
+ }).on("error", (error) => {
102
+ node_fs_1.default.unlinkSync(tempPath);
103
+ resolve({ ok: false, error: error instanceof Error ? error.message : String(error) });
104
+ });
105
+ });
106
+ }
107
+ async function installMkcert() {
108
+ const platform = node_os_1.default.platform();
109
+ // Try package manager first
110
+ if (platform === "darwin") {
111
+ try {
112
+ (0, node_child_process_1.execFileSync)("brew", ["--version"], { stdio: "ignore" });
113
+ console.log("[Lasso] Installing mkcert via Homebrew...");
114
+ const installResult = await new Promise((resolve) => {
115
+ const install = (0, node_child_process_1.spawn)("brew", ["install", "mkcert", "nss"], { stdio: "inherit" });
116
+ install.on("close", (code) => {
117
+ if (code === 0)
118
+ resolve({ ok: true });
119
+ else
120
+ resolve({ ok: false, error: `Homebrew install failed with code ${code}` });
121
+ });
122
+ install.on("error", (error) => {
123
+ resolve({ ok: false, error: error instanceof Error ? error.message : String(error) });
124
+ });
125
+ });
126
+ if (installResult.ok) {
127
+ (0, node_child_process_1.execFileSync)("mkcert", ["-install"], { stdio: "ignore" });
128
+ console.log("[Lasso] ✓ mkcert installed and CA configured");
129
+ return { ok: true };
130
+ }
131
+ }
132
+ catch {
133
+ console.log("[Lasso] Homebrew not available, trying direct download...");
134
+ }
135
+ }
136
+ else if (platform === "linux") {
137
+ try {
138
+ (0, node_child_process_1.execFileSync)("apt-get", ["--version"], { stdio: "ignore" });
139
+ console.log("[Lasso] Installing mkcert via apt-get...");
140
+ const installResult = await new Promise((resolve) => {
141
+ const install = (0, node_child_process_1.spawn)("sudo", ["apt-get", "install", "-y", "mkcert"], { stdio: "inherit" });
142
+ install.on("close", (code) => {
143
+ if (code === 0)
144
+ resolve({ ok: true });
145
+ else
146
+ resolve({ ok: false, error: `apt-get install failed with code ${code}` });
147
+ });
148
+ install.on("error", (error) => {
149
+ resolve({ ok: false, error: error instanceof Error ? error.message : String(error) });
150
+ });
151
+ });
152
+ if (installResult.ok) {
153
+ (0, node_child_process_1.execFileSync)("mkcert", ["-install"], { stdio: "ignore" });
154
+ console.log("[Lasso] ✓ mkcert installed and CA configured");
155
+ return { ok: true };
156
+ }
157
+ }
158
+ catch {
159
+ try {
160
+ (0, node_child_process_1.execFileSync)("dnf", ["--version"], { stdio: "ignore" });
161
+ console.log("[Lasso] Installing mkcert via dnf...");
162
+ const installResult = await new Promise((resolve) => {
163
+ const install = (0, node_child_process_1.spawn)("sudo", ["dnf", "install", "-y", "mkcert"], { stdio: "inherit" });
164
+ install.on("close", (code) => {
165
+ if (code === 0)
166
+ resolve({ ok: true });
167
+ else
168
+ resolve({ ok: false, error: `dnf install failed with code ${code}` });
169
+ });
170
+ install.on("error", (error) => {
171
+ resolve({ ok: false, error: error instanceof Error ? error.message : String(error) });
172
+ });
173
+ });
174
+ if (installResult.ok) {
175
+ (0, node_child_process_1.execFileSync)("mkcert", ["-install"], { stdio: "ignore" });
176
+ console.log("[Lasso] ✓ mkcert installed and CA configured");
177
+ return { ok: true };
178
+ }
179
+ }
180
+ catch {
181
+ console.log("[Lasso] Package manager not available, trying direct download...");
182
+ }
183
+ }
184
+ }
185
+ // Fallback: Download mkcert binary directly from GitHub
186
+ console.log("[Lasso] Downloading mkcert binary from GitHub...");
187
+ const downloadResult = await downloadMkcert();
188
+ if (!downloadResult.ok || !downloadResult.path) {
189
+ return { ok: false, error: downloadResult.error || "Failed to download mkcert binary" };
31
190
  }
191
+ // Copy binary to a permanent location
192
+ const binDir = node_path_1.default.join(node_os_1.default.homedir(), ".lasso", "bin");
193
+ node_fs_1.default.mkdirSync(binDir, { recursive: true, mode: 0o755 });
194
+ const mkcertPath = node_path_1.default.join(binDir, "mkcert");
195
+ node_fs_1.default.copyFileSync(downloadResult.path, mkcertPath);
196
+ node_fs_1.default.chmodSync(mkcertPath, 0o755);
197
+ // Clean up temp file
198
+ try {
199
+ node_fs_1.default.unlinkSync(downloadResult.path);
200
+ }
201
+ catch { }
202
+ // Install the local CA using the downloaded binary
203
+ try {
204
+ (0, node_child_process_1.execFileSync)(mkcertPath, ["-install"], { stdio: "inherit" });
205
+ console.log("[Lasso] ✓ mkcert installed and CA configured");
206
+ return { ok: true };
207
+ }
208
+ catch (error) {
209
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
210
+ }
211
+ }
212
+ async function ensureHostCertificate(onProgress) {
213
+ node_fs_1.default.mkdirSync(paths_1.HOST_DIR, { recursive: true, mode: 0o700 });
214
+ if (hasCertificate()) {
215
+ const certStatus = hostCertificateStatus();
216
+ return { ok: true, trusted: certStatus.trusted };
217
+ }
218
+ onProgress?.("Generating SSL certificate...");
219
+ // Try mkcert first - it creates system-trusted certificates
220
+ if (!isMkcertInstalled()) {
221
+ onProgress?.("mkcert not found, installing...");
222
+ const installResult = await installMkcert();
223
+ if (!installResult.ok) {
224
+ console.warn(`[Lasso] ${installResult.error}`);
225
+ console.warn("[Lasso] Falling back to OpenSSL (certificate will show as not secure)");
226
+ }
227
+ }
228
+ if (isMkcertInstalled()) {
229
+ try {
230
+ onProgress?.("Creating trusted certificate with mkcert...");
231
+ const mkcertPath = getMkcertPath();
232
+ (0, node_child_process_1.execFileSync)(mkcertPath, ["-cert-file", exports.TLS_CERT_FILE, "-key-file", exports.TLS_KEY_FILE, "localhost", "127.0.0.1", "::1", "*.lasso"], { stdio: "ignore" });
233
+ node_fs_1.default.chmodSync(exports.TLS_KEY_FILE, 0o600);
234
+ onProgress?.("✓ SSL certificate ready (trusted)");
235
+ return { ok: true, trusted: true };
236
+ }
237
+ catch (mkcertError) {
238
+ console.warn("[Lasso] mkcert failed, falling back to OpenSSL");
239
+ }
240
+ }
241
+ // Fallback to OpenSSL - creates self-signed certificate (not trusted by browsers)
32
242
  const configFile = node_path_1.default.join(node_os_1.default.tmpdir(), `lasso-host-${process.pid}.cnf`);
33
243
  try {
244
+ onProgress?.("Creating self-signed certificate with OpenSSL...");
34
245
  node_fs_1.default.writeFileSync(configFile, [
35
246
  "[req]", "distinguished_name = req_distinguished_name", "x509_extensions = v3_req", "prompt = no",
36
247
  "[req_distinguished_name]", "CN = *.lasso", "[v3_req]",
@@ -38,6 +249,7 @@ function ensureHostCertificate() {
38
249
  ].join("\n"), { mode: 0o600 });
39
250
  (0, node_child_process_1.execFileSync)("openssl", ["req", "-x509", "-nodes", "-newkey", "rsa:2048", "-keyout", exports.TLS_KEY_FILE, "-out", exports.TLS_CERT_FILE, "-days", "825", "-config", configFile], { stdio: "ignore" });
40
251
  node_fs_1.default.chmodSync(exports.TLS_KEY_FILE, 0o600);
252
+ onProgress?.("✓ SSL certificate ready (self-signed)");
41
253
  return { ok: true, trusted: false };
42
254
  }
43
255
  catch (error) {
@@ -48,5 +260,17 @@ function ensureHostCertificate() {
48
260
  }
49
261
  }
50
262
  function hostCertificateStatus() {
51
- return { present: hasCertificate(), certificate: exports.TLS_CERT_FILE, key: exports.TLS_KEY_FILE };
263
+ // Check if certificate was created by mkcert (trusted) or OpenSSL (self-signed)
264
+ let trusted = false;
265
+ if (hasCertificate()) {
266
+ try {
267
+ const certContent = node_fs_1.default.readFileSync(exports.TLS_CERT_FILE, "utf8");
268
+ // mkcert certificates include "mkcert" in the issuer
269
+ trusted = certContent.includes("mkcert");
270
+ }
271
+ catch {
272
+ // If we can't read the cert, assume not trusted
273
+ }
274
+ }
275
+ return { present: hasCertificate(), certificate: exports.TLS_CERT_FILE, key: exports.TLS_KEY_FILE, trusted };
52
276
  }
@@ -7,6 +7,7 @@ export interface HostHealth {
7
7
  proxyPort?: number | null;
8
8
  httpsPort?: number | null;
9
9
  https?: boolean;
10
+ trusted?: boolean;
10
11
  dnsPort?: number | null;
11
12
  running?: number;
12
13
  registered?: number;
@@ -60,10 +60,10 @@ class LassoHost {
60
60
  proxies = new Map();
61
61
  constructor(opts) {
62
62
  this.opts = opts;
63
- this.server = node_http_1.default.createServer((req, res) => this.handleRequest(req, res));
64
- this.server.on("upgrade", (req, socket, head) => this.handleUpgrade(req, socket, head));
65
- this.secureServer = node_https_1.default.createServer({}, (req, res) => this.handleRequest(req, res));
66
- this.secureServer.on("upgrade", (req, socket, head) => this.handleUpgrade(req, socket, head));
63
+ this.server = node_http_1.default.createServer((req, res) => this.handleRequest(req, res, false));
64
+ this.server.on("upgrade", (req, socket, head) => this.handleUpgrade(req, socket, head, false));
65
+ this.secureServer = node_https_1.default.createServer({}, (req, res) => this.handleRequest(req, res, true));
66
+ this.secureServer.on("upgrade", (req, socket, head) => this.handleUpgrade(req, socket, head, true));
67
67
  this.resolver = (0, dns_1.createDomainResolver)();
68
68
  this.registry = (0, registry_1.loadRegistry)();
69
69
  for (const domain of Object.keys(this.registry))
@@ -246,6 +246,7 @@ class LassoHost {
246
246
  ];
247
247
  }
248
248
  health(res) {
249
+ const certStatus = (0, certificates_1.hostCertificateStatus)();
249
250
  writeJson(res, 200, {
250
251
  ok: true,
251
252
  service: exports.SERVICE_NAME,
@@ -256,14 +257,15 @@ class LassoHost {
256
257
  dnsPort: this.dnsHandle?.port ?? null,
257
258
  proxyPort: this.opts.proxyPort,
258
259
  httpsPort: this.opts.httpsPort,
259
- https: (0, certificates_1.hostCertificateStatus)().present,
260
+ https: certStatus.present,
261
+ trusted: certStatus.trusted,
260
262
  running: this.running.size,
261
263
  registered: Object.keys(this.registry).length,
262
264
  projects: this.projectList(),
263
265
  });
264
266
  }
265
267
  // ---- request handling --------------------------------------------
266
- handleRequest(req, res) {
268
+ handleRequest(req, res, isSecure = false) {
267
269
  const url = req.url || "/";
268
270
  const route = this.routes().find((r) => r.route === `${req.method} ${url}`);
269
271
  if (route) {
@@ -283,6 +285,13 @@ class LassoHost {
283
285
  writeJson(res, 404, { ok: false, error: "Lasso Host only proxies *.lasso domains.", host: req.headers.host });
284
286
  return;
285
287
  }
288
+ // Redirect HTTP to HTTPS for .lasso domains
289
+ if (!isSecure) {
290
+ const httpsUrl = `https://${hostname}${url}`;
291
+ res.writeHead(301, { Location: httpsUrl });
292
+ res.end();
293
+ return;
294
+ }
286
295
  void this.proxyToProject(hostname, req, res);
287
296
  }
288
297
  async proxyToProject(hostname, req, res) {
@@ -309,13 +318,20 @@ class LassoHost {
309
318
  res.destroy();
310
319
  });
311
320
  }
312
- handleUpgrade(req, socket, head) {
321
+ handleUpgrade(req, socket, head, isSecure) {
313
322
  const hostname = (0, paths_1.domainOf)(req.headers.host || "");
314
323
  if (!(0, paths_1.isLassoDomain)(hostname)) {
315
324
  socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
316
325
  socket.destroy();
317
326
  return;
318
327
  }
328
+ // Redirect HTTP WebSocket upgrades to HTTPS
329
+ if (!isSecure) {
330
+ const wsUrl = `wss://${hostname}${req.url}`;
331
+ socket.write(`HTTP/1.1 301 Moved Permanently\r\nLocation: ${wsUrl}\r\nConnection: close\r\n\r\n`);
332
+ socket.destroy();
333
+ return;
334
+ }
319
335
  void (async () => {
320
336
  let runtime;
321
337
  try {
@@ -345,7 +361,7 @@ class LassoHost {
345
361
  this.dnsHandle = null;
346
362
  this.log(`dns server failed to start on ${this.opts.dnsPort}: ${line(error)}`);
347
363
  }
348
- const certificate = (0, certificates_1.ensureHostCertificate)();
364
+ const certificate = await (0, certificates_1.ensureHostCertificate)((message) => this.log(message));
349
365
  if (!certificate.ok)
350
366
  this.log(`https certificate unavailable: ${certificate.error}`);
351
367
  else {
package/dist/cli/index.js CHANGED
@@ -147,7 +147,7 @@ const daemon = program.command("daemon [action]")
147
147
  console.log(chalk_1.default.bold("Lasso Host"));
148
148
  console.log(chalk_1.default.dim(` Status: ${report.running ? chalk_1.default.green("running") + (report.pid ? ` (pid ${report.pid})` : "") : chalk_1.default.red("not running")}`));
149
149
  console.log(chalk_1.default.dim(` Proxy port: ${(0, paths_1.hostProxyPort)(env)}`));
150
- console.log(chalk_1.default.dim(` HTTPS port: ${(0, paths_1.hostHttpsPort)(env)}${report.health?.https ? chalk_1.default.green(" (certificate ready)") : chalk_1.default.yellow(" (certificate unavailable)")}`));
150
+ console.log(chalk_1.default.dim(` HTTPS port: ${(0, paths_1.hostHttpsPort)(env)}${report.health?.https ? (report.health.trusted ? chalk_1.default.green(" (trusted)") : chalk_1.default.yellow(" (self-signed)")) : chalk_1.default.yellow(" (certificate unavailable)")}`));
151
151
  console.log(chalk_1.default.dim(` DNS port: ${(0, paths_1.hostDnsPort)(env)}`));
152
152
  console.log(chalk_1.default.dim(` Domain resolution: ${report.resolver.installed ? chalk_1.default.green("configured") : chalk_1.default.yellow("not configured")} — ${report.resolver.detail}`));
153
153
  console.log(chalk_1.default.dim(` Automatic startup: ${report.autoStart ? chalk_1.default.green("enabled (LaunchAgent)") : chalk_1.default.yellow("disabled")}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lasso-ai/cli",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Select any part of your running app, describe a change, and let AI edit the real source code.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/cli/index.js",