@dutodev/cli 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ C,
4
+ TRUNCATED_NOTE,
5
+ bodyNote,
3
6
  formatBytes,
4
7
  formatDuration,
5
8
  formatTime,
@@ -7,7 +10,7 @@ import {
7
10
  normalizeName,
8
11
  statusColor,
9
12
  validateName
10
- } from "./chunk-26EQC6HB.js";
13
+ } from "./chunk-74IJFYGT.js";
11
14
 
12
15
  // src/ui/brand.ts
13
16
  function colorOn(stream = process.stdout) {
@@ -30,6 +33,7 @@ function paint(tone, text, stream) {
30
33
  var mark = () => `${paint("signal", "\u25B0")} ${paint("strong", "duto")}`;
31
34
  var ok = (msg) => `${paint("signal", "\u2714")} ${msg}`;
32
35
  var fail = (msg) => `${paint("err", "\u2716", process.stderr)} ${msg}`;
36
+ var warn = (msg) => `${paint("warn", "\u26A0")} ${msg}`;
33
37
 
34
38
  // src/index.ts
35
39
  import { hostname } from "os";
@@ -517,6 +521,921 @@ function findConfig(cwd) {
517
521
  }
518
522
  }
519
523
 
524
+ // src/inspector/jsonView.ts
525
+ function jsonSyntaxError(text) {
526
+ try {
527
+ JSON.parse(text);
528
+ return null;
529
+ } catch (e) {
530
+ const message2 = String(e && e.message ? e.message : e);
531
+ const m = /position (\d+)/.exec(message2);
532
+ if (m) return { position: Number(m[1]), message: message2 };
533
+ const lc = /line (\d+) column (\d+)/.exec(message2);
534
+ if (lc) {
535
+ let idx = 0;
536
+ for (let ln = 1; ln < Number(lc[1]); ln++) {
537
+ const nl = text.indexOf("\n", idx);
538
+ if (nl < 0) return { position: text.length, message: message2 };
539
+ idx = nl + 1;
540
+ }
541
+ return { position: Math.min(idx + Number(lc[2]) - 1, text.length), message: message2 };
542
+ }
543
+ return { position: text.length, message: message2 };
544
+ }
545
+ }
546
+ function jsonTokens(text) {
547
+ const out = [];
548
+ let depth = 0;
549
+ let i = 0;
550
+ while (i < text.length) {
551
+ const c = text.charAt(i);
552
+ let j = i + 1;
553
+ let kind;
554
+ if (c === " " || c === "\n" || c === " " || c === "\r") {
555
+ while (j < text.length && " \n \r".indexOf(text.charAt(j)) >= 0) j++;
556
+ kind = "space";
557
+ } else if (c === '"') {
558
+ while (j < text.length && text.charAt(j) !== '"') j += text.charAt(j) === "\\" ? 2 : 1;
559
+ j = Math.min(j + 1, text.length);
560
+ let k = j;
561
+ while (k < text.length && " \n \r".indexOf(text.charAt(k)) >= 0) k++;
562
+ kind = text.charAt(k) === ":" ? "key" : "string";
563
+ } else if ("{}[],:".indexOf(c) >= 0) {
564
+ kind = "punct";
565
+ if (c === "}" || c === "]") depth = Math.max(0, depth - 1);
566
+ } else {
567
+ while (j < text.length && ' \n \r{}[],:"'.indexOf(text.charAt(j)) < 0) j++;
568
+ const word = text.slice(i, j);
569
+ kind = word === "true" || word === "false" || word === "null" ? "literal" : "number";
570
+ }
571
+ out.push({ kind, text: text.slice(i, j), depth });
572
+ if (kind === "punct" && (c === "{" || c === "[")) depth++;
573
+ i = j;
574
+ }
575
+ return out;
576
+ }
577
+ function jsonIndent(text) {
578
+ let out = "";
579
+ let depth = 0;
580
+ let i = 0;
581
+ const ws = " \n \r";
582
+ while (i < text.length) {
583
+ const c = text.charAt(i);
584
+ if (ws.indexOf(c) >= 0) {
585
+ i++;
586
+ } else if (c === '"') {
587
+ let j = i + 1;
588
+ while (j < text.length && text.charAt(j) !== '"') j += text.charAt(j) === "\\" ? 2 : 1;
589
+ out += text.slice(i, j + 1);
590
+ i = j + 1;
591
+ } else if (c === "{" || c === "[") {
592
+ let j = i + 1;
593
+ while (j < text.length && ws.indexOf(text.charAt(j)) >= 0) j++;
594
+ if (text.charAt(j) === (c === "{" ? "}" : "]")) {
595
+ out += c + text.charAt(j);
596
+ i = j + 1;
597
+ } else {
598
+ depth++;
599
+ out += c + "\n" + " ".repeat(depth);
600
+ i++;
601
+ }
602
+ } else if (c === "}" || c === "]") {
603
+ depth = Math.max(0, depth - 1);
604
+ out += "\n" + " ".repeat(depth) + c;
605
+ i++;
606
+ } else if (c === ",") {
607
+ out += ",\n" + " ".repeat(depth);
608
+ i++;
609
+ } else if (c === ":") {
610
+ out += ": ";
611
+ i++;
612
+ } else {
613
+ let j = i + 1;
614
+ while (j < text.length && (ws + '{}[],:"').indexOf(text.charAt(j)) < 0) j++;
615
+ out += text.slice(i, j);
616
+ i = j;
617
+ }
618
+ }
619
+ return out;
620
+ }
621
+
622
+ // src/inspector/page.ts
623
+ var BG = "#111413";
624
+ var BAR = "#0b0d0c";
625
+ var LINE = "#222826";
626
+ var ROW_LINE = "#1a1f1d";
627
+ var INPUT_LINE = "#2a312e";
628
+ var TEXT = "#e7e9e6";
629
+ var SELECTED = "#16201e";
630
+ var JSON_KEY = "#93c5fd";
631
+ var JSON_STRING = "#fdba74";
632
+ var JSON_VALUE = "#c4b5fd";
633
+ var MONO = '"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace';
634
+ var STYLE = `
635
+ :root { color-scheme: dark; }
636
+ * { box-sizing: border-box; }
637
+ html, body { height: 100%; }
638
+ body { margin: 0; background: ${BG}; color: ${TEXT}; font-family: ${MONO}; font-size: 13px; line-height: 1.45; }
639
+ button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
640
+ :focus-visible { outline: 1px solid ${C.signal}; outline-offset: -1px; }
641
+ .app { display: flex; flex-direction: column; height: 100vh; }
642
+ .top { display: flex; justify-content: space-between; gap: 16px; padding: 8px 12px; background: ${BAR}; border-bottom: 1px solid ${LINE}; color: ${C.muted}; }
643
+ .top b { color: ${C.signal}; font-weight: 600; }
644
+ .tunnels { display: flex; flex-wrap: wrap; gap: 4px 14px; justify-content: flex-end; min-width: 0; }
645
+ .tunnels .k { color: ${C.strong}; }
646
+ .banner { padding: 6px 12px; background: ${BAR}; border-bottom: 1px solid ${C.err}; color: ${C.err}; }
647
+ .main { flex: 1; min-height: 0; display: grid; grid-template-columns: 320px 1fr; }
648
+ .side { display: flex; flex-direction: column; min-height: 0; border-right: 1px solid ${LINE}; }
649
+ .search { margin: 8px 12px; padding: 5px 8px; border: 1px solid ${INPUT_LINE}; border-radius: 5px; background: ${BG}; color: ${TEXT}; font: inherit; outline: none; }
650
+ .search:focus { border-color: ${C.signal}; }
651
+ .search::placeholder { color: ${C.muted}; }
652
+ .list { flex: 1; min-height: 0; overflow-y: auto; outline: none; }
653
+ .row { display: grid; grid-template-columns: 14px 42px minmax(0, 1fr) 34px 56px; gap: 8px; padding: 5px 12px; border-bottom: 1px solid ${ROW_LINE}; align-items: center; cursor: pointer; white-space: nowrap; }
654
+ .list.multi .row { grid-template-columns: 14px 56px 42px minmax(0, 1fr) 34px 56px; }
655
+ .row:hover { background: #141917; }
656
+ .row.sel { background: ${SELECTED}; box-shadow: inset 2px 0 0 ${C.signal}; }
657
+ .row span { overflow: hidden; text-overflow: ellipsis; }
658
+ .row .dur { text-align: right; }
659
+ .m { font-weight: 700; }
660
+ .mut { color: ${C.muted}; }
661
+ .empty { padding: 12px; color: ${C.muted}; }
662
+ .detail { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
663
+ .pane { flex: 1; }
664
+ .head { padding: 8px 12px; overflow-wrap: anywhere; }
665
+ .head b { color: ${C.strong}; }
666
+ .tabs { display: flex; gap: 14px; padding: 8px 12px 0; border-bottom: 1px solid ${LINE}; }
667
+ .tab { color: ${C.muted}; padding-bottom: 6px; border-bottom: 2px solid transparent; }
668
+ .tab:hover { color: ${TEXT}; }
669
+ .tab.on { color: ${C.strong}; border-bottom-color: ${C.signal}; }
670
+ .content { flex: 1; min-height: 0; overflow: auto; }
671
+ .content pre { margin: 0; padding: 10px 12px; line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
672
+ .note { padding: 10px 12px; color: ${C.muted}; }
673
+ .trunc { padding: 0 12px 10px; color: ${C.warn}; }
674
+ .jerr { padding: 10px 12px 0; color: ${C.err}; }
675
+ .bad { background: ${C.err}40; color: ${C.strong}; }
676
+ .k { color: ${JSON_KEY}; } .s { color: ${JSON_STRING}; } .n { color: ${JSON_VALUE}; }
677
+ .fold.closed > .inner { display: none; }
678
+ .fold > .sum { display: none; color: ${C.muted}; }
679
+ .fold.closed > .sum { display: inline; }
680
+ .open { cursor: pointer; }
681
+ .open:hover { color: ${C.signal}; }
682
+ table { border-collapse: collapse; margin: 6px 12px 10px; }
683
+ th, td { text-align: left; vertical-align: top; padding: 3px 16px 3px 0; border-bottom: 1px solid ${ROW_LINE}; overflow-wrap: anywhere; }
684
+ th { color: ${JSON_KEY}; font-weight: 400; white-space: nowrap; }
685
+ .sect { padding: 10px 12px 0; color: ${C.muted}; }
686
+ .btns { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 8px 12px; border-top: 1px solid ${LINE}; }
687
+ .btn { border: 1px solid ${INPUT_LINE}; border-radius: 5px; padding: 3px 8px; }
688
+ .btn:hover:not(:disabled) { border-color: ${C.muted}; }
689
+ .btn.pri { border-color: ${C.signal}; color: ${C.signal}; }
690
+ .btn:disabled { color: ${C.muted}; border-color: ${ROW_LINE}; cursor: default; }
691
+ .msg { color: ${C.muted}; }
692
+ .msg.err { color: ${C.err}; }
693
+ [hidden] { display: none !important; }
694
+ @media (max-width: 720px) {
695
+ .main { grid-template-columns: 1fr; grid-template-rows: 40vh 1fr; }
696
+ .side { border-right: 0; border-bottom: 1px solid ${LINE}; }
697
+ }
698
+ `;
699
+ var SCRIPT = String.raw`"use strict";
700
+ var jsonSyntaxError = (${jsonSyntaxError.toString()});
701
+ var jsonTokens = (${jsonTokens.toString()});
702
+ var jsonIndent = (${jsonIndent.toString()});
703
+ var COLORS = ${JSON.stringify(C)};
704
+ var TOKEN = new URLSearchParams(location.search).get("t") || "";
705
+ var TABS = ["Corpo", "Cabeçalhos", "Resposta", "Raw"];
706
+
707
+ var state = { items: [], tunnels: [], selected: null, detail: null, detailKey: "", tab: "Corpo", ended: false };
708
+ var listSeq = 0, detailSeq = 0, msgTimer = null;
709
+ var es = null, lost = false, replaying = false;
710
+ function fail(e) { if (!state.ended) console.error(e); }
711
+
712
+ function $(id) { return document.getElementById(id); }
713
+ function el(tag, cls, text) {
714
+ var e = document.createElement(tag);
715
+ if (cls) e.className = cls;
716
+ if (text !== undefined && text !== null) e.textContent = String(text);
717
+ return e;
718
+ }
719
+ function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
720
+
721
+ function banner(text) {
722
+ var b = $("banner");
723
+ b.textContent = text || "";
724
+ b.hidden = !text;
725
+ }
726
+ function ended() {
727
+ state.ended = true;
728
+ banner("Sessão encerrada; abra o inspetor de novo pela tecla i.");
729
+ if (es) es.close();
730
+ }
731
+
732
+ function api(path, method) {
733
+ return fetch(path, { method: method || "GET", headers: { "X-Duto-Inspetor": TOKEN }, cache: "no-store" }).then(function (r) {
734
+ if (r.status === 403) { ended(); throw new Error("403"); }
735
+ return r.json().then(function (body) { return { status: r.status, body: body }; });
736
+ });
737
+ }
738
+
739
+ function statusInfo(it) {
740
+ if (it.status !== null && it.status !== undefined) {
741
+ var s = it.status;
742
+ var color = s >= 500 ? COLORS.err : s >= 400 ? COLORS.warn : s >= 300 ? COLORS.signal : COLORS.ok;
743
+ return { text: String(s), color: color };
744
+ }
745
+ if (it.error) return { text: "erro", color: COLORS.err };
746
+ return { text: "…", color: COLORS.muted };
747
+ }
748
+ function duration(it) { return it.durationMs === null || it.durationMs === undefined ? "" : it.durationMs + " ms"; }
749
+ function size(n) {
750
+ if (n < 1024) return n + " B";
751
+ if (n < 1024 * 1024) return (n / 1024).toFixed(1).replace(".", ",") + " KB";
752
+ return (n / 1024 / 1024).toFixed(1).replace(".", ",") + " MB";
753
+ }
754
+ function methodOf(it) { return it.websocket ? "WS" : it.method; }
755
+ function header(h, name) {
756
+ for (var k in h) if (k.toLowerCase() === name) return h[k];
757
+ return "";
758
+ }
759
+
760
+ // Mesma regra do filtro da tela do terminal.
761
+ function visible() {
762
+ var f = $("filter").value.trim().toLowerCase();
763
+ if (!f) return state.items;
764
+ return state.items.filter(function (it) {
765
+ return (it.tunnel + " " + (it.websocket ? "ws" : it.method) + " " + it.path + " " + (it.status === null || it.status === undefined ? "" : it.status)).toLowerCase().indexOf(f) >= 0;
766
+ });
767
+ }
768
+
769
+ function renderTop() {
770
+ var n = state.items.length;
771
+ $("count").textContent = n + (n === 1 ? " requisição" : " requisições");
772
+ var box = $("tunnels");
773
+ clear(box);
774
+ state.tunnels.forEach(function (t) {
775
+ var s = el("span");
776
+ s.appendChild(el("span", "k", t.key));
777
+ s.appendChild(document.createTextNode(" " + (t.url || "sem endereço ainda")));
778
+ box.appendChild(s);
779
+ });
780
+ }
781
+
782
+ function renderList() {
783
+ var list = $("list");
784
+ var multi = state.tunnels.length > 1;
785
+ list.classList.toggle("multi", multi);
786
+ clear(list);
787
+ var items = visible();
788
+ if (items.length === 0) {
789
+ list.appendChild(el("div", "empty", state.items.length === 0 ? "Nenhuma requisição ainda." : "Nada casa com o filtro."));
790
+ return;
791
+ }
792
+ items.forEach(function (it) {
793
+ var row = el("div", "row" + (it.id === state.selected ? " sel" : ""));
794
+ row.setAttribute("role", "option");
795
+ row.setAttribute("aria-selected", it.id === state.selected ? "true" : "false");
796
+ row.dataset.id = String(it.id);
797
+ row.appendChild(el("span", "mut", it.replayOf !== undefined ? "↻" : ""));
798
+ if (multi) row.appendChild(el("span", "mut", it.tunnel));
799
+ row.appendChild(el("span", "m", methodOf(it)));
800
+ var p = el("span", "", it.path);
801
+ p.title = it.path;
802
+ row.appendChild(p);
803
+ var st = statusInfo(it);
804
+ var s = el("span", "", st.text);
805
+ s.style.color = st.color;
806
+ row.appendChild(s);
807
+ row.appendChild(el("span", "mut dur", duration(it)));
808
+ row.addEventListener("click", function () { select(it.id); });
809
+ list.appendChild(row);
810
+ });
811
+ }
812
+
813
+ function loadList() {
814
+ var seq = ++listSeq;
815
+ return Promise.all([api("/api/requisicoes"), api("/api/sessao")]).then(function (rs) {
816
+ if (seq !== listSeq) return;
817
+ state.items = rs[0].body.items || [];
818
+ state.tunnels = rs[1].body.tunnels || [];
819
+ renderTop();
820
+ if (state.selected === null && state.items.length > 0) return select(state.items[0].id);
821
+ renderList();
822
+ }).catch(fail);
823
+ }
824
+
825
+ function select(id) {
826
+ if (state.selected !== id) {
827
+ state.selected = id;
828
+ state.detail = null;
829
+ state.detailKey = "";
830
+ hideMsg();
831
+ }
832
+ renderList();
833
+ var row = document.querySelector('.row[data-id="' + id + '"]');
834
+ if (row) row.scrollIntoView({ block: "nearest" });
835
+ return loadDetail(id);
836
+ }
837
+
838
+ function loadDetail(id) {
839
+ var seq = ++detailSeq;
840
+ return api("/api/requisicoes/" + id).then(function (r) {
841
+ if (seq !== detailSeq || state.selected !== id) return;
842
+ var key = JSON.stringify(r.body);
843
+ if (key === state.detailKey) return; // nada mudou: mantém dobras e rolagem
844
+ state.detailKey = key;
845
+ state.detail = r.status === 200 ? r.body : null;
846
+ if (r.status !== 200) renderGone(r.body && r.body.error ? r.body.error : "Essa requisição já saiu da lista.");
847
+ else renderDetail();
848
+ }).catch(fail);
849
+ }
850
+
851
+ function renderGone(text) {
852
+ $("detail").hidden = true;
853
+ $("none").hidden = false;
854
+ $("none").textContent = text;
855
+ }
856
+
857
+ function renderDetail() {
858
+ var d = state.detail;
859
+ if (!d) return;
860
+ $("none").hidden = true;
861
+ $("detail").hidden = false;
862
+
863
+ var head = $("head");
864
+ clear(head);
865
+ head.appendChild(el("b", "", methodOf(d) + " " + d.path));
866
+ head.appendChild(document.createTextNode(" "));
867
+ var st = statusInfo(d);
868
+ var s = el("span", "", st.text);
869
+ s.style.color = st.color;
870
+ head.appendChild(s);
871
+ var parts = [];
872
+ if (duration(d)) parts.push(duration(d));
873
+ parts.push(size(d.bytes));
874
+ parts.push(d.tunnel);
875
+ if (d.replayOf !== undefined) parts.push("↻ reenvio de #" + d.replayOf);
876
+ head.appendChild(el("span", "mut", " · " + parts.join(" · ")));
877
+ if (d.error) head.appendChild(el("div", "msg err", d.error));
878
+
879
+ var tabs = document.querySelectorAll(".tab");
880
+ for (var i = 0; i < tabs.length; i++) {
881
+ var on = tabs[i].dataset.tab === state.tab;
882
+ tabs[i].classList.toggle("on", on);
883
+ tabs[i].setAttribute("aria-selected", on ? "true" : "false");
884
+ }
885
+ renderContent();
886
+ renderButtons();
887
+ }
888
+
889
+ function renderContent() {
890
+ var d = state.detail;
891
+ var box = $("content");
892
+ var top = box.scrollTop;
893
+ clear(box);
894
+ if (state.tab === "Corpo") renderBody(box, d.reqBody, d.reqNote, d.reqTruncatedNote, d.reqHeaders);
895
+ else if (state.tab === "Cabeçalhos") box.appendChild(table(d.reqHeaders));
896
+ else if (state.tab === "Resposta") {
897
+ box.appendChild(el("div", "sect", "Cabeçalhos"));
898
+ box.appendChild(table(d.resHeaders));
899
+ box.appendChild(el("div", "sect", "Corpo"));
900
+ renderBody(box, d.resBody, d.resNote, d.resTruncatedNote, d.resHeaders);
901
+ } else {
902
+ var lines = [d.method + " " + d.path + " HTTP/1.1"];
903
+ for (var k in d.reqHeaders) lines.push(k + ": " + d.reqHeaders[k]);
904
+ lines.push("");
905
+ lines.push(d.reqNote !== null ? d.reqNote : d.reqBody || "");
906
+ box.appendChild(el("pre", "", lines.join("\n")));
907
+ }
908
+ box.scrollTop = top;
909
+ }
910
+
911
+ function table(h) {
912
+ var keys = Object.keys(h || {});
913
+ if (keys.length === 0) return el("div", "note", "Sem cabeçalhos.");
914
+ var t = el("table");
915
+ keys.forEach(function (k) {
916
+ var tr = el("tr");
917
+ tr.appendChild(el("th", "", k));
918
+ tr.appendChild(el("td", "", h[k]));
919
+ t.appendChild(tr);
920
+ });
921
+ return t;
922
+ }
923
+
924
+ function renderBody(box, body, note, truncNote, headers) {
925
+ if (note !== null) {
926
+ box.appendChild(el("div", "note", note));
927
+ return;
928
+ }
929
+ if (body === null || body === undefined) {
930
+ box.appendChild(el("div", "note", "Sem corpo."));
931
+ } else if (header(headers, "content-type").toLowerCase().indexOf("json") >= 0) {
932
+ var err = jsonSyntaxError(body);
933
+ if (err === null) box.appendChild(jsonTree(jsonIndent(body)));
934
+ else {
935
+ box.appendChild(el("div", "jerr", err.message));
936
+ var pre = el("pre");
937
+ var at = Math.min(err.position, Math.max(0, body.length - 1));
938
+ pre.appendChild(document.createTextNode(body.slice(0, at)));
939
+ pre.appendChild(el("span", "bad", body.slice(at, at + 1) || " "));
940
+ pre.appendChild(document.createTextNode(body.slice(at + 1)));
941
+ box.appendChild(pre);
942
+ }
943
+ } else {
944
+ box.appendChild(el("pre", "", body));
945
+ }
946
+ if (truncNote !== null) box.appendChild(el("div", "trunc", truncNote));
947
+ }
948
+
949
+ var KIND = { key: "k", string: "s", number: "n", literal: "n", punct: "", space: "" };
950
+
951
+ // JSON colorido; clicar no { ou [ recolhe para "{ ▸ N campos }".
952
+ function jsonTree(text) {
953
+ var pre = el("pre");
954
+ var stack = [{ node: pre }];
955
+ jsonTokens(text).forEach(function (t) {
956
+ var top = stack[stack.length - 1];
957
+ if (top.wrap && t.kind !== "space" && !(t.kind === "punct" && (t.text === "}" || t.text === "]"))) top.has = true;
958
+ if (t.kind === "punct" && (t.text === "{" || t.text === "[")) {
959
+ var wrap = el("span", "fold");
960
+ var open = el("span", "open", t.text);
961
+ var inner = el("span", "inner");
962
+ var sum = el("span", "sum");
963
+ wrap.appendChild(open);
964
+ wrap.appendChild(inner);
965
+ wrap.appendChild(sum);
966
+ top.node.appendChild(wrap);
967
+ open.addEventListener("click", function () { if (wrap.dataset.n) wrap.classList.toggle("closed"); });
968
+ stack.push({ node: inner, wrap: wrap, open: open, sum: sum, depth: t.depth, obj: t.text === "{", count: 0, has: false });
969
+ return;
970
+ }
971
+ if (t.kind === "punct" && (t.text === "}" || t.text === "]") && stack.length > 1) {
972
+ var c = stack.pop();
973
+ var n = c.has ? c.count + 1 : 0;
974
+ if (n > 0) {
975
+ c.wrap.dataset.n = String(n);
976
+ c.open.title = "recolher ou abrir";
977
+ c.sum.textContent = " ▸ " + n + (c.obj ? (n === 1 ? " campo " : " campos ") : n === 1 ? " item " : " itens ");
978
+ } else c.open.className = "";
979
+ c.wrap.appendChild(document.createTextNode(t.text));
980
+ return;
981
+ }
982
+ if (top.wrap && t.kind === "punct" && t.text === "," && t.depth === top.depth + 1) top.count++;
983
+ var cls = KIND[t.kind];
984
+ top.node.appendChild(cls ? el("span", cls, t.text) : document.createTextNode(t.text));
985
+ });
986
+ return pre;
987
+ }
988
+
989
+ function renderButtons() {
990
+ var d = state.detail;
991
+ var replay = $("replay");
992
+ replay.disabled = d.replayBlocked !== null || replaying;
993
+ replay.title = d.replayBlocked || "";
994
+ replay.parentNode.title = d.replayBlocked || "";
995
+
996
+ var copyBody = $("copyBody");
997
+ var b = copyable(d);
998
+ copyBody.disabled = b.text === null;
999
+ copyBody.title = b.text === null ? b.note || "" : "";
1000
+ copyBody.parentNode.title = copyBody.title;
1001
+
1002
+ var curl = $("copyCurl");
1003
+ curl.disabled = d.curl === null;
1004
+ curl.title = d.curl === null ? d.curlNote || "" : "";
1005
+ curl.parentNode.title = curl.title;
1006
+ var notes = [];
1007
+ if (d.curl !== null && d.curlNote) notes.push(d.curlNote);
1008
+ var tunnel = state.tunnels.filter(function (t) { return t.key === d.tunnel; })[0];
1009
+ if (d.curl !== null && tunnel && tunnel.auth) notes.push("Troque SENHA pela senha do túnel.");
1010
+ $("curlNote").textContent = notes.join(" ");
1011
+ }
1012
+
1013
+ // "Copiar corpo" copia o corpo da aba ativa: a resposta na aba Resposta, senão a requisição.
1014
+ function copyable(d) {
1015
+ return state.tab === "Resposta" ? { text: d.resBody, note: d.resNote } : { text: d.reqBody, note: d.reqNote };
1016
+ }
1017
+
1018
+ function showMsg(text, isErr) {
1019
+ var m = $("msg");
1020
+ m.textContent = text;
1021
+ m.className = "msg" + (isErr ? " err" : "");
1022
+ m.hidden = false;
1023
+ if (msgTimer) clearTimeout(msgTimer);
1024
+ msgTimer = null;
1025
+ }
1026
+ function hideMsg() {
1027
+ if (msgTimer) clearTimeout(msgTimer);
1028
+ msgTimer = null;
1029
+ $("msg").hidden = true;
1030
+ }
1031
+
1032
+ function copy(text) {
1033
+ navigator.clipboard.writeText(text).then(function () {
1034
+ showMsg("Copiado.", false);
1035
+ msgTimer = setTimeout(hideMsg, 2000);
1036
+ }, function () {
1037
+ showMsg("Não deu para copiar.", true);
1038
+ });
1039
+ }
1040
+
1041
+ function replay() {
1042
+ var d = state.detail;
1043
+ if (!d || state.ended) return;
1044
+ if (d.replayBlocked !== null) return showMsg(d.replayBlocked, true);
1045
+ if (replaying) return;
1046
+ replaying = true;
1047
+ $("replay").disabled = true;
1048
+ api("/api/requisicoes/" + d.id + "/reenviar", "POST").then(function (r) {
1049
+ replaying = false;
1050
+ if (state.detail) renderButtons();
1051
+ if (r.body && r.body.ok) {
1052
+ hideMsg();
1053
+ return loadList().then(function () { return select(r.body.id); });
1054
+ }
1055
+ showMsg(r.body && r.body.reason ? r.body.reason : "Não deu para reenviar.", true);
1056
+ }).catch(function (e) {
1057
+ replaying = false;
1058
+ if (state.detail) renderButtons();
1059
+ fail(e);
1060
+ });
1061
+ }
1062
+
1063
+ function move(delta) {
1064
+ var items = visible();
1065
+ if (items.length === 0) return;
1066
+ var i = -1;
1067
+ for (var j = 0; j < items.length; j++) if (items[j].id === state.selected) i = j;
1068
+ var next = i < 0 ? 0 : Math.max(0, Math.min(items.length - 1, i + delta));
1069
+ if (items[next].id !== state.selected) select(items[next].id);
1070
+ }
1071
+
1072
+ function connect() {
1073
+ if (state.ended) return;
1074
+ es = new EventSource("/api/eventos?t=" + encodeURIComponent(TOKEN));
1075
+ es.addEventListener("open", function () {
1076
+ if (state.ended) return;
1077
+ banner("");
1078
+ // Reconectou: o que mudou no meio do caminho não chegou por evento.
1079
+ if (lost) {
1080
+ lost = false;
1081
+ loadList();
1082
+ if (state.selected !== null) loadDetail(state.selected);
1083
+ }
1084
+ });
1085
+ es.addEventListener("error", function () {
1086
+ if (state.ended) return;
1087
+ lost = true;
1088
+ banner("Desconectado da sessão.");
1089
+ // Resposta não 200 (ex.: 403 de outra sessão) fecha o EventSource de vez.
1090
+ if (es.readyState === 2) probe();
1091
+ });
1092
+ es.addEventListener("mudou", function (ev) {
1093
+ var ids = [];
1094
+ try { ids = JSON.parse(ev.data).ids || []; } catch (e) {}
1095
+ loadList();
1096
+ // Lista vazia de ids: algo mudou fora da assinatura; busca o detalhe de novo também.
1097
+ if (state.selected !== null && (ids.length === 0 || ids.indexOf(state.selected) >= 0)) loadDetail(state.selected);
1098
+ });
1099
+ }
1100
+
1101
+ // EventSource fechado: a API diz se a sessão acabou (403 → ended) ou se dá para reconectar.
1102
+ function probe() {
1103
+ api("/api/sessao").then(function () {
1104
+ if (!state.ended) setTimeout(connect, 3000);
1105
+ }, function () {
1106
+ if (!state.ended) setTimeout(connect, 3000);
1107
+ });
1108
+ }
1109
+
1110
+ function init() {
1111
+ var tabs = document.querySelectorAll(".tab");
1112
+ for (var i = 0; i < tabs.length; i++) {
1113
+ tabs[i].addEventListener("click", function (ev) {
1114
+ state.tab = ev.currentTarget.dataset.tab;
1115
+ if (state.detail) renderDetail();
1116
+ });
1117
+ }
1118
+ $("filter").addEventListener("input", renderList);
1119
+ $("replay").addEventListener("click", replay);
1120
+ $("copyBody").addEventListener("click", function () {
1121
+ var b = state.detail ? copyable(state.detail) : { text: null };
1122
+ if (b.text !== null && b.text !== undefined) copy(b.text);
1123
+ });
1124
+ $("copyCurl").addEventListener("click", function () { if (state.detail && state.detail.curl !== null) copy(state.detail.curl); });
1125
+
1126
+ document.addEventListener("keydown", function (ev) {
1127
+ if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
1128
+ var inFilter = document.activeElement === $("filter");
1129
+ if (ev.key === "ArrowDown" || ev.key === "ArrowUp") {
1130
+ ev.preventDefault();
1131
+ move(ev.key === "ArrowDown" ? 1 : -1);
1132
+ } else if (ev.key === "Escape") {
1133
+ if ($("filter").value) {
1134
+ $("filter").value = "";
1135
+ renderList();
1136
+ }
1137
+ if (inFilter) $("filter").blur();
1138
+ } else if (ev.key === "/" && !inFilter) {
1139
+ ev.preventDefault();
1140
+ $("filter").focus();
1141
+ } else if ((ev.key === "r" || ev.key === "R") && !inFilter && !ev.repeat) {
1142
+ replay();
1143
+ }
1144
+ });
1145
+
1146
+ loadList();
1147
+ connect();
1148
+ }
1149
+
1150
+ init();
1151
+ `;
1152
+ function inspectorPage(nonce) {
1153
+ const tabs = ["Corpo", "Cabe\xE7alhos", "Resposta", "Raw"].map((t, i) => `<button type="button" class="tab${i === 0 ? " on" : ""}" role="tab" data-tab="${t}">${t}</button>`).join("");
1154
+ return `<!doctype html>
1155
+ <html lang="pt-BR">
1156
+ <head>
1157
+ <meta charset="utf-8">
1158
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1159
+ <title>duto \xB7 inspetor</title>
1160
+ <link rel="icon" href="data:,">
1161
+ <style nonce="${nonce}">${STYLE}</style>
1162
+ </head>
1163
+ <body>
1164
+ <div class="app">
1165
+ <div class="top"><span><b>\u25B0 duto</b> \xB7 inspetor \xB7 <span id="count">0 requisi\xE7\xF5es</span></span><span class="tunnels" id="tunnels"></span></div>
1166
+ <div class="banner" id="banner" role="status" hidden></div>
1167
+ <div class="main">
1168
+ <div class="side">
1169
+ <input class="search" id="filter" type="search" placeholder="filtrar\u2026" aria-label="Filtrar requisi\xE7\xF5es" autocomplete="off" spellcheck="false">
1170
+ <div class="list" id="list" role="listbox" aria-label="Requisi\xE7\xF5es"></div>
1171
+ </div>
1172
+ <div class="detail">
1173
+ <div class="empty" id="none">Escolha uma requisi\xE7\xE3o na lista.</div>
1174
+ <div class="detail pane" id="detail" hidden>
1175
+ <div class="head" id="head"></div>
1176
+ <div class="tabs" role="tablist">${tabs}</div>
1177
+ <div class="content" id="content"></div>
1178
+ <div class="btns">
1179
+ <span><button type="button" class="btn pri" id="replay">\u21BB Reenviar</button></span>
1180
+ <span><button type="button" class="btn" id="copyBody">Copiar corpo</button></span>
1181
+ <span><button type="button" class="btn" id="copyCurl">Copiar como curl</button></span>
1182
+ <span class="msg" id="curlNote"></span>
1183
+ <span class="msg" id="msg" role="status" hidden></span>
1184
+ </div>
1185
+ </div>
1186
+ </div>
1187
+ </div>
1188
+ </div>
1189
+ <script nonce="${nonce}">${SCRIPT}</script>
1190
+ </body>
1191
+ </html>
1192
+ `;
1193
+ }
1194
+
1195
+ // src/inspector/server.ts
1196
+ import { createHash as createHash2, randomBytes, timingSafeEqual } from "crypto";
1197
+ import { createServer } from "http";
1198
+
1199
+ // src/session/replay.ts
1200
+ var tunnelClosedReason = (tunnel2) => `O t\xFAnel ${tunnel2} n\xE3o est\xE1 aberto nesta sess\xE3o.`;
1201
+ function replayBlock(r) {
1202
+ if (!r) return "Essa requisi\xE7\xE3o j\xE1 saiu da lista.";
1203
+ if (r.websocket) return "WebSocket n\xE3o d\xE1 para reenviar.";
1204
+ if (r.noReplay) return r.noReplay;
1205
+ if (r.access?.kind === "denied") return "Barrada pela senha do t\xFAnel; n\xE3o d\xE1 para reenviar.";
1206
+ if (!r.reqComplete) return "A requisi\xE7\xE3o ainda n\xE3o chegou inteira.";
1207
+ if (r.reqTruncated) return "Corpo acima de 64 KB; n\xE3o d\xE1 para reenviar.";
1208
+ if (r.reqBodyBytes > 0 && r.reqRaw === null) return "Corpo bin\xE1rio n\xE3o \xE9 guardado; n\xE3o d\xE1 para reenviar.";
1209
+ return null;
1210
+ }
1211
+ var DROP = /* @__PURE__ */ new Set(["connection", "keep-alive", "transfer-encoding", "expect", "content-length", "upgrade", "te", "trailer", "proxy-connection"]);
1212
+ function replayHeaders(r) {
1213
+ const out = {};
1214
+ for (const [k, v] of Object.entries(r.reqHeaders)) if (!DROP.has(k.toLowerCase())) out[k] = v;
1215
+ const hadLength = Object.keys(r.reqHeaders).some((k) => k.toLowerCase() === "content-length");
1216
+ if (r.reqBodyBytes > 0 || hadLength) out["content-length"] = String(r.reqRaw?.length ?? 0);
1217
+ return out;
1218
+ }
1219
+
1220
+ // src/inspector/curl.ts
1221
+ var SKIP = /* @__PURE__ */ new Set(["host", "content-length", "transfer-encoding", "connection", "keep-alive", "expect", "upgrade", "te", "trailer", "proxy-connection"]);
1222
+ var TUNNEL_ADDED = /* @__PURE__ */ new Set(["cdn-loop", "x-forwarded-for", "x-forwarded-proto", "x-real-ip", "true-client-ip"]);
1223
+ var skip = (name) => {
1224
+ const k = name.toLowerCase();
1225
+ return SKIP.has(k) || TUNNEL_ADDED.has(k) || k.startsWith("cf-");
1226
+ };
1227
+ function shellQuote(s) {
1228
+ return "'" + s.replace(/'/g, "'\\''") + "'";
1229
+ }
1230
+ function toCurl(r, o) {
1231
+ const parts = [`curl -X ${shellQuote(r.method)} ${shellQuote(o.publicUrl.replace(/\/+$/, "") + r.path)}`];
1232
+ for (const [k, v] of Object.entries(r.reqHeaders)) {
1233
+ if (!skip(k)) parts.push(`-H ${shellQuote(`${k}: ${v}`)}`);
1234
+ }
1235
+ if (o.authUser !== null) parts.push(`-H ${shellQuote(`X-Duto-Auth: ${o.authUser}:SENHA`)}`);
1236
+ if (r.reqBody !== null) parts.push(`--data-binary ${shellQuote(r.reqBody)}`);
1237
+ return parts.join(" \\\n ");
1238
+ }
1239
+
1240
+ // src/inspector/server.ts
1241
+ var DEFAULT_PORT = 4141;
1242
+ var PORT_TRIES = 20;
1243
+ var NO_TOKEN = "Abra o inspetor pela tecla i da tela do duto ou pelo endere\xE7o impresso no terminal.";
1244
+ var GONE = "Essa requisi\xE7\xE3o j\xE1 saiu da lista.";
1245
+ var NO_URL = "O t\xFAnel ainda n\xE3o tem endere\xE7o p\xFAblico.";
1246
+ var CURL_BYTES = "O corpo do curl pode n\xE3o ser id\xEAntico ao recebido (bytes fora de UTF-8).";
1247
+ var sha = (s) => createHash2("sha256").update(s).digest();
1248
+ var same = (a, b) => timingSafeEqual(sha(a), sha(b));
1249
+ var first = (v) => Array.isArray(v) ? v[0] : v;
1250
+ function listItem(r) {
1251
+ return {
1252
+ id: r.id,
1253
+ tunnel: r.tunnel,
1254
+ method: r.method,
1255
+ path: r.path,
1256
+ status: r.status,
1257
+ durationMs: r.durationMs,
1258
+ bytes: r.bytes,
1259
+ startedAt: r.startedAt,
1260
+ websocket: r.websocket,
1261
+ ...r.replayOf !== void 0 && { replayOf: r.replayOf },
1262
+ ...r.error !== void 0 && { error: r.error }
1263
+ };
1264
+ }
1265
+ function detail(r, session) {
1266
+ const tunnel2 = session.getState().tunnels.find((t) => t.key === r.tunnel);
1267
+ const tunnelOpen = !!tunnel2 && (tunnel2.status === "live" || tunnel2.status === "no-listener");
1268
+ let curl = null;
1269
+ let curlNote = null;
1270
+ if (!tunnel2?.url) curlNote = NO_URL;
1271
+ else {
1272
+ curl = toCurl(r, { publicUrl: tunnel2.url, authUser: session.authUser(r.tunnel) });
1273
+ if (r.reqRaw && !Buffer.from(r.reqBody ?? "", "utf8").equals(r.reqRaw)) curlNote = CURL_BYTES;
1274
+ }
1275
+ return {
1276
+ ...listItem(r),
1277
+ reqHeaders: r.reqHeaders,
1278
+ resHeaders: r.resHeaders,
1279
+ reqBody: r.reqBody,
1280
+ resBody: r.resBody,
1281
+ reqNote: bodyNote(r, "req"),
1282
+ resNote: bodyNote(r, "res"),
1283
+ reqTruncatedNote: r.reqTruncated ? TRUNCATED_NOTE : null,
1284
+ resTruncatedNote: r.resTruncated ? TRUNCATED_NOTE : null,
1285
+ replayBlocked: replayBlock(r) ?? (tunnelOpen ? null : tunnelClosedReason(r.tunnel)),
1286
+ curl,
1287
+ curlNote
1288
+ };
1289
+ }
1290
+ var sig = (r) => `${r.status}|${r.durationMs}|${r.bytes}|${r.resBody?.length ?? -1}|${r.reqComplete}|${r.error ?? ""}|${Object.keys(r.resHeaders).length > 0}`;
1291
+ async function startInspector(o) {
1292
+ const { session } = o;
1293
+ const eventMs = o.eventMs ?? 150;
1294
+ const pingMs = o.pingMs ?? 25e3;
1295
+ const token = randomBytes(24).toString("base64url");
1296
+ let port = 0;
1297
+ const clients = /* @__PURE__ */ new Set();
1298
+ let seen = /* @__PURE__ */ new Map();
1299
+ let timer = null;
1300
+ const prime = () => seen = new Map(session.records.list().map((r) => [r.id, sig(r)]));
1301
+ const flush = () => {
1302
+ timer = null;
1303
+ const next = /* @__PURE__ */ new Map();
1304
+ const ids = [];
1305
+ for (const r of session.records.list()) {
1306
+ const s = sig(r);
1307
+ next.set(r.id, s);
1308
+ if (seen.get(r.id) !== s) ids.push(r.id);
1309
+ }
1310
+ seen = next;
1311
+ const msg = `event: mudou
1312
+ data: ${JSON.stringify({ ids })}
1313
+
1314
+ `;
1315
+ for (const c of clients) c.write(msg);
1316
+ };
1317
+ const schedule = () => {
1318
+ if (clients.size > 0 && !timer) timer = setTimeout(flush, eventMs);
1319
+ };
1320
+ const unsubs = [session.records.subscribe(schedule), session.subscribe(schedule)];
1321
+ const pinger = setInterval(() => {
1322
+ for (const c of clients) c.write(": ping\n\n");
1323
+ }, pingMs);
1324
+ pinger.unref();
1325
+ const base = (res) => {
1326
+ res.setHeader("Cache-Control", "no-store");
1327
+ res.setHeader("X-Content-Type-Options", "nosniff");
1328
+ res.setHeader("Referrer-Policy", "no-referrer");
1329
+ res.setHeader("X-Frame-Options", "DENY");
1330
+ };
1331
+ const text = (res, status, body) => {
1332
+ res.writeHead(status, { "Content-Type": "text/plain; charset=utf-8" });
1333
+ res.end(body);
1334
+ };
1335
+ const json = (res, status, body) => {
1336
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
1337
+ res.end(JSON.stringify(body));
1338
+ };
1339
+ const handle = (req, res) => {
1340
+ base(res);
1341
+ const host = first(req.headers.host)?.toLowerCase();
1342
+ if (host !== `127.0.0.1:${port}` && host !== `localhost:${port}`) return text(res, 403, "Host n\xE3o permitido.");
1343
+ let url2;
1344
+ try {
1345
+ url2 = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
1346
+ } catch {
1347
+ return text(res, 404, "N\xE3o encontrado.");
1348
+ }
1349
+ const path = url2.pathname;
1350
+ const method = req.method ?? "GET";
1351
+ const queryToken = url2.searchParams.get("t") ?? "";
1352
+ const headerToken = first(req.headers["x-duto-inspetor"]) ?? "";
1353
+ if (method === "GET" && path === "/") {
1354
+ if (!same(queryToken, token)) return text(res, 403, NO_TOKEN);
1355
+ const nonce = randomBytes(16).toString("base64url");
1356
+ res.writeHead(200, {
1357
+ "Content-Type": "text/html; charset=utf-8",
1358
+ "Content-Security-Policy": `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`
1359
+ });
1360
+ return void res.end(o.page(nonce));
1361
+ }
1362
+ const isEvents = method === "GET" && path === "/api/eventos";
1363
+ const detailId = /^\/api\/requisicoes\/([1-9]\d{0,15})(\/reenviar)?$/.exec(path);
1364
+ const known = method === "GET" && (path === "/api/sessao" || path === "/api/requisicoes" || detailId && !detailId[2]) || isEvents || method === "POST" && detailId && detailId[2];
1365
+ if (!known) return text(res, 404, "N\xE3o encontrado.");
1366
+ if (!same(headerToken, token) && !(isEvents && same(queryToken, token))) return json(res, 403, { error: NO_TOKEN });
1367
+ if (isEvents) {
1368
+ res.writeHead(200, { "Content-Type": "text/event-stream; charset=utf-8", Connection: "keep-alive" });
1369
+ res.write(": duto\n\n");
1370
+ if (clients.size === 0) prime();
1371
+ clients.add(res);
1372
+ res.on("close", () => clients.delete(res));
1373
+ return;
1374
+ }
1375
+ if (path === "/api/sessao") {
1376
+ return json(res, 200, { tunnels: session.getState().tunnels.map((t) => ({ key: t.key, url: t.url, auth: !!t.auth })) });
1377
+ }
1378
+ if (path === "/api/requisicoes") return json(res, 200, { items: session.records.list().map(listItem) });
1379
+ const id = Number(detailId[1]);
1380
+ if (!Number.isSafeInteger(id)) return json(res, 404, { error: GONE });
1381
+ if (method === "POST") return json(res, 200, session.replay(id));
1382
+ const r = session.records.get(id);
1383
+ if (!r) return json(res, 404, { error: GONE });
1384
+ return json(res, 200, detail(r, session));
1385
+ };
1386
+ const server = createServer((req, res) => {
1387
+ try {
1388
+ handle(req, res);
1389
+ } catch {
1390
+ if (!res.headersSent) text(res, 500, "Erro interno.");
1391
+ else res.end();
1392
+ }
1393
+ });
1394
+ const listen = (p) => new Promise((resolve, reject) => {
1395
+ const onError = (e) => reject(e);
1396
+ server.once("error", onError);
1397
+ server.listen(p, "127.0.0.1", () => {
1398
+ server.off("error", onError);
1399
+ resolve();
1400
+ });
1401
+ });
1402
+ const want = o.port ?? DEFAULT_PORT;
1403
+ try {
1404
+ let bound = false;
1405
+ if (want !== 0) {
1406
+ for (let p = want; p <= want + PORT_TRIES && !bound; p++) {
1407
+ try {
1408
+ await listen(p);
1409
+ bound = true;
1410
+ } catch (e) {
1411
+ if (e.code !== "EADDRINUSE") throw e;
1412
+ }
1413
+ }
1414
+ }
1415
+ if (!bound) await listen(0);
1416
+ } catch (e) {
1417
+ for (const u of unsubs) u();
1418
+ clearInterval(pinger);
1419
+ throw e;
1420
+ }
1421
+ port = server.address().port;
1422
+ const url = `http://127.0.0.1:${port}`;
1423
+ return {
1424
+ port,
1425
+ url,
1426
+ openUrl: `${url}/?t=${token}`,
1427
+ close: () => new Promise((resolve) => {
1428
+ for (const u of unsubs) u();
1429
+ clearInterval(pinger);
1430
+ if (timer) clearTimeout(timer);
1431
+ for (const c of clients) c.end();
1432
+ clients.clear();
1433
+ server.close(() => resolve());
1434
+ server.closeAllConnections();
1435
+ })
1436
+ };
1437
+ }
1438
+
520
1439
  // src/session/session.ts
521
1440
  import { spawn } from "child_process";
522
1441
 
@@ -540,11 +1459,11 @@ async function isPortListening(port, timeoutMs = 500) {
540
1459
  }
541
1460
 
542
1461
  // src/session/auth.ts
543
- import { createHash as createHash2, createHmac, randomBytes, timingSafeEqual } from "crypto";
1462
+ import { createHash as createHash3, createHmac, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
544
1463
  var TUNNEL_COOKIE = "__Host-duto_tunel";
545
1464
  var QR_TTL_MS = 5 * 6e4;
546
- var sha = (s) => createHash2("sha256").update(s).digest();
547
- var same = (a, b) => timingSafeEqual(sha(a), sha(b));
1465
+ var sha2 = (s) => createHash3("sha256").update(s).digest();
1466
+ var same2 = (a, b) => timingSafeEqual2(sha2(a), sha2(b));
548
1467
  function splitPair(raw) {
549
1468
  const i = raw.indexOf(":");
550
1469
  return i < 0 ? null : { user: raw.slice(0, i), password: raw.slice(i + 1) };
@@ -553,11 +1472,11 @@ function parseBasic(value) {
553
1472
  const m = /^basic +([A-Za-z0-9+/]+={0,2})$/i.exec(value?.trim() ?? "");
554
1473
  return m ? splitPair(Buffer.from(m[1], "base64").toString("utf8")) : null;
555
1474
  }
556
- var first = (v) => Array.isArray(v) ? v[0] : v;
1475
+ var first2 = (v) => Array.isArray(v) ? v[0] : v;
557
1476
  var bareHost = (s) => s.replace(/:\d+$/, "").toLowerCase();
558
1477
  function sameHostOrigin(h) {
559
- if (first(h["sec-fetch-site"])?.toLowerCase() === "same-site") return false;
560
- const origin = first(h.origin);
1478
+ if (first2(h["sec-fetch-site"])?.toLowerCase() === "same-site") return false;
1479
+ const origin = first2(h.origin);
561
1480
  if (origin === void 0) return true;
562
1481
  let host;
563
1482
  try {
@@ -580,27 +1499,27 @@ var TunnelAuth = class {
580
1499
  this.credentials = o.credentials;
581
1500
  this.allow = o.allow;
582
1501
  this.now = o.now ?? Date.now;
583
- this.secret = o.secret ?? randomBytes(32);
1502
+ this.secret = o.secret ?? randomBytes2(32);
584
1503
  this.qrUser = o.credentials[0]?.user ?? "";
585
1504
  }
586
1505
  access(h) {
587
1506
  if (this.allow) {
588
- const ip = first(h["cf-connecting-ip"]);
1507
+ const ip = first2(h["cf-connecting-ip"]);
589
1508
  const reason = this.allow.match(ip);
590
1509
  if (reason) return { kind: "allow", reason };
591
1510
  }
592
1511
  const cookieUser = this.cookieUser(h.cookie);
593
1512
  if (cookieUser && sameHostOrigin(h)) return { kind: "user", user: cookieUser };
594
- const duto = first(h["x-duto-auth"]);
595
- const cand = duto !== void 0 ? splitPair(duto) : parseBasic(first(h.authorization));
1513
+ const duto = first2(h["x-duto-auth"]);
1514
+ const cand = duto !== void 0 ? splitPair(duto) : parseBasic(first2(h.authorization));
596
1515
  if (cand && this.checkPassword(cand.user, cand.password)) return { kind: "user", user: cand.user };
597
1516
  return { kind: "denied" };
598
1517
  }
599
1518
  checkPassword(user, password) {
600
1519
  let ok2 = false;
601
1520
  for (const c of this.credentials) {
602
- const userOk = same(c.user, user);
603
- const passOk = same(c.password, password);
1521
+ const userOk = same2(c.user, user);
1522
+ const passOk = same2(c.password, password);
604
1523
  if (userOk && passOk) ok2 = true;
605
1524
  }
606
1525
  return ok2;
@@ -612,19 +1531,19 @@ var TunnelAuth = class {
612
1531
  return `${TUNNEL_COOKIE}=; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Path=/`;
613
1532
  }
614
1533
  issueQrToken() {
615
- this.qr = { token: randomBytes(24).toString("base64url"), expiresAt: this.now() + QR_TTL_MS };
1534
+ this.qr = { token: randomBytes2(24).toString("base64url"), expiresAt: this.now() + QR_TTL_MS };
616
1535
  return { ...this.qr };
617
1536
  }
618
1537
  consumeQrToken(token) {
619
1538
  const qr = this.qr;
620
- if (!qr || !same(qr.token, token)) return false;
1539
+ if (!qr || !same2(qr.token, token)) return false;
621
1540
  this.qr = null;
622
1541
  return this.now() <= qr.expiresAt;
623
1542
  }
624
1543
  strip(h) {
625
1544
  const out = { ...h };
626
1545
  delete out["x-duto-auth"];
627
- const basic = parseBasic(first(h.authorization));
1546
+ const basic = parseBasic(first2(h.authorization));
628
1547
  if (basic && this.checkPassword(basic.user, basic.password)) delete out.authorization;
629
1548
  if (h.cookie !== void 0) {
630
1549
  const parts = (Array.isArray(h.cookie) ? h.cookie.join("; ") : h.cookie).split(";").map((p) => p.trim()).filter(Boolean);
@@ -648,10 +1567,10 @@ var TunnelAuth = class {
648
1567
  for (const p of list) {
649
1568
  const item = p.trim();
650
1569
  if (!item.startsWith(`${TUNNEL_COOKIE}=`)) continue;
651
- const [b64, sig, ...resto] = item.slice(TUNNEL_COOKIE.length + 1).split(".");
652
- if (!b64 || !sig || resto.length) continue;
1570
+ const [b64, sig2, ...resto] = item.slice(TUNNEL_COOKIE.length + 1).split(".");
1571
+ if (!b64 || !sig2 || resto.length) continue;
653
1572
  const user = Buffer.from(b64, "base64url").toString("utf8");
654
- if (same(sig, this.sign(user)) && this.credentials.some((c) => c.user === user)) return user;
1573
+ if (same2(sig2, this.sign(user)) && this.credentials.some((c) => c.user === user)) return user;
655
1574
  }
656
1575
  return null;
657
1576
  }
@@ -682,7 +1601,7 @@ function cors401(origin) {
682
1601
  }
683
1602
 
684
1603
  // src/session/proxy.ts
685
- import { Agent, createServer, request } from "http";
1604
+ import { Agent, createServer as createServer2, request } from "http";
686
1605
  import { connect as connect2 } from "net";
687
1606
 
688
1607
  // src/session/errorPage.ts
@@ -787,30 +1706,10 @@ ${o.error ? `<p class="error" role="alert">Usu\xE1rio ou senha incorretos.</p>
787
1706
  );
788
1707
  }
789
1708
 
790
- // src/session/replay.ts
791
- function replayBlock(r) {
792
- if (!r) return "Essa requisi\xE7\xE3o j\xE1 saiu da lista.";
793
- if (r.websocket) return "WebSocket n\xE3o d\xE1 para reenviar.";
794
- if (r.noReplay) return r.noReplay;
795
- if (r.access?.kind === "denied") return "Barrada pela senha do t\xFAnel; n\xE3o d\xE1 para reenviar.";
796
- if (!r.reqComplete) return "A requisi\xE7\xE3o ainda n\xE3o chegou inteira.";
797
- if (r.reqTruncated) return "Corpo acima de 64 KB; n\xE3o d\xE1 para reenviar.";
798
- if (r.reqBodyBytes > 0 && r.reqRaw === null) return "Corpo bin\xE1rio n\xE3o \xE9 guardado; n\xE3o d\xE1 para reenviar.";
799
- return null;
800
- }
801
- var DROP = /* @__PURE__ */ new Set(["connection", "keep-alive", "transfer-encoding", "expect", "content-length", "upgrade", "te", "trailer", "proxy-connection"]);
802
- function replayHeaders(r) {
803
- const out = {};
804
- for (const [k, v] of Object.entries(r.reqHeaders)) if (!DROP.has(k.toLowerCase())) out[k] = v;
805
- const hadLength = Object.keys(r.reqHeaders).some((k) => k.toLowerCase() === "content-length");
806
- if (r.reqBodyBytes > 0 || hadLength) out["content-length"] = String(r.reqRaw?.length ?? 0);
807
- return out;
808
- }
809
-
810
1709
  // src/session/proxy.ts
811
1710
  var DENIED_NO_REPLAY = "Barrada pela senha do t\xFAnel; n\xE3o d\xE1 para reenviar.";
812
1711
  var PREFLIGHT_NO_REPLAY = "Preflight de CORS n\xE3o d\xE1 para reenviar.";
813
- var TEXT = /^(text\/|application\/(json|xml|javascript|x-www-form-urlencoded|graphql)|[^;]*\+(json|xml))/i;
1712
+ var TEXT2 = /^(text\/|application\/(json|xml|javascript|x-www-form-urlencoded|graphql)|[^;]*\+(json|xml))/i;
814
1713
  var MAX_BODY = 64 * 1024;
815
1714
  var LOCAL_AGENT = new Agent({ keepAlive: false });
816
1715
  function flat(h) {
@@ -1043,7 +1942,7 @@ async function startProxy(o) {
1043
1942
  return { id, started };
1044
1943
  }
1045
1944
  function trackResponse(id, started, up, reqCap) {
1046
- const resCap = new Capture(TEXT.test(up.headers["content-type"] ?? ""), max);
1945
+ const resCap = new Capture(TEXT2.test(up.headers["content-type"] ?? ""), max);
1047
1946
  let bytes = 0;
1048
1947
  up.on("data", (c) => {
1049
1948
  bytes += c.length;
@@ -1068,7 +1967,7 @@ async function startProxy(o) {
1068
1967
  const blocked = replayBlock(r);
1069
1968
  if (blocked || !r) return { ok: false, reason: blocked ?? "Essa requisi\xE7\xE3o j\xE1 saiu da lista." };
1070
1969
  const route = [...o.routes.values()].find((x) => x.tunnel === r.tunnel);
1071
- if (!route) return { ok: false, reason: `O t\xFAnel ${r.tunnel} n\xE3o est\xE1 aberto nesta sess\xE3o.` };
1970
+ if (!route) return { ok: false, reason: tunnelClosedReason(r.tunnel) };
1072
1971
  const newId = nextId++;
1073
1972
  const started = Date.now();
1074
1973
  const headers = replayHeaders(r);
@@ -1102,7 +2001,7 @@ async function startProxy(o) {
1102
2001
  })();
1103
2002
  return { ok: true, id: newId };
1104
2003
  }
1105
- const server = createServer(async (req, res) => {
2004
+ const server = createServer2(async (req, res) => {
1106
2005
  req.pause();
1107
2006
  const route = await routeFor(req);
1108
2007
  if (!route) {
@@ -1131,7 +2030,7 @@ async function startProxy(o) {
1131
2030
  return;
1132
2031
  }
1133
2032
  const { id, started } = record(req, route, headers, false, access, preflight ? PREFLIGHT_NO_REPLAY : void 0);
1134
- const reqCap = new Capture(TEXT.test(req.headers["content-type"] ?? ""), max);
2033
+ const reqCap = new Capture(TEXT2.test(req.headers["content-type"] ?? ""), max);
1135
2034
  req.pause();
1136
2035
  const host = await localHost(route.port);
1137
2036
  const upstream = request(
@@ -1201,8 +2100,8 @@ async function startProxy(o) {
1201
2100
  target.write(`${raw}\r
1202
2101
  `);
1203
2102
  if (head.length) target.write(head);
1204
- target.once("data", (first2) => {
1205
- const status = Number(/^HTTP\/\d\.\d (\d{3})/.exec(first2.toString("latin1"))?.[1] ?? 0) || null;
2103
+ target.once("data", (first3) => {
2104
+ const status = Number(/^HTTP\/\d\.\d (\d{3})/.exec(first3.toString("latin1"))?.[1] ?? 0) || null;
1206
2105
  o.store.update(id, { status, durationMs: Date.now() - started });
1207
2106
  });
1208
2107
  target.pipe(client);
@@ -1420,6 +2319,10 @@ var Session = class {
1420
2319
  const { token, expiresAt } = auth.issueQrToken();
1421
2320
  return { url: `${t.url}/__duto/qr/${token}`, expiresAt };
1422
2321
  }
2322
+ /** Primeiro usuário configurado do túnel, ou null quando ele não tem senha. */
2323
+ authUser(key) {
2324
+ return this.auths.get(key)?.qrUser ?? null;
2325
+ }
1423
2326
  /** Cria o TunnelAuth na primeira abertura (precisa do allow resolvido) e o mantém depois. */
1424
2327
  async authFor(key) {
1425
2328
  const existing = this.auths.get(key);
@@ -1687,11 +2590,24 @@ async function runSession(specs, source, flags) {
1687
2590
  fail2(err instanceof Error ? err.message : String(err));
1688
2591
  }
1689
2592
  const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname(), cloudflaredPath, source, runDir: join6(dutoHome(), "run") });
1690
- if (wantsUi(flags)) {
1691
- const { runFullscreen } = await import("./fullscreen-7P5GBCQX.js");
1692
- process.exit(await runFullscreen(session, specs, "0.6.0"));
2593
+ let inspector = null;
2594
+ let inspectorError;
2595
+ if (flags.inspect !== false) {
2596
+ try {
2597
+ inspector = await startInspector({ session, page: inspectorPage });
2598
+ } catch (err) {
2599
+ inspectorError = err instanceof Error ? err.message : String(err);
2600
+ }
1693
2601
  }
1694
- console.log(`${mark()} ${paint("muted", `${"0.6.0"} \xB7 ${source} \xB7 Ctrl+C para encerrar`)}`);
2602
+ if (wantsUi(flags)) {
2603
+ const { runFullscreen } = await import("./fullscreen-YK365XSO.js");
2604
+ const code = await runFullscreen(session, specs, "0.7.0", inspector, inspectorError);
2605
+ await inspector?.close().catch(() => void 0);
2606
+ process.exit(code);
2607
+ }
2608
+ console.log(`${mark()} ${paint("muted", `${"0.7.0"} \xB7 ${source} \xB7 Ctrl+C para encerrar`)}`);
2609
+ if (inspector) console.log(paint("muted", `inspetor: ${inspector.openUrl}`));
2610
+ if (inspectorError) console.log(warn(`inspetor n\xE3o subiu: ${inspectorError}`));
1695
2611
  const stopLog = runPlain(session, (l) => console.log(l));
1696
2612
  const stopped = new Promise((resolve) => {
1697
2613
  const onSignal = () => {
@@ -1708,6 +2624,7 @@ async function runSession(specs, source, flags) {
1708
2624
  await stopped;
1709
2625
  stopLog();
1710
2626
  await session.stop();
2627
+ await inspector?.close().catch(() => void 0);
1711
2628
  process.exit(0);
1712
2629
  }
1713
2630
  async function tunnel(rawPort, flags) {
@@ -1745,8 +2662,8 @@ ${paint("signal", "Dica:")} crie um duto.yml com os t\xFAneis do projeto e rode
1745
2662
  return runSession([{ name, port, ...host ? { host } : {}, ...keepRedirects ? { rewriteRedirects: false } : {}, ...auth.length ? { auth } : {}, ...allow.length ? { allow } : {} }], "linha de comando", flags);
1746
2663
  }
1747
2664
  var cli = cac("duto");
1748
- cli.command("[port]", "Sem porta: abre os t\xFAneis do duto.yml. Com porta: abre um t\xFAnel para ela").option("--name <nome>", "Nome do endere\xE7o fixo (ex.: web \u2192 https://web-<usu\xE1rio>.duto.dev)").option("--host <host>", "Host que o servi\xE7o local recebe no lugar do p\xFAblico (ex.: meuapp.test)").option("--no-rewrite-redirects", "Com --host: n\xE3o troca o host local pelo p\xFAblico nos redirecionamentos").option("--auth <usuario:senha>", "Pede usu\xE1rio e senha para abrir o endere\xE7o (repet\xEDvel)").option("--permitir <faixa>", "Com --auth: IP, faixa (CIDR) ou nome (stripe, github) que entra sem senha (repet\xEDvel)").option("--no-ui", "Sem tela cheia: s\xF3 um log das requisi\xE7\xF5es (\xFAtil em scripts)").option("--ui", "For\xE7a a tela cheia (no Windows ela vem desligada)").action((port, opts) => tunnel(port, opts));
1749
- cli.command("http <port>", "O mesmo que duto <porta> (para quem vem do ngrok)").option("--name <nome>", "Nome do endere\xE7o fixo").option("--host <host>", "Host que o servi\xE7o local recebe no lugar do p\xFAblico").option("--no-rewrite-redirects", "Com --host: mant\xE9m os redirecionamentos como o servi\xE7o mandou").option("--auth <usuario:senha>", "Pede usu\xE1rio e senha para abrir o endere\xE7o (repet\xEDvel)").option("--permitir <faixa>", "Com --auth: IP, faixa (CIDR) ou nome (stripe, github) que entra sem senha (repet\xEDvel)").option("--no-ui", "Sem tela cheia").action((port, opts) => tunnel(port, opts));
2665
+ cli.command("[port]", "Sem porta: abre os t\xFAneis do duto.yml. Com porta: abre um t\xFAnel para ela").option("--name <nome>", "Nome do endere\xE7o fixo (ex.: web \u2192 https://web-<usu\xE1rio>.duto.dev)").option("--host <host>", "Host que o servi\xE7o local recebe no lugar do p\xFAblico (ex.: meuapp.test)").option("--no-rewrite-redirects", "Com --host: n\xE3o troca o host local pelo p\xFAblico nos redirecionamentos").option("--auth <usuario:senha>", "Pede usu\xE1rio e senha para abrir o endere\xE7o (repet\xEDvel)").option("--permitir <faixa>", "Com --auth: IP, faixa (CIDR) ou nome (stripe, github) que entra sem senha (repet\xEDvel)").option("--no-ui", "Sem tela cheia: s\xF3 um log das requisi\xE7\xF5es (\xFAtil em scripts)").option("--no-inspect", "N\xE3o sobe o inspetor no navegador (127.0.0.1:4141)").option("--ui", "For\xE7a a tela cheia (no Windows ela vem desligada)").action((port, opts) => tunnel(port, opts));
2666
+ cli.command("http <port>", "O mesmo que duto <porta> (para quem vem do ngrok)").option("--name <nome>", "Nome do endere\xE7o fixo").option("--host <host>", "Host que o servi\xE7o local recebe no lugar do p\xFAblico").option("--no-rewrite-redirects", "Com --host: mant\xE9m os redirecionamentos como o servi\xE7o mandou").option("--auth <usuario:senha>", "Pede usu\xE1rio e senha para abrir o endere\xE7o (repet\xEDvel)").option("--permitir <faixa>", "Com --auth: IP, faixa (CIDR) ou nome (stripe, github) que entra sem senha (repet\xEDvel)").option("--no-ui", "Sem tela cheia").option("--no-inspect", "N\xE3o sobe o inspetor no navegador (127.0.0.1:4141)").action((port, opts) => tunnel(port, opts));
1750
2667
  cli.command("login <token>", "Salva o authtoken (pegue em https://duto.dev)").action(async (token) => {
1751
2668
  try {
1752
2669
  const me = await createApiClient({ baseUrl: apiUrl(), token: String(token) }).me();
@@ -1785,7 +2702,7 @@ cli.help(
1785
2702
  body: s.body.replace(/^\s*For more info, run any command with the `--help` flag:/m, " Mais detalhes de um comando: duto <comando> --help").replace("Display this message", "Mostra esta ajuda").replace("Display version number", "Mostra a vers\xE3o")
1786
2703
  }))
1787
2704
  );
1788
- cli.version("0.6.0");
2705
+ cli.version("0.7.0");
1789
2706
  try {
1790
2707
  cli.parse(process.argv, { run: false });
1791
2708
  await cli.runMatchedCommand();