@dutodev/cli 1.8.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -12
- package/dist/index.js +4 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Put a local service on the internet at a stable HTTPS URL, with one command:
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
duto 3000 --name web # https://web-<user>.duto.
|
|
6
|
+
duto 3000 --name web # https://web-<user>.duto.page → localhost:3000
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
The URL stays the same from one session to the next. That makes it good for webhooks, login callbacks, testing on your phone, or showing someone your work without deploying.
|
|
@@ -20,8 +20,13 @@ Requires Node 18 or later. On first run, the CLI downloads a pinned release of t
|
|
|
20
20
|
|
|
21
21
|
## Get started
|
|
22
22
|
|
|
23
|
-
1.
|
|
24
|
-
|
|
23
|
+
1. Connect the CLI to your account (once per machine). It opens your browser so you can approve it:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
duto login
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
2. On CI and servers with no browser, create a token on the **Machines** page at [duto.dev](https://duto.dev) and pass it:
|
|
25
30
|
|
|
26
31
|
```
|
|
27
32
|
duto login <token>
|
|
@@ -30,8 +35,8 @@ Requires Node 18 or later. On first run, the CLI downloads a pinned release of t
|
|
|
30
35
|
3. Open a tunnel:
|
|
31
36
|
|
|
32
37
|
```
|
|
33
|
-
duto 3000 # https://<user>.duto.
|
|
34
|
-
duto 3000 --name web # https://web-<user>.duto.
|
|
38
|
+
duto 3000 # https://<user>.duto.page
|
|
39
|
+
duto 3000 --name web # https://web-<user>.duto.page
|
|
35
40
|
```
|
|
36
41
|
|
|
37
42
|
Every name you use with `--name` keeps the same URL, and only you can use it. Names take lowercase letters and numbers only, no hyphens. The account limit counts named tunnels online at once: closed ones do not count, and names unused for 30 days are released.
|
|
@@ -47,13 +52,13 @@ tunnels:
|
|
|
47
52
|
webhook: 4000
|
|
48
53
|
```
|
|
49
54
|
|
|
50
|
-
All tunnels start together, in one screen. The CLI looks for the file in the current folder and its parents, up to the git root. If you commit it, everyone on the team gets their own addresses (`web-<user>.duto.
|
|
55
|
+
All tunnels start together, in one screen. The CLI looks for the file in the current folder and its parents, up to the git root. If you commit it, everyone on the team gets their own addresses (`web-<user>.duto.page`) from the same file.
|
|
51
56
|
|
|
52
57
|
**Edit while it runs.** Save `duto.yml` and the change applies right away, without restarting: new tunnels open, removed ones close, and a new `port`, `host`, `headers`, `auth` or `allow` counts from the next request, on the same URL. Visitors signed in with a password stay signed in while their user is still in `auth`. A save with a mistake changes nothing: the tunnels keep the last valid config and the screen shows the error with its line until you save a fixed file. Tunnels opened with `n` on the screen are left alone. Saving `.env` or `.env.local` applies the file again with the new values.
|
|
53
58
|
|
|
54
59
|
### Host and headers
|
|
55
60
|
|
|
56
|
-
By default, your local service gets the request as it arrived from the internet: `Host: web-<user>.duto.
|
|
61
|
+
By default, your local service gets the request as it arrived from the internet: `Host: web-<user>.duto.page`, `X-Forwarded-Proto: https` and the Cloudflare headers (`CF-Connecting-IP` and others). That is enough for most apps. When your app decides things by domain, you have two tools per tunnel:
|
|
57
62
|
|
|
58
63
|
```yaml
|
|
59
64
|
tunnels:
|
|
@@ -79,7 +84,7 @@ tunnels:
|
|
|
79
84
|
**What `host:` does:**
|
|
80
85
|
|
|
81
86
|
- Your service receives the Host you set, and `X-Forwarded-Host` is not sent. To send it anyway, put it in `headers:`.
|
|
82
|
-
- Redirects come back with the public address. A `Location: http://admin.myapp.test:8081/login` reaches the browser as `https://admin-<user>.duto.
|
|
87
|
+
- Redirects come back with the public address. A `Location: http://admin.myapp.test:8081/login` reaches the browser as `https://admin-<user>.duto.page/login`. This also applies inside encoded parameters of the `Location`. To turn it off, set `rewrite_redirects: false` on the tunnel.
|
|
83
88
|
- Links in response bodies (HTML, JSON) are not rewritten. URLs your app builds from the Host only open on your machine.
|
|
84
89
|
- On the command line: `duto 8081 --host admin.myapp.test`, and `--no-rewrite-redirects` to turn the swap off.
|
|
85
90
|
|
|
@@ -91,7 +96,7 @@ Ways out, from simplest to most work:
|
|
|
91
96
|
2. If the app needs the local domain for other decisions and reads `X-Forwarded-Host`, pass the domain through `headers:` and keep the public Host.
|
|
92
97
|
3. Set the public callback URL in the app, if it lets you.
|
|
93
98
|
|
|
94
|
-
In every case, register `https://<name>-<user>.duto.
|
|
99
|
+
In every case, register `https://<name>-<user>.duto.page/<callback path>` as the callback URL at the provider. The comparison is usually exact, with no wildcards: each person on the team has their own address.
|
|
95
100
|
|
|
96
101
|
**What you cannot set.** Some headers belong to Cloudflare, to Duto or to the HTTP connection itself. `duto.yml` refuses them, with the line and what to do:
|
|
97
102
|
|
|
@@ -153,12 +158,12 @@ People get in one of these ways:
|
|
|
153
158
|
|---|---|
|
|
154
159
|
| Person in a browser | Opens the URL, sees the "This tunnel is password protected" page and signs in with user and password |
|
|
155
160
|
| Phone | Scans the QR code (key `m`, see below) and gets in without typing |
|
|
156
|
-
| `curl`, Postman, scripts | `curl -u client:password https://backend-<user>.duto.
|
|
161
|
+
| `curl`, Postman, scripts | `curl -u client:password https://backend-<user>.duto.page/` (`Authorization: Basic`) |
|
|
157
162
|
| An API that already uses `Authorization: Bearer` | Send `X-Duto-Auth: client:password`; the `Bearer` goes through to the app untouched |
|
|
158
163
|
|
|
159
164
|
Without credentials, a browser is sent to the sign-in page and other clients get `401`. After 10 wrong passwords from one address in 5 minutes, that address is locked out for the rest of the window (the sign-in page answers `429` with `Retry-After`, other clients keep getting `401`). After 100 wrong passwords from all addresses, every password attempt is locked out the same way. A browser that already signed in keeps working. Your local app never receives Duto's credentials: the `Authorization: Basic` that matched, `X-Duto-Auth` and the tunnel cookie are stripped before the request is forwarded (other cookies pass through). An `Authorization: Bearer` from your own app passes unchanged.
|
|
160
165
|
|
|
161
|
-
**Signing out and sessions.** In a browser, signing in sets a cookie that is valid for that tunnel only. To sign out, open `/__duto/logout`: it clears the cookie and returns to the sign-in page. The cookie lasts until the browser closes or the CLI stops. Restarting the CLI signs everyone out, because the key that signs the cookies is new every session. The cookie only counts on requests to the tunnel itself: a page on another origin (another tunnel, another site) that calls the tunnel has to send `X-Duto-Auth` or `Authorization: Basic`. So opening the tunnel from a link on another `*.duto.
|
|
166
|
+
**Signing out and sessions.** In a browser, signing in sets a cookie that is valid for that tunnel only. To sign out, open `/__duto/logout`: it clears the cookie and returns to the sign-in page. The cookie lasts until the browser closes or the CLI stops. Restarting the CLI signs everyone out, because the key that signs the cookies is new every session. The cookie only counts on requests to the tunnel itself: a page on another origin (another tunnel, another site) that calls the tunnel has to send `X-Duto-Auth` or `Authorization: Basic`. So opening the tunnel from a link on another `*.duto.page` address (the dashboard or another tunnel) asks for the password again; a link from outside (email, chat) or a typed URL gets straight in.
|
|
162
167
|
|
|
163
168
|
**QR code.** With the full screen open, key `m` shows the QR of the selected tunnel in place of the request list. On a tunnel without a password, the QR points to the tunnel URL. On a tunnel with a password, it points to a link that signs in directly, as the first user in the `auth` list. That link is valid for 5 minutes and for one device only: once opened, or after it expires, it falls back to the sign-in page. On the QR screen, `g` generates a new link (and invalidates the previous one) and `esc` goes back to the list.
|
|
164
169
|
|
|
@@ -231,7 +236,7 @@ The page has the list on the left and the details on the right, with the tabs **
|
|
|
231
236
|
|
|
232
237
|
| Option | What it does |
|
|
233
238
|
|---|---|
|
|
234
|
-
| `--name <name>` | Fixed address `<name>-<user>.duto.
|
|
239
|
+
| `--name <name>` | Fixed address `<name>-<user>.duto.page` |
|
|
235
240
|
| `--host <host>` | Host the local service receives instead of the public one |
|
|
236
241
|
| `--no-rewrite-redirects` | With `--host`, keep redirects as the service sent them |
|
|
237
242
|
| `--auth <user:password>` | Ask for a username and password to open the address (repeatable) |
|
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ import { join as join7, relative } from "path";
|
|
|
36
36
|
import cac from "cac";
|
|
37
37
|
|
|
38
38
|
// src/version.ts
|
|
39
|
-
var CLI_VERSION = "1.8.
|
|
39
|
+
var CLI_VERSION = "1.8.1";
|
|
40
40
|
|
|
41
41
|
// src/api.ts
|
|
42
42
|
var DutoApiError = class extends Error {
|
|
@@ -561,7 +561,7 @@ async function browserLogin(o) {
|
|
|
561
561
|
const start = await api.startLogin({
|
|
562
562
|
deviceId: o.deviceId,
|
|
563
563
|
deviceName: hostname(),
|
|
564
|
-
cliVersion: "1.8.
|
|
564
|
+
cliVersion: "1.8.1",
|
|
565
565
|
nodeVersion: process.version,
|
|
566
566
|
os: process.platform,
|
|
567
567
|
arch: process.arch
|
|
@@ -1729,7 +1729,7 @@ function redirectIssues(r, ctx) {
|
|
|
1729
1729
|
{
|
|
1730
1730
|
level: "warn",
|
|
1731
1731
|
title: "Redirect drops to http",
|
|
1732
|
-
detail: `Location is ${location}.
|
|
1732
|
+
detail: `Location is ${location}. Duto tunnels only serve https, so the browser takes an extra hop and loses POST bodies. Have the app trust X-Forwarded-Proto.`
|
|
1733
1733
|
}
|
|
1734
1734
|
];
|
|
1735
1735
|
}
|
|
@@ -3650,7 +3650,7 @@ ${paint("signal", "Tip:")} create a duto.yml with your project's tunnels and jus
|
|
|
3650
3650
|
return runSession([{ name, port, ...host ? { host } : {}, ...keepRedirects ? { rewriteRedirects: false } : {}, ...auth.length ? { auth } : {}, ...allow.length ? { allow } : {} }], "command line", flags);
|
|
3651
3651
|
}
|
|
3652
3652
|
var cli = cac("duto");
|
|
3653
|
-
cli.command("[port]", "No port: open the tunnels in duto.yml. With a port: open a tunnel to it").option("--name <name>", "Fixed address name (e.g. web \u2192 https://web-<user>.duto.
|
|
3653
|
+
cli.command("[port]", "No port: open the tunnels in duto.yml. With a port: open a tunnel to it").option("--name <name>", "Fixed address name (e.g. web \u2192 https://web-<user>.duto.page)").option("--host <host>", "Host the local service receives instead of the public one (e.g. myapp.test)").option("--no-rewrite-redirects", "With --host: do not swap the local host for the public one in redirects").option("--auth <user:password>", "Ask for a username and password to open the address (repeatable)").option("--allow <range>", "With --auth: IP, CIDR range or name (stripe, github) that gets in without a password (repeatable)").option("--no-ui", "No full screen: just a request log (useful in scripts)").option("--no-inspect", "Do not start the browser inspector (127.0.0.1:4141)").option("--ui", "Force full screen (off by default on Windows)").action((port, opts) => tunnel(port, opts));
|
|
3654
3654
|
cli.command("http <port>", "Same as duto <port> (for people coming from ngrok)").option("--name <name>", "Fixed address name").option("--host <host>", "Host the local service receives instead of the public one").option("--no-rewrite-redirects", "With --host: keep redirects as the service sent them").option("--auth <user:password>", "Ask for a username and password to open the address (repeatable)").option("--allow <range>", "With --auth: IP, CIDR range or name (stripe, github) that gets in without a password (repeatable)").option("--no-ui", "No full screen").option("--no-inspect", "Do not start the browser inspector (127.0.0.1:4141)").action((port, opts) => tunnel(port, opts));
|
|
3655
3655
|
cli.command("login [token]", "Log in through the browser, or save an authtoken").action(async (token) => {
|
|
3656
3656
|
try {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dutodev/cli",
|
|
3
|
-
"version": "1.8.
|
|
4
|
-
"description": "Expose a local port at https://<name>-<user>.duto.
|
|
3
|
+
"version": "1.8.1",
|
|
4
|
+
"description": "Expose a local port at https://<name>-<user>.duto.page with one command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|