@dutodev/cli 0.5.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
@@ -788,7 +1707,9 @@ ${o.error ? `<p class="error" role="alert">Usu\xE1rio ou senha incorretos.</p>
788
1707
  }
789
1708
 
790
1709
  // src/session/proxy.ts
791
- var TEXT = /^(text\/|application\/(json|xml|javascript|x-www-form-urlencoded|graphql)|[^;]*\+(json|xml))/i;
1710
+ var DENIED_NO_REPLAY = "Barrada pela senha do t\xFAnel; n\xE3o d\xE1 para reenviar.";
1711
+ var PREFLIGHT_NO_REPLAY = "Preflight de CORS n\xE3o d\xE1 para reenviar.";
1712
+ var TEXT2 = /^(text\/|application\/(json|xml|javascript|x-www-form-urlencoded|graphql)|[^;]*\+(json|xml))/i;
792
1713
  var MAX_BODY = 64 * 1024;
793
1714
  var LOCAL_AGENT = new Agent({ keepAlive: false });
794
1715
  function flat(h) {
@@ -805,8 +1726,11 @@ var Capture = class {
805
1726
  max;
806
1727
  chunks = [];
807
1728
  size = 0;
1729
+ /** Bytes vistos, capturados ou não. */
1730
+ total = 0;
808
1731
  truncated = false;
809
1732
  push(c) {
1733
+ this.total += c.length;
810
1734
  if (!this.enabled) return;
811
1735
  const room = this.max - this.size;
812
1736
  if (room <= 0) {
@@ -818,6 +1742,9 @@ var Capture = class {
818
1742
  this.chunks.push(part);
819
1743
  this.size += part.length;
820
1744
  }
1745
+ raw() {
1746
+ return this.enabled && this.size > 0 ? Buffer.concat(this.chunks) : null;
1747
+ }
821
1748
  text() {
822
1749
  return this.enabled && this.size > 0 ? Buffer.concat(this.chunks).toString("utf8") : null;
823
1750
  }
@@ -986,7 +1913,7 @@ async function startProxy(o) {
986
1913
  await new Promise((r) => setTimeout(r, 50));
987
1914
  }
988
1915
  }
989
- function record(req, route, headers, websocket, access) {
1916
+ function record(req, route, headers, websocket, access, noReplay) {
990
1917
  const id = nextId++;
991
1918
  const started = Date.now();
992
1919
  o.store.add({
@@ -1003,12 +1930,78 @@ async function startProxy(o) {
1003
1930
  resHeaders: {},
1004
1931
  reqBody: null,
1005
1932
  resBody: null,
1933
+ reqRaw: null,
1934
+ reqBodyBytes: 0,
1935
+ reqTruncated: false,
1936
+ reqComplete: false,
1006
1937
  truncated: false,
1007
- ...access && { access }
1938
+ resTruncated: false,
1939
+ ...access && { access },
1940
+ ...noReplay && { noReplay }
1008
1941
  });
1009
1942
  return { id, started };
1010
1943
  }
1011
- const server = createServer(async (req, res) => {
1944
+ function trackResponse(id, started, up, reqCap) {
1945
+ const resCap = new Capture(TEXT2.test(up.headers["content-type"] ?? ""), max);
1946
+ let bytes = 0;
1947
+ up.on("data", (c) => {
1948
+ bytes += c.length;
1949
+ resCap.push(c);
1950
+ });
1951
+ up.on(
1952
+ "end",
1953
+ () => o.store.update(id, {
1954
+ status: up.statusCode ?? null,
1955
+ durationMs: Date.now() - started,
1956
+ bytes,
1957
+ resHeaders: flat(up.headers),
1958
+ resBody: resCap.text(),
1959
+ truncated: !!reqCap?.truncated || resCap.truncated,
1960
+ resTruncated: resCap.truncated,
1961
+ ...reqCap && { reqBody: reqCap.text(), reqRaw: reqCap.truncated ? null : reqCap.raw(), reqBodyBytes: reqCap.total, reqTruncated: reqCap.truncated }
1962
+ })
1963
+ );
1964
+ }
1965
+ function replay(id) {
1966
+ const r = o.store.get(id);
1967
+ const blocked = replayBlock(r);
1968
+ if (blocked || !r) return { ok: false, reason: blocked ?? "Essa requisi\xE7\xE3o j\xE1 saiu da lista." };
1969
+ const route = [...o.routes.values()].find((x) => x.tunnel === r.tunnel);
1970
+ if (!route) return { ok: false, reason: tunnelClosedReason(r.tunnel) };
1971
+ const newId = nextId++;
1972
+ const started = Date.now();
1973
+ const headers = replayHeaders(r);
1974
+ o.store.add({
1975
+ id: newId,
1976
+ tunnel: r.tunnel,
1977
+ method: r.method,
1978
+ path: r.path,
1979
+ status: null,
1980
+ durationMs: null,
1981
+ bytes: 0,
1982
+ startedAt: started,
1983
+ websocket: false,
1984
+ reqHeaders: headers,
1985
+ resHeaders: {},
1986
+ reqBody: r.reqBody,
1987
+ resBody: null,
1988
+ reqRaw: r.reqRaw,
1989
+ reqBodyBytes: r.reqBodyBytes,
1990
+ reqTruncated: false,
1991
+ reqComplete: true,
1992
+ truncated: false,
1993
+ resTruncated: false,
1994
+ replayOf: r.id
1995
+ });
1996
+ void (async () => {
1997
+ const host = await localHost(route.port);
1998
+ const upstream = request({ host, port: route.port, agent: LOCAL_AGENT, method: r.method, path: r.path, headers }, (up) => trackResponse(newId, started, up));
1999
+ upstream.on("error", (err) => o.store.update(newId, { status: 503, durationMs: Date.now() - started, error: err.message || err.code || err.name }));
2000
+ upstream.end(r.reqRaw ?? void 0);
2001
+ })();
2002
+ return { ok: true, id: newId };
2003
+ }
2004
+ const server = createServer2(async (req, res) => {
1012
2005
  req.pause();
1013
2006
  const route = await routeFor(req);
1014
2007
  if (!route) {
@@ -1031,13 +2024,13 @@ async function startProxy(o) {
1031
2024
  const headers = delivered(req.headers, route);
1032
2025
  if (access?.kind === "denied") {
1033
2026
  const html = wantsHtml(req.headers.accept);
1034
- const { id: id2, started: started2 } = record(req, route, headers, false, html ? void 0 : access);
2027
+ const { id: id2, started: started2 } = record(req, route, headers, false, html ? void 0 : access, DENIED_NO_REPLAY);
1035
2028
  const { status, out, body } = deny(req, res, html);
1036
2029
  o.store.update(id2, { status, durationMs: Date.now() - started2, bytes: Buffer.byteLength(body), resHeaders: out });
1037
2030
  return;
1038
2031
  }
1039
- const { id, started } = record(req, route, headers, false, access);
1040
- const reqCap = new Capture(TEXT.test(req.headers["content-type"] ?? ""), max);
2032
+ const { id, started } = record(req, route, headers, false, access, preflight ? PREFLIGHT_NO_REPLAY : void 0);
2033
+ const reqCap = new Capture(TEXT2.test(req.headers["content-type"] ?? ""), max);
1041
2034
  req.pause();
1042
2035
  const host = await localHost(route.port);
1043
2036
  const upstream = request(
@@ -1051,35 +2044,19 @@ async function startProxy(o) {
1051
2044
  up.resume();
1052
2045
  return;
1053
2046
  }
1054
- const resCap = new Capture(TEXT.test(up.headers["content-type"] ?? ""), max);
1055
- let bytes = 0;
1056
2047
  if (route.host && route.rewriteRedirects !== false && typeof up.headers.location === "string") {
1057
2048
  up.headers.location = rewriteLocation(up.headers.location, route.host, hostOf(req));
1058
2049
  }
1059
2050
  res.writeHead(up.statusCode ?? 502, up.statusMessage, up.headers);
1060
- up.on("data", (c) => {
1061
- bytes += c.length;
1062
- resCap.push(c);
1063
- });
2051
+ trackResponse(id, started, up, reqCap);
1064
2052
  up.pipe(res);
1065
- up.on(
1066
- "end",
1067
- () => o.store.update(id, {
1068
- status: up.statusCode ?? null,
1069
- durationMs: Date.now() - started,
1070
- bytes,
1071
- resHeaders: flat(up.headers),
1072
- reqBody: reqCap.text(),
1073
- resBody: resCap.text(),
1074
- truncated: reqCap.truncated || resCap.truncated
1075
- })
1076
- );
1077
2053
  }
1078
2054
  );
1079
2055
  res.on("close", () => {
1080
2056
  if (!res.writableFinished) upstream.destroy();
1081
2057
  });
1082
2058
  upstream.on("error", (err) => {
2059
+ req.resume();
1083
2060
  if (res.destroyed && res.writableFinished === false && !res.headersSent) return;
1084
2061
  const html = wantsHtml(req.headers.accept);
1085
2062
  const msg = html ? unavailablePage({ tunnel: route.tunnel, port: route.port, retry: req.method === "GET" }) : `Nada escutando em localhost:${route.port}. Suba o servidor e tente de novo.`;
@@ -1089,6 +2066,7 @@ async function startProxy(o) {
1089
2066
  } else res.destroy();
1090
2067
  o.store.update(id, { status: 503, durationMs: Date.now() - started, bytes: res.headersSent ? 0 : Buffer.byteLength(msg), error: err.message || err.code || err.name, reqBody: reqCap.text() });
1091
2068
  });
2069
+ req.on("end", () => o.store.update(id, { reqComplete: true, reqRaw: reqCap.truncated ? null : reqCap.raw(), reqBodyBytes: reqCap.total, reqTruncated: reqCap.truncated, reqBody: reqCap.text() }));
1092
2070
  if (preflight) {
1093
2071
  req.resume();
1094
2072
  upstream.end();
@@ -1103,7 +2081,7 @@ async function startProxy(o) {
1103
2081
  if (!route) return void client.end("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
1104
2082
  const access = route.auth?.access(req.headers);
1105
2083
  const headers = delivered(req.headers, route);
1106
- const { id, started } = record(req, route, headers, true, access);
2084
+ const { id, started } = record(req, route, headers, true, access, access?.kind === "denied" ? DENIED_NO_REPLAY : void 0);
1107
2085
  if (access?.kind === "denied") {
1108
2086
  client.end(WS_DENIED);
1109
2087
  o.store.update(id, { status: 401, durationMs: Date.now() - started });
@@ -1122,8 +2100,8 @@ async function startProxy(o) {
1122
2100
  target.write(`${raw}\r
1123
2101
  `);
1124
2102
  if (head.length) target.write(head);
1125
- target.once("data", (first2) => {
1126
- 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;
1127
2105
  o.store.update(id, { status, durationMs: Date.now() - started });
1128
2106
  });
1129
2107
  target.pipe(client);
@@ -1146,6 +2124,7 @@ async function startProxy(o) {
1146
2124
  });
1147
2125
  return {
1148
2126
  port: server.address().port,
2127
+ replay,
1149
2128
  close: () => new Promise((resolve) => {
1150
2129
  for (const s of sockets) s.destroy();
1151
2130
  server.close(() => resolve());
@@ -1241,6 +2220,9 @@ var RecordStore = class {
1241
2220
  Object.assign(r, patch);
1242
2221
  this.emit();
1243
2222
  }
2223
+ get(id) {
2224
+ return this.items.find((x) => x.id === id);
2225
+ }
1244
2226
  /** Mais recentes primeiro. Mesma referência até a próxima mudança (serve de snapshot para a tela). */
1245
2227
  list() {
1246
2228
  this.snapshot ??= [...this.items].reverse();
@@ -1337,6 +2319,10 @@ var Session = class {
1337
2319
  const { token, expiresAt } = auth.issueQrToken();
1338
2320
  return { url: `${t.url}/__duto/qr/${token}`, expiresAt };
1339
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
+ }
1340
2326
  /** Cria o TunnelAuth na primeira abertura (precisa do allow resolvido) e o mantém depois. */
1341
2327
  async authFor(key) {
1342
2328
  const existing = this.auths.get(key);
@@ -1348,6 +2334,10 @@ var Session = class {
1348
2334
  this.auths.set(key, auth);
1349
2335
  return auth;
1350
2336
  }
2337
+ /** Reenvia uma requisição da lista ao serviço local. */
2338
+ replay(id) {
2339
+ return this.proxy ? this.proxy.replay(id) : { ok: false, reason: "Sess\xE3o ainda n\xE3o come\xE7ou." };
2340
+ }
1351
2341
  async stop() {
1352
2342
  if (this.stopped) return;
1353
2343
  this.stopped = true;
@@ -1600,11 +2590,24 @@ async function runSession(specs, source, flags) {
1600
2590
  fail2(err instanceof Error ? err.message : String(err));
1601
2591
  }
1602
2592
  const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname(), cloudflaredPath, source, runDir: join6(dutoHome(), "run") });
1603
- if (wantsUi(flags)) {
1604
- const { runFullscreen } = await import("./fullscreen-DKIFHB6J.js");
1605
- process.exit(await runFullscreen(session, specs, "0.5.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
+ }
1606
2601
  }
1607
- console.log(`${mark()} ${paint("muted", `${"0.5.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}`));
1608
2611
  const stopLog = runPlain(session, (l) => console.log(l));
1609
2612
  const stopped = new Promise((resolve) => {
1610
2613
  const onSignal = () => {
@@ -1621,6 +2624,7 @@ async function runSession(specs, source, flags) {
1621
2624
  await stopped;
1622
2625
  stopLog();
1623
2626
  await session.stop();
2627
+ await inspector?.close().catch(() => void 0);
1624
2628
  process.exit(0);
1625
2629
  }
1626
2630
  async function tunnel(rawPort, flags) {
@@ -1658,8 +2662,8 @@ ${paint("signal", "Dica:")} crie um duto.yml com os t\xFAneis do projeto e rode
1658
2662
  return runSession([{ name, port, ...host ? { host } : {}, ...keepRedirects ? { rewriteRedirects: false } : {}, ...auth.length ? { auth } : {}, ...allow.length ? { allow } : {} }], "linha de comando", flags);
1659
2663
  }
1660
2664
  var cli = cac("duto");
1661
- 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));
1662
- 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));
1663
2667
  cli.command("login <token>", "Salva o authtoken (pegue em https://duto.dev)").action(async (token) => {
1664
2668
  try {
1665
2669
  const me = await createApiClient({ baseUrl: apiUrl(), token: String(token) }).me();
@@ -1698,7 +2702,7 @@ cli.help(
1698
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")
1699
2703
  }))
1700
2704
  );
1701
- cli.version("0.5.0");
2705
+ cli.version("0.7.0");
1702
2706
  try {
1703
2707
  cli.parse(process.argv, { run: false });
1704
2708
  await cli.runMatchedCommand();