@lasso-ai/cli 1.4.0 → 1.4.1

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
  };
@@ -16,21 +16,107 @@ exports.TLS_CERT_FILE = node_path_1.default.join(paths_1.HOST_DIR, "host.crt");
16
16
  function hasCertificate() {
17
17
  return node_fs_1.default.existsSync(exports.TLS_KEY_FILE) && node_fs_1.default.existsSync(exports.TLS_CERT_FILE);
18
18
  }
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 };
19
+ function isMkcertInstalled() {
23
20
  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 };
21
+ (0, node_child_process_1.execFileSync)("mkcert", ["-version"], { stdio: "ignore" });
22
+ return true;
28
23
  }
29
24
  catch {
30
- // mkcert is optional; OpenSSL still provides a usable local certificate.
25
+ return false;
26
+ }
27
+ }
28
+ async function installMkcert() {
29
+ return new Promise((resolve) => {
30
+ const platform = node_os_1.default.platform();
31
+ let installCommand;
32
+ let installArgs;
33
+ if (platform === "darwin") {
34
+ // macOS: Check if Homebrew is available
35
+ try {
36
+ (0, node_child_process_1.execFileSync)("brew", ["--version"], { stdio: "ignore" });
37
+ installCommand = "brew";
38
+ installArgs = ["install", "mkcert", "nss"];
39
+ }
40
+ catch {
41
+ return resolve({ ok: false, error: "Homebrew not found. Please install Homebrew first: https://brew.sh/" });
42
+ }
43
+ }
44
+ else if (platform === "linux") {
45
+ // Linux: Try different package managers
46
+ try {
47
+ (0, node_child_process_1.execFileSync)("apt-get", ["--version"], { stdio: "ignore" });
48
+ installCommand = "sudo";
49
+ installArgs = ["apt-get", "install", "-y", "mkcert"];
50
+ }
51
+ catch {
52
+ try {
53
+ (0, node_child_process_1.execFileSync)("dnf", ["--version"], { stdio: "ignore" });
54
+ installCommand = "sudo";
55
+ installArgs = ["dnf", "install", "-y", "mkcert"];
56
+ }
57
+ catch {
58
+ return resolve({ ok: false, error: "Could not detect package manager. Please install mkcert manually: https://mkcert.dev/" });
59
+ }
60
+ }
61
+ }
62
+ else {
63
+ return resolve({ ok: false, error: `Automatic mkcert installation not supported on ${platform}. Please install manually: https://mkcert.dev/` });
64
+ }
65
+ console.log(`[Lasso] Installing mkcert (this may require sudo password)...`);
66
+ const install = (0, node_child_process_1.spawn)(installCommand, installArgs, { stdio: "inherit" });
67
+ install.on("close", (code) => {
68
+ if (code === 0) {
69
+ // Install the local CA
70
+ try {
71
+ (0, node_child_process_1.execFileSync)("mkcert", ["-install"], { stdio: "ignore" });
72
+ console.log("[Lasso] ✓ mkcert installed and CA configured");
73
+ resolve({ ok: true });
74
+ }
75
+ catch (error) {
76
+ resolve({ ok: false, error: error instanceof Error ? error.message : String(error) });
77
+ }
78
+ }
79
+ else {
80
+ resolve({ ok: false, error: `mkcert installation failed with code ${code}` });
81
+ }
82
+ });
83
+ install.on("error", (error) => {
84
+ resolve({ ok: false, error: error instanceof Error ? error.message : String(error) });
85
+ });
86
+ });
87
+ }
88
+ async function ensureHostCertificate(onProgress) {
89
+ node_fs_1.default.mkdirSync(paths_1.HOST_DIR, { recursive: true, mode: 0o700 });
90
+ if (hasCertificate()) {
91
+ const certStatus = hostCertificateStatus();
92
+ return { ok: true, trusted: certStatus.trusted };
93
+ }
94
+ onProgress?.("Generating SSL certificate...");
95
+ // Try mkcert first - it creates system-trusted certificates
96
+ if (!isMkcertInstalled()) {
97
+ onProgress?.("mkcert not found, installing...");
98
+ const installResult = await installMkcert();
99
+ if (!installResult.ok) {
100
+ console.warn(`[Lasso] ${installResult.error}`);
101
+ console.warn("[Lasso] Falling back to OpenSSL (certificate will show as not secure)");
102
+ }
31
103
  }
104
+ if (isMkcertInstalled()) {
105
+ try {
106
+ onProgress?.("Creating trusted certificate with mkcert...");
107
+ (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" });
108
+ node_fs_1.default.chmodSync(exports.TLS_KEY_FILE, 0o600);
109
+ onProgress?.("✓ SSL certificate ready (trusted)");
110
+ return { ok: true, trusted: true };
111
+ }
112
+ catch (mkcertError) {
113
+ console.warn("[Lasso] mkcert failed, falling back to OpenSSL");
114
+ }
115
+ }
116
+ // Fallback to OpenSSL - creates self-signed certificate (not trusted by browsers)
32
117
  const configFile = node_path_1.default.join(node_os_1.default.tmpdir(), `lasso-host-${process.pid}.cnf`);
33
118
  try {
119
+ onProgress?.("Creating self-signed certificate with OpenSSL...");
34
120
  node_fs_1.default.writeFileSync(configFile, [
35
121
  "[req]", "distinguished_name = req_distinguished_name", "x509_extensions = v3_req", "prompt = no",
36
122
  "[req_distinguished_name]", "CN = *.lasso", "[v3_req]",
@@ -38,6 +124,7 @@ function ensureHostCertificate() {
38
124
  ].join("\n"), { mode: 0o600 });
39
125
  (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
126
  node_fs_1.default.chmodSync(exports.TLS_KEY_FILE, 0o600);
127
+ onProgress?.("✓ SSL certificate ready (self-signed)");
41
128
  return { ok: true, trusted: false };
42
129
  }
43
130
  catch (error) {
@@ -48,5 +135,17 @@ function ensureHostCertificate() {
48
135
  }
49
136
  }
50
137
  function hostCertificateStatus() {
51
- return { present: hasCertificate(), certificate: exports.TLS_CERT_FILE, key: exports.TLS_KEY_FILE };
138
+ // Check if certificate was created by mkcert (trusted) or OpenSSL (self-signed)
139
+ let trusted = false;
140
+ if (hasCertificate()) {
141
+ try {
142
+ const certContent = node_fs_1.default.readFileSync(exports.TLS_CERT_FILE, "utf8");
143
+ // mkcert certificates include "mkcert" in the issuer
144
+ trusted = certContent.includes("mkcert");
145
+ }
146
+ catch {
147
+ // If we can't read the cert, assume not trusted
148
+ }
149
+ }
150
+ return { present: hasCertificate(), certificate: exports.TLS_CERT_FILE, key: exports.TLS_KEY_FILE, trusted };
52
151
  }
@@ -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.1",
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",