portless 0.9.3 → 0.9.5

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/README.md CHANGED
@@ -166,7 +166,7 @@ portless proxy stop # Stop the proxy
166
166
  --tld <tld> Use a custom TLD instead of .localhost (e.g. test)
167
167
  --wildcard Allow unregistered subdomains to fall back to parent route
168
168
  --app-port <number> Use a fixed port for the app (skip auto-assignment)
169
- --force Override a route registered by another process
169
+ --force Kill the existing process and take over its route
170
170
  --name <name> Use <name> as the app name
171
171
  ```
172
172
 
@@ -505,7 +505,7 @@ function createProxyServer(options) {
505
505
  };
506
506
  if (tls) {
507
507
  const h2Server = http2.createSecureServer({
508
- cert: tls.cert,
508
+ cert: tls.ca ? Buffer.concat([tls.cert, tls.ca]) : tls.cert,
509
509
  key: tls.key,
510
510
  allowHTTP1: true,
511
511
  ...tls.SNICallback ? { SNICallback: tls.SNICallback } : {}
@@ -1297,16 +1297,30 @@ var RouteStore = class _RouteStore {
1297
1297
  fs4.writeFileSync(this.routesPath, JSON.stringify(routes, null, 2), { mode: this.fileMode });
1298
1298
  fixOwnership(this.routesPath);
1299
1299
  }
1300
+ /**
1301
+ * Register a route. When `force` is true and the hostname is already claimed
1302
+ * by another live process, that process is sent SIGTERM before the route is
1303
+ * replaced. Returns the PID of the killed process (if any) so the caller can
1304
+ * log it.
1305
+ */
1300
1306
  addRoute(hostname, port, pid, force = false) {
1301
1307
  this.ensureDir();
1302
1308
  if (!this.acquireLock()) {
1303
1309
  throw new Error("Failed to acquire route lock");
1304
1310
  }
1311
+ let killedPid;
1305
1312
  try {
1306
1313
  const routes = this.loadRoutes(true);
1307
1314
  const existing = routes.find((r) => r.hostname === hostname);
1308
- if (existing && existing.pid !== pid && this.isProcessAlive(existing.pid) && !force) {
1309
- throw new RouteConflictError(hostname, existing.pid);
1315
+ if (existing && existing.pid !== pid && this.isProcessAlive(existing.pid)) {
1316
+ if (!force) {
1317
+ throw new RouteConflictError(hostname, existing.pid);
1318
+ }
1319
+ try {
1320
+ process.kill(existing.pid, "SIGTERM");
1321
+ killedPid = existing.pid;
1322
+ } catch {
1323
+ }
1310
1324
  }
1311
1325
  const filtered = routes.filter((r) => r.hostname !== hostname);
1312
1326
  filtered.push({ hostname, port, pid });
@@ -1314,6 +1328,7 @@ var RouteStore = class _RouteStore {
1314
1328
  } finally {
1315
1329
  this.releaseLock();
1316
1330
  }
1331
+ return killedPid;
1317
1332
  }
1318
1333
  removeRoute(hostname) {
1319
1334
  this.ensureDir();
package/dist/cli.js CHANGED
@@ -35,7 +35,7 @@ import {
35
35
  waitForProxy,
36
36
  writeTldFile,
37
37
  writeTlsMarker
38
- } from "./chunk-63BOQNMU.js";
38
+ } from "./chunk-D3LR3J7L.js";
39
39
 
40
40
  // src/colors.ts
41
41
  function supportsColor() {
@@ -471,10 +471,13 @@ async function generateHostCertAsync(stateDir, hostname) {
471
471
  fixOwnership(keyPath, certPath);
472
472
  return { certPath, keyPath };
473
473
  }
474
- function createSNICallback(stateDir, defaultCert, defaultKey, tld = "localhost") {
474
+ function createSNICallback(stateDir, defaultCert, defaultKey, tld = "localhost", caCert) {
475
475
  const cache = /* @__PURE__ */ new Map();
476
476
  const pending = /* @__PURE__ */ new Map();
477
- const defaultCtx = tls.createSecureContext({ cert: defaultCert, key: defaultKey });
477
+ const defaultCtx = tls.createSecureContext({
478
+ cert: caCert ? Buffer.concat([defaultCert, caCert]) : defaultCert,
479
+ key: defaultKey
480
+ });
478
481
  return (servername, cb) => {
479
482
  if (servername === tld) {
480
483
  cb(null, defaultCtx);
@@ -490,8 +493,9 @@ function createSNICallback(stateDir, defaultCert, defaultKey, tld = "localhost")
490
493
  const keyPath = path.join(hostDir, `${safeName}-key.pem`);
491
494
  if (fileExists(certPath) && fileExists(keyPath) && isCertValid(certPath) && isCertSignatureStrong(certPath)) {
492
495
  try {
496
+ const hostCert = fs.readFileSync(certPath);
493
497
  const ctx = tls.createSecureContext({
494
- cert: fs.readFileSync(certPath),
498
+ cert: caCert ? Buffer.concat([hostCert, caCert]) : hostCert,
495
499
  key: fs.readFileSync(keyPath)
496
500
  });
497
501
  cache.set(servername, ctx);
@@ -505,11 +509,14 @@ function createSNICallback(stateDir, defaultCert, defaultKey, tld = "localhost")
505
509
  return;
506
510
  }
507
511
  const promise = generateHostCertAsync(stateDir, servername).then(async (generated) => {
508
- const [cert, key] = await Promise.all([
512
+ const [hostCert, key] = await Promise.all([
509
513
  fs.promises.readFile(generated.certPath),
510
514
  fs.promises.readFile(generated.keyPath)
511
515
  ]);
512
- return tls.createSecureContext({ cert, key });
516
+ return tls.createSecureContext({
517
+ cert: caCert ? Buffer.concat([hostCert, caCert]) : hostCert,
518
+ key
519
+ });
513
520
  });
514
521
  pending.set(servername, promise);
515
522
  promise.then((ctx) => {
@@ -1157,8 +1164,9 @@ portless
1157
1164
  } else {
1158
1165
  console.log(colors_default.green(`-- Using port ${port}`));
1159
1166
  }
1167
+ let killedPid;
1160
1168
  try {
1161
- store.addRoute(hostname, port, process.pid, force);
1169
+ killedPid = store.addRoute(hostname, port, process.pid, force);
1162
1170
  } catch (err) {
1163
1171
  if (err instanceof RouteConflictError) {
1164
1172
  console.error(colors_default.red(`Error: ${err.message}`));
@@ -1166,6 +1174,9 @@ portless
1166
1174
  }
1167
1175
  throw err;
1168
1176
  }
1177
+ if (killedPid !== void 0) {
1178
+ console.log(colors_default.yellow(`Killed existing process (PID ${killedPid})`));
1179
+ }
1169
1180
  const finalUrl = formatUrl(hostname, proxyPort, tls2);
1170
1181
  console.log(colors_default.cyan.bold(`
1171
1182
  -> ${finalUrl}
@@ -1233,7 +1244,7 @@ ${colors_default.bold("Usage:")}
1233
1244
 
1234
1245
  ${colors_default.bold("Options:")}
1235
1246
  --name <name> Override the inferred base name (worktree prefix still applies)
1236
- --force Override an existing route registered by another process
1247
+ --force Kill the existing process and take over its route
1237
1248
  --app-port <number> Use a fixed port for the app (skip auto-assignment)
1238
1249
  --help, -h Show this help
1239
1250
 
@@ -1388,7 +1399,7 @@ ${colors_default.bold("Options:")}
1388
1399
  --tld <tld> Use a custom TLD instead of .localhost (e.g. test, dev)
1389
1400
  --wildcard Allow unregistered subdomains to fall back to parent route
1390
1401
  --app-port <number> Use a fixed port for the app (skip auto-assignment)
1391
- --force Override an existing route registered by another process
1402
+ --force Kill the existing process and take over its route
1392
1403
  --name <name> Use <name> as the app name (bypasses subcommand dispatch)
1393
1404
  -- Stop flag parsing; everything after is passed to the child
1394
1405
 
@@ -1427,7 +1438,7 @@ ${colors_default.bold("Reserved names:")}
1427
1438
  process.exit(0);
1428
1439
  }
1429
1440
  function printVersion() {
1430
- console.log("0.9.3");
1441
+ console.log("0.9.5");
1431
1442
  process.exit(0);
1432
1443
  }
1433
1444
  async function handleTrust() {
@@ -1954,10 +1965,12 @@ ${colors_default.bold("Usage:")}
1954
1965
  }
1955
1966
  const cert = fs3.readFileSync(certs.certPath);
1956
1967
  const key = fs3.readFileSync(certs.keyPath);
1968
+ const ca = fs3.readFileSync(certs.caPath);
1957
1969
  tlsOptions = {
1958
1970
  cert,
1959
1971
  key,
1960
- SNICallback: createSNICallback(stateDir, cert, key, tld)
1972
+ ca,
1973
+ SNICallback: createSNICallback(stateDir, cert, key, tld, ca)
1961
1974
  };
1962
1975
  }
1963
1976
  }
package/dist/index.d.ts CHANGED
@@ -26,6 +26,8 @@ interface ProxyServerOptions {
26
26
  tls?: {
27
27
  cert: Buffer;
28
28
  key: Buffer;
29
+ /** CA certificate to include in the chain so clients can verify the leaf. */
30
+ ca?: Buffer;
29
31
  /** SNI callback for per-hostname certificate selection. */
30
32
  SNICallback?: (servername: string, cb: (err: Error | null, ctx?: node_tls.SecureContext) => void) => void;
31
33
  };
@@ -65,8 +67,8 @@ interface RouteMapping extends RouteInfo {
65
67
  pid: number;
66
68
  }
67
69
  /**
68
- * Thrown when a route is already registered by a live process and --force
69
- * was not specified.
70
+ * Thrown when a route is already registered by a live process and --force was
71
+ * not specified. With --force, the existing process is killed instead.
70
72
  */
71
73
  declare class RouteConflictError extends Error {
72
74
  readonly hostname: string;
@@ -106,7 +108,13 @@ declare class RouteStore {
106
108
  */
107
109
  loadRoutes(persistCleanup?: boolean): RouteMapping[];
108
110
  private saveRoutes;
109
- addRoute(hostname: string, port: number, pid: number, force?: boolean): void;
111
+ /**
112
+ * Register a route. When `force` is true and the hostname is already claimed
113
+ * by another live process, that process is sent SIGTERM before the route is
114
+ * replaced. Returns the PID of the killed process (if any) so the caller can
115
+ * log it.
116
+ */
117
+ addRoute(hostname: string, port: number, pid: number, force?: boolean): number | undefined;
110
118
  removeRoute(hostname: string): void;
111
119
  }
112
120
 
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  parseHostname,
21
21
  removeBlock,
22
22
  syncHostsFile
23
- } from "./chunk-63BOQNMU.js";
23
+ } from "./chunk-D3LR3J7L.js";
24
24
  export {
25
25
  DIR_MODE,
26
26
  FILE_MODE,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portless",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "Replace port numbers with stable, named .localhost URLs. For humans and agents.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",