@corenel/sidecar 0.1.0 → 0.1.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.
- package/dist/cli.js +319 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
|
|
2
3
|
|
|
3
4
|
// src/cli.ts
|
|
4
5
|
import { resolve as resolve2 } from "node:path";
|
|
@@ -76,16 +77,19 @@ var SidecarServer = class {
|
|
|
76
77
|
}
|
|
77
78
|
onHello(m) {
|
|
78
79
|
if (m.protocol !== SIDECAR_PROTOCOL) {
|
|
80
|
+
this.opts.log?.(`denied: unsupported protocol ${m.protocol}`);
|
|
79
81
|
this.ch.send({ t: "denied", reason: `protocol ${m.protocol} unsupported` });
|
|
80
82
|
return;
|
|
81
83
|
}
|
|
82
84
|
if (!safeEqual(m.token ?? "", this.opts.token)) {
|
|
85
|
+
this.opts.log?.("denied: invalid token");
|
|
83
86
|
this.ch.send({ t: "denied", reason: "invalid token" });
|
|
84
87
|
return;
|
|
85
88
|
}
|
|
86
89
|
this.authed = true;
|
|
90
|
+
this.opts.log?.(`paired \u2014 ${this.tools.size} ${this.tools.size === 1 ? "tool" : "tools"} advertised`);
|
|
87
91
|
const tools = [...this.tools.values()].map(({ run: _run, ...decl }) => decl);
|
|
88
|
-
this.ch.send({ t: "welcome", tools, features: ["http-proxy"] });
|
|
92
|
+
this.ch.send({ t: "welcome", tools, features: ["http-proxy"], ...this.opts.info ? { info: this.opts.info } : {} });
|
|
89
93
|
}
|
|
90
94
|
/** Proxy a host HTTP request server-side. Restricted to LOOPBACK targets — the
|
|
91
95
|
* sidecar must never be usable as an SSRF pivot to internal/metadata hosts. The
|
|
@@ -122,13 +126,16 @@ var SidecarServer = class {
|
|
|
122
126
|
async onInvoke(m) {
|
|
123
127
|
const tool = this.tools.get(m.name);
|
|
124
128
|
if (!tool) {
|
|
129
|
+
this.opts.log?.(`invoke ${m.name}: unknown tool`);
|
|
125
130
|
this.ch.send({ t: "invoke-result", id: m.id, ok: false, error: `unknown tool ${m.name}` });
|
|
126
131
|
return;
|
|
127
132
|
}
|
|
133
|
+
this.opts.log?.(`invoke ${m.name}`);
|
|
128
134
|
try {
|
|
129
135
|
const result = await tool.run(m.args, this.makeContext());
|
|
130
136
|
this.ch.send({ t: "invoke-result", id: m.id, ok: true, result });
|
|
131
137
|
} catch (e) {
|
|
138
|
+
this.opts.log?.(`invoke ${m.name} failed: ${errMessage(e)}`);
|
|
132
139
|
this.ch.send({ t: "invoke-result", id: m.id, ok: false, error: errMessage(e) });
|
|
133
140
|
}
|
|
134
141
|
}
|
|
@@ -228,16 +235,42 @@ function wsChannel(ws) {
|
|
|
228
235
|
};
|
|
229
236
|
}
|
|
230
237
|
|
|
238
|
+
// src/lanOrigin.ts
|
|
239
|
+
function isPrivateLanOrigin(origin) {
|
|
240
|
+
let host;
|
|
241
|
+
try {
|
|
242
|
+
host = new URL(origin).hostname;
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
const h = host.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
|
|
247
|
+
if (h === "localhost" || h.endsWith(".localhost")) return true;
|
|
248
|
+
if (h.includes(":")) return h === "::1" || /^f[cd]/.test(h) || /^fe[89ab]/.test(h);
|
|
249
|
+
const m = h.match(/^(\d{1,3})\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/);
|
|
250
|
+
if (!m) return false;
|
|
251
|
+
const a = Number(m[1]);
|
|
252
|
+
const b = Number(m[2]);
|
|
253
|
+
return a === 127 || a === 10 || a === 192 && b === 168 || a === 172 && b >= 16 && b <= 31 || a === 169 && b === 254;
|
|
254
|
+
}
|
|
255
|
+
|
|
231
256
|
// src/server.ts
|
|
232
257
|
function startSidecar(opts) {
|
|
233
258
|
const host = opts.host ?? "127.0.0.1";
|
|
234
259
|
const allowed = new Set(opts.allowedOrigins ?? []);
|
|
235
|
-
const verifyClient = (
|
|
236
|
-
|
|
237
|
-
|
|
260
|
+
const verifyClient = (info) => {
|
|
261
|
+
const origin = info.origin;
|
|
262
|
+
const ip = info.req.socket?.remoteAddress || "?";
|
|
263
|
+
const lan = !!origin && !!opts.allowLan && isPrivateLanOrigin(origin);
|
|
264
|
+
const ok = !origin || allowed.has(origin) || lan;
|
|
265
|
+
opts.log?.(ok ? `upgrade allowed \u2014 origin ${origin || "(native client)"}${lan ? " (LAN)" : ""} (${ip})` : `upgrade REFUSED \u2014 origin ${origin} not in allowlist [${[...allowed].join(", ") || "(none)"}]${opts.allowLan ? "" : " (pass --allow-lan to accept any LAN origin)"} (${ip})`);
|
|
266
|
+
return ok;
|
|
238
267
|
};
|
|
239
|
-
const onConnection = (ws,
|
|
240
|
-
|
|
268
|
+
const onConnection = (ws, req) => {
|
|
269
|
+
const origin = req.headers.origin || "(native client)";
|
|
270
|
+
const ip = req.socket.remoteAddress || "?";
|
|
271
|
+
opts.log?.(`client connected \u2014 ${origin} (${ip})`);
|
|
272
|
+
ws.on("close", () => opts.log?.(`client disconnected \u2014 ${origin}`));
|
|
273
|
+
new SidecarServer({ channel: wsChannel(ws), token: opts.token, tools: opts.tools, info: opts.info, log: opts.log });
|
|
241
274
|
};
|
|
242
275
|
if (opts.tls) {
|
|
243
276
|
const httpsServer = createHttpsServer({ cert: opts.tls.cert, key: opts.tls.key });
|
|
@@ -502,9 +535,252 @@ async function loadOrCreateSelfSigned(extraHosts = []) {
|
|
|
502
535
|
return { cert: pems.cert, key: pems.private, sans };
|
|
503
536
|
}
|
|
504
537
|
|
|
538
|
+
// src/tunnel/registry.ts
|
|
539
|
+
var providers = /* @__PURE__ */ new Map();
|
|
540
|
+
function registerTunnel(p) {
|
|
541
|
+
providers.set(p.id, p);
|
|
542
|
+
}
|
|
543
|
+
function getTunnel(id) {
|
|
544
|
+
const direct = providers.get(id);
|
|
545
|
+
if (direct) return direct;
|
|
546
|
+
for (const p of providers.values()) if (p.aliases?.includes(id)) return p;
|
|
547
|
+
return void 0;
|
|
548
|
+
}
|
|
549
|
+
function tunnelIds() {
|
|
550
|
+
return [...providers.keys()];
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// src/tunnel/zrok.ts
|
|
554
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
555
|
+
function extractShareUrl(s) {
|
|
556
|
+
const full = s.match(/https?:\/\/[A-Za-z0-9.-]+(?::\d+)?(?:\/[^\s"'\\]*)?/);
|
|
557
|
+
if (full) return full[0];
|
|
558
|
+
const ep = s.match(/endpoints:(?:\\[nrt]|\s)*([A-Za-z0-9][A-Za-z0-9.-]*\.[A-Za-z]{2,}(?:\/[^\s"'\\]*)?)/);
|
|
559
|
+
return ep ? `https://${ep[1]}` : null;
|
|
560
|
+
}
|
|
561
|
+
var START_TIMEOUT_MS = 25e3;
|
|
562
|
+
function shareArgs(port, name) {
|
|
563
|
+
const a = ["share", "public", "--headless", "--backend-mode", "proxy"];
|
|
564
|
+
if (name) a.push("-n", `public:${name}`);
|
|
565
|
+
a.push(`http://localhost:${port}`);
|
|
566
|
+
return a;
|
|
567
|
+
}
|
|
568
|
+
var tail = (s, n = 6) => s.trim().split("\n").slice(-n).join("\n");
|
|
569
|
+
function zrokBins() {
|
|
570
|
+
const env = process.env.ZROK_BIN?.trim();
|
|
571
|
+
return env ? [env] : ["zrok", "zrok2"];
|
|
572
|
+
}
|
|
573
|
+
function stopChild(child) {
|
|
574
|
+
return new Promise((res) => {
|
|
575
|
+
if (child.exitCode !== null || child.signalCode) {
|
|
576
|
+
res();
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const hard = setTimeout(() => {
|
|
580
|
+
try {
|
|
581
|
+
child.kill("SIGKILL");
|
|
582
|
+
} catch {
|
|
583
|
+
}
|
|
584
|
+
res();
|
|
585
|
+
}, 2e3);
|
|
586
|
+
child.once("exit", () => {
|
|
587
|
+
clearTimeout(hard);
|
|
588
|
+
res();
|
|
589
|
+
});
|
|
590
|
+
try {
|
|
591
|
+
child.kill("SIGTERM");
|
|
592
|
+
} catch {
|
|
593
|
+
clearTimeout(hard);
|
|
594
|
+
res();
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
function reserveName(bin, name) {
|
|
599
|
+
return new Promise((res, rej) => {
|
|
600
|
+
const child = spawn2(bin, ["create", "name", name], { stdio: ["ignore", "ignore", "ignore"] });
|
|
601
|
+
let settled = false;
|
|
602
|
+
const done = (fn) => {
|
|
603
|
+
if (settled) return;
|
|
604
|
+
settled = true;
|
|
605
|
+
clearTimeout(t);
|
|
606
|
+
fn();
|
|
607
|
+
};
|
|
608
|
+
child.on("error", (e) => done(() => e?.code === "ENOENT" ? rej(e) : res()));
|
|
609
|
+
child.on("exit", () => done(() => res()));
|
|
610
|
+
const t = setTimeout(() => done(() => {
|
|
611
|
+
try {
|
|
612
|
+
child.kill();
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
res();
|
|
616
|
+
}), 15e3);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
async function startWith(bin, port, name) {
|
|
620
|
+
if (name) await reserveName(bin, name);
|
|
621
|
+
return new Promise((resolveHandle, reject) => {
|
|
622
|
+
const child = spawn2(bin, shareArgs(port, name), { stdio: ["ignore", "pipe", "pipe"] });
|
|
623
|
+
let out = "";
|
|
624
|
+
let settled = false;
|
|
625
|
+
const finish = (fn) => {
|
|
626
|
+
if (settled) return;
|
|
627
|
+
settled = true;
|
|
628
|
+
clearTimeout(timer);
|
|
629
|
+
fn();
|
|
630
|
+
};
|
|
631
|
+
const onData = (b) => {
|
|
632
|
+
out += b.toString();
|
|
633
|
+
const url = extractShareUrl(out);
|
|
634
|
+
if (url) finish(() => resolveHandle({ url, stop: () => stopChild(child) }));
|
|
635
|
+
};
|
|
636
|
+
child.stdout?.on("data", onData);
|
|
637
|
+
child.stderr?.on("data", onData);
|
|
638
|
+
child.on("error", (e) => finish(() => reject(e)));
|
|
639
|
+
child.on("exit", (code) => finish(() => reject(new Error(
|
|
640
|
+
`${bin} exited (code ${code}) before a share URL appeared \u2014 is the environment enabled? Run \`${bin} enable <token>\` once.
|
|
641
|
+
${tail(out)}`
|
|
642
|
+
))));
|
|
643
|
+
const timer = setTimeout(() => finish(() => {
|
|
644
|
+
void stopChild(child);
|
|
645
|
+
reject(new Error(`${bin} produced no share URL within ${START_TIMEOUT_MS / 1e3}s.
|
|
646
|
+
${tail(out)}`));
|
|
647
|
+
}), START_TIMEOUT_MS);
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
var zrokProvider = {
|
|
651
|
+
id: "zrok",
|
|
652
|
+
description: "zrok public share (needs the zrok CLI [`zrok`/`zrok2`] installed + `enable <token>` run once \u2014 https://zrok.io)",
|
|
653
|
+
start({ port, name }) {
|
|
654
|
+
const bins = zrokBins();
|
|
655
|
+
const tryAt = (i) => {
|
|
656
|
+
if (i >= bins.length) {
|
|
657
|
+
return Promise.reject(new Error(
|
|
658
|
+
`zrok not found (tried: ${bins.join(", ")}). Install the zrok CLI (https://docs.zrok.io) and run its \`enable <token>\` once, or set ZROK_BIN to the binary name/path.`
|
|
659
|
+
));
|
|
660
|
+
}
|
|
661
|
+
return startWith(bins[i], port, name).catch((e) => {
|
|
662
|
+
if (e?.code === "ENOENT") return tryAt(i + 1);
|
|
663
|
+
throw e;
|
|
664
|
+
});
|
|
665
|
+
};
|
|
666
|
+
return tryAt(0);
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
// src/tunnel/cloudflared.ts
|
|
671
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
672
|
+
function extractTunnelUrl(s) {
|
|
673
|
+
const m = s.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i);
|
|
674
|
+
return m ? m[0] : null;
|
|
675
|
+
}
|
|
676
|
+
var START_TIMEOUT_MS2 = 25e3;
|
|
677
|
+
function tunnelArgs(port, host = "localhost") {
|
|
678
|
+
return ["tunnel", "--no-autoupdate", "--url", `http://${host}:${port}`];
|
|
679
|
+
}
|
|
680
|
+
var tail2 = (s, n = 6) => s.trim().split("\n").slice(-n).join("\n");
|
|
681
|
+
function cloudflaredBins() {
|
|
682
|
+
const env = process.env.CLOUDFLARED_BIN?.trim();
|
|
683
|
+
return env ? [env] : ["cloudflared"];
|
|
684
|
+
}
|
|
685
|
+
function stopChild2(child) {
|
|
686
|
+
return new Promise((res) => {
|
|
687
|
+
if (child.exitCode !== null || child.signalCode) {
|
|
688
|
+
res();
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const hard = setTimeout(() => {
|
|
692
|
+
try {
|
|
693
|
+
child.kill("SIGKILL");
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
696
|
+
res();
|
|
697
|
+
}, 2e3);
|
|
698
|
+
child.once("exit", () => {
|
|
699
|
+
clearTimeout(hard);
|
|
700
|
+
res();
|
|
701
|
+
});
|
|
702
|
+
try {
|
|
703
|
+
child.kill("SIGTERM");
|
|
704
|
+
} catch {
|
|
705
|
+
clearTimeout(hard);
|
|
706
|
+
res();
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
function startWith2(bin, port, host, log) {
|
|
711
|
+
return new Promise((resolveHandle, reject) => {
|
|
712
|
+
const child = spawn3(bin, tunnelArgs(port, host), { stdio: ["ignore", "pipe", "pipe"] });
|
|
713
|
+
let out = "";
|
|
714
|
+
let settled = false;
|
|
715
|
+
let tailBuf = "";
|
|
716
|
+
const finish = (fn) => {
|
|
717
|
+
if (settled) return;
|
|
718
|
+
settled = true;
|
|
719
|
+
clearTimeout(timer);
|
|
720
|
+
fn();
|
|
721
|
+
};
|
|
722
|
+
const onData = (b) => {
|
|
723
|
+
const text = b.toString();
|
|
724
|
+
if (!settled) {
|
|
725
|
+
out += text;
|
|
726
|
+
const url = extractTunnelUrl(out);
|
|
727
|
+
if (url) finish(() => resolveHandle({ url, stop: () => stopChild2(child) }));
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
tailBuf += text;
|
|
731
|
+
let nl;
|
|
732
|
+
while ((nl = tailBuf.indexOf("\n")) >= 0) {
|
|
733
|
+
const line = tailBuf.slice(0, nl).replace(/\r$/, "").trim();
|
|
734
|
+
tailBuf = tailBuf.slice(nl + 1);
|
|
735
|
+
if (line) log?.(`cloudflared: ${line}`);
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
child.stdout?.on("data", onData);
|
|
739
|
+
child.stderr?.on("data", onData);
|
|
740
|
+
child.on("error", (e) => finish(() => reject(e)));
|
|
741
|
+
child.on("exit", (code) => {
|
|
742
|
+
if (settled) {
|
|
743
|
+
log?.(`cloudflared exited (code ${code}) \u2014 the tunnel is down`);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
finish(() => reject(new Error(`${bin} exited (code ${code}) before a tunnel URL appeared.
|
|
747
|
+
${tail2(out)}`)));
|
|
748
|
+
});
|
|
749
|
+
const timer = setTimeout(() => finish(() => {
|
|
750
|
+
void stopChild2(child);
|
|
751
|
+
reject(new Error(`${bin} produced no tunnel URL within ${START_TIMEOUT_MS2 / 1e3}s.
|
|
752
|
+
${tail2(out)}`));
|
|
753
|
+
}), START_TIMEOUT_MS2);
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
var cloudflaredProvider = {
|
|
757
|
+
id: "cloudflared",
|
|
758
|
+
aliases: ["cloudflare"],
|
|
759
|
+
description: "Cloudflare quick tunnel (needs the `cloudflared` CLI installed \u2014 https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/ \u2014 no account, no interstitial; URL is ephemeral)",
|
|
760
|
+
start({ port, host, log }) {
|
|
761
|
+
const bins = cloudflaredBins();
|
|
762
|
+
const tryAt = (i) => {
|
|
763
|
+
if (i >= bins.length) {
|
|
764
|
+
return Promise.reject(new Error(
|
|
765
|
+
`cloudflared not found (tried: ${bins.join(", ")}). Install the cloudflared CLI (https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/), or set CLOUDFLARED_BIN to the binary name/path.`
|
|
766
|
+
));
|
|
767
|
+
}
|
|
768
|
+
return startWith2(bins[i], port, host || "localhost", log).catch((e) => {
|
|
769
|
+
if (e?.code === "ENOENT") return tryAt(i + 1);
|
|
770
|
+
throw e;
|
|
771
|
+
});
|
|
772
|
+
};
|
|
773
|
+
return tryAt(0);
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
// src/tunnel/index.ts
|
|
778
|
+
registerTunnel(cloudflaredProvider);
|
|
779
|
+
registerTunnel(zrokProvider);
|
|
780
|
+
|
|
505
781
|
// src/cli.ts
|
|
506
782
|
function parseArgs(argv) {
|
|
507
|
-
const out = { port: 4858, host: "127.0.0.1", root: process.cwd(), allowOrigins: [], allowShell: false, selfSigned: false };
|
|
783
|
+
const out = { port: 4858, host: "127.0.0.1", root: process.cwd(), allowOrigins: [], allowShell: false, allowLan: false, selfSigned: false };
|
|
508
784
|
for (let i = 0; i < argv.length; i++) {
|
|
509
785
|
const a = argv[i];
|
|
510
786
|
if (a === "--port") out.port = Number(argv[++i]);
|
|
@@ -512,9 +788,12 @@ function parseArgs(argv) {
|
|
|
512
788
|
else if (a === "--root") out.root = resolve2(argv[++i]);
|
|
513
789
|
else if (a === "--allow-origin") out.allowOrigins.push(argv[++i]);
|
|
514
790
|
else if (a === "--allow-shell") out.allowShell = true;
|
|
791
|
+
else if (a === "--allow-lan") out.allowLan = true;
|
|
515
792
|
else if (a === "--tls-cert") out.tlsCert = resolve2(argv[++i]);
|
|
516
793
|
else if (a === "--tls-key") out.tlsKey = resolve2(argv[++i]);
|
|
517
794
|
else if (a === "--self-signed") out.selfSigned = true;
|
|
795
|
+
else if (a === "--tunnel") out.tunnel = argv[++i];
|
|
796
|
+
else if (a === "--tunnel-name") out.tunnelName = argv[++i];
|
|
518
797
|
}
|
|
519
798
|
return out;
|
|
520
799
|
}
|
|
@@ -534,15 +813,41 @@ async function main() {
|
|
|
534
813
|
tls = { cert: ss.cert, key: ss.key };
|
|
535
814
|
selfSigned = true;
|
|
536
815
|
}
|
|
816
|
+
const tunnelProvider = args.tunnel ? getTunnel(args.tunnel) : void 0;
|
|
817
|
+
if (args.tunnel && !tunnelProvider) {
|
|
818
|
+
throw new Error(`unknown --tunnel "${args.tunnel}". Available: ${tunnelIds().join(", ") || "(none)"}`);
|
|
819
|
+
}
|
|
820
|
+
if (tunnelProvider) {
|
|
821
|
+
args.host = "127.0.0.1";
|
|
822
|
+
tls = void 0;
|
|
823
|
+
selfSigned = false;
|
|
824
|
+
}
|
|
537
825
|
const allowedOrigins = [.../* @__PURE__ */ new Set([...DEFAULT_ALLOWED_ORIGINS, ...args.allowOrigins])];
|
|
826
|
+
const slog = (line) => {
|
|
827
|
+
process.stdout.write(` [${(/* @__PURE__ */ new Date()).toTimeString().slice(0, 8)}] ${line}
|
|
828
|
+
`);
|
|
829
|
+
};
|
|
538
830
|
const handle = await startSidecar({
|
|
539
831
|
port: args.port,
|
|
540
832
|
host: args.host,
|
|
541
833
|
token,
|
|
542
834
|
tools: [...nodeFsTools(args.root), ...args.allowShell ? [nodeShellTool(args.root)] : []],
|
|
543
835
|
allowedOrigins,
|
|
544
|
-
tls
|
|
836
|
+
tls,
|
|
837
|
+
// Advertised in the handshake so the host can show the working directory when
|
|
838
|
+
// browsing this sidecar as a file source.
|
|
839
|
+
info: { root: args.root, platform: process.platform },
|
|
840
|
+
log: slog,
|
|
841
|
+
allowLan: args.allowLan
|
|
545
842
|
});
|
|
843
|
+
let tunnel = null;
|
|
844
|
+
let tunnelWss = null;
|
|
845
|
+
if (tunnelProvider) {
|
|
846
|
+
process.stdout.write(`starting ${tunnelProvider.id} tunnel${args.tunnelName ? ` (reserved name "${args.tunnelName}")` : ""}\u2026
|
|
847
|
+
`);
|
|
848
|
+
tunnel = await tunnelProvider.start({ port: handle.port, host: handle.host, name: args.tunnelName, log: slog });
|
|
849
|
+
tunnelWss = tunnel.url.replace(/^http/, "ws");
|
|
850
|
+
}
|
|
546
851
|
const scheme = handle.secure ? "wss" : "ws";
|
|
547
852
|
const exposed = handle.host !== "127.0.0.1" && handle.host !== "localhost";
|
|
548
853
|
const connectHosts = handle.host === "0.0.0.0" ? lanIPv4s().length ? lanIPv4s() : ["127.0.0.1"] : [handle.host];
|
|
@@ -553,8 +858,8 @@ async function main() {
|
|
|
553
858
|
` root: ${args.root}`,
|
|
554
859
|
` tools: fs_read/write/list${args.allowShell ? " + shell_exec" : " (shell off; pass --allow-shell to enable)"}`,
|
|
555
860
|
` token: ${token} (also at ${tokenPath()})`,
|
|
556
|
-
` connect at: ${connectUrls.join(" ")}`,
|
|
557
|
-
` origins: ${allowedOrigins.join(", ")}${args.allowOrigins.length ? "" : " (canonical defaults; pass --allow-origin to add dev/self-hosted)"}`,
|
|
861
|
+
tunnelWss ? ` connect at: ${tunnelWss} (public ${args.tunnel} tunnel${args.tunnelName ? ", stable" : ""} \u2014 pair from any device with this URL + the token)` : ` connect at: ${connectUrls.join(" ")}`,
|
|
862
|
+
` origins: ${allowedOrigins.join(", ")}${args.allowLan ? " + any private-LAN origin (--allow-lan)" : args.allowOrigins.length ? "" : " (canonical defaults; pass --allow-origin to add dev/self-hosted)"}`,
|
|
558
863
|
selfSigned ? ` cert: self-signed (in ~/.corenel) \u2014 trust it once: open ${connectUrls.map((u) => u.replace(/^wss/, "https")).join(" or ")} in this browser and accept the warning, then pair.` : null,
|
|
559
864
|
exposed ? ` WARNING: bound to ${handle.host} (not loopback) \u2014 reachable off-box. The pairing token + Origin allowlist are the only protection; keep the token secret.` : null,
|
|
560
865
|
!handle.secure && exposed ? ` NOTE: a browser on an https page cannot connect to ws:// \u2014 serve wss:// with --self-signed (or --tls-cert/--tls-key) for LAN pairing.` : null,
|
|
@@ -562,7 +867,10 @@ async function main() {
|
|
|
562
867
|
].filter((l) => l !== null).join("\n")
|
|
563
868
|
);
|
|
564
869
|
const shutdown = () => {
|
|
565
|
-
|
|
870
|
+
const closeServer = () => handle.close().then(() => process.exit(0));
|
|
871
|
+
if (tunnel) void tunnel.stop().catch(() => {
|
|
872
|
+
}).then(closeServer);
|
|
873
|
+
else void closeServer();
|
|
566
874
|
};
|
|
567
875
|
process.on("SIGINT", shutdown);
|
|
568
876
|
process.on("SIGTERM", shutdown);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@corenel/sidecar",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"description": "Prompd sidecar — a local capability server. Run it next to Prompd (prompd.app) so the browser app can reach your machine: local LLMs (Ollama/LM Studio/vLLM) without CORS, your filesystem, and (opt-in) shell.",
|