exposeme 0.1.0 → 0.1.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.
- package/README.md +1 -1
- package/dist/{chunk-VZEPY6PL.js → chunk-G7DC5EY2.js} +281 -45
- package/dist/chunk-G7DC5EY2.js.map +1 -0
- package/dist/cli.js +35 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +27 -8
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-VZEPY6PL.js.map +0 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ npx exposeme 3000
|
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
You get a 6-digit code and a shareable link. The person you share it with opens
|
|
10
|
-
the link (or enters the code at [
|
|
10
|
+
the link (or enters the code at [exposeme.anishroy.com](https://exposeme.anishroy.com)),
|
|
11
11
|
and your `localhost:3000` streams to their browser over a direct WebRTC data
|
|
12
12
|
channel — no relay servers touching your traffic.
|
|
13
13
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import net from "net";
|
|
4
5
|
import nodeDataChannel2 from "node-datachannel";
|
|
5
6
|
import { config as loadDotenv } from "dotenv";
|
|
6
|
-
import
|
|
7
|
+
import kleur2 from "kleur";
|
|
7
8
|
|
|
8
9
|
// ../signaling/dist/index.js
|
|
9
10
|
import { initializeApp, deleteApp } from "firebase/app";
|
|
@@ -305,6 +306,21 @@ function connectHost(signaling, opts = {}) {
|
|
|
305
306
|
finishOnOpen(pc, dc, opts, resolve, reject);
|
|
306
307
|
});
|
|
307
308
|
}
|
|
309
|
+
function wireAbort(signal, pc, isSettled, reject) {
|
|
310
|
+
if (!signal) return;
|
|
311
|
+
const onAbort = () => {
|
|
312
|
+
if (isSettled()) return;
|
|
313
|
+
try {
|
|
314
|
+
pc.close();
|
|
315
|
+
} catch {
|
|
316
|
+
}
|
|
317
|
+
reject(
|
|
318
|
+
signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason ?? "Aborted"))
|
|
319
|
+
);
|
|
320
|
+
};
|
|
321
|
+
if (signal.aborted) onAbort();
|
|
322
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
323
|
+
}
|
|
308
324
|
function connectViewer(signaling, opts = {}) {
|
|
309
325
|
return new Promise((resolve, reject) => {
|
|
310
326
|
const pc = new PeerConnection("exposeme-viewer", {
|
|
@@ -389,6 +405,16 @@ function finishOnOpen(pc, dc, opts, resolve, reject) {
|
|
|
389
405
|
}
|
|
390
406
|
}, opts.timeoutMs ?? DEFAULT_TIMEOUT);
|
|
391
407
|
timer?.unref?.();
|
|
408
|
+
wireAbort(
|
|
409
|
+
opts.signal,
|
|
410
|
+
pc,
|
|
411
|
+
() => settled,
|
|
412
|
+
(err) => {
|
|
413
|
+
settled = true;
|
|
414
|
+
if (timer) clearTimeout(timer);
|
|
415
|
+
reject(err);
|
|
416
|
+
}
|
|
417
|
+
);
|
|
392
418
|
if (dc.isOpen()) {
|
|
393
419
|
settled = true;
|
|
394
420
|
if (timer) clearTimeout(timer);
|
|
@@ -487,7 +513,7 @@ function splitResponseHeaders(rawHeaders) {
|
|
|
487
513
|
|
|
488
514
|
// src/tunnel.ts
|
|
489
515
|
var BUFFER_HIGH_WATER = 1024 * 1024;
|
|
490
|
-
function serveTunnel(dc, port, jar) {
|
|
516
|
+
function serveTunnel(dc, port, jar, onRequest) {
|
|
491
517
|
const stats = { requestCount: 0 };
|
|
492
518
|
const inflight = /* @__PURE__ */ new Map();
|
|
493
519
|
const lowSignal = new BufferedAmountSignal(dc);
|
|
@@ -527,6 +553,7 @@ function serveTunnel(dc, port, jar) {
|
|
|
527
553
|
}
|
|
528
554
|
}
|
|
529
555
|
async function complete(id, req) {
|
|
556
|
+
const start = Date.now();
|
|
530
557
|
try {
|
|
531
558
|
const cookieHeader = jar.header(req.url);
|
|
532
559
|
const res = await proxyRequest(
|
|
@@ -541,6 +568,13 @@ function serveTunnel(dc, port, jar) {
|
|
|
541
568
|
);
|
|
542
569
|
jar.store(res.setCookies);
|
|
543
570
|
stats.requestCount++;
|
|
571
|
+
onRequest?.({
|
|
572
|
+
method: req.method,
|
|
573
|
+
url: req.url,
|
|
574
|
+
status: res.status,
|
|
575
|
+
bytes: res.body.byteLength,
|
|
576
|
+
durationMs: Date.now() - start
|
|
577
|
+
});
|
|
544
578
|
await send({
|
|
545
579
|
t: "res_head",
|
|
546
580
|
id,
|
|
@@ -553,11 +587,14 @@ function serveTunnel(dc, port, jar) {
|
|
|
553
587
|
}
|
|
554
588
|
await send({ t: "res_end", id });
|
|
555
589
|
} catch (err) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
590
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
591
|
+
onRequest?.({
|
|
592
|
+
method: req.method,
|
|
593
|
+
url: req.url,
|
|
594
|
+
durationMs: Date.now() - start,
|
|
595
|
+
error: message
|
|
560
596
|
});
|
|
597
|
+
await send({ t: "res_err", id, message });
|
|
561
598
|
}
|
|
562
599
|
}
|
|
563
600
|
async function send(msg) {
|
|
@@ -589,6 +626,107 @@ function bufferToString(buf) {
|
|
|
589
626
|
return buf.toString("utf-8");
|
|
590
627
|
}
|
|
591
628
|
|
|
629
|
+
// src/ui.ts
|
|
630
|
+
import kleur from "kleur";
|
|
631
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
632
|
+
var SPINNER_INTERVAL_MS = 120;
|
|
633
|
+
var StatusUI = class {
|
|
634
|
+
constructor(out = process.stdout) {
|
|
635
|
+
this.out = out;
|
|
636
|
+
this.tty = out.isTTY === true;
|
|
637
|
+
}
|
|
638
|
+
out;
|
|
639
|
+
tty;
|
|
640
|
+
spinnerTimer;
|
|
641
|
+
statusVisible = false;
|
|
642
|
+
lastStatic = "";
|
|
643
|
+
frame = 0;
|
|
644
|
+
/** Static status (connected/disconnected). */
|
|
645
|
+
status(text) {
|
|
646
|
+
this.stopSpinner();
|
|
647
|
+
if (!this.tty) {
|
|
648
|
+
if (text !== this.lastStatic) {
|
|
649
|
+
this.lastStatic = text;
|
|
650
|
+
this.out.write(` status ${text}
|
|
651
|
+
`);
|
|
652
|
+
}
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
this.render(` status ${text}`);
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Animated waiting status. `text()` is re-evaluated every tick so it can
|
|
659
|
+
* embed a live countdown. On non-TTY streams prints a single static line.
|
|
660
|
+
*/
|
|
661
|
+
spin(text) {
|
|
662
|
+
this.stopSpinner();
|
|
663
|
+
if (!this.tty) {
|
|
664
|
+
const line = `${kleur.yellow("\u25CF")} ${text()}`;
|
|
665
|
+
if (line !== this.lastStatic) {
|
|
666
|
+
this.lastStatic = line;
|
|
667
|
+
this.out.write(` status ${line}
|
|
668
|
+
`);
|
|
669
|
+
}
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const tick = () => {
|
|
673
|
+
const glyph = SPINNER_FRAMES[this.frame++ % SPINNER_FRAMES.length] ?? "";
|
|
674
|
+
this.render(` status ${kleur.yellow(glyph)} ${text()}`);
|
|
675
|
+
};
|
|
676
|
+
tick();
|
|
677
|
+
this.spinnerTimer = setInterval(tick, SPINNER_INTERVAL_MS);
|
|
678
|
+
this.spinnerTimer.unref?.();
|
|
679
|
+
}
|
|
680
|
+
/** Print a line above the status line. */
|
|
681
|
+
log(line) {
|
|
682
|
+
if (!this.tty || !this.statusVisible) {
|
|
683
|
+
this.out.write(line + "\n");
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
this.out.write("\r\x1B[2K" + line + "\n");
|
|
687
|
+
this.statusVisible = false;
|
|
688
|
+
if (!this.spinnerTimer) this.render(this.current);
|
|
689
|
+
}
|
|
690
|
+
/** Finish the in-place line so subsequent output starts on a fresh row. */
|
|
691
|
+
close() {
|
|
692
|
+
this.stopSpinner();
|
|
693
|
+
if (this.tty && this.statusVisible) {
|
|
694
|
+
this.out.write("\n");
|
|
695
|
+
this.statusVisible = false;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
current = "";
|
|
699
|
+
render(line) {
|
|
700
|
+
this.current = line;
|
|
701
|
+
this.out.write("\r\x1B[2K" + line);
|
|
702
|
+
this.statusVisible = true;
|
|
703
|
+
}
|
|
704
|
+
stopSpinner() {
|
|
705
|
+
if (this.spinnerTimer) {
|
|
706
|
+
clearInterval(this.spinnerTimer);
|
|
707
|
+
this.spinnerTimer = void 0;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
function formatBytes(n) {
|
|
712
|
+
if (n < 1024) return `${n} B`;
|
|
713
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} kB`;
|
|
714
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
715
|
+
}
|
|
716
|
+
function formatRemaining(ms) {
|
|
717
|
+
if (ms <= 0) return "now";
|
|
718
|
+
const s = Math.round(ms / 1e3);
|
|
719
|
+
if (s < 90) return `${s}s`;
|
|
720
|
+
return `${Math.round(s / 60)}m`;
|
|
721
|
+
}
|
|
722
|
+
function colorStatus(status) {
|
|
723
|
+
const text = String(status);
|
|
724
|
+
if (status < 300) return kleur.green(text);
|
|
725
|
+
if (status < 400) return kleur.cyan(text);
|
|
726
|
+
if (status < 500) return kleur.yellow(text);
|
|
727
|
+
return kleur.red(text);
|
|
728
|
+
}
|
|
729
|
+
|
|
592
730
|
// src/index.ts
|
|
593
731
|
var DEFAULT_API_URL = "https://expose-api.anishroy.com";
|
|
594
732
|
var DEFAULT_FIREBASE_CONFIG = {
|
|
@@ -598,24 +736,23 @@ var DEFAULT_FIREBASE_CONFIG = {
|
|
|
598
736
|
projectId: "exposeme-230a6",
|
|
599
737
|
appId: "1:242575918594:web:8a62140d71bc63d49d891b"
|
|
600
738
|
};
|
|
739
|
+
var EXPIRY_MARGIN_MS = 6e4;
|
|
740
|
+
var MAX_CONSECUTIVE_FAILURES = 5;
|
|
601
741
|
function readCliConfig(env) {
|
|
602
742
|
return { apiUrl: env.EXPOSEME_API_URL ?? DEFAULT_API_URL };
|
|
603
743
|
}
|
|
604
744
|
function parseArgs(argv) {
|
|
745
|
+
if (argv.includes("-h") || argv.includes("--help")) return { kind: "help" };
|
|
746
|
+
if (argv.includes("-v") || argv.includes("--version")) return { kind: "version" };
|
|
605
747
|
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
606
748
|
const portStr = positional[0];
|
|
607
|
-
if (!portStr) throw new Error("Usage: npx exposeme <port>");
|
|
749
|
+
if (!portStr) throw new Error("Usage: npx exposeme <port> (see --help)");
|
|
608
750
|
const port = Number(portStr);
|
|
609
751
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
610
752
|
throw new Error(`Invalid port: ${portStr}`);
|
|
611
753
|
}
|
|
612
|
-
return { port };
|
|
754
|
+
return { kind: "run", port };
|
|
613
755
|
}
|
|
614
|
-
var label = {
|
|
615
|
-
waiting: kleur.yellow("\u25CF waiting"),
|
|
616
|
-
connected: kleur.green("\u25CF connected"),
|
|
617
|
-
disconnected: kleur.red("\u25CF disconnected")
|
|
618
|
-
};
|
|
619
756
|
async function run(opts) {
|
|
620
757
|
const env = opts.env ?? process.env;
|
|
621
758
|
loadDotenv();
|
|
@@ -624,49 +761,148 @@ async function run(opts) {
|
|
|
624
761
|
const firebaseConfig = firebasePrefix ? firebaseConfigFromEnv(env, firebasePrefix) : DEFAULT_FIREBASE_CONFIG;
|
|
625
762
|
const api = new WorkerApi(apiUrl);
|
|
626
763
|
const jar = new CookieJar();
|
|
627
|
-
|
|
764
|
+
const ui = new StatusUI();
|
|
765
|
+
ui.log(kleur2.bold("\n ExposeMe") + kleur2.dim(` \u2192 tunneling localhost:${opts.port}
|
|
628
766
|
`));
|
|
629
|
-
|
|
767
|
+
if (!await isPortListening(opts.port)) {
|
|
768
|
+
ui.log(
|
|
769
|
+
` ${kleur2.yellow("warn")} nothing is listening on localhost:${opts.port} yet \u2014 ` + kleur2.dim("viewers will see errors until your server is up") + "\n"
|
|
770
|
+
);
|
|
771
|
+
}
|
|
630
772
|
const client = createSignalingClient(firebaseConfig);
|
|
631
|
-
|
|
632
|
-
printCode(
|
|
633
|
-
|
|
773
|
+
let session = await api.createSession();
|
|
774
|
+
printCode(ui, session);
|
|
775
|
+
let shuttingDown = false;
|
|
776
|
+
let abort = new AbortController();
|
|
777
|
+
let signaling = null;
|
|
778
|
+
let handle;
|
|
779
|
+
const shutdown = () => {
|
|
780
|
+
if (shuttingDown) return;
|
|
781
|
+
shuttingDown = true;
|
|
782
|
+
abort.abort(named("Shutdown"));
|
|
783
|
+
ui.close();
|
|
784
|
+
console.log(kleur2.dim("\n releasing code and cleaning up\u2026"));
|
|
785
|
+
const force = setTimeout(() => process.exit(0), 3e3);
|
|
786
|
+
force.unref?.();
|
|
787
|
+
void (async () => {
|
|
788
|
+
try {
|
|
789
|
+
handle?.close();
|
|
790
|
+
} catch {
|
|
791
|
+
}
|
|
792
|
+
await signaling?.cleanup().catch(() => {
|
|
793
|
+
});
|
|
794
|
+
await api.releaseSession(session.code, session.sessionId).catch(() => {
|
|
795
|
+
});
|
|
796
|
+
await client.destroy().catch(() => {
|
|
797
|
+
});
|
|
798
|
+
nodeDataChannel2.cleanup();
|
|
799
|
+
process.exit(0);
|
|
800
|
+
})();
|
|
801
|
+
};
|
|
802
|
+
process.on("SIGINT", shutdown);
|
|
803
|
+
process.on("SIGTERM", shutdown);
|
|
804
|
+
let consecutiveFailures = 0;
|
|
805
|
+
for (; ; ) {
|
|
806
|
+
if (Date.now() > session.expiresAt - EXPIRY_MARGIN_MS) {
|
|
807
|
+
await api.releaseSession(session.code, session.sessionId).catch(() => {
|
|
808
|
+
});
|
|
809
|
+
session = await api.createSession();
|
|
810
|
+
ui.log(` ${kleur2.dim("code expired \u2014 minted a fresh one")}
|
|
634
811
|
`);
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
812
|
+
printCode(ui, session);
|
|
813
|
+
}
|
|
814
|
+
signaling = client.session(session.code);
|
|
815
|
+
abort = new AbortController();
|
|
816
|
+
const expiresAt = session.expiresAt;
|
|
817
|
+
const expiryTimer = setTimeout(
|
|
818
|
+
() => abort.abort(named("CodeExpired")),
|
|
819
|
+
Math.max(0, expiresAt - EXPIRY_MARGIN_MS / 2 - Date.now())
|
|
820
|
+
);
|
|
821
|
+
expiryTimer.unref?.();
|
|
822
|
+
ui.spin(
|
|
823
|
+
() => `waiting for a viewer ${kleur2.dim(
|
|
824
|
+
`\xB7 code expires in ${formatRemaining(expiresAt - Date.now())}`
|
|
825
|
+
)}`
|
|
826
|
+
);
|
|
827
|
+
try {
|
|
828
|
+
handle = await connectHost(signaling, { timeoutMs: 0, signal: abort.signal });
|
|
829
|
+
consecutiveFailures = 0;
|
|
830
|
+
} catch (err) {
|
|
831
|
+
clearTimeout(expiryTimer);
|
|
832
|
+
await signaling.cleanup().catch(() => {
|
|
833
|
+
});
|
|
834
|
+
signaling = null;
|
|
835
|
+
if (shuttingDown) return neverResolve();
|
|
836
|
+
if (err.name === "CodeExpired") continue;
|
|
837
|
+
consecutiveFailures++;
|
|
838
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) throw err;
|
|
839
|
+
ui.log(
|
|
840
|
+
` ${kleur2.yellow("warn")} handshake failed (${err.message}) \u2014 retrying`
|
|
841
|
+
);
|
|
842
|
+
await delay2(1e3);
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
clearTimeout(expiryTimer);
|
|
846
|
+
ui.status(kleur2.green("\u25CF connected"));
|
|
847
|
+
serveTunnel(handle.dc, opts.port, jar, (log) => ui.log(formatRequest(log)));
|
|
848
|
+
await channelClosed(handle);
|
|
849
|
+
if (shuttingDown) return neverResolve();
|
|
850
|
+
ui.log(` ${kleur2.red("\u25CF")} ${kleur2.dim("viewer disconnected \u2014 waiting for the next one")}`);
|
|
639
851
|
try {
|
|
640
|
-
handle
|
|
852
|
+
handle.close();
|
|
641
853
|
} catch {
|
|
642
854
|
}
|
|
855
|
+
handle = void 0;
|
|
643
856
|
await signaling.cleanup().catch(() => {
|
|
644
857
|
});
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
};
|
|
650
|
-
|
|
651
|
-
console.log(kleur.dim("\n releasing code and cleaning up\u2026"));
|
|
652
|
-
void cleanup().then(() => process.exit(0));
|
|
653
|
-
});
|
|
654
|
-
let handle;
|
|
655
|
-
handle = await connectHost(signaling);
|
|
656
|
-
console.log(` status ${label.connected}
|
|
657
|
-
`);
|
|
658
|
-
serveTunnel(handle.dc, opts.port, jar);
|
|
659
|
-
handle.dc.onClosed(() => {
|
|
660
|
-
console.log(` status ${label.disconnected}
|
|
858
|
+
signaling = null;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
function printCode(ui, session) {
|
|
862
|
+
ui.log(` code ${kleur2.bold().cyan(session.code)}`);
|
|
863
|
+
ui.log(` share ${kleur2.underline(session.viewerUrl)}
|
|
661
864
|
`);
|
|
865
|
+
}
|
|
866
|
+
function formatRequest(log) {
|
|
867
|
+
const method = log.method.padEnd(4);
|
|
868
|
+
if (log.error !== void 0) {
|
|
869
|
+
return ` ${kleur2.dim(method)} ${log.url} ${kleur2.red("ERR")} ${kleur2.dim(log.error)}`;
|
|
870
|
+
}
|
|
871
|
+
const size = log.bytes !== void 0 ? formatBytes(log.bytes) : "";
|
|
872
|
+
return ` ${kleur2.dim(method)} ${log.url} ${colorStatus(log.status ?? 0)}` + kleur2.dim(` \xB7 ${size} \xB7 ${log.durationMs} ms`);
|
|
873
|
+
}
|
|
874
|
+
function channelClosed(handle) {
|
|
875
|
+
return new Promise((resolve) => {
|
|
876
|
+
handle.dc.onClosed(() => resolve());
|
|
877
|
+
handle.pc.onStateChange((state) => {
|
|
878
|
+
if (state === "disconnected" || state === "failed" || state === "closed") {
|
|
879
|
+
resolve();
|
|
880
|
+
}
|
|
881
|
+
});
|
|
662
882
|
});
|
|
663
|
-
|
|
883
|
+
}
|
|
884
|
+
function isPortListening(port) {
|
|
885
|
+
const probe = (host) => new Promise((resolve) => {
|
|
886
|
+
const sock = net.connect({ port, host });
|
|
887
|
+
const done = (ok) => {
|
|
888
|
+
sock.destroy();
|
|
889
|
+
resolve(ok);
|
|
890
|
+
};
|
|
891
|
+
sock.once("connect", () => done(true));
|
|
892
|
+
sock.once("error", () => done(false));
|
|
893
|
+
sock.setTimeout(1e3, () => done(false));
|
|
664
894
|
});
|
|
895
|
+
return probe("127.0.0.1").then((ok) => ok ? true : probe("::1"));
|
|
665
896
|
}
|
|
666
|
-
function
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
897
|
+
function named(name) {
|
|
898
|
+
const err = new Error(name);
|
|
899
|
+
err.name = name;
|
|
900
|
+
return err;
|
|
901
|
+
}
|
|
902
|
+
var delay2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
903
|
+
function neverResolve() {
|
|
904
|
+
return new Promise(() => {
|
|
905
|
+
});
|
|
670
906
|
}
|
|
671
907
|
|
|
672
908
|
export {
|
|
@@ -680,4 +916,4 @@ export {
|
|
|
680
916
|
parseArgs,
|
|
681
917
|
run
|
|
682
918
|
};
|
|
683
|
-
//# sourceMappingURL=chunk-
|
|
919
|
+
//# sourceMappingURL=chunk-G7DC5EY2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../../signaling/src/firebase.ts","../../signaling/src/config.ts","../../shared/src/constants.ts","../../shared/src/paths.ts","../../shared/src/protocol.ts","../../shared/src/code.ts","../src/api.ts","../src/cookies.ts","../src/peer.ts","../src/proxy.ts","../src/tunnel.ts","../src/ui.ts"],"sourcesContent":["import net from \"node:net\";\nimport nodeDataChannel from \"node-datachannel\";\nimport { config as loadDotenv } from \"dotenv\";\nimport kleur from \"kleur\";\nimport {\n createSignalingClient,\n firebaseConfigFromEnv,\n type FirebaseConfig,\n type SignalingSession,\n} from \"@exposeme/signaling\";\nimport type { CreateSessionResponse } from \"@exposeme/shared\";\nimport { WorkerApi } from \"./api.js\";\nimport { CookieJar } from \"./cookies.js\";\nimport { connectHost, type PeerConnectionHandle } from \"./peer.js\";\nimport { serveTunnel, type RequestLog } from \"./tunnel.js\";\nimport { StatusUI, colorStatus, formatBytes, formatRemaining } from \"./ui.js\";\n\nexport { WorkerApi } from \"./api.js\";\nexport { CookieJar, parseSetCookie } from \"./cookies.js\";\nexport { connectHost, connectViewer } from \"./peer.js\";\nexport { serveTunnel, type DataChannelLike, type RequestLog } from \"./tunnel.js\";\nexport { proxyRequest } from \"./proxy.js\";\n\nexport interface RunOptions {\n port: number;\n env?: NodeJS.ProcessEnv;\n}\n\ninterface CliConfig {\n apiUrl: string;\n}\n\n// Production defaults so the published CLI is zero-config; env vars override.\n// The Firebase web config is not secret — RTDB access is gated by security\n// rules + the 6-digit code, not by hiding these values.\nconst DEFAULT_API_URL = \"https://expose-api.anishroy.com\";\nconst DEFAULT_FIREBASE_CONFIG: FirebaseConfig = {\n apiKey: \"AIzaSyA2YgT7R1gNlJhtp3mW-Sx4BOMEJPDYJgA\",\n authDomain: \"exposeme-230a6.firebaseapp.com\",\n databaseURL: \"https://exposeme-230a6-default-rtdb.firebaseio.com\",\n projectId: \"exposeme-230a6\",\n appId: \"1:242575918594:web:8a62140d71bc63d49d891b\",\n};\n\n/** Re-mint the code when less than this much of its TTL remains. */\nconst EXPIRY_MARGIN_MS = 60_000;\n/** Give up after this many consecutive handshake failures without a connect. */\nconst MAX_CONSECUTIVE_FAILURES = 5;\n\nfunction readCliConfig(env: NodeJS.ProcessEnv): CliConfig {\n return { apiUrl: env.EXPOSEME_API_URL ?? DEFAULT_API_URL };\n}\n\nexport type ParsedArgs =\n | { kind: \"run\"; port: number }\n | { kind: \"help\" }\n | { kind: \"version\" };\n\n/** Parse `exposeme <port>` argv. */\nexport function parseArgs(argv: string[]): ParsedArgs {\n if (argv.includes(\"-h\") || argv.includes(\"--help\")) return { kind: \"help\" };\n if (argv.includes(\"-v\") || argv.includes(\"--version\")) return { kind: \"version\" };\n const positional = argv.filter((a) => !a.startsWith(\"-\"));\n const portStr = positional[0];\n if (!portStr) throw new Error(\"Usage: npx exposeme <port> (see --help)\");\n const port = Number(portStr);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new Error(`Invalid port: ${portStr}`);\n }\n return { kind: \"run\", port };\n}\n\n/**\n * Run the tunnel host. Mints a code and then serves viewers for as long as the\n * process lives: waits patiently (re-minting the code when it nears expiry),\n * serves a connected viewer, and re-arms for the next viewer after a\n * disconnect. SIGINT/SIGTERM release the code and clean up.\n */\nexport async function run(opts: RunOptions): Promise<void> {\n const env = opts.env ?? process.env;\n loadDotenv();\n const { apiUrl } = readCliConfig(env);\n // .env is shared with the viewer, which needs VITE_-prefixed vars; accept\n // either spelling so one file works for both, else fall back to prod config.\n const firebasePrefix = env.FIREBASE_API_KEY\n ? \"FIREBASE_\"\n : env.VITE_FIREBASE_API_KEY\n ? \"VITE_FIREBASE_\"\n : null;\n const firebaseConfig = firebasePrefix\n ? firebaseConfigFromEnv(env, firebasePrefix)\n : DEFAULT_FIREBASE_CONFIG;\n\n const api = new WorkerApi(apiUrl);\n const jar = new CookieJar();\n const ui = new StatusUI();\n\n ui.log(kleur.bold(\"\\n ExposeMe\") + kleur.dim(` → tunneling localhost:${opts.port}\\n`));\n\n if (!(await isPortListening(opts.port))) {\n ui.log(\n ` ${kleur.yellow(\"warn\")} nothing is listening on localhost:${opts.port} yet — ` +\n kleur.dim(\"viewers will see errors until your server is up\") + \"\\n\"\n );\n }\n\n const client = createSignalingClient(firebaseConfig);\n let session = await api.createSession();\n printCode(ui, session);\n\n let shuttingDown = false;\n let abort = new AbortController();\n let signaling: SignalingSession | null = null;\n let handle: PeerConnectionHandle | undefined;\n\n const shutdown = (): void => {\n if (shuttingDown) return;\n shuttingDown = true;\n abort.abort(named(\"Shutdown\"));\n ui.close();\n console.log(kleur.dim(\"\\n releasing code and cleaning up…\"));\n // Never hang on cleanup — the code's TTL is the backstop.\n const force = setTimeout(() => process.exit(0), 3000);\n force.unref?.();\n void (async () => {\n try {\n handle?.close();\n } catch {\n /* noop */\n }\n await signaling?.cleanup().catch(() => {});\n await api.releaseSession(session.code, session.sessionId).catch(() => {});\n await client.destroy().catch(() => {});\n nodeDataChannel.cleanup();\n process.exit(0);\n })();\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n let consecutiveFailures = 0;\n\n for (;;) {\n // Re-mint when the code is expired or close to it, so a printed code is\n // always worth sharing.\n if (Date.now() > session.expiresAt - EXPIRY_MARGIN_MS) {\n await api.releaseSession(session.code, session.sessionId).catch(() => {});\n session = await api.createSession();\n ui.log(` ${kleur.dim(\"code expired — minted a fresh one\")}\\n`);\n printCode(ui, session);\n }\n\n signaling = client.session(session.code);\n abort = new AbortController();\n const expiresAt = session.expiresAt;\n const expiryTimer = setTimeout(\n () => abort.abort(named(\"CodeExpired\")),\n Math.max(0, expiresAt - EXPIRY_MARGIN_MS / 2 - Date.now())\n );\n expiryTimer.unref?.();\n\n ui.spin(\n () =>\n `waiting for a viewer ${kleur.dim(\n `· code expires in ${formatRemaining(expiresAt - Date.now())}`\n )}`\n );\n\n try {\n handle = await connectHost(signaling, { timeoutMs: 0, signal: abort.signal });\n consecutiveFailures = 0;\n } catch (err) {\n clearTimeout(expiryTimer);\n await signaling.cleanup().catch(() => {});\n signaling = null;\n if (shuttingDown) return neverResolve();\n if ((err as Error).name === \"CodeExpired\") continue; // loop re-mints\n consecutiveFailures++;\n if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) throw err;\n ui.log(\n ` ${kleur.yellow(\"warn\")} handshake failed (${(err as Error).message}) — retrying`\n );\n await delay(1000);\n continue;\n }\n clearTimeout(expiryTimer);\n\n ui.status(kleur.green(\"● connected\"));\n serveTunnel(handle.dc, opts.port, jar, (log) => ui.log(formatRequest(log)));\n\n // Wait until this viewer goes away, then re-arm for the next one.\n await channelClosed(handle);\n if (shuttingDown) return neverResolve();\n\n ui.log(` ${kleur.red(\"●\")} ${kleur.dim(\"viewer disconnected — waiting for the next one\")}`);\n try {\n handle.close();\n } catch {\n /* noop */\n }\n handle = undefined;\n await signaling.cleanup().catch(() => {});\n signaling = null;\n }\n}\n\nfunction printCode(ui: StatusUI, session: CreateSessionResponse): void {\n ui.log(` code ${kleur.bold().cyan(session.code)}`);\n ui.log(` share ${kleur.underline(session.viewerUrl)}\\n`);\n}\n\nfunction formatRequest(log: RequestLog): string {\n const method = log.method.padEnd(4);\n if (log.error !== undefined) {\n return ` ${kleur.dim(method)} ${log.url} ${kleur.red(\"ERR\")} ${kleur.dim(log.error)}`;\n }\n const size = log.bytes !== undefined ? formatBytes(log.bytes) : \"\";\n return (\n ` ${kleur.dim(method)} ${log.url} ${colorStatus(log.status ?? 0)}` +\n kleur.dim(` · ${size} · ${log.durationMs} ms`)\n );\n}\n\n/** Resolves when the viewer's data channel closes or the peer dies. */\nfunction channelClosed(handle: PeerConnectionHandle): Promise<void> {\n return new Promise((resolve) => {\n handle.dc.onClosed(() => resolve());\n handle.pc.onStateChange((state) => {\n if (state === \"disconnected\" || state === \"failed\" || state === \"closed\") {\n resolve();\n }\n });\n });\n}\n\n/** Probe localhost:<port> (v4 and v6) for a listener. */\nfunction isPortListening(port: number): Promise<boolean> {\n const probe = (host: string): Promise<boolean> =>\n new Promise((resolve) => {\n const sock = net.connect({ port, host });\n const done = (ok: boolean): void => {\n sock.destroy();\n resolve(ok);\n };\n sock.once(\"connect\", () => done(true));\n sock.once(\"error\", () => done(false));\n sock.setTimeout(1000, () => done(false));\n });\n return probe(\"127.0.0.1\").then((ok) => (ok ? true : probe(\"::1\")));\n}\n\nfunction named(name: string): Error {\n const err = new Error(name);\n err.name = name;\n return err;\n}\n\nconst delay = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\n\n/** Park forever; shutdown() owns process exit. */\nfunction neverResolve(): Promise<void> {\n return new Promise<void>(() => {});\n}\n","import { initializeApp, deleteApp, type FirebaseApp } from \"firebase/app\";\nimport {\n getDatabase,\n ref,\n set,\n get,\n push,\n onValue,\n onChildAdded,\n remove,\n off,\n type Database,\n} from \"firebase/database\";\nimport {\n hostCandidatesPath,\n hostOfferPath,\n sessionPath,\n viewerAnswerPath,\n viewerCandidatesPath,\n type IceCandidateInit,\n type SessionDescriptionInit,\n} from \"@exposeme/shared\";\nimport type { FirebaseConfig, SignalingSession, Unsubscribe } from \"./types.js\";\n\nlet appCounter = 0;\n\n/** Initialize a Firebase app + RTDB handle from config. */\nexport function createSignalingClient(config: FirebaseConfig): {\n db: Database;\n app: FirebaseApp;\n /** Bind a signaling session to a code. */\n session(code: string): SignalingSession;\n /** Tear down the underlying Firebase app. */\n destroy(): Promise<void>;\n} {\n // Unique name lets a single process (e.g. the loopback harness) hold more\n // than one app without collision.\n const app = initializeApp(config, `exposeme-${appCounter++}`);\n const db = getDatabase(app);\n\n return {\n db,\n app,\n session: (code) => new FirebaseSignalingSession(db, code),\n destroy: () => deleteApp(app),\n };\n}\n\nclass FirebaseSignalingSession implements SignalingSession {\n private unsubs: Unsubscribe[] = [];\n\n constructor(\n private readonly db: Database,\n public readonly code: string\n ) {}\n\n // ── Host role ────────────────────────────────────────────────────────────\n\n async writeOffer(offer: SessionDescriptionInit): Promise<void> {\n await set(ref(this.db, hostOfferPath(this.code)), offer);\n }\n\n async pushHostCandidate(candidate: IceCandidateInit): Promise<void> {\n await push(ref(this.db, hostCandidatesPath(this.code)), candidate);\n }\n\n onAnswer(cb: (answer: SessionDescriptionInit) => void): Unsubscribe {\n const r = ref(this.db, viewerAnswerPath(this.code));\n const handler = onValue(r, (snap) => {\n const val = snap.val() as SessionDescriptionInit | null;\n if (val) cb(val);\n });\n const unsub = () => off(r, \"value\", handler);\n this.unsubs.push(unsub);\n return unsub;\n }\n\n onViewerCandidate(cb: (candidate: IceCandidateInit) => void): Unsubscribe {\n return this.subscribeCandidates(viewerCandidatesPath(this.code), cb);\n }\n\n // ── Viewer role ──────────────────────────────────────────────────────────\n\n async readOffer(): Promise<SessionDescriptionInit | null> {\n const snap = await get(ref(this.db, hostOfferPath(this.code)));\n return (snap.val() as SessionDescriptionInit | null) ?? null;\n }\n\n async writeAnswer(answer: SessionDescriptionInit): Promise<void> {\n await set(ref(this.db, viewerAnswerPath(this.code)), answer);\n }\n\n async pushViewerCandidate(candidate: IceCandidateInit): Promise<void> {\n await push(ref(this.db, viewerCandidatesPath(this.code)), candidate);\n }\n\n onHostCandidate(cb: (candidate: IceCandidateInit) => void): Unsubscribe {\n return this.subscribeCandidates(hostCandidatesPath(this.code), cb);\n }\n\n // ── Shared ───────────────────────────────────────────────────────────────\n\n private subscribeCandidates(\n path: string,\n cb: (candidate: IceCandidateInit) => void\n ): Unsubscribe {\n const r = ref(this.db, path);\n const handler = onChildAdded(r, (snap) => {\n const val = snap.val() as IceCandidateInit | null;\n if (val) cb(val);\n });\n const unsub = () => off(r, \"child_added\", handler);\n this.unsubs.push(unsub);\n return unsub;\n }\n\n async cleanup(): Promise<void> {\n for (const unsub of this.unsubs.splice(0)) unsub();\n await remove(ref(this.db, sessionPath(this.code)));\n }\n}\n","import type { FirebaseConfig } from \"./types.js\";\n\n/**\n * Build a FirebaseConfig from a record of env vars. Works for both the CLI\n * (process.env, FIREBASE_* keys) and the viewer (import.meta.env, VITE_FIREBASE_*\n * keys) — pass the appropriate source and prefix.\n */\nexport function firebaseConfigFromEnv(\n env: Record<string, string | undefined>,\n prefix = \"FIREBASE_\"\n): FirebaseConfig {\n const get = (k: string): string => {\n const v = env[`${prefix}${k}`];\n if (!v) {\n throw new Error(\n `Missing Firebase config env var ${prefix}${k}. See .env.example.`\n );\n }\n return v;\n };\n return {\n apiKey: get(\"API_KEY\"),\n authDomain: get(\"AUTH_DOMAIN\"),\n databaseURL: get(\"DATABASE_URL\"),\n projectId: get(\"PROJECT_ID\"),\n appId: get(\"APP_ID\"),\n };\n}\n","/** Number of digits in an ExposeMe session code. */\nexport const CODE_LENGTH = 6;\n\n/** Regex a valid code must match (exactly CODE_LENGTH digits). */\nexport const CODE_REGEX = new RegExp(`^\\\\d{${CODE_LENGTH}}$`);\n\n/** Default lifetime of a session code before it expires (ms). 30 minutes. */\nexport const CODE_TTL_MS = 30 * 60 * 1000;\n\n/**\n * Max size (bytes) of a single raw body chunk before base64 encoding.\n * Kept well under the ~16 KiB safe SCTP message size so the base64-expanded\n * envelope still fits comfortably inside one data-channel message.\n */\nexport const CHUNK_SIZE = 12 * 1024;\n\n/** Public STUN servers used for ICE. TURN is intentionally omitted for v1. */\nexport const STUN_SERVERS = [\n \"stun:stun.l.google.com:19302\",\n \"stun:stun1.l.google.com:19302\",\n];\n\n/** Label used for the single application data channel. */\nexport const DATA_CHANNEL_LABEL = \"exposeme\";\n","/**\n * RTDB path helpers. All signaling state for a session lives under\n * `/sessions/{code}`. The host writes under `host/`, the viewer under `viewer/`.\n */\n\nexport const sessionPath = (code: string) => `sessions/${code}`;\n\nexport const hostOfferPath = (code: string) => `sessions/${code}/host/offer`;\nexport const hostCandidatesPath = (code: string) =>\n `sessions/${code}/host/candidates`;\n\nexport const viewerAnswerPath = (code: string) =>\n `sessions/${code}/viewer/answer`;\nexport const viewerCandidatesPath = (code: string) =>\n `sessions/${code}/viewer/candidates`;\n","import { CHUNK_SIZE } from \"./constants.js\";\nimport type { HeaderList } from \"./types.js\";\n\n/**\n * Data-channel wire protocol. Every message is a JSON envelope with a\n * discriminant `t`. A single HTTP exchange is a stream of messages sharing an\n * `id`: a HEAD, zero or more CHUNKs, then an END (or an ERR).\n *\n * Bodies are sent as base64 of the raw bytes so binary survives intact.\n */\n\nexport interface ReqHead {\n t: \"req_head\";\n id: string;\n method: string;\n /** Path + query relative to the tunneled origin, e.g. `/assets/logo.png`. */\n url: string;\n headers: HeaderList;\n}\n\nexport interface ReqChunk {\n t: \"req_chunk\";\n id: string;\n seq: number;\n dataB64: string;\n}\n\nexport interface ReqEnd {\n t: \"req_end\";\n id: string;\n}\n\nexport interface ResHead {\n t: \"res_head\";\n id: string;\n status: number;\n statusText: string;\n headers: HeaderList;\n}\n\nexport interface ResChunk {\n t: \"res_chunk\";\n id: string;\n seq: number;\n dataB64: string;\n}\n\nexport interface ResEnd {\n t: \"res_end\";\n id: string;\n}\n\nexport interface ResErr {\n t: \"res_err\";\n id: string;\n message: string;\n}\n\nexport type TunnelMessage =\n | ReqHead\n | ReqChunk\n | ReqEnd\n | ResHead\n | ResChunk\n | ResEnd\n | ResErr;\n\nexport const encode = (msg: TunnelMessage): string => JSON.stringify(msg);\n\nexport const decode = (raw: string): TunnelMessage =>\n JSON.parse(raw) as TunnelMessage;\n\n/** base64 helpers that work in both Node and the browser. */\nexport const toBase64 = (bytes: Uint8Array): string => {\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(bytes).toString(\"base64\");\n }\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!);\n }\n return btoa(binary);\n};\n\nexport const fromBase64 = (b64: string): Uint8Array => {\n if (typeof Buffer !== \"undefined\") {\n return new Uint8Array(Buffer.from(b64, \"base64\"));\n }\n const binary = atob(b64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n};\n\n/**\n * Split a body into ordered chunk messages of at most CHUNK_SIZE raw bytes each.\n * An empty body yields no chunk messages.\n */\nexport function chunkBody(\n id: string,\n kind: \"req\" | \"res\",\n body: Uint8Array\n): Array<ReqChunk | ResChunk> {\n const t = kind === \"req\" ? \"req_chunk\" : \"res_chunk\";\n const chunks: Array<ReqChunk | ResChunk> = [];\n let seq = 0;\n for (let offset = 0; offset < body.length; offset += CHUNK_SIZE) {\n const slice = body.subarray(offset, offset + CHUNK_SIZE);\n chunks.push({ t, id, seq: seq++, dataB64: toBase64(slice) } as\n | ReqChunk\n | ResChunk);\n }\n return chunks;\n}\n\n/**\n * Accumulates ordered body chunks for a single id and concatenates them.\n * Reassembles by `seq` so out-of-order delivery is tolerated (data channels are\n * ordered by default, but this keeps the reassembler robust and testable).\n */\nexport class ChunkAssembler {\n private parts = new Map<number, Uint8Array>();\n\n push(seq: number, dataB64: string): void {\n this.parts.set(seq, fromBase64(dataB64));\n }\n\n concat(): Uint8Array {\n const ordered = [...this.parts.entries()]\n .sort((a, b) => a[0] - b[0])\n .map(([, v]) => v);\n const total = ordered.reduce((n, p) => n + p.length, 0);\n const out = new Uint8Array(total);\n let offset = 0;\n for (const part of ordered) {\n out.set(part, offset);\n offset += part.length;\n }\n return out;\n }\n}\n","import { CODE_LENGTH, CODE_REGEX } from \"./constants.js\";\n\n/**\n * Generate a random N-digit code as a zero-padded string.\n * `rand` returns a float in [0, 1); defaults to Math.random. The Worker passes\n * a crypto-backed source.\n */\nexport function generateCode(rand: () => number = Math.random): string {\n const max = 10 ** CODE_LENGTH;\n const n = Math.floor(rand() * max);\n return String(n).padStart(CODE_LENGTH, \"0\");\n}\n\nexport const isValidCode = (code: string): boolean => CODE_REGEX.test(code);\n","import type { CreateSessionResponse } from \"@exposeme/shared\";\n\n/** Thin client for the ExposeMe Worker API. */\nexport class WorkerApi {\n constructor(private readonly baseUrl: string) {\n this.baseUrl = baseUrl.replace(/\\/$/, \"\");\n }\n\n /** POST /api/sessions — mint a code + session. */\n async createSession(): Promise<CreateSessionResponse> {\n const res = await fetch(`${this.baseUrl}/api/sessions`, { method: \"POST\" });\n if (!res.ok) {\n throw new Error(\n `Failed to create session (${res.status}): ${await res.text()}`\n );\n }\n return (await res.json()) as CreateSessionResponse;\n }\n\n /** DELETE /api/sessions/:code — release the code early. Best-effort. */\n async releaseSession(code: string, sessionId: string): Promise<void> {\n const url = `${this.baseUrl}/api/sessions/${code}?sessionId=${encodeURIComponent(\n sessionId\n )}`;\n await fetch(url, { method: \"DELETE\" }).catch(() => {\n /* best-effort cleanup; TTL will expire it anyway */\n });\n }\n}\n","import type { HeaderList } from \"@exposeme/shared\";\n\ninterface StoredCookie {\n value: string;\n path: string;\n expires?: number; // epoch ms\n}\n\n/**\n * A deliberately simple host-side cookie jar. Because the viewer's Service\n * Worker cannot participate in normal cross-origin cookie handling, the host\n * owns cookies for the tunneled origin: it captures `Set-Cookie` from responses\n * and replays a `Cookie` header on matching outbound requests.\n *\n * v1 scope: single tunneled origin, name-keyed, honoring `Path` and `Expires`/\n * `Max-Age`. Domain and `Secure`/`SameSite` are ignored (localhost, one origin).\n */\nexport class CookieJar {\n private cookies = new Map<string, StoredCookie>();\n\n /** Record cookies from a response's `Set-Cookie` header values. */\n store(setCookieValues: string[], now = Date.now()): void {\n for (const raw of setCookieValues) {\n const parsed = parseSetCookie(raw);\n if (!parsed) continue;\n if (parsed.expires !== undefined && parsed.expires <= now) {\n this.cookies.delete(parsed.name); // deletion cookie\n continue;\n }\n this.cookies.set(parsed.name, {\n value: parsed.value,\n path: parsed.path,\n expires: parsed.expires,\n });\n }\n }\n\n /** Build a `Cookie` header value for a request path, or null if none apply. */\n header(requestPath: string, now = Date.now()): string | null {\n const pairs: string[] = [];\n for (const [name, c] of this.cookies) {\n if (c.expires !== undefined && c.expires <= now) {\n this.cookies.delete(name);\n continue;\n }\n if (pathMatches(requestPath, c.path)) pairs.push(`${name}=${c.value}`);\n }\n return pairs.length ? pairs.join(\"; \") : null;\n }\n\n /** Number of live cookies (for tests/introspection). */\n get size(): number {\n return this.cookies.size;\n }\n}\n\nfunction pathMatches(requestPath: string, cookiePath: string): boolean {\n if (cookiePath === \"/\" || cookiePath === \"\") return true;\n const reqPath = requestPath.split(\"?\")[0] ?? \"/\";\n if (reqPath === cookiePath) return true;\n return (\n reqPath.startsWith(cookiePath) &&\n (cookiePath.endsWith(\"/\") || reqPath[cookiePath.length] === \"/\")\n );\n}\n\ninterface ParsedCookie {\n name: string;\n value: string;\n path: string;\n expires?: number;\n}\n\n/** Parse one Set-Cookie header value. Returns null if malformed. */\nexport function parseSetCookie(raw: string): ParsedCookie | null {\n const parts = raw.split(\";\");\n const first = parts[0]?.trim() ?? \"\";\n const eq = first.indexOf(\"=\");\n if (eq <= 0) return null;\n\n const name = first.slice(0, eq).trim();\n const value = first.slice(eq + 1).trim();\n let path = \"/\";\n let expires: number | undefined;\n let maxAge: number | undefined;\n\n for (let i = 1; i < parts.length; i++) {\n const attr = parts[i]!.trim();\n const aEq = attr.indexOf(\"=\");\n const key = (aEq === -1 ? attr : attr.slice(0, aEq)).toLowerCase();\n const val = aEq === -1 ? \"\" : attr.slice(aEq + 1).trim();\n if (key === \"path\") path = val || \"/\";\n else if (key === \"expires\") {\n const t = Date.parse(val);\n if (!Number.isNaN(t)) expires = t;\n } else if (key === \"max-age\") {\n const n = Number(val);\n if (!Number.isNaN(n)) maxAge = n;\n }\n }\n\n // Max-Age takes precedence over Expires per RFC 6265, but we can't call\n // Date.now() deterministically here — encode as relative and resolve later.\n if (maxAge !== undefined) {\n expires = Date.now() + maxAge * 1000;\n }\n\n return { name, value, path, expires };\n}\n","import nodeDataChannel from \"node-datachannel\";\nimport {\n DATA_CHANNEL_LABEL,\n STUN_SERVERS,\n type IceCandidateInit,\n type SessionDescriptionInit,\n} from \"@exposeme/shared\";\nimport type { SignalingSession } from \"@exposeme/signaling\";\n\nconst { PeerConnection } = nodeDataChannel;\ntype PeerConnection = InstanceType<typeof PeerConnection>;\ntype DataChannel = ReturnType<PeerConnection[\"createDataChannel\"]>;\n\nexport interface PeerConnectionHandle {\n pc: PeerConnection;\n dc: DataChannel;\n close(): void;\n}\n\nexport interface ConnectOptions {\n iceServers?: string[];\n /** Milliseconds to wait for the data channel to open before rejecting. 0 disables the timeout. */\n timeoutMs?: number;\n /** Abort waiting (e.g. code expiry, shutdown): closes the peer and rejects. */\n signal?: AbortSignal;\n}\n\nconst DEFAULT_TIMEOUT = 30_000;\n\n/**\n * Host role: create the peer + data channel, publish the offer/ICE via\n * signaling, apply the viewer's answer/ICE, resolve once the channel opens.\n */\nexport function connectHost(\n signaling: SignalingSession,\n opts: ConnectOptions = {}\n): Promise<PeerConnectionHandle> {\n return new Promise((resolve, reject) => {\n const pc = new PeerConnection(\"exposeme-host\", {\n iceServers: opts.iceServers ?? STUN_SERVERS,\n });\n const pending = new PendingCandidates(pc);\n\n pc.onLocalDescription((sdp, type) => {\n void signaling.writeOffer({ type: type as \"offer\", sdp });\n });\n pc.onLocalCandidate((candidate, mid) => {\n void signaling.pushHostCandidate({ candidate, sdpMid: mid });\n });\n\n // onValue re-delivers the answer on viewer reloads / Firebase re-syncs;\n // applying it twice throws inside the Firebase callback and kills the\n // process, so accept only the first answer and swallow later ones.\n let answerApplied = false;\n signaling.onAnswer((answer) => {\n if (answerApplied) return;\n answerApplied = true;\n try {\n pc.setRemoteDescription(answer.sdp, answer.type);\n pending.flush();\n } catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)));\n }\n });\n signaling.onViewerCandidate((c) => pending.add(c));\n\n const dc = pc.createDataChannel(DATA_CHANNEL_LABEL);\n finishOnOpen(pc, dc, opts, resolve, reject);\n });\n}\n\n/** Wire an AbortSignal to tear down a pending peer and reject its promise. */\nfunction wireAbort(\n signal: AbortSignal | undefined,\n pc: PeerConnection,\n isSettled: () => boolean,\n reject: (e: Error) => void\n): void {\n if (!signal) return;\n const onAbort = (): void => {\n if (isSettled()) return;\n try {\n pc.close();\n } catch {\n /* noop */\n }\n reject(\n signal.reason instanceof Error\n ? signal.reason\n : new Error(String(signal.reason ?? \"Aborted\"))\n );\n };\n if (signal.aborted) onAbort();\n else signal.addEventListener(\"abort\", onAbort, { once: true });\n}\n\n/**\n * Viewer role (also used by the Node loopback test): wait for the host's\n * offer, answer it, exchange ICE, resolve with the inbound data channel.\n */\nexport function connectViewer(\n signaling: SignalingSession,\n opts: ConnectOptions = {}\n): Promise<PeerConnectionHandle> {\n return new Promise((resolve, reject) => {\n const pc = new PeerConnection(\"exposeme-viewer\", {\n iceServers: opts.iceServers ?? STUN_SERVERS,\n });\n const pending = new PendingCandidates(pc);\n let dc: DataChannel | undefined;\n\n pc.onLocalDescription((sdp, type) => {\n void signaling.writeAnswer({ type: type as \"answer\", sdp });\n });\n pc.onLocalCandidate((candidate, mid) => {\n void signaling.pushViewerCandidate({ candidate, sdpMid: mid });\n });\n\n pc.onDataChannel((incoming) => {\n dc = incoming;\n finishOnOpen(pc, incoming, opts, resolve, reject);\n });\n\n signaling.onHostCandidate((c) => pending.add(c));\n\n // Fetch the offer (may need a brief poll if the host writes it slightly\n // after the viewer starts listening).\n void pollForOffer(signaling)\n .then((offer) => {\n pc.setRemoteDescription(offer.sdp, offer.type); // triggers answer\n pending.flush();\n })\n .catch(reject);\n\n // Guard: if onDataChannel never fires we still time out via finishOnOpen\n // only after dc exists, so add a fallback timer here too.\n if (opts.timeoutMs !== 0) {\n const t = setTimeout(() => {\n if (!dc) reject(new Error(\"Timed out waiting for data channel\"));\n }, opts.timeoutMs ?? DEFAULT_TIMEOUT);\n t.unref?.();\n }\n });\n}\n\nasync function pollForOffer(\n signaling: SignalingSession,\n attempts = 40,\n intervalMs = 250\n): Promise<SessionDescriptionInit> {\n for (let i = 0; i < attempts; i++) {\n const offer = await signaling.readOffer();\n if (offer) return offer;\n await delay(intervalMs);\n }\n throw new Error(\"Host offer not found\");\n}\n\nconst delay = (ms: number) => new Promise((r) => setTimeout(r, ms));\n\n/** Buffers remote ICE candidates until the remote description is set. */\nclass PendingCandidates {\n private queue: IceCandidateInit[] = [];\n private ready = false;\n\n constructor(private readonly pc: PeerConnection) {}\n\n add(c: IceCandidateInit): void {\n if (this.ready) this.apply(c);\n else this.queue.push(c);\n }\n\n flush(): void {\n this.ready = true;\n for (const c of this.queue.splice(0)) this.apply(c);\n }\n\n private apply(c: IceCandidateInit): void {\n try {\n this.pc.addRemoteCandidate(c.candidate, c.sdpMid ?? \"0\");\n } catch {\n /* ignore malformed/duplicate candidates */\n }\n }\n}\n\nfunction finishOnOpen(\n pc: PeerConnection,\n dc: DataChannel,\n opts: ConnectOptions,\n resolve: (h: PeerConnectionHandle) => void,\n reject: (e: Error) => void\n): void {\n const handle: PeerConnectionHandle = {\n pc,\n dc,\n close() {\n try {\n dc.close();\n } catch {\n /* noop */\n }\n try {\n pc.close();\n } catch {\n /* noop */\n }\n },\n };\n\n let settled = false;\n const timer =\n opts.timeoutMs === 0\n ? undefined\n : setTimeout(() => {\n if (!settled) {\n settled = true;\n reject(new Error(\"Timed out waiting for data channel to open\"));\n }\n }, opts.timeoutMs ?? DEFAULT_TIMEOUT);\n timer?.unref?.();\n\n wireAbort(\n opts.signal,\n pc,\n () => settled,\n (err) => {\n settled = true;\n if (timer) clearTimeout(timer);\n reject(err);\n }\n );\n\n if (dc.isOpen()) {\n settled = true;\n if (timer) clearTimeout(timer);\n resolve(handle);\n return;\n }\n dc.onOpen(() => {\n if (settled) return;\n settled = true;\n if (timer) clearTimeout(timer);\n resolve(handle);\n });\n}\n","import http from \"node:http\";\nimport type { HeaderList } from \"@exposeme/shared\";\n\nexport interface ProxyRequest {\n method: string;\n /** Path + query relative to the tunneled origin. */\n url: string;\n headers: HeaderList;\n body: Uint8Array;\n}\n\nexport interface ProxyResponse {\n status: number;\n statusText: string;\n headers: HeaderList;\n body: Uint8Array;\n /** Set-Cookie values pulled out for the host cookie jar. */\n setCookies: string[];\n}\n\n/** Hop-by-hop headers that must not be forwarded in either direction. */\nconst HOP_BY_HOP = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n]);\n\n/**\n * Forward one request to `localhost:<port>` and collect the full response.\n * `cookieHeader`, if provided, overrides the request's Cookie header (the host\n * owns cookies). `accept-encoding` is stripped so the local server returns an\n * identity body the browser can consume from a Service-Worker-built Response.\n */\nexport function proxyRequest(\n port: number,\n req: ProxyRequest,\n cookieHeader: string | null\n): Promise<ProxyResponse> {\n return new Promise((resolve, reject) => {\n const outHeaders: Record<string, string | string[]> = {};\n for (const [k, v] of req.headers) {\n const lk = k.toLowerCase();\n if (HOP_BY_HOP.has(lk)) continue;\n if (lk === \"host\") continue; // rewritten below\n if (lk === \"accept-encoding\") continue; // force identity\n if (lk === \"cookie\") continue; // host jar owns cookies\n // Merge duplicate headers into an array.\n const existing = outHeaders[lk];\n if (existing === undefined) outHeaders[lk] = v;\n else if (Array.isArray(existing)) existing.push(v);\n else outHeaders[lk] = [existing, v];\n }\n outHeaders[\"host\"] = `localhost:${port}`;\n if (cookieHeader) outHeaders[\"cookie\"] = cookieHeader;\n\n // Dev servers may bind only one of IPv4/IPv6 localhost (e.g. Vite on ::1),\n // so try 127.0.0.1 first and fall back to ::1 on connection refusal.\n const attempt = (host: string, fallback?: string): void => {\n const request = http.request(\n {\n host,\n port,\n method: req.method,\n path: req.url,\n headers: outHeaders,\n },\n (res) => {\n const chunks: Buffer[] = [];\n res.on(\"data\", (c: Buffer) => chunks.push(c));\n res.on(\"end\", () => {\n const { headers, setCookies } = splitResponseHeaders(\n res.rawHeaders\n );\n resolve({\n status: res.statusCode ?? 502,\n statusText: res.statusMessage ?? \"\",\n headers,\n body: new Uint8Array(Buffer.concat(chunks)),\n setCookies,\n });\n });\n }\n );\n\n request.on(\"error\", (err: NodeJS.ErrnoException) => {\n if (fallback && err.code === \"ECONNREFUSED\") attempt(fallback);\n else reject(err);\n });\n if (req.body.length > 0) request.write(Buffer.from(req.body));\n request.end();\n };\n\n attempt(\"127.0.0.1\", \"::1\");\n });\n}\n\n/**\n * Convert Node's flat rawHeaders array into a HeaderList, pulling out Set-Cookie\n * (host-managed) and dropping hop-by-hop headers the browser shouldn't see.\n */\nfunction splitResponseHeaders(rawHeaders: string[]): {\n headers: HeaderList;\n setCookies: string[];\n} {\n const headers: HeaderList = [];\n const setCookies: string[] = [];\n for (let i = 0; i < rawHeaders.length; i += 2) {\n const name = rawHeaders[i]!;\n const value = rawHeaders[i + 1] ?? \"\";\n const lower = name.toLowerCase();\n if (lower === \"set-cookie\") {\n setCookies.push(value);\n continue; // not forwarded to the viewer\n }\n if (HOP_BY_HOP.has(lower)) continue;\n if (lower === \"content-length\") continue; // body length may change; SW recomputes\n headers.push([name, value]);\n }\n return { headers, setCookies };\n}\n","import {\n ChunkAssembler,\n chunkBody,\n decode,\n encode,\n type HeaderList,\n type TunnelMessage,\n} from \"@exposeme/shared\";\nimport { proxyRequest } from \"./proxy.js\";\nimport type { CookieJar } from \"./cookies.js\";\n\n/**\n * Minimal data-channel surface the tunnel needs. Matches node-datachannel's\n * DataChannel (and is trivially mockable in tests).\n */\nexport interface DataChannelLike {\n sendMessage(msg: string): boolean;\n onMessage(cb: (msg: string | Buffer | ArrayBuffer) => void): void;\n isOpen(): boolean;\n bufferedAmount?(): number;\n maxMessageSize?(): number;\n setBufferedAmountLowThreshold?(n: number): void;\n onBufferedAmountLow?(cb: () => void): void;\n}\n\n/** Pause sending when the channel's buffer grows beyond this many bytes. */\nconst BUFFER_HIGH_WATER = 1024 * 1024; // 1 MiB\n\ninterface InFlight {\n method: string;\n url: string;\n headers: HeaderList;\n body: ChunkAssembler;\n}\n\n/** Per-request summary passed to `serveTunnel`'s `onRequest` callback. */\nexport interface RequestLog {\n method: string;\n url: string;\n /** HTTP status of the proxied response; absent when the proxy itself failed. */\n status?: number;\n bytes?: number;\n durationMs: number;\n error?: string;\n}\n\n/**\n * Attach the host-side request handler to a data channel: reassemble incoming\n * requests, proxy them to `localhost:<port>`, and stream responses back.\n * Returns a stats object updated as requests complete.\n */\nexport function serveTunnel(\n dc: DataChannelLike,\n port: number,\n jar: CookieJar,\n onRequest?: (log: RequestLog) => void\n): { requestCount: number } {\n const stats = { requestCount: 0 };\n const inflight = new Map<string, InFlight>();\n const lowSignal = new BufferedAmountSignal(dc);\n\n dc.onMessage((raw) => {\n const text = typeof raw === \"string\" ? raw : bufferToString(raw);\n let msg: TunnelMessage;\n try {\n msg = decode(text);\n } catch {\n return; // ignore non-protocol frames\n }\n handleMessage(msg);\n });\n\n function handleMessage(msg: TunnelMessage): void {\n switch (msg.t) {\n case \"req_head\":\n inflight.set(msg.id, {\n method: msg.method,\n url: msg.url,\n headers: msg.headers,\n body: new ChunkAssembler(),\n });\n break;\n case \"req_chunk\": {\n inflight.get(msg.id)?.body.push(msg.seq, msg.dataB64);\n break;\n }\n case \"req_end\": {\n const req = inflight.get(msg.id);\n if (!req) return;\n inflight.delete(msg.id);\n void complete(msg.id, req);\n break;\n }\n default:\n break; // responses flow the other direction; ignore\n }\n }\n\n async function complete(id: string, req: InFlight): Promise<void> {\n const start = Date.now();\n try {\n const cookieHeader = jar.header(req.url);\n const res = await proxyRequest(\n port,\n {\n method: req.method,\n url: req.url,\n headers: req.headers,\n body: req.body.concat(),\n },\n cookieHeader\n );\n jar.store(res.setCookies);\n stats.requestCount++;\n onRequest?.({\n method: req.method,\n url: req.url,\n status: res.status,\n bytes: res.body.byteLength,\n durationMs: Date.now() - start,\n });\n\n await send({\n t: \"res_head\",\n id,\n status: res.status,\n statusText: res.statusText,\n headers: res.headers,\n });\n for (const chunk of chunkBody(id, \"res\", res.body)) {\n await send(chunk);\n }\n await send({ t: \"res_end\", id });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n onRequest?.({\n method: req.method,\n url: req.url,\n durationMs: Date.now() - start,\n error: message,\n });\n await send({ t: \"res_err\", id, message });\n }\n }\n\n async function send(msg: TunnelMessage): Promise<void> {\n await lowSignal.awaitDrain();\n if (dc.isOpen()) dc.sendMessage(encode(msg));\n }\n\n return stats;\n}\n\n/** Simple backpressure helper around a channel's buffered-amount signal. */\nclass BufferedAmountSignal {\n private waiters: Array<() => void> = [];\n\n constructor(private readonly dc: DataChannelLike) {\n if (dc.setBufferedAmountLowThreshold && dc.onBufferedAmountLow) {\n dc.setBufferedAmountLowThreshold(BUFFER_HIGH_WATER / 2);\n dc.onBufferedAmountLow(() => {\n for (const w of this.waiters.splice(0)) w();\n });\n }\n }\n\n awaitDrain(): Promise<void> {\n const buffered = this.dc.bufferedAmount?.() ?? 0;\n if (buffered < BUFFER_HIGH_WATER) return Promise.resolve();\n return new Promise((resolve) => this.waiters.push(resolve));\n }\n}\n\nfunction bufferToString(buf: Buffer | ArrayBuffer): string {\n if (buf instanceof ArrayBuffer) return Buffer.from(buf).toString(\"utf-8\");\n return buf.toString(\"utf-8\");\n}\n","import kleur from \"kleur\";\n\nconst SPINNER_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\nconst SPINNER_INTERVAL_MS = 120;\n\n/**\n * Terminal status line that updates in place on a TTY (with a spinner for\n * waiting states) and degrades to plain append-only lines when piped.\n * `log()` prints above the status line, keeping the status pinned at the\n * bottom.\n */\nexport class StatusUI {\n private readonly tty: boolean;\n private spinnerTimer: ReturnType<typeof setInterval> | undefined;\n private statusVisible = false;\n private lastStatic = \"\";\n private frame = 0;\n\n constructor(private readonly out: NodeJS.WriteStream = process.stdout) {\n this.tty = out.isTTY === true;\n }\n\n /** Static status (connected/disconnected). */\n status(text: string): void {\n this.stopSpinner();\n if (!this.tty) {\n if (text !== this.lastStatic) {\n this.lastStatic = text;\n this.out.write(` status ${text}\\n`);\n }\n return;\n }\n this.render(` status ${text}`);\n }\n\n /**\n * Animated waiting status. `text()` is re-evaluated every tick so it can\n * embed a live countdown. On non-TTY streams prints a single static line.\n */\n spin(text: () => string): void {\n this.stopSpinner();\n if (!this.tty) {\n const line = `${kleur.yellow(\"●\")} ${text()}`;\n if (line !== this.lastStatic) {\n this.lastStatic = line;\n this.out.write(` status ${line}\\n`);\n }\n return;\n }\n const tick = (): void => {\n const glyph = SPINNER_FRAMES[this.frame++ % SPINNER_FRAMES.length] ?? \"\";\n this.render(` status ${kleur.yellow(glyph)} ${text()}`);\n };\n tick();\n this.spinnerTimer = setInterval(tick, SPINNER_INTERVAL_MS);\n this.spinnerTimer.unref?.();\n }\n\n /** Print a line above the status line. */\n log(line: string): void {\n if (!this.tty || !this.statusVisible) {\n this.out.write(line + \"\\n\");\n return;\n }\n this.out.write(\"\\r\\x1b[2K\" + line + \"\\n\");\n this.statusVisible = false;\n // The status line is re-rendered by the active spinner tick, or now.\n if (!this.spinnerTimer) this.render(this.current);\n }\n\n /** Finish the in-place line so subsequent output starts on a fresh row. */\n close(): void {\n this.stopSpinner();\n if (this.tty && this.statusVisible) {\n this.out.write(\"\\n\");\n this.statusVisible = false;\n }\n }\n\n private current = \"\";\n\n private render(line: string): void {\n this.current = line;\n this.out.write(\"\\r\\x1b[2K\" + line);\n this.statusVisible = true;\n }\n\n private stopSpinner(): void {\n if (this.spinnerTimer) {\n clearInterval(this.spinnerTimer);\n this.spinnerTimer = undefined;\n }\n }\n}\n\n/** Human-readable byte count: 512 B, 34.2 kB, 1.8 MB. */\nexport function formatBytes(n: number): string {\n if (n < 1024) return `${n} B`;\n if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} kB`;\n return `${(n / (1024 * 1024)).toFixed(1)} MB`;\n}\n\n/** Compact remaining-time string: 29m, 45s. */\nexport function formatRemaining(ms: number): string {\n if (ms <= 0) return \"now\";\n const s = Math.round(ms / 1000);\n if (s < 90) return `${s}s`;\n return `${Math.round(s / 60)}m`;\n}\n\n/** Color an HTTP status code by class. */\nexport function colorStatus(status: number): string {\n const text = String(status);\n if (status < 300) return kleur.green(text);\n if (status < 400) return kleur.cyan(text);\n if (status < 500) return kleur.yellow(text);\n return kleur.red(text);\n}\n"],"mappings":"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","names":["nodeDataChannel","kleur","get","kleur","nodeDataChannel","delay"]}
|
package/dist/cli.js
CHANGED
|
@@ -2,19 +2,50 @@
|
|
|
2
2
|
import {
|
|
3
3
|
parseArgs,
|
|
4
4
|
run
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-G7DC5EY2.js";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
8
|
+
import { createRequire } from "module";
|
|
9
|
+
var USAGE = `
|
|
10
|
+
Usage
|
|
11
|
+
$ npx exposeme <port>
|
|
12
|
+
|
|
13
|
+
Example
|
|
14
|
+
$ npx exposeme 3000
|
|
15
|
+
|
|
16
|
+
Options
|
|
17
|
+
-h, --help Show this help
|
|
18
|
+
-v, --version Print the version
|
|
19
|
+
|
|
20
|
+
Docs
|
|
21
|
+
https://github.com/iamanishroy/exposeme
|
|
22
|
+
`;
|
|
23
|
+
function version() {
|
|
24
|
+
try {
|
|
25
|
+
const pkg = createRequire(import.meta.url)("../package.json");
|
|
26
|
+
return pkg.version;
|
|
27
|
+
} catch {
|
|
28
|
+
return "unknown";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
8
31
|
async function main() {
|
|
9
|
-
let
|
|
32
|
+
let args;
|
|
10
33
|
try {
|
|
11
|
-
|
|
34
|
+
args = parseArgs(process.argv.slice(2));
|
|
12
35
|
} catch (err) {
|
|
13
36
|
console.error(err.message);
|
|
14
37
|
process.exit(1);
|
|
15
38
|
}
|
|
39
|
+
if (args.kind === "help") {
|
|
40
|
+
console.log(USAGE);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (args.kind === "version") {
|
|
44
|
+
console.log(version());
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
16
47
|
try {
|
|
17
|
-
await run({ port });
|
|
48
|
+
await run({ port: args.port });
|
|
18
49
|
} catch (err) {
|
|
19
50
|
console.error(`
|
|
20
51
|
\u2716 ${err.message}
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { parseArgs, run } from \"./index.js\";\n\nasync function main(): Promise<void> {\n let
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\nimport { parseArgs, run, type ParsedArgs } from \"./index.js\";\n\nconst USAGE = `\n Usage\n $ npx exposeme <port>\n\n Example\n $ npx exposeme 3000\n\n Options\n -h, --help Show this help\n -v, --version Print the version\n\n Docs\n https://github.com/iamanishroy/exposeme\n`;\n\nfunction version(): string {\n try {\n const pkg = createRequire(import.meta.url)(\"../package.json\") as {\n version: string;\n };\n return pkg.version;\n } catch {\n return \"unknown\";\n }\n}\n\nasync function main(): Promise<void> {\n let args: ParsedArgs;\n try {\n args = parseArgs(process.argv.slice(2));\n } catch (err) {\n console.error((err as Error).message);\n process.exit(1);\n }\n\n if (args.kind === \"help\") {\n console.log(USAGE);\n return;\n }\n if (args.kind === \"version\") {\n console.log(version());\n return;\n }\n\n try {\n await run({ port: args.port });\n } catch (err) {\n console.error(`\\n ✖ ${(err as Error).message}\\n`);\n process.exit(1);\n }\n}\n\nvoid main();\n"],"mappings":";;;;;;;AAAA,SAAS,qBAAqB;AAG9B,IAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAed,SAAS,UAAkB;AACzB,MAAI;AACF,UAAM,MAAM,cAAc,YAAY,GAAG,EAAE,iBAAiB;AAG5D,WAAO,IAAI;AAAA,EACb,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,OAAsB;AACnC,MAAI;AACJ,MAAI;AACF,WAAO,UAAU,QAAQ,KAAK,MAAM,CAAC,CAAC;AAAA,EACxC,SAAS,KAAK;AACZ,YAAQ,MAAO,IAAc,OAAO;AACpC,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI,KAAK,SAAS,QAAQ;AACxB,YAAQ,IAAI,KAAK;AACjB;AAAA,EACF;AACA,MAAI,KAAK,SAAS,WAAW;AAC3B,YAAQ,IAAI,QAAQ,CAAC;AACrB;AAAA,EACF;AAEA,MAAI;AACF,UAAM,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC;AAAA,EAC/B,SAAS,KAAK;AACZ,YAAQ,MAAM;AAAA,WAAU,IAAc,OAAO;AAAA,CAAI;AACjD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,KAAK,KAAK;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -49,8 +49,10 @@ interface PeerConnectionHandle {
|
|
|
49
49
|
}
|
|
50
50
|
interface ConnectOptions {
|
|
51
51
|
iceServers?: string[];
|
|
52
|
-
/** Milliseconds to wait for the data channel to open before rejecting. */
|
|
52
|
+
/** Milliseconds to wait for the data channel to open before rejecting. 0 disables the timeout. */
|
|
53
53
|
timeoutMs?: number;
|
|
54
|
+
/** Abort waiting (e.g. code expiry, shutdown): closes the peer and rejects. */
|
|
55
|
+
signal?: AbortSignal;
|
|
54
56
|
}
|
|
55
57
|
/**
|
|
56
58
|
* Host role: create the peer + data channel, publish the offer/ICE via
|
|
@@ -76,12 +78,22 @@ interface DataChannelLike {
|
|
|
76
78
|
setBufferedAmountLowThreshold?(n: number): void;
|
|
77
79
|
onBufferedAmountLow?(cb: () => void): void;
|
|
78
80
|
}
|
|
81
|
+
/** Per-request summary passed to `serveTunnel`'s `onRequest` callback. */
|
|
82
|
+
interface RequestLog {
|
|
83
|
+
method: string;
|
|
84
|
+
url: string;
|
|
85
|
+
/** HTTP status of the proxied response; absent when the proxy itself failed. */
|
|
86
|
+
status?: number;
|
|
87
|
+
bytes?: number;
|
|
88
|
+
durationMs: number;
|
|
89
|
+
error?: string;
|
|
90
|
+
}
|
|
79
91
|
/**
|
|
80
92
|
* Attach the host-side request handler to a data channel: reassemble incoming
|
|
81
93
|
* requests, proxy them to `localhost:<port>`, and stream responses back.
|
|
82
94
|
* Returns a stats object updated as requests complete.
|
|
83
95
|
*/
|
|
84
|
-
declare function serveTunnel(dc: DataChannelLike, port: number, jar: CookieJar): {
|
|
96
|
+
declare function serveTunnel(dc: DataChannelLike, port: number, jar: CookieJar, onRequest?: (log: RequestLog) => void): {
|
|
85
97
|
requestCount: number;
|
|
86
98
|
};
|
|
87
99
|
|
|
@@ -112,15 +124,22 @@ interface RunOptions {
|
|
|
112
124
|
port: number;
|
|
113
125
|
env?: NodeJS.ProcessEnv;
|
|
114
126
|
}
|
|
115
|
-
|
|
116
|
-
|
|
127
|
+
type ParsedArgs = {
|
|
128
|
+
kind: "run";
|
|
117
129
|
port: number;
|
|
130
|
+
} | {
|
|
131
|
+
kind: "help";
|
|
132
|
+
} | {
|
|
133
|
+
kind: "version";
|
|
118
134
|
};
|
|
135
|
+
/** Parse `exposeme <port>` argv. */
|
|
136
|
+
declare function parseArgs(argv: string[]): ParsedArgs;
|
|
119
137
|
/**
|
|
120
|
-
* Run the tunnel host
|
|
121
|
-
*
|
|
122
|
-
*
|
|
138
|
+
* Run the tunnel host. Mints a code and then serves viewers for as long as the
|
|
139
|
+
* process lives: waits patiently (re-minting the code when it nears expiry),
|
|
140
|
+
* serves a connected viewer, and re-arms for the next viewer after a
|
|
141
|
+
* disconnect. SIGINT/SIGTERM release the code and clean up.
|
|
123
142
|
*/
|
|
124
143
|
declare function run(opts: RunOptions): Promise<void>;
|
|
125
144
|
|
|
126
|
-
export { CookieJar, type DataChannelLike, type RunOptions, WorkerApi, connectHost, connectViewer, parseArgs, parseSetCookie, proxyRequest, run, serveTunnel };
|
|
145
|
+
export { CookieJar, type DataChannelLike, type ParsedArgs, type RequestLog, type RunOptions, WorkerApi, connectHost, connectViewer, parseArgs, parseSetCookie, proxyRequest, run, serveTunnel };
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../../signaling/src/firebase.ts","../../signaling/src/config.ts","../../shared/src/constants.ts","../../shared/src/paths.ts","../../shared/src/protocol.ts","../../shared/src/code.ts","../src/api.ts","../src/cookies.ts","../src/peer.ts","../src/proxy.ts","../src/tunnel.ts"],"sourcesContent":["import nodeDataChannel from \"node-datachannel\";\nimport { config as loadDotenv } from \"dotenv\";\nimport kleur from \"kleur\";\nimport {\n createSignalingClient,\n firebaseConfigFromEnv,\n type FirebaseConfig,\n type SignalingSession,\n} from \"@exposeme/signaling\";\nimport { WorkerApi } from \"./api.js\";\nimport { CookieJar } from \"./cookies.js\";\nimport { connectHost } from \"./peer.js\";\nimport { serveTunnel } from \"./tunnel.js\";\n\nexport { WorkerApi } from \"./api.js\";\nexport { CookieJar, parseSetCookie } from \"./cookies.js\";\nexport { connectHost, connectViewer } from \"./peer.js\";\nexport { serveTunnel, type DataChannelLike } from \"./tunnel.js\";\nexport { proxyRequest } from \"./proxy.js\";\n\nexport interface RunOptions {\n port: number;\n env?: NodeJS.ProcessEnv;\n}\n\ninterface CliConfig {\n apiUrl: string;\n}\n\n// Production defaults so the published CLI is zero-config; env vars override.\n// The Firebase web config is not secret — RTDB access is gated by security\n// rules + the 6-digit code, not by hiding these values.\nconst DEFAULT_API_URL = \"https://expose-api.anishroy.com\";\nconst DEFAULT_FIREBASE_CONFIG: FirebaseConfig = {\n apiKey: \"AIzaSyA2YgT7R1gNlJhtp3mW-Sx4BOMEJPDYJgA\",\n authDomain: \"exposeme-230a6.firebaseapp.com\",\n databaseURL: \"https://exposeme-230a6-default-rtdb.firebaseio.com\",\n projectId: \"exposeme-230a6\",\n appId: \"1:242575918594:web:8a62140d71bc63d49d891b\",\n};\n\nfunction readCliConfig(env: NodeJS.ProcessEnv): CliConfig {\n return { apiUrl: env.EXPOSEME_API_URL ?? DEFAULT_API_URL };\n}\n\n/** Parse `exposeme <port>` argv into a port number. */\nexport function parseArgs(argv: string[]): { port: number } {\n const positional = argv.filter((a) => !a.startsWith(\"-\"));\n const portStr = positional[0];\n if (!portStr) throw new Error(\"Usage: npx exposeme <port>\");\n const port = Number(portStr);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new Error(`Invalid port: ${portStr}`);\n }\n return { port };\n}\n\nconst label = {\n waiting: kleur.yellow(\"● waiting\"),\n connected: kleur.green(\"● connected\"),\n disconnected: kleur.red(\"● disconnected\"),\n};\n\n/**\n * Run the tunnel host: mint a code, publish signaling, accept a viewer, and\n * proxy its requests to localhost:<port>. Resolves when the process is asked to\n * stop; wires SIGINT for graceful cleanup.\n */\nexport async function run(opts: RunOptions): Promise<void> {\n const env = opts.env ?? process.env;\n loadDotenv();\n const { apiUrl } = readCliConfig(env);\n // .env is shared with the viewer, which needs VITE_-prefixed vars; accept\n // either spelling so one file works for both, else fall back to prod config.\n const firebasePrefix = env.FIREBASE_API_KEY\n ? \"FIREBASE_\"\n : env.VITE_FIREBASE_API_KEY\n ? \"VITE_FIREBASE_\"\n : null;\n const firebaseConfig = firebasePrefix\n ? firebaseConfigFromEnv(env, firebasePrefix)\n : DEFAULT_FIREBASE_CONFIG;\n\n const api = new WorkerApi(apiUrl);\n const jar = new CookieJar();\n\n console.log(kleur.bold(\"\\n ExposeMe\") + kleur.dim(` → tunneling localhost:${opts.port}\\n`));\n\n const session = await api.createSession();\n const client = createSignalingClient(firebaseConfig);\n const signaling: SignalingSession = client.session(session.code);\n\n printCode(session.code, session.viewerUrl);\n console.log(` status ${label.waiting}\\n`);\n\n let cleanedUp = false;\n const cleanup = async (): Promise<void> => {\n if (cleanedUp) return;\n cleanedUp = true;\n try {\n handle?.close();\n } catch {\n /* noop */\n }\n await signaling.cleanup().catch(() => {});\n await api.releaseSession(session.code, session.sessionId);\n await client.destroy().catch(() => {});\n nodeDataChannel.cleanup();\n };\n\n process.on(\"SIGINT\", () => {\n console.log(kleur.dim(\"\\n releasing code and cleaning up…\"));\n void cleanup().then(() => process.exit(0));\n });\n\n let handle: Awaited<ReturnType<typeof connectHost>> | undefined;\n handle = await connectHost(signaling);\n\n console.log(` status ${label.connected}\\n`);\n serveTunnel(handle.dc, opts.port, jar);\n\n handle.dc.onClosed(() => {\n console.log(` status ${label.disconnected}\\n`);\n });\n\n // Keep the process alive until SIGINT.\n await new Promise<void>(() => {});\n}\n\nfunction printCode(code: string, viewerUrl: string): void {\n console.log(` code ${kleur.bold().cyan(code)}`);\n console.log(` share ${kleur.underline(viewerUrl)}\\n`);\n}\n","import { initializeApp, deleteApp, type FirebaseApp } from \"firebase/app\";\nimport {\n getDatabase,\n ref,\n set,\n get,\n push,\n onValue,\n onChildAdded,\n remove,\n off,\n type Database,\n} from \"firebase/database\";\nimport {\n hostCandidatesPath,\n hostOfferPath,\n sessionPath,\n viewerAnswerPath,\n viewerCandidatesPath,\n type IceCandidateInit,\n type SessionDescriptionInit,\n} from \"@exposeme/shared\";\nimport type { FirebaseConfig, SignalingSession, Unsubscribe } from \"./types.js\";\n\nlet appCounter = 0;\n\n/** Initialize a Firebase app + RTDB handle from config. */\nexport function createSignalingClient(config: FirebaseConfig): {\n db: Database;\n app: FirebaseApp;\n /** Bind a signaling session to a code. */\n session(code: string): SignalingSession;\n /** Tear down the underlying Firebase app. */\n destroy(): Promise<void>;\n} {\n // Unique name lets a single process (e.g. the loopback harness) hold more\n // than one app without collision.\n const app = initializeApp(config, `exposeme-${appCounter++}`);\n const db = getDatabase(app);\n\n return {\n db,\n app,\n session: (code) => new FirebaseSignalingSession(db, code),\n destroy: () => deleteApp(app),\n };\n}\n\nclass FirebaseSignalingSession implements SignalingSession {\n private unsubs: Unsubscribe[] = [];\n\n constructor(\n private readonly db: Database,\n public readonly code: string\n ) {}\n\n // ── Host role ────────────────────────────────────────────────────────────\n\n async writeOffer(offer: SessionDescriptionInit): Promise<void> {\n await set(ref(this.db, hostOfferPath(this.code)), offer);\n }\n\n async pushHostCandidate(candidate: IceCandidateInit): Promise<void> {\n await push(ref(this.db, hostCandidatesPath(this.code)), candidate);\n }\n\n onAnswer(cb: (answer: SessionDescriptionInit) => void): Unsubscribe {\n const r = ref(this.db, viewerAnswerPath(this.code));\n const handler = onValue(r, (snap) => {\n const val = snap.val() as SessionDescriptionInit | null;\n if (val) cb(val);\n });\n const unsub = () => off(r, \"value\", handler);\n this.unsubs.push(unsub);\n return unsub;\n }\n\n onViewerCandidate(cb: (candidate: IceCandidateInit) => void): Unsubscribe {\n return this.subscribeCandidates(viewerCandidatesPath(this.code), cb);\n }\n\n // ── Viewer role ──────────────────────────────────────────────────────────\n\n async readOffer(): Promise<SessionDescriptionInit | null> {\n const snap = await get(ref(this.db, hostOfferPath(this.code)));\n return (snap.val() as SessionDescriptionInit | null) ?? null;\n }\n\n async writeAnswer(answer: SessionDescriptionInit): Promise<void> {\n await set(ref(this.db, viewerAnswerPath(this.code)), answer);\n }\n\n async pushViewerCandidate(candidate: IceCandidateInit): Promise<void> {\n await push(ref(this.db, viewerCandidatesPath(this.code)), candidate);\n }\n\n onHostCandidate(cb: (candidate: IceCandidateInit) => void): Unsubscribe {\n return this.subscribeCandidates(hostCandidatesPath(this.code), cb);\n }\n\n // ── Shared ───────────────────────────────────────────────────────────────\n\n private subscribeCandidates(\n path: string,\n cb: (candidate: IceCandidateInit) => void\n ): Unsubscribe {\n const r = ref(this.db, path);\n const handler = onChildAdded(r, (snap) => {\n const val = snap.val() as IceCandidateInit | null;\n if (val) cb(val);\n });\n const unsub = () => off(r, \"child_added\", handler);\n this.unsubs.push(unsub);\n return unsub;\n }\n\n async cleanup(): Promise<void> {\n for (const unsub of this.unsubs.splice(0)) unsub();\n await remove(ref(this.db, sessionPath(this.code)));\n }\n}\n","import type { FirebaseConfig } from \"./types.js\";\n\n/**\n * Build a FirebaseConfig from a record of env vars. Works for both the CLI\n * (process.env, FIREBASE_* keys) and the viewer (import.meta.env, VITE_FIREBASE_*\n * keys) — pass the appropriate source and prefix.\n */\nexport function firebaseConfigFromEnv(\n env: Record<string, string | undefined>,\n prefix = \"FIREBASE_\"\n): FirebaseConfig {\n const get = (k: string): string => {\n const v = env[`${prefix}${k}`];\n if (!v) {\n throw new Error(\n `Missing Firebase config env var ${prefix}${k}. See .env.example.`\n );\n }\n return v;\n };\n return {\n apiKey: get(\"API_KEY\"),\n authDomain: get(\"AUTH_DOMAIN\"),\n databaseURL: get(\"DATABASE_URL\"),\n projectId: get(\"PROJECT_ID\"),\n appId: get(\"APP_ID\"),\n };\n}\n","/** Number of digits in an ExposeMe session code. */\nexport const CODE_LENGTH = 6;\n\n/** Regex a valid code must match (exactly CODE_LENGTH digits). */\nexport const CODE_REGEX = new RegExp(`^\\\\d{${CODE_LENGTH}}$`);\n\n/** Default lifetime of a session code before it expires (ms). 30 minutes. */\nexport const CODE_TTL_MS = 30 * 60 * 1000;\n\n/**\n * Max size (bytes) of a single raw body chunk before base64 encoding.\n * Kept well under the ~16 KiB safe SCTP message size so the base64-expanded\n * envelope still fits comfortably inside one data-channel message.\n */\nexport const CHUNK_SIZE = 12 * 1024;\n\n/** Public STUN servers used for ICE. TURN is intentionally omitted for v1. */\nexport const STUN_SERVERS = [\n \"stun:stun.l.google.com:19302\",\n \"stun:stun1.l.google.com:19302\",\n];\n\n/** Label used for the single application data channel. */\nexport const DATA_CHANNEL_LABEL = \"exposeme\";\n","/**\n * RTDB path helpers. All signaling state for a session lives under\n * `/sessions/{code}`. The host writes under `host/`, the viewer under `viewer/`.\n */\n\nexport const sessionPath = (code: string) => `sessions/${code}`;\n\nexport const hostOfferPath = (code: string) => `sessions/${code}/host/offer`;\nexport const hostCandidatesPath = (code: string) =>\n `sessions/${code}/host/candidates`;\n\nexport const viewerAnswerPath = (code: string) =>\n `sessions/${code}/viewer/answer`;\nexport const viewerCandidatesPath = (code: string) =>\n `sessions/${code}/viewer/candidates`;\n","import { CHUNK_SIZE } from \"./constants.js\";\nimport type { HeaderList } from \"./types.js\";\n\n/**\n * Data-channel wire protocol. Every message is a JSON envelope with a\n * discriminant `t`. A single HTTP exchange is a stream of messages sharing an\n * `id`: a HEAD, zero or more CHUNKs, then an END (or an ERR).\n *\n * Bodies are sent as base64 of the raw bytes so binary survives intact.\n */\n\nexport interface ReqHead {\n t: \"req_head\";\n id: string;\n method: string;\n /** Path + query relative to the tunneled origin, e.g. `/assets/logo.png`. */\n url: string;\n headers: HeaderList;\n}\n\nexport interface ReqChunk {\n t: \"req_chunk\";\n id: string;\n seq: number;\n dataB64: string;\n}\n\nexport interface ReqEnd {\n t: \"req_end\";\n id: string;\n}\n\nexport interface ResHead {\n t: \"res_head\";\n id: string;\n status: number;\n statusText: string;\n headers: HeaderList;\n}\n\nexport interface ResChunk {\n t: \"res_chunk\";\n id: string;\n seq: number;\n dataB64: string;\n}\n\nexport interface ResEnd {\n t: \"res_end\";\n id: string;\n}\n\nexport interface ResErr {\n t: \"res_err\";\n id: string;\n message: string;\n}\n\nexport type TunnelMessage =\n | ReqHead\n | ReqChunk\n | ReqEnd\n | ResHead\n | ResChunk\n | ResEnd\n | ResErr;\n\nexport const encode = (msg: TunnelMessage): string => JSON.stringify(msg);\n\nexport const decode = (raw: string): TunnelMessage =>\n JSON.parse(raw) as TunnelMessage;\n\n/** base64 helpers that work in both Node and the browser. */\nexport const toBase64 = (bytes: Uint8Array): string => {\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(bytes).toString(\"base64\");\n }\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!);\n }\n return btoa(binary);\n};\n\nexport const fromBase64 = (b64: string): Uint8Array => {\n if (typeof Buffer !== \"undefined\") {\n return new Uint8Array(Buffer.from(b64, \"base64\"));\n }\n const binary = atob(b64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n};\n\n/**\n * Split a body into ordered chunk messages of at most CHUNK_SIZE raw bytes each.\n * An empty body yields no chunk messages.\n */\nexport function chunkBody(\n id: string,\n kind: \"req\" | \"res\",\n body: Uint8Array\n): Array<ReqChunk | ResChunk> {\n const t = kind === \"req\" ? \"req_chunk\" : \"res_chunk\";\n const chunks: Array<ReqChunk | ResChunk> = [];\n let seq = 0;\n for (let offset = 0; offset < body.length; offset += CHUNK_SIZE) {\n const slice = body.subarray(offset, offset + CHUNK_SIZE);\n chunks.push({ t, id, seq: seq++, dataB64: toBase64(slice) } as\n | ReqChunk\n | ResChunk);\n }\n return chunks;\n}\n\n/**\n * Accumulates ordered body chunks for a single id and concatenates them.\n * Reassembles by `seq` so out-of-order delivery is tolerated (data channels are\n * ordered by default, but this keeps the reassembler robust and testable).\n */\nexport class ChunkAssembler {\n private parts = new Map<number, Uint8Array>();\n\n push(seq: number, dataB64: string): void {\n this.parts.set(seq, fromBase64(dataB64));\n }\n\n concat(): Uint8Array {\n const ordered = [...this.parts.entries()]\n .sort((a, b) => a[0] - b[0])\n .map(([, v]) => v);\n const total = ordered.reduce((n, p) => n + p.length, 0);\n const out = new Uint8Array(total);\n let offset = 0;\n for (const part of ordered) {\n out.set(part, offset);\n offset += part.length;\n }\n return out;\n }\n}\n","import { CODE_LENGTH, CODE_REGEX } from \"./constants.js\";\n\n/**\n * Generate a random N-digit code as a zero-padded string.\n * `rand` returns a float in [0, 1); defaults to Math.random. The Worker passes\n * a crypto-backed source.\n */\nexport function generateCode(rand: () => number = Math.random): string {\n const max = 10 ** CODE_LENGTH;\n const n = Math.floor(rand() * max);\n return String(n).padStart(CODE_LENGTH, \"0\");\n}\n\nexport const isValidCode = (code: string): boolean => CODE_REGEX.test(code);\n","import type { CreateSessionResponse } from \"@exposeme/shared\";\n\n/** Thin client for the ExposeMe Worker API. */\nexport class WorkerApi {\n constructor(private readonly baseUrl: string) {\n this.baseUrl = baseUrl.replace(/\\/$/, \"\");\n }\n\n /** POST /api/sessions — mint a code + session. */\n async createSession(): Promise<CreateSessionResponse> {\n const res = await fetch(`${this.baseUrl}/api/sessions`, { method: \"POST\" });\n if (!res.ok) {\n throw new Error(\n `Failed to create session (${res.status}): ${await res.text()}`\n );\n }\n return (await res.json()) as CreateSessionResponse;\n }\n\n /** DELETE /api/sessions/:code — release the code early. Best-effort. */\n async releaseSession(code: string, sessionId: string): Promise<void> {\n const url = `${this.baseUrl}/api/sessions/${code}?sessionId=${encodeURIComponent(\n sessionId\n )}`;\n await fetch(url, { method: \"DELETE\" }).catch(() => {\n /* best-effort cleanup; TTL will expire it anyway */\n });\n }\n}\n","import type { HeaderList } from \"@exposeme/shared\";\n\ninterface StoredCookie {\n value: string;\n path: string;\n expires?: number; // epoch ms\n}\n\n/**\n * A deliberately simple host-side cookie jar. Because the viewer's Service\n * Worker cannot participate in normal cross-origin cookie handling, the host\n * owns cookies for the tunneled origin: it captures `Set-Cookie` from responses\n * and replays a `Cookie` header on matching outbound requests.\n *\n * v1 scope: single tunneled origin, name-keyed, honoring `Path` and `Expires`/\n * `Max-Age`. Domain and `Secure`/`SameSite` are ignored (localhost, one origin).\n */\nexport class CookieJar {\n private cookies = new Map<string, StoredCookie>();\n\n /** Record cookies from a response's `Set-Cookie` header values. */\n store(setCookieValues: string[], now = Date.now()): void {\n for (const raw of setCookieValues) {\n const parsed = parseSetCookie(raw);\n if (!parsed) continue;\n if (parsed.expires !== undefined && parsed.expires <= now) {\n this.cookies.delete(parsed.name); // deletion cookie\n continue;\n }\n this.cookies.set(parsed.name, {\n value: parsed.value,\n path: parsed.path,\n expires: parsed.expires,\n });\n }\n }\n\n /** Build a `Cookie` header value for a request path, or null if none apply. */\n header(requestPath: string, now = Date.now()): string | null {\n const pairs: string[] = [];\n for (const [name, c] of this.cookies) {\n if (c.expires !== undefined && c.expires <= now) {\n this.cookies.delete(name);\n continue;\n }\n if (pathMatches(requestPath, c.path)) pairs.push(`${name}=${c.value}`);\n }\n return pairs.length ? pairs.join(\"; \") : null;\n }\n\n /** Number of live cookies (for tests/introspection). */\n get size(): number {\n return this.cookies.size;\n }\n}\n\nfunction pathMatches(requestPath: string, cookiePath: string): boolean {\n if (cookiePath === \"/\" || cookiePath === \"\") return true;\n const reqPath = requestPath.split(\"?\")[0] ?? \"/\";\n if (reqPath === cookiePath) return true;\n return (\n reqPath.startsWith(cookiePath) &&\n (cookiePath.endsWith(\"/\") || reqPath[cookiePath.length] === \"/\")\n );\n}\n\ninterface ParsedCookie {\n name: string;\n value: string;\n path: string;\n expires?: number;\n}\n\n/** Parse one Set-Cookie header value. Returns null if malformed. */\nexport function parseSetCookie(raw: string): ParsedCookie | null {\n const parts = raw.split(\";\");\n const first = parts[0]?.trim() ?? \"\";\n const eq = first.indexOf(\"=\");\n if (eq <= 0) return null;\n\n const name = first.slice(0, eq).trim();\n const value = first.slice(eq + 1).trim();\n let path = \"/\";\n let expires: number | undefined;\n let maxAge: number | undefined;\n\n for (let i = 1; i < parts.length; i++) {\n const attr = parts[i]!.trim();\n const aEq = attr.indexOf(\"=\");\n const key = (aEq === -1 ? attr : attr.slice(0, aEq)).toLowerCase();\n const val = aEq === -1 ? \"\" : attr.slice(aEq + 1).trim();\n if (key === \"path\") path = val || \"/\";\n else if (key === \"expires\") {\n const t = Date.parse(val);\n if (!Number.isNaN(t)) expires = t;\n } else if (key === \"max-age\") {\n const n = Number(val);\n if (!Number.isNaN(n)) maxAge = n;\n }\n }\n\n // Max-Age takes precedence over Expires per RFC 6265, but we can't call\n // Date.now() deterministically here — encode as relative and resolve later.\n if (maxAge !== undefined) {\n expires = Date.now() + maxAge * 1000;\n }\n\n return { name, value, path, expires };\n}\n","import nodeDataChannel from \"node-datachannel\";\nimport {\n DATA_CHANNEL_LABEL,\n STUN_SERVERS,\n type IceCandidateInit,\n type SessionDescriptionInit,\n} from \"@exposeme/shared\";\nimport type { SignalingSession } from \"@exposeme/signaling\";\n\nconst { PeerConnection } = nodeDataChannel;\ntype PeerConnection = InstanceType<typeof PeerConnection>;\ntype DataChannel = ReturnType<PeerConnection[\"createDataChannel\"]>;\n\nexport interface PeerConnectionHandle {\n pc: PeerConnection;\n dc: DataChannel;\n close(): void;\n}\n\nexport interface ConnectOptions {\n iceServers?: string[];\n /** Milliseconds to wait for the data channel to open before rejecting. */\n timeoutMs?: number;\n}\n\nconst DEFAULT_TIMEOUT = 30_000;\n\n/**\n * Host role: create the peer + data channel, publish the offer/ICE via\n * signaling, apply the viewer's answer/ICE, resolve once the channel opens.\n */\nexport function connectHost(\n signaling: SignalingSession,\n opts: ConnectOptions = {}\n): Promise<PeerConnectionHandle> {\n return new Promise((resolve, reject) => {\n const pc = new PeerConnection(\"exposeme-host\", {\n iceServers: opts.iceServers ?? STUN_SERVERS,\n });\n const pending = new PendingCandidates(pc);\n\n pc.onLocalDescription((sdp, type) => {\n void signaling.writeOffer({ type: type as \"offer\", sdp });\n });\n pc.onLocalCandidate((candidate, mid) => {\n void signaling.pushHostCandidate({ candidate, sdpMid: mid });\n });\n\n // onValue re-delivers the answer on viewer reloads / Firebase re-syncs;\n // applying it twice throws inside the Firebase callback and kills the\n // process, so accept only the first answer and swallow later ones.\n let answerApplied = false;\n signaling.onAnswer((answer) => {\n if (answerApplied) return;\n answerApplied = true;\n try {\n pc.setRemoteDescription(answer.sdp, answer.type);\n pending.flush();\n } catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)));\n }\n });\n signaling.onViewerCandidate((c) => pending.add(c));\n\n const dc = pc.createDataChannel(DATA_CHANNEL_LABEL);\n finishOnOpen(pc, dc, opts, resolve, reject);\n });\n}\n\n/**\n * Viewer role (also used by the Node loopback test): wait for the host's\n * offer, answer it, exchange ICE, resolve with the inbound data channel.\n */\nexport function connectViewer(\n signaling: SignalingSession,\n opts: ConnectOptions = {}\n): Promise<PeerConnectionHandle> {\n return new Promise((resolve, reject) => {\n const pc = new PeerConnection(\"exposeme-viewer\", {\n iceServers: opts.iceServers ?? STUN_SERVERS,\n });\n const pending = new PendingCandidates(pc);\n let dc: DataChannel | undefined;\n\n pc.onLocalDescription((sdp, type) => {\n void signaling.writeAnswer({ type: type as \"answer\", sdp });\n });\n pc.onLocalCandidate((candidate, mid) => {\n void signaling.pushViewerCandidate({ candidate, sdpMid: mid });\n });\n\n pc.onDataChannel((incoming) => {\n dc = incoming;\n finishOnOpen(pc, incoming, opts, resolve, reject);\n });\n\n signaling.onHostCandidate((c) => pending.add(c));\n\n // Fetch the offer (may need a brief poll if the host writes it slightly\n // after the viewer starts listening).\n void pollForOffer(signaling)\n .then((offer) => {\n pc.setRemoteDescription(offer.sdp, offer.type); // triggers answer\n pending.flush();\n })\n .catch(reject);\n\n // Guard: if onDataChannel never fires we still time out via finishOnOpen\n // only after dc exists, so add a fallback timer here too.\n if (opts.timeoutMs !== 0) {\n const t = setTimeout(() => {\n if (!dc) reject(new Error(\"Timed out waiting for data channel\"));\n }, opts.timeoutMs ?? DEFAULT_TIMEOUT);\n t.unref?.();\n }\n });\n}\n\nasync function pollForOffer(\n signaling: SignalingSession,\n attempts = 40,\n intervalMs = 250\n): Promise<SessionDescriptionInit> {\n for (let i = 0; i < attempts; i++) {\n const offer = await signaling.readOffer();\n if (offer) return offer;\n await delay(intervalMs);\n }\n throw new Error(\"Host offer not found\");\n}\n\nconst delay = (ms: number) => new Promise((r) => setTimeout(r, ms));\n\n/** Buffers remote ICE candidates until the remote description is set. */\nclass PendingCandidates {\n private queue: IceCandidateInit[] = [];\n private ready = false;\n\n constructor(private readonly pc: PeerConnection) {}\n\n add(c: IceCandidateInit): void {\n if (this.ready) this.apply(c);\n else this.queue.push(c);\n }\n\n flush(): void {\n this.ready = true;\n for (const c of this.queue.splice(0)) this.apply(c);\n }\n\n private apply(c: IceCandidateInit): void {\n try {\n this.pc.addRemoteCandidate(c.candidate, c.sdpMid ?? \"0\");\n } catch {\n /* ignore malformed/duplicate candidates */\n }\n }\n}\n\nfunction finishOnOpen(\n pc: PeerConnection,\n dc: DataChannel,\n opts: ConnectOptions,\n resolve: (h: PeerConnectionHandle) => void,\n reject: (e: Error) => void\n): void {\n const handle: PeerConnectionHandle = {\n pc,\n dc,\n close() {\n try {\n dc.close();\n } catch {\n /* noop */\n }\n try {\n pc.close();\n } catch {\n /* noop */\n }\n },\n };\n\n let settled = false;\n const timer =\n opts.timeoutMs === 0\n ? undefined\n : setTimeout(() => {\n if (!settled) {\n settled = true;\n reject(new Error(\"Timed out waiting for data channel to open\"));\n }\n }, opts.timeoutMs ?? DEFAULT_TIMEOUT);\n timer?.unref?.();\n\n if (dc.isOpen()) {\n settled = true;\n if (timer) clearTimeout(timer);\n resolve(handle);\n return;\n }\n dc.onOpen(() => {\n if (settled) return;\n settled = true;\n if (timer) clearTimeout(timer);\n resolve(handle);\n });\n}\n","import http from \"node:http\";\nimport type { HeaderList } from \"@exposeme/shared\";\n\nexport interface ProxyRequest {\n method: string;\n /** Path + query relative to the tunneled origin. */\n url: string;\n headers: HeaderList;\n body: Uint8Array;\n}\n\nexport interface ProxyResponse {\n status: number;\n statusText: string;\n headers: HeaderList;\n body: Uint8Array;\n /** Set-Cookie values pulled out for the host cookie jar. */\n setCookies: string[];\n}\n\n/** Hop-by-hop headers that must not be forwarded in either direction. */\nconst HOP_BY_HOP = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n]);\n\n/**\n * Forward one request to `localhost:<port>` and collect the full response.\n * `cookieHeader`, if provided, overrides the request's Cookie header (the host\n * owns cookies). `accept-encoding` is stripped so the local server returns an\n * identity body the browser can consume from a Service-Worker-built Response.\n */\nexport function proxyRequest(\n port: number,\n req: ProxyRequest,\n cookieHeader: string | null\n): Promise<ProxyResponse> {\n return new Promise((resolve, reject) => {\n const outHeaders: Record<string, string | string[]> = {};\n for (const [k, v] of req.headers) {\n const lk = k.toLowerCase();\n if (HOP_BY_HOP.has(lk)) continue;\n if (lk === \"host\") continue; // rewritten below\n if (lk === \"accept-encoding\") continue; // force identity\n if (lk === \"cookie\") continue; // host jar owns cookies\n // Merge duplicate headers into an array.\n const existing = outHeaders[lk];\n if (existing === undefined) outHeaders[lk] = v;\n else if (Array.isArray(existing)) existing.push(v);\n else outHeaders[lk] = [existing, v];\n }\n outHeaders[\"host\"] = `localhost:${port}`;\n if (cookieHeader) outHeaders[\"cookie\"] = cookieHeader;\n\n // Dev servers may bind only one of IPv4/IPv6 localhost (e.g. Vite on ::1),\n // so try 127.0.0.1 first and fall back to ::1 on connection refusal.\n const attempt = (host: string, fallback?: string): void => {\n const request = http.request(\n {\n host,\n port,\n method: req.method,\n path: req.url,\n headers: outHeaders,\n },\n (res) => {\n const chunks: Buffer[] = [];\n res.on(\"data\", (c: Buffer) => chunks.push(c));\n res.on(\"end\", () => {\n const { headers, setCookies } = splitResponseHeaders(\n res.rawHeaders\n );\n resolve({\n status: res.statusCode ?? 502,\n statusText: res.statusMessage ?? \"\",\n headers,\n body: new Uint8Array(Buffer.concat(chunks)),\n setCookies,\n });\n });\n }\n );\n\n request.on(\"error\", (err: NodeJS.ErrnoException) => {\n if (fallback && err.code === \"ECONNREFUSED\") attempt(fallback);\n else reject(err);\n });\n if (req.body.length > 0) request.write(Buffer.from(req.body));\n request.end();\n };\n\n attempt(\"127.0.0.1\", \"::1\");\n });\n}\n\n/**\n * Convert Node's flat rawHeaders array into a HeaderList, pulling out Set-Cookie\n * (host-managed) and dropping hop-by-hop headers the browser shouldn't see.\n */\nfunction splitResponseHeaders(rawHeaders: string[]): {\n headers: HeaderList;\n setCookies: string[];\n} {\n const headers: HeaderList = [];\n const setCookies: string[] = [];\n for (let i = 0; i < rawHeaders.length; i += 2) {\n const name = rawHeaders[i]!;\n const value = rawHeaders[i + 1] ?? \"\";\n const lower = name.toLowerCase();\n if (lower === \"set-cookie\") {\n setCookies.push(value);\n continue; // not forwarded to the viewer\n }\n if (HOP_BY_HOP.has(lower)) continue;\n if (lower === \"content-length\") continue; // body length may change; SW recomputes\n headers.push([name, value]);\n }\n return { headers, setCookies };\n}\n","import {\n ChunkAssembler,\n chunkBody,\n decode,\n encode,\n type HeaderList,\n type TunnelMessage,\n} from \"@exposeme/shared\";\nimport { proxyRequest } from \"./proxy.js\";\nimport type { CookieJar } from \"./cookies.js\";\n\n/**\n * Minimal data-channel surface the tunnel needs. Matches node-datachannel's\n * DataChannel (and is trivially mockable in tests).\n */\nexport interface DataChannelLike {\n sendMessage(msg: string): boolean;\n onMessage(cb: (msg: string | Buffer | ArrayBuffer) => void): void;\n isOpen(): boolean;\n bufferedAmount?(): number;\n maxMessageSize?(): number;\n setBufferedAmountLowThreshold?(n: number): void;\n onBufferedAmountLow?(cb: () => void): void;\n}\n\n/** Pause sending when the channel's buffer grows beyond this many bytes. */\nconst BUFFER_HIGH_WATER = 1024 * 1024; // 1 MiB\n\ninterface InFlight {\n method: string;\n url: string;\n headers: HeaderList;\n body: ChunkAssembler;\n}\n\n/**\n * Attach the host-side request handler to a data channel: reassemble incoming\n * requests, proxy them to `localhost:<port>`, and stream responses back.\n * Returns a stats object updated as requests complete.\n */\nexport function serveTunnel(\n dc: DataChannelLike,\n port: number,\n jar: CookieJar\n): { requestCount: number } {\n const stats = { requestCount: 0 };\n const inflight = new Map<string, InFlight>();\n const lowSignal = new BufferedAmountSignal(dc);\n\n dc.onMessage((raw) => {\n const text = typeof raw === \"string\" ? raw : bufferToString(raw);\n let msg: TunnelMessage;\n try {\n msg = decode(text);\n } catch {\n return; // ignore non-protocol frames\n }\n handleMessage(msg);\n });\n\n function handleMessage(msg: TunnelMessage): void {\n switch (msg.t) {\n case \"req_head\":\n inflight.set(msg.id, {\n method: msg.method,\n url: msg.url,\n headers: msg.headers,\n body: new ChunkAssembler(),\n });\n break;\n case \"req_chunk\": {\n inflight.get(msg.id)?.body.push(msg.seq, msg.dataB64);\n break;\n }\n case \"req_end\": {\n const req = inflight.get(msg.id);\n if (!req) return;\n inflight.delete(msg.id);\n void complete(msg.id, req);\n break;\n }\n default:\n break; // responses flow the other direction; ignore\n }\n }\n\n async function complete(id: string, req: InFlight): Promise<void> {\n try {\n const cookieHeader = jar.header(req.url);\n const res = await proxyRequest(\n port,\n {\n method: req.method,\n url: req.url,\n headers: req.headers,\n body: req.body.concat(),\n },\n cookieHeader\n );\n jar.store(res.setCookies);\n stats.requestCount++;\n\n await send({\n t: \"res_head\",\n id,\n status: res.status,\n statusText: res.statusText,\n headers: res.headers,\n });\n for (const chunk of chunkBody(id, \"res\", res.body)) {\n await send(chunk);\n }\n await send({ t: \"res_end\", id });\n } catch (err) {\n await send({\n t: \"res_err\",\n id,\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n async function send(msg: TunnelMessage): Promise<void> {\n await lowSignal.awaitDrain();\n if (dc.isOpen()) dc.sendMessage(encode(msg));\n }\n\n return stats;\n}\n\n/** Simple backpressure helper around a channel's buffered-amount signal. */\nclass BufferedAmountSignal {\n private waiters: Array<() => void> = [];\n\n constructor(private readonly dc: DataChannelLike) {\n if (dc.setBufferedAmountLowThreshold && dc.onBufferedAmountLow) {\n dc.setBufferedAmountLowThreshold(BUFFER_HIGH_WATER / 2);\n dc.onBufferedAmountLow(() => {\n for (const w of this.waiters.splice(0)) w();\n });\n }\n }\n\n awaitDrain(): Promise<void> {\n const buffered = this.dc.bufferedAmount?.() ?? 0;\n if (buffered < BUFFER_HIGH_WATER) return Promise.resolve();\n return new Promise((resolve) => this.waiters.push(resolve));\n }\n}\n\nfunction bufferToString(buf: Buffer | ArrayBuffer): string {\n if (buf instanceof ArrayBuffer) return Buffer.from(buf).toString(\"utf-8\");\n return buf.toString(\"utf-8\");\n}\n"],"mappings":"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","names":["nodeDataChannel","get","nodeDataChannel"]}
|