@dutodev/cli 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +33 -11
  2. package/dist/index.js +3 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -51,25 +51,47 @@ Todos os túneis sobem juntos, numa única tela. A CLI procura o arquivo na past
51
51
 
52
52
  ### Host e cabeçalhos
53
53
 
54
- Por padrão, o serviço local recebe o Host público (`web-<usuário>.duto.dev`). Se ele escolhe o que responder pelo Host, como um proxy reverso local ou um app com vários domínios, troque o Host com `host:` e ajuste cabeçalhos com `headers:`:
54
+ Por padrão, o serviço local recebe a requisição como ela chegou da internet: `Host: web-<usuário>.duto.dev`, `X-Forwarded-Proto: https` e os cabeçalhos da Cloudflare (`CF-Connecting-IP` e outros). Na maioria dos apps isso basta. Quando o app decide algo pelo domínio, há duas ferramentas por túnel:
55
55
 
56
56
  ```yaml
57
57
  tunnels:
58
58
  loja:
59
59
  port: 8080
60
- host: loja.meuapp.test # o serviço recebe este Host
61
- painel:
62
- port: 8080 # mesma porta: vale quando cada túnel tem um host
63
- host: painel.meuapp.test
64
- headers:
65
- X-Ambiente: tunel # acrescenta ou sobrescreve
60
+ headers: # acrescenta ou sobrescreve cabeçalhos
61
+ X-Forwarded-Host: loja.meuapp.test
66
62
  X-Debug: "" # valor vazio remove o cabeçalho
63
+ painel:
64
+ port: 8081
65
+ host: painel.meuapp.test # troca o Host que o serviço recebe
67
66
  ```
68
67
 
69
- - Com `host:`, o `X-Forwarded-Host` não é enviado, para o app não ver o domínio público. Se quiser enviá-lo, coloque-o em `headers:`.
70
- - Redirecionamentos voltam com o endereço público: um `Location: http://loja.meuapp.test:8080/login` chega ao navegador como `https://loja-<usuário>.duto.dev/login`. Isso vale também dentro de parâmetros codificados, como o `redirect_uri` de um login OAuth. Para desligar, use `rewrite_redirects: false` no túnel.
71
- - Na linha de comando: `duto 8080 --host loja.meuapp.test` (e `--no-rewrite-redirects` para desligar a troca).
72
- - Links dentro das páginas (HTML, JSON) não são reescritos: se o app montar URLs a partir do Host, elas só abrem na sua máquina.
68
+ **Qual usar:**
69
+
70
+ | Situação | Use | Por quê |
71
+ |---|---|---|
72
+ | Um proxy reverso local (Traefik, Nginx, Caddy) escolhe o serviço pelo Host | `host:` | O proxy só conhece os domínios locais |
73
+ | Vários túneis na mesma porta, um por domínio | `host:` ou `headers:` diferentes em cada um | A porta pode se repetir; cada túnel tem o próprio endereço |
74
+ | O app identifica o domínio ou o cliente pelo `X-Forwarded-Host`, mas monta URLs a partir do `Host` | `headers:` com `X-Forwarded-Host` | O app vê o domínio local e continua gerando links públicos |
75
+ | Login OAuth em que o app calcula o `redirect_uri` sozinho | evite `host:`; tente `headers:` | Veja abaixo |
76
+
77
+ **O que o `host:` faz:**
78
+
79
+ - O serviço recebe o Host informado, e o `X-Forwarded-Host` não é enviado. Para enviá-lo assim mesmo, coloque-o em `headers:`.
80
+ - Os redirecionamentos voltam com o endereço público. Um `Location: http://painel.meuapp.test:8081/login` chega ao navegador como `https://painel-<usuário>.duto.dev/login`. Isso vale também dentro de parâmetros codificados no `Location`. Para desligar, use `rewrite_redirects: false` no túnel.
81
+ - Links no corpo das respostas (HTML, JSON) não são reescritos. URLs que o app montar a partir do Host só abrem na sua máquina.
82
+ - Na linha de comando: `duto 8081 --host painel.meuapp.test`, e `--no-rewrite-redirects` para desligar a troca.
83
+
84
+ **OAuth e `host:`.** No fluxo de autorização por código, o app manda um `redirect_uri` duas vezes: na ida do navegador ao provedor de login e depois, servidor a servidor, ao trocar o código por token. O provedor exige que as duas sejam idênticas. Com `host:`, o app gera as duas com o domínio local. O Duto corrige a primeira, que passa pelo navegador, mas não alcança a segunda, que não passa pelo túnel. O provedor recusa a troca, em geral com `invalid_request` ou `invalid_grant`, e o app costuma voltar ao login em loop (`ERR_TOO_MANY_REDIRECTS`).
85
+
86
+ Saídas, da mais simples para a mais trabalhosa:
87
+
88
+ 1. Não troque o Host. O app gera o `redirect_uri` com o endereço público nas duas vezes.
89
+ 2. Se o app precisa ver o domínio local para outras decisões e lê o `X-Forwarded-Host`, passe o domínio por `headers:` e mantenha o Host público.
90
+ 3. Configure no app a URL pública de retorno, se ele permitir.
91
+
92
+ Em todos os casos, cadastre `https://<nome>-<usuário>.duto.dev/<caminho de retorno>` como URL de retorno no provedor. A comparação costuma ser exata, sem curingas: cada pessoa do time tem o próprio endereço.
93
+
94
+ **Sobre o `X-Forwarded-*`.** `X-Forwarded-Host`, `X-Forwarded-Proto` e `X-Forwarded-For` são uma convenção antiga entre proxies reversos para contar ao app o que o cliente pediu de verdade. A versão padronizada é o cabeçalho `Forwarded` (RFC 7239), menos usado na prática. Frameworks costumam ler esses cabeçalhos só quando o proxy é marcado como confiável (`trusted proxies`, `trust proxy`, `USE_X_FORWARDED_HOST`), então o efeito de `headers:` depende de como o app está configurado. Por outro lado, se um app lê o `X-Forwarded-Host` sem essa checagem, qualquer cliente pode forjá-lo em produção. Vale garantir que o proxy da frente sobrescreva ou remova esse cabeçalho.
73
95
 
74
96
  ## A tela
75
97
 
package/dist/index.js CHANGED
@@ -263,7 +263,6 @@ function parseConfig(text, file = CONFIG_FILES[0]) {
263
263
  throw fail3('declare os t\xFAneis em "tunnels:", um por linha (ex.: web: 3000).');
264
264
  }
265
265
  const specs = [];
266
- const seen = /* @__PURE__ */ new Map();
267
266
  for (const pair of tunnels.items) {
268
267
  const raw = isScalar(pair.key) ? String(pair.key.value) : "";
269
268
  const line = lineAt(pair.key?.range?.[0]);
@@ -317,16 +316,7 @@ function parseConfig(text, file = CONFIG_FILES[0]) {
317
316
  if (value === false) spec.rewriteRedirects = false;
318
317
  }
319
318
  }
320
- const users = seen.get(port) ?? [];
321
- const clash = users.find((u) => !u.host || !spec.host || u.host === spec.host);
322
- if (clash) {
323
- throw fail3(
324
- clash.host && clash.host === spec.host ? `a porta ${port} com host ${spec.host} aparece em "${clash.raw}" e em "${raw}"` : `a porta ${port} aparece em "${clash.raw}" e em "${raw}". Para usar a mesma porta, d\xEA um host a cada um.`,
325
- line
326
- );
327
- }
328
319
  if (specs.some((s) => s.name === name)) throw fail3(`o nome "${raw}" aparece duas vezes`, line);
329
- seen.set(port, [...users, { raw, host: spec.host }]);
330
320
  specs.push(spec);
331
321
  }
332
322
  return specs;
@@ -920,9 +910,9 @@ async function runSession(specs, source, flags) {
920
910
  const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname(), cloudflaredPath, source });
921
911
  if (wantsUi(flags)) {
922
912
  const { runFullscreen } = await import("./fullscreen-5437X7MP.js");
923
- process.exit(await runFullscreen(session, specs, "0.4.1"));
913
+ process.exit(await runFullscreen(session, specs, "0.4.2"));
924
914
  }
925
- console.log(`${mark()} ${paint("muted", `${"0.4.1"} \xB7 ${source} \xB7 Ctrl+C para encerrar`)}`);
915
+ console.log(`${mark()} ${paint("muted", `${"0.4.2"} \xB7 ${source} \xB7 Ctrl+C para encerrar`)}`);
926
916
  const stopLog = runPlain(session, (l) => console.log(l));
927
917
  const stopped = new Promise((resolve) => {
928
918
  const onSignal = () => {
@@ -1009,7 +999,7 @@ cli.help(
1009
999
  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")
1010
1000
  }))
1011
1001
  );
1012
- cli.version("0.4.1");
1002
+ cli.version("0.4.2");
1013
1003
  try {
1014
1004
  cli.parse(process.argv, { run: false });
1015
1005
  await cli.runMatchedCommand();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dutodev/cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Exponha uma porta local em https://<nome>-<usuário>.duto.dev com um comando.",
5
5
  "license": "MIT",
6
6
  "type": "module",