@monster-orange/den 0.4.4 → 0.4.5
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/CHANGELOG.md +169 -0
- package/README.md +10 -9
- package/docs/distributions.md +223 -26
- package/docs/reference.md +8 -3
- package/package.json +1 -1
- package/src/shared/auth/sudo-vault.ts +115 -11
- package/src/shared/cli-command.ts +186 -0
- package/src/shared/confirm-code.ts +34 -0
- package/src/shared/den-config.ts +7 -0
- package/src/shared/dev-metrics/emit.ts +31 -1
- package/src/shared/edge/config.ts +39 -0
- package/src/shared/edge/render.ts +92 -18
- package/src/shared/edge/runtime.ts +57 -1
- package/src/shared/edge/types.ts +11 -1
- package/src/shared/github-app.ts +94 -0
- package/src/shared/github-device.ts +286 -0
- package/src/shared/secrets/onepassword.ts +27 -4
- package/src/shared/web-owner.ts +91 -0
- package/src/shared/web-session.ts +135 -1
- package/src/ui/cli/flag-parser.ts +1 -1
- package/src/ui/cli/web-command.ts +512 -33
- package/src/ui/server/device-flows.ts +274 -0
- package/src/ui/server/edge-exposure.ts +16 -4
- package/src/ui/server/github-routes.ts +321 -0
- package/src/ui/server/http-errors.ts +10 -0
- package/src/ui/server/index.ts +463 -27
- package/src/ui/server/login-waits.ts +203 -0
- package/src/ui/server/pair-requests.ts +237 -0
- package/src/ui/server/pair-routes.ts +196 -0
- package/src/ui/server/public/error-page.js +452 -40
- package/src/ui/server/public/error.html +59 -0
- package/src/ui/server/public/i18n/es.js +74 -8
- package/src/ui/server/public/i18n/ja.js +74 -8
- package/src/ui/server/public/local-dev.html +16 -4
- package/src/ui/server/public/qr.js +317 -0
- package/src/ui/server/public/settings-page.js +10 -3
- package/src/ui/server/public/settings-registry.js +392 -1
- package/src/ui/server/public/shell.js +123 -10
- package/src/ui/server/public/topbar.js +7 -3
- package/src/ui/server/public/workspace-picker.js +1 -1
- package/src/ui/server/request-guard.ts +102 -35
- package/src/ui/server/route-classes.ts +102 -2
- package/src/ui/server/ui-assets.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,175 @@ All notable changes to Den are documented here. Each release lists the
|
|
|
4
4
|
config and manifest changes a distribution must make to move to it under
|
|
5
5
|
**Migrations**.
|
|
6
6
|
|
|
7
|
+
## [0.4.5] - 2026-11-07
|
|
8
|
+
|
|
9
|
+
Sign in with GitHub as the owner, from your other devices; approve a
|
|
10
|
+
computer with a code; see that you're signed in (SEC-2a). Save the admin
|
|
11
|
+
password without editing YAML (LK-4). Dev-metrics stay on this machine
|
|
12
|
+
unless you opt in.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Sign in with GitHub.** A Den has one owner, a GitHub account:
|
|
17
|
+
`<cli> web owner link` runs GitHub's device flow in the terminal (a code
|
|
18
|
+
to enter at https://github.com/login/device) and stores only
|
|
19
|
+
`github:<id>` and the login (`web-owner.json`, 0600), never the token.
|
|
20
|
+
`web owner show` and `web owner unlink` too. The signed-out page then
|
|
21
|
+
has **Sign in with GitHub**: the server runs the flow, and the owner's
|
|
22
|
+
approval sets the same never-expiring session a one-time link does;
|
|
23
|
+
any other account sees "@<login> isn't allowed on this Den". At most
|
|
24
|
+
three sign-ins wait at once. Before an owner is linked the page says to
|
|
25
|
+
run `web owner link`. See docs/distributions.md, "Signing in with GitHub".
|
|
26
|
+
- **Settings › Access.** The owner (link and unlink: shell-grade, so a
|
|
27
|
+
signed-in browser and never an agent's token), the
|
|
28
|
+
linked devices (browser, how it signed in, the client address the edge
|
|
29
|
+
vouched for, first and last seen) each with **Revoke**, which ends that
|
|
30
|
+
session at once, and **Pair a device**: a one-time link and its QR code
|
|
31
|
+
for an edge hostname you pick.
|
|
32
|
+
- **`web.github_client_id`**: the GitHub App's client id (public). The
|
|
33
|
+
default is Den's own App, "Monster Orange Den" (`Iv23liKL12gnN6WvRV5b`);
|
|
34
|
+
a distribution sets its own App's id here, and an empty value turns Sign
|
|
35
|
+
in with GitHub off (the page then offers only the code and the link).
|
|
36
|
+
The App needs "Enable Device Flow" ticked and "Expire user authorization
|
|
37
|
+
tokens" off (docs/distributions.md, "The GitHub App").
|
|
38
|
+
- **Expiring GitHub tokens are noticed.** If the App hands out expiring
|
|
39
|
+
tokens (`expires_in` or `refresh_token` in the answer), sign-in still
|
|
40
|
+
completes, and Den says once to switch off token expiry in the App's
|
|
41
|
+
settings (never with the token). `GET /api/web/owner` reports it as
|
|
42
|
+
`tokenExpiry`. Den never asks for `offline_access`.
|
|
43
|
+
- **Approve this computer with a code.** The signed-out page shows a
|
|
44
|
+
six-character code (`XXX-XXX`, about 30 bits; the page polls with its own
|
|
45
|
+
128-bit id) and the command that approves it on the machine Den runs on:
|
|
46
|
+
`<cli> web approve <code>` (any case, dash optional; `--list` shows what
|
|
47
|
+
waits). The page signs itself in when it's approved, onto the page it
|
|
48
|
+
asked for. From a machine with SSH access the page gives the one-liner
|
|
49
|
+
(`ssh <host> '<cli> web approve <code>'`) and, for a host name, an Open
|
|
50
|
+
Terminal link. Codes expire after 10 minutes, are single-use, at most
|
|
51
|
+
five wait at once (the oldest drops) and are kept in memory only;
|
|
52
|
+
approvals are rate-limited and audited with who approved. The device is
|
|
53
|
+
labelled "approved with a code".
|
|
54
|
+
- **Settings › Access › Waiting devices**: the computers waiting with a
|
|
55
|
+
code (browser and OS, address, when), each with **Approve** and **Deny**.
|
|
56
|
+
- **`web.machine_name`** (Settings › Access › Machine name, in user.yml):
|
|
57
|
+
the name the signed-out page gives this machine. Default: the short
|
|
58
|
+
hostname. It's never taken from the browser's Host; the ssh one-liner's
|
|
59
|
+
target is the Host the browser used (without the port).
|
|
60
|
+
- **The signed-out page says where to act.** "Sign in to <product>", which
|
|
61
|
+
machine Den runs on and why it asks (once per computer), Sign in with
|
|
62
|
+
GitHub (only with a client id), Approve this computer, and the one-time
|
|
63
|
+
link under "Other ways". Every command it prints names the machine it
|
|
64
|
+
runs on, has a copy button, and is `<cli>` when that's on PATH and this
|
|
65
|
+
distribution's, else the absolute path of its bin. English, Japanese and
|
|
66
|
+
Spanish.
|
|
67
|
+
- **See that you're signed in.** A chip at the foot of the rail says
|
|
68
|
+
"Local", "Signed in · this device" or "@<login>" and opens Settings ›
|
|
69
|
+
Access. The first page after a sign-in says "This device is signed in
|
|
70
|
+
to <product> on <machine>", once. Linked devices lists every session
|
|
71
|
+
(GitHub, a code, a one-time link) with how it signed in, when, from
|
|
72
|
+
where, last seen and Revoke, and marks the one you're using as "This
|
|
73
|
+
device".
|
|
74
|
+
- **Save the admin password without editing YAML (LK-4).** When a secrets
|
|
75
|
+
backend that keeps secrets (1Password, Doppler) is available and no
|
|
76
|
+
`secrets.refs.sudo` is set, the admin password prompt (shown when a
|
|
77
|
+
command is about to run sudo, LK-0) still offers "Save to <backend>".
|
|
78
|
+
Saving creates the item (1Password: "<Product> admin password" in your
|
|
79
|
+
default vault; Doppler: `<PREFIX>_LOCAL_ADMIN_PWD`) and writes
|
|
80
|
+
`secrets.refs.sudo` to your `config/local/user.yml`, so the password
|
|
81
|
+
survives restarts. A ref in the distribution's config still wins. The
|
|
82
|
+
vault status says so as `canSave`. A save error no longer carries the
|
|
83
|
+
backend's command line.
|
|
84
|
+
- **`edge.proxy_protocol.trusted_ips`** (empty by default): the front
|
|
85
|
+
proxies (a TCP passthrough in front of the edge) whose PROXY protocol
|
|
86
|
+
every entrypoint accepts, so Den sees the real client through them.
|
|
87
|
+
- **`web.edge_unattested: local | signin`** (default `signin`, read only
|
|
88
|
+
from `config/local/user.yml`): whether an edge client Den can't place
|
|
89
|
+
signs in. `local` keeps 0.4.4's (and 0.4.3's) behaviour.
|
|
90
|
+
|
|
91
|
+
### Changed
|
|
92
|
+
|
|
93
|
+
- **Dev-metrics are local unless you opt in.** Den sent its dev-metrics
|
|
94
|
+
whenever a DSN was present (in the env or a synced
|
|
95
|
+
`dev-metrics.config`). Sending now needs `metrics: { share: true }` in
|
|
96
|
+
`config/local/user.yml` (the boolean only); a DSN alone never sends. The
|
|
97
|
+
local `events.jsonl` is unchanged, and `CI=true` and
|
|
98
|
+
`DEV_METRICS_DISABLED=1` still stop sending.
|
|
99
|
+
- **`<cli> web login` waits.** It prints the link and an 8-character code,
|
|
100
|
+
and blocks until the link is used ("Signed in: <browser> (<address>) at
|
|
101
|
+
<time>") or expires ("Expired: nobody used the link", exit 1). The link
|
|
102
|
+
asks for the code before it signs in; a wrong code is refused (five burn
|
|
103
|
+
the link, and wrong codes are rate-limited) and the link stays
|
|
104
|
+
single-use. `--no-wait` prints the link alone, as before, and asks for no
|
|
105
|
+
code; with no server running it behaves like `--no-wait` and says so.
|
|
106
|
+
- **Edge visitors Den can't place sign in.** A client the edge reports
|
|
107
|
+
with no `X-Forwarded-For`, or with a Docker/VM gateway or bridge address
|
|
108
|
+
(what a TCP passthrough or Docker Desktop and OrbStack hand it), now sees
|
|
109
|
+
the signed-out page with Sign in with GitHub, unless
|
|
110
|
+
`web.edge_unattested: local`. Loopback and this machine's own addresses,
|
|
111
|
+
as the edge reports them, stay local, and localhost straight to the
|
|
112
|
+
server's port never signs in.
|
|
113
|
+
- **The client is the right-most `X-Forwarded-For` entry**, the one the
|
|
114
|
+
edge appended (0.4.4 and earlier read the left-most, which a client could
|
|
115
|
+
write).
|
|
116
|
+
- **The edge believes forwarded headers only from its own networks.** Den
|
|
117
|
+
renders `forwardedHeaders.trustedIPs` (loopback and the Docker ranges)
|
|
118
|
+
on every entrypoint, never `forwardedHeaders.insecure`. An `edge.args`
|
|
119
|
+
entry that sets `forwardedHeaders` or `proxyProtocol` is dropped with a
|
|
120
|
+
notice from `edge up`.
|
|
121
|
+
- **"Your edge is reachable from other machines"** now reads "A browser it
|
|
122
|
+
can't place as this one signs in first (Sign in with GitHub, or a
|
|
123
|
+
pairing link)." (with `web.edge_unattested: local`, that anyone it can't
|
|
124
|
+
place can use terminals).
|
|
125
|
+
- **`edge up` says when the running edge's command differs** from the one
|
|
126
|
+
it renders, and why, before recreating it.
|
|
127
|
+
|
|
128
|
+
### Fixed
|
|
129
|
+
|
|
130
|
+
- **The workspace commands run in is named: "CLI default".** Local Dev's
|
|
131
|
+
workspace list marked it with a bare accent dot and no label, and
|
|
132
|
+
picking a workspace in the top bar never moved the dot (by design: that
|
|
133
|
+
picks the view), so it looked broken. It now says "CLI default", with
|
|
134
|
+
a tooltip: CLI commands run there when you don't name a workspace, and
|
|
135
|
+
the top bar only changes what pages show. The top bar's Workspace menu
|
|
136
|
+
tags it "CLI default" (was "focus") and offers "Make <ws> the CLI
|
|
137
|
+
default"; the Workspaces pane's badge reads "active · CLI default" and
|
|
138
|
+
its button "Make CLI default". Nothing behaves differently.
|
|
139
|
+
|
|
140
|
+
- **A browser signs in on a plain-http LAN bind** (`DEN_BIND_HOST` with
|
|
141
|
+
`web.allow_plain_http_remote`). A `web login --origin http://<LAN IP>:<port>`
|
|
142
|
+
link got 403 `origin_rejected`: the browser sends a plain-http origin that
|
|
143
|
+
isn't localhost no `Sec-Fetch-*` headers, and a navigation sends no
|
|
144
|
+
Origin. The sign-in GETs (`/auth` and the GitHub sign-in poll), which
|
|
145
|
+
carry their own one-time credential, now pass with neither header. Either
|
|
146
|
+
header, when present, is still checked, and every other GET that runs
|
|
147
|
+
something still needs this server's page or a bearer.
|
|
148
|
+
- **`web up` with `DEN_BIND_HOST` on one address is ready when it serves.**
|
|
149
|
+
Its readiness probe (and `web sessions`/`web logout`'s calls) asked
|
|
150
|
+
127.0.0.1, so a server bound to a LAN IP or `::1` printed "did not become
|
|
151
|
+
ready" and exited non-zero. They ask the bound address now (loopback for
|
|
152
|
+
0.0.0.0 and `::`), with a Host the allowlist accepts and the CLI's
|
|
153
|
+
bearer: a LAN address answers an unsigned probe 401 `signin_required`.
|
|
154
|
+
`web up` names the bound address (`http://<LAN IP>:<port>`), not
|
|
155
|
+
localhost, which a LAN-only bind doesn't answer.
|
|
156
|
+
|
|
157
|
+
### Migrations
|
|
158
|
+
|
|
159
|
+
- **One edge recreate.** The edge's command changes (forwarded-header
|
|
160
|
+
trust), so the first `<cli> edge up` (or `stack up`) after upgrading
|
|
161
|
+
recreates it once, and says so: "<org>-edge's settings changed
|
|
162
|
+
(forwarded-header trust): recreating it once." Routes drop only while it
|
|
163
|
+
restarts.
|
|
164
|
+
- **Edge visitors now see Sign in with GitHub** where 0.4.4 let a client
|
|
165
|
+
it couldn't place in. Link an owner (`<cli> web owner link`), approve
|
|
166
|
+
the computer with its code (`<cli> web approve <code>`) or pair with
|
|
167
|
+
`<cli> web login --host <name>`;
|
|
168
|
+
or set `web.edge_unattested: local` in `config/local/user.yml` to keep
|
|
169
|
+
0.4.4's behaviour on that machine.
|
|
170
|
+
- **Scripts that run `<cli> web login` and read its output** should pass
|
|
171
|
+
`--no-wait`: it now waits for the link to be used.
|
|
172
|
+
- **Remove `forwardedHeaders.*` and `proxyProtocol.*` from `edge.args`.**
|
|
173
|
+
They are ignored now; use `edge.proxy_protocol.trusted_ips` for a front
|
|
174
|
+
proxy.
|
|
175
|
+
|
|
7
176
|
## [0.4.4] - 2026-11-07
|
|
8
177
|
|
|
9
178
|
The admin password is for sudo, and nothing else (LK-0). An agent's
|
package/README.md
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<picture>
|
|
13
|
-
<source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.
|
|
14
|
-
<img src="docs/images/0.4.
|
|
13
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.5/hero-dark.png">
|
|
14
|
+
<img src="docs/images/0.4.5/hero-light.png" width="100%" alt="Den's Graph page: the demo workspace's services grouped by application, all healthy, with the status chip and the left rail">
|
|
15
15
|
</picture>
|
|
16
16
|
|
|
17
17
|
## What Den is
|
|
@@ -24,7 +24,7 @@ It doesn't replace your tools. Your repos, Docker, your test runners, your secre
|
|
|
24
24
|
yours. Den connects them and turns on good practice from the start.
|
|
25
25
|
|
|
26
26
|
<p align="center">
|
|
27
|
-
<img src="docs/images/0.4.
|
|
27
|
+
<img src="docs/images/0.4.5/up.gif" width="100%" alt="make up in the Graph page: every stack down, then demo-api, demo-web and demo-notes come up in dependency order until the status chip reads All 3 healthy">
|
|
28
28
|
</p>
|
|
29
29
|
|
|
30
30
|
## How it helps
|
|
@@ -46,17 +46,18 @@ yours. Den connects them and turns on good practice from the start.
|
|
|
46
46
|
and audit trail as a person, under a token of its own. Agents can read and investigate, and acting is opt-in.
|
|
47
47
|
Starting an agent never asks for your admin password: Den asks for it only when a command it runs needs sudo.
|
|
48
48
|
- **Safe on localhost, signed in beyond it.** On localhost there is nothing to sign in to. Reached through the
|
|
49
|
-
edge or your network, every page and call needs a sign-in, and there is no password:
|
|
50
|
-
|
|
49
|
+
edge from another machine, or over your network, every page and call needs a sign-in, and there is no password:
|
|
50
|
+
Sign in with GitHub as the Den's owner, approve the computer with a code (`acme web approve <code>`), or open a
|
|
51
|
+
one-time link from `acme web login`.
|
|
51
52
|
- **Opinionated, not locked in.** Good defaults are switched on, and almost everything can be switched off. The
|
|
52
53
|
safety guards stay on. Den never needs our accounts, secrets or certificates. Everything specific to you
|
|
53
54
|
lives in your own repo.
|
|
54
55
|
|
|
55
56
|
<p align="center">
|
|
56
|
-
<img src="docs/images/0.4.
|
|
57
|
-
<img src="docs/images/0.4.
|
|
58
|
-
<img src="docs/images/0.4.
|
|
59
|
-
<img src="docs/images/0.4.
|
|
57
|
+
<img src="docs/images/0.4.5/agents.png" width="49%" alt="The Agents tab: the demo workspace's sessions on the left, and one open in a terminal pane on the right, running demo-api's test and then the service">
|
|
58
|
+
<img src="docs/images/0.4.5/workspaces.png" width="49%" alt="The Workspaces page: the demo workspace's repos with their health, mode and Start, Stop, Restart and Logs actions">
|
|
59
|
+
<img src="docs/images/0.4.5/status-drawer.png" width="49%" alt="The status drawer: nothing needs attention, and every service in the demo workspace, by application, with its latency">
|
|
60
|
+
<img src="docs/images/0.4.5/edge.png" width="49%" alt="The Edge page: the edge container up, its ports and TLS, and every route it serves, Den's own and each stack's, with its upstream">
|
|
60
61
|
</p>
|
|
61
62
|
|
|
62
63
|
## Get started
|
package/docs/distributions.md
CHANGED
|
@@ -12,6 +12,7 @@ Den's own checkout is itself a distribution, named `den`.
|
|
|
12
12
|
- [The bin shim](#the-bin-shim)
|
|
13
13
|
- [make: den.mk](#make-denmk)
|
|
14
14
|
- [Two roots](#two-roots)
|
|
15
|
+
- [Dev metrics: local unless you opt in](#dev-metrics-local-unless-you-opt-in)
|
|
15
16
|
- [Pinning and upgrading](#pinning-and-upgrading)
|
|
16
17
|
- [Developing Den against a distribution](#developing-den-against-a-distribution)
|
|
17
18
|
|
|
@@ -379,9 +380,23 @@ a ref that is not set is **not configured**, and its feature says so
|
|
|
379
380
|
("not configured: set secrets.provider and secrets.refs.sudo in
|
|
380
381
|
config/acme.yml") instead of running a CLI. Typing the password always
|
|
381
382
|
works; only the saved password needs `refs.sudo`.
|
|
383
|
+
|
|
384
|
+
**Saving writes `refs.sudo` for you** (LK-4, 0.4.5). When `secrets.provider`
|
|
385
|
+
is `1password` or `doppler`, its CLI is available, and no `refs.sudo` is
|
|
386
|
+
set, the admin password prompt (shown when a command is about to run sudo)
|
|
387
|
+
still offers "Save to 1Password". Saving creates the item
|
|
388
|
+
(1Password: a Password item "<Product> admin password", field `password`,
|
|
389
|
+
in your default vault; Doppler: the key `<PREFIX>_LOCAL_ADMIN_PWD`) and
|
|
390
|
+
then writes that ref to `config/local/user.yml`, because the password is
|
|
391
|
+
the person's, not the distribution's. From then on it survives restarts.
|
|
392
|
+
A failed save writes nothing, and its error never carries the password.
|
|
393
|
+
`env` keeps nothing past the process, so it offers no save. A `refs.sudo`
|
|
394
|
+
in the distribution's config always wins over the one in user.yml.
|
|
395
|
+
|
|
382
396
|
`/api/sudo-password/vault-status` reports `backend`, `label`,
|
|
383
|
-
`configured`, `
|
|
384
|
-
|
|
397
|
+
`configured`, `canSave` (saving works with no YAML to edit),
|
|
398
|
+
`backendAvailable`, `reason` and `hasStored` (`opAvailable` is a
|
|
399
|
+
deprecated alias of `backendAvailable`, kept for one minor).
|
|
385
400
|
|
|
386
401
|
The admin password is for sudo, and nothing else (LK-0). It is no sign-in
|
|
387
402
|
and no access check: starting, stopping and resuming agents, sessions and
|
|
@@ -1209,11 +1224,13 @@ are the same directory.
|
|
|
1209
1224
|
|
|
1210
1225
|
On localhost nothing asks you to sign in: every page, the agents and the
|
|
1211
1226
|
terminal work as they are, and the admin password (R8) is the only prompt,
|
|
1212
|
-
asked only when a command Den runs needs sudo.
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1227
|
+
asked only when a command Den runs needs sudo. Through the edge, a browser
|
|
1228
|
+
the edge shows is on this machine asks for nothing either. Signing in is
|
|
1229
|
+
for everything else: the edge for a client it can't place here (0.4.5), a
|
|
1230
|
+
LAN address, or a bind that isn't loopback (`DEN_BIND_HOST`). From another
|
|
1231
|
+
device, sign in with GitHub as the Den's owner, approve the computer with
|
|
1232
|
+
the code its page shows, or open a one-time link (below, and
|
|
1233
|
+
[Signing in with GitHub](#signing-in-with-github)).
|
|
1217
1234
|
|
|
1218
1235
|
There is no password. Reading `web.secret` (32 random bytes, mode 0600 in
|
|
1219
1236
|
the state dir, made at the first `web up`) proves you are this computer's
|
|
@@ -1221,7 +1238,10 @@ user, and the CLI does that for you:
|
|
|
1221
1238
|
|
|
1222
1239
|
```bash
|
|
1223
1240
|
./bin/acme web open # start if needed, and sign this browser in (a new tab)
|
|
1224
|
-
./bin/acme web login #
|
|
1241
|
+
./bin/acme web login # a one-time sign-in link and its code; waits until it's used
|
|
1242
|
+
./bin/acme web login --no-wait # the link alone (asks for no code), and return
|
|
1243
|
+
./bin/acme web approve ABC-123 # approve the computer showing this code
|
|
1244
|
+
./bin/acme web approve --list # the computers waiting with a code
|
|
1225
1245
|
./bin/acme web sessions # the signed-in browsers: id, created, last seen, host, user agent
|
|
1226
1246
|
./bin/acme web logout <id> # sign out one browser (an id prefix from `web sessions`)
|
|
1227
1247
|
./bin/acme web logout # sign out every browser
|
|
@@ -1238,7 +1258,19 @@ user, and the CLI does that for you:
|
|
|
1238
1258
|
among them; its URL comes from the edge config, and a name the edge
|
|
1239
1259
|
doesn't route here is refused, saying what to add), and
|
|
1240
1260
|
`web login --origin http://<lan name>:<port>` for any other allowed name
|
|
1241
|
-
(the signed-out page there shows the exact command).
|
|
1261
|
+
(the signed-out page there shows the exact command, under "Other ways").
|
|
1262
|
+
- **`web login` waits** (0.4.5). Besides the link it prints an
|
|
1263
|
+
8-character code ("It asks for this code: ABCD-EFGH") and blocks until
|
|
1264
|
+
the link is used, then says who: "Signed in: Safari on macOS
|
|
1265
|
+
(100.64.0.7) at 10:42". Nobody within the minute: "Expired: nobody used
|
|
1266
|
+
the link", exit 1. The link's page asks for the code before it signs in,
|
|
1267
|
+
so the link alone is useless; once opened it waits 5 minutes for the
|
|
1268
|
+
code. A wrong code is refused; five burn the link ("Refused: too many
|
|
1269
|
+
wrong codes", exit 1), more than 20 wrong a minute (any link) are slowed,
|
|
1270
|
+
and the link still works once. The CLI asks the running server through
|
|
1271
|
+
its bearer and never logs the code. `--no-wait` prints the link alone and
|
|
1272
|
+
returns, as before (no code); with no server running `web login` does
|
|
1273
|
+
that too, and says so.
|
|
1242
1274
|
- **The cookie** (`den_session_<port>`) is HttpOnly, SameSite=Strict and
|
|
1243
1275
|
Path=/, and Secure when the request came over HTTPS through the edge. The
|
|
1244
1276
|
session lives server-side (`web-sessions.json`, 0600, in the state dir)
|
|
@@ -1256,36 +1288,111 @@ user, and the CLI does that for you:
|
|
|
1256
1288
|
The Host allowlist, the Origin check and the CSRF token are what stop
|
|
1257
1289
|
another site or a rebinding DNS name. From another machine, every page
|
|
1258
1290
|
and call needs a signed-in browser (or a bearer). A page there shows
|
|
1259
|
-
"Sign in to
|
|
1291
|
+
"Sign in to <name>" and the ways to sign in (below); it never asks for a
|
|
1260
1292
|
secret. `/api` answers 401 `signin_required`.
|
|
1261
|
-
- **The
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
`
|
|
1293
|
+
- **The signed-out page says where to act.** It names the machine Den runs
|
|
1294
|
+
on ("You're opening Den from another computer. Den runs on
|
|
1295
|
+
**studio**, so it needs to know this is you."), then:
|
|
1296
|
+
- **Sign in with GitHub**, only when a client id is set (below);
|
|
1297
|
+
- **Approve this computer**: a code (`XXX-XXX`) and the command that
|
|
1298
|
+
approves it, to run on that machine (`<cli> web approve <code>`), or
|
|
1299
|
+
Settings › Access › Waiting devices there. The page signs itself in
|
|
1300
|
+
when it's approved and goes to the page you asked for; it counts down
|
|
1301
|
+
the 10 minutes, and an expired code is a click away from a new one.
|
|
1302
|
+
From a computer with SSH access to it, the page gives the one-liner
|
|
1303
|
+
(`ssh <host> '<cli> web approve <code>'`) and, when the host is a name,
|
|
1304
|
+
an Open Terminal (`ssh://`) link;
|
|
1305
|
+
- **Other ways**, collapsed: the one-time link with
|
|
1306
|
+
`web login --origin`.
|
|
1307
|
+
|
|
1308
|
+
Every command the page prints names the machine it runs on and has a
|
|
1309
|
+
copy button. It is `<cli>` when the first `<cli>` on that machine's PATH
|
|
1310
|
+
is this distribution's, else the absolute path of its bin.
|
|
1311
|
+
|
|
1312
|
+
- **The machine's name** on the page is `web.machine_name` from
|
|
1313
|
+
`config/local/user.yml` (Settings › Access › Machine name), else the short
|
|
1314
|
+
host name (up to the first dot). It is never taken from the browser's
|
|
1315
|
+
Host. The ssh one-liner's target is the Host the browser used, without
|
|
1316
|
+
the port (the machine's host name if it had none).
|
|
1317
|
+
- **Codes.** About 30 bits (Crockford base32: no I, L, O or U; typed in any
|
|
1318
|
+
case, the dash optional); the page polls with a separate 128-bit id. A
|
|
1319
|
+
code expires after 10 minutes and works once; at most five wait at once
|
|
1320
|
+
(a sixth drops the oldest). They live in the server's memory only.
|
|
1321
|
+
Approving is rate-limited (10 a minute) and recorded in the activity log
|
|
1322
|
+
with who approved, never the code. The new session is "approved with a
|
|
1323
|
+
code". **Deny** (Settings › Access, or `web deny` through the API) tells
|
|
1324
|
+
the page it was refused.
|
|
1325
|
+
- **Seeing that you're signed in.** The chip at the foot of the rail says
|
|
1326
|
+
"Local" (this machine), "Signed in · this device" (a code or a one-time
|
|
1327
|
+
link) or "@<login>" (GitHub), and opens Settings › Access. The first page
|
|
1328
|
+
after a sign-in says "This device is signed in to Den on <machine>",
|
|
1329
|
+
once. Linked devices lists every session (GitHub, a code, a one-time
|
|
1330
|
+
link): how it signed in, when, from where (the address and the browser
|
|
1331
|
+
and OS), last seen, and **Revoke**; the one you're using says "This
|
|
1332
|
+
device".
|
|
1333
|
+
- **The edge counts as local only when it shows the client is.** A
|
|
1334
|
+
request for the edge hostname from the edge (an edge peer, below) is
|
|
1335
|
+
treated as loopback (no sign-in on any route, the agents and the
|
|
1336
|
+
terminal included) when the client the edge reports is this machine.
|
|
1337
|
+
Host, Origin, CSRF and the agent-token rules still apply. Who the client
|
|
1338
|
+
is comes from the right-most `X-Forwarded-For` entry, the one the edge
|
|
1339
|
+
itself appended (anything left of it came from the client, and is never
|
|
1340
|
+
believed):
|
|
1266
1341
|
- loopback, or one of this machine's own interface addresses (a LAN
|
|
1267
|
-
address, or a VPN's such as a Tailscale `100.x`): local;
|
|
1342
|
+
address, or a VPN's such as a Tailscale `100.x`): local, no sign-in;
|
|
1268
1343
|
- missing, or a container/VM network's address (the Docker ranges, or a
|
|
1269
1344
|
subnet of one of this machine's Docker/OrbStack/VM bridges): Den can't
|
|
1270
1345
|
tell (Docker Desktop and OrbStack on macOS hand the edge their
|
|
1271
1346
|
gateway's address, and a TCP passthrough in front of the edge hides the
|
|
1272
|
-
client), so
|
|
1273
|
-
- any other address: another machine, which
|
|
1274
|
-
|
|
1347
|
+
client), so it signs in: `web.edge_unattested`, below;
|
|
1348
|
+
- any other address: another machine, which signs in.
|
|
1349
|
+
|
|
1350
|
+
`web.edge_unattested: local` in `config/local/user.yml` counts a client
|
|
1351
|
+
Den can't place as local again (0.4.4's behaviour). It is read only from
|
|
1352
|
+
that file (a choice for that machine); the distribution config's key is
|
|
1353
|
+
ignored with a warning. The default is `signin`. A request straight to
|
|
1354
|
+
the server's port on localhost (loopback Host and peer) never signs in.
|
|
1275
1355
|
With `DEN_BIND_HOST` beyond loopback (a deliberate exposure, other than
|
|
1276
|
-
the Linux edge's Docker bridge address) the edge
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1356
|
+
the Linux edge's Docker bridge address) the edge always signs in.
|
|
1357
|
+
|
|
1358
|
+
- **The edge believes forwarded headers only from its own networks.** Den
|
|
1359
|
+
renders each entrypoint with `forwardedHeaders.trustedIPs` set to
|
|
1360
|
+
loopback and the Docker ranges (`127.0.0.1/32`, `::1/128`,
|
|
1361
|
+
`172.16.0.0/12`, `192.168.65.0/24`), never `forwardedHeaders.insecure`,
|
|
1362
|
+
so a client can't hand the edge an `X-Forwarded-For` of its own. An
|
|
1363
|
+
`edge.args` entry that sets `forwardedHeaders` or `proxyProtocol` is
|
|
1364
|
+
dropped, and `edge up` says so.
|
|
1365
|
+
- **A TCP passthrough in front of the edge** (a gateway that publishes
|
|
1366
|
+
`:443` and passes TLS through) hides the client: the edge sees the
|
|
1367
|
+
gateway's address. Have the gateway send the PROXY protocol, and list it
|
|
1368
|
+
in `edge.proxy_protocol.trusted_ips` (empty by default; IP addresses or
|
|
1369
|
+
CIDRs; `config/local/user.yml`'s list replaces the team's):
|
|
1370
|
+
|
|
1371
|
+
```yaml
|
|
1372
|
+
edge:
|
|
1373
|
+
proxy_protocol:
|
|
1374
|
+
trusted_ips: [172.18.0.5]
|
|
1375
|
+
```
|
|
1376
|
+
|
|
1377
|
+
Every entrypoint then takes the PROXY protocol from those addresses, and
|
|
1378
|
+
the client it carries is what Den sees. Configuring the gateway is the
|
|
1379
|
+
gateway's job.
|
|
1380
|
+
|
|
1381
|
+
- **`edge up` recreates the edge when its command changed.** It compares
|
|
1382
|
+
the running edge's command with the one it renders and says why ("acme-edge's
|
|
1383
|
+
settings changed (forwarded-header trust): recreating it once. Routes
|
|
1384
|
+
drop only while it restarts."). Upgrading to 0.4.5 does this once.
|
|
1280
1385
|
|
|
1281
1386
|
- **"Your edge is reachable from other machines".** When Den can tell, at
|
|
1282
1387
|
start, that the edge is reachable beyond this machine (an edge hostname
|
|
1283
1388
|
that resolves to an address that isn't loopback, such as a LAN or
|
|
1284
1389
|
Tailscale address, or the edge container publishing a port on every
|
|
1285
1390
|
interface, from `docker inspect`), it writes "Your edge is reachable from
|
|
1286
|
-
other machines
|
|
1287
|
-
|
|
1288
|
-
per browser).
|
|
1391
|
+
other machines. A browser it can't place as this one signs in first
|
|
1392
|
+
(Sign in with GitHub, or a pairing link)." once to `web.log`, and every
|
|
1393
|
+
page shows it as a banner (dismissed per browser). With
|
|
1394
|
+
`web.edge_unattested: local` it says instead that anyone it can't place
|
|
1395
|
+
can use terminals, and to remove the key to have them sign in.
|
|
1289
1396
|
- **localhost is the canonical name.** A cookie set on `localhost` isn't
|
|
1290
1397
|
sent to `127.0.0.1`, so the links Den prints use `localhost`.
|
|
1291
1398
|
- **Browsers.** Chromium, Firefox and Safari 16.4 or later: a sign-in link
|
|
@@ -1319,6 +1426,76 @@ logout --all` the old token is refused; the CLI reads the new one.
|
|
|
1319
1426
|
421 `misdirected`, so a LAN client can't send the edge's name straight to
|
|
1320
1427
|
the server's port.
|
|
1321
1428
|
|
|
1429
|
+
## Signing in with GitHub
|
|
1430
|
+
|
|
1431
|
+
A Den has one owner: a GitHub account. Once it is linked, that account
|
|
1432
|
+
signs in from any device (a phone, another laptop, a browser through the
|
|
1433
|
+
edge) with **Sign in with GitHub** on the signed-out page. Den stores only
|
|
1434
|
+
the account's id and login, never a token, and no other account gets in.
|
|
1435
|
+
|
|
1436
|
+
```bash
|
|
1437
|
+
./bin/acme web owner link # link the owner: GitHub's device flow, in the terminal
|
|
1438
|
+
./bin/acme web owner # show who it is (also: web owner show, --json)
|
|
1439
|
+
./bin/acme web owner unlink # forget the owner (signed-in browsers stay signed in)
|
|
1440
|
+
```
|
|
1441
|
+
|
|
1442
|
+
- **`web owner link`** prints `https://github.com/login/device` and a code.
|
|
1443
|
+
Enter the code there and approve; Den asks GitHub who approved, stores
|
|
1444
|
+
`owner: github:<id>` and the login in `web-owner.json` (0600, in the
|
|
1445
|
+
state dir) and throws the token away. A second link is refused until
|
|
1446
|
+
`web owner unlink`. Settings › Access does the same in the browser
|
|
1447
|
+
(Link your GitHub account, Unlink): shell-grade actions, for a signed-in
|
|
1448
|
+
browser and never an agent's token.
|
|
1449
|
+
- **Sign in with GitHub** on the signed-out page shows a code and
|
|
1450
|
+
`github.com/login/device`. The server runs the flow, and when the owner
|
|
1451
|
+
approves it sets the same never-expiring session cookie a one-time link
|
|
1452
|
+
sets, and the page reloads signed in. Another account sees "@<login>
|
|
1453
|
+
isn't allowed on this Den". The token is thrown away either way. At most
|
|
1454
|
+
three sign-ins wait for GitHub at once; a code expires as GitHub says
|
|
1455
|
+
(15 minutes). With no owner linked the page says to run
|
|
1456
|
+
`web owner link`.
|
|
1457
|
+
- **Settings › Access** lists the linked devices (the browser, how it
|
|
1458
|
+
signed in, the client address when the edge vouched for one, created and
|
|
1459
|
+
last seen; "This device" on yours), each with **Revoke**, which ends its session at once (its
|
|
1460
|
+
open terminals included). **Pair a device** makes a one-time link and a
|
|
1461
|
+
QR code for an edge hostname you pick (`web login --host <name>` in the
|
|
1462
|
+
browser): scan it on the other device within 60 seconds.
|
|
1463
|
+
- **`web.github_client_id`** is the GitHub App's client id (public; no
|
|
1464
|
+
secret). The default is Den's own App, **Monster Orange Den**
|
|
1465
|
+
(`Iv23liKL12gnN6WvRV5b`), so Sign in with GitHub works out of the box. A
|
|
1466
|
+
distribution that ships its own App sets its client id here (below);
|
|
1467
|
+
`config/local/user.yml`'s wins over the team's. An empty value
|
|
1468
|
+
(`github_client_id: ""`) turns Sign in with GitHub off: the page then
|
|
1469
|
+
offers the code and the one-time link only.
|
|
1470
|
+
- **Expiring tokens.** If the App hands out expiring tokens (GitHub's
|
|
1471
|
+
answer carries `expires_in` or a `refresh_token`), sign-in still
|
|
1472
|
+
completes, and Den says once, in the log and on stderr, to switch off
|
|
1473
|
+
token expiry in the App's settings ("Expire user authorization tokens");
|
|
1474
|
+
it never prints the token. `GET /api/web/owner` reports it as
|
|
1475
|
+
`tokenExpiry: true`.
|
|
1476
|
+
|
|
1477
|
+
### The GitHub App (once, by whoever ships the Den)
|
|
1478
|
+
|
|
1479
|
+
Den's default App is Monster Orange Den. A distribution that wants its own
|
|
1480
|
+
(its name on GitHub's approval page) needs a GitHub App with the device
|
|
1481
|
+
flow on. Create it once (organization or account › Settings › Developer settings › GitHub
|
|
1482
|
+
Apps › New GitHub App):
|
|
1483
|
+
|
|
1484
|
+
1. No callback URL; untick "Request user authorization during
|
|
1485
|
+
installation". Webhook off.
|
|
1486
|
+
2. **Enable Device Flow**: ticked. The device flow needs only the client
|
|
1487
|
+
id.
|
|
1488
|
+
3. **Expire user authorization tokens**: off. Den never asks for
|
|
1489
|
+
`offline_access`, and keeps no token either way.
|
|
1490
|
+
4. Account permissions: none. Den reads only `GET /user` (the id and the
|
|
1491
|
+
login), which needs none. Repository permissions are only for features
|
|
1492
|
+
that act on a repository (Den's default App has Issues for feedback).
|
|
1493
|
+
5. **Where can this GitHub App be installed**: Any account. GitHub lets only
|
|
1494
|
+
members of the owning organization authorize a private App, so anyone
|
|
1495
|
+
else couldn't sign in.
|
|
1496
|
+
6. Put the **Client ID** in `web.github_client_id`. Never generate or
|
|
1497
|
+
share a client secret or a private key for Den: it never uses one.
|
|
1498
|
+
|
|
1322
1499
|
## Running the web UI as a service (macOS)
|
|
1323
1500
|
|
|
1324
1501
|
Den ships a launchd installer that runs the web UI at login and restarts it if
|
|
@@ -2129,6 +2306,26 @@ with both workspaces named. Any key of `agents:` other than `max_concurrent`
|
|
|
2129
2306
|
and `persona_packs` is an agent id, and its only key is `grants`. A grant
|
|
2130
2307
|
naming a workspace that doesn't exist stops `start`, naming the key.
|
|
2131
2308
|
|
|
2309
|
+
## Dev metrics: local unless you opt in
|
|
2310
|
+
|
|
2311
|
+
Den records each command it runs (its name, surface, duration and exit
|
|
2312
|
+
code, never arguments or values) as one JSON line in
|
|
2313
|
+
`.dev-metrics/events.jsonl` under the distribution root. That file stays
|
|
2314
|
+
on the machine.
|
|
2315
|
+
|
|
2316
|
+
Nothing is sent anywhere unless the engineer opts in, per machine:
|
|
2317
|
+
|
|
2318
|
+
```yaml
|
|
2319
|
+
# config/local/user.yml (gitignored)
|
|
2320
|
+
metrics:
|
|
2321
|
+
share: true
|
|
2322
|
+
```
|
|
2323
|
+
|
|
2324
|
+
Only the boolean `true` counts. With it, and a Sentry DSN in
|
|
2325
|
+
`DEV_METRICS_SENTRY_DSN` (or a synced `dev-metrics.config`), each line is
|
|
2326
|
+
also sent, best effort. A DSN on its own never sends. `CI=true` and
|
|
2327
|
+
`DEV_METRICS_DISABLED=1` stop sending even when opted in.
|
|
2328
|
+
|
|
2132
2329
|
## Pinning and upgrading
|
|
2133
2330
|
|
|
2134
2331
|
`package.json` pins an exact Den version (`create` writes the version that
|
package/docs/reference.md
CHANGED
|
@@ -110,9 +110,14 @@ That is `examples/demo/demo-api/.den.yml`; the other demo repos are in
|
|
|
110
110
|
workspace's checkout, with an MCP server pinned to that workspace and a
|
|
111
111
|
token of its own. The web UI's Agents tab lists them and opens their
|
|
112
112
|
terminals.
|
|
113
|
-
- **Browser sign-in beyond loopback.** On localhost no route needs a sign-in
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
- **Browser sign-in beyond loopback.** On localhost no route needs a sign-in,
|
|
114
|
+
nor through the edge when it shows the browser is on this machine.
|
|
115
|
+
Anywhere else, every route needs one: Sign in with GitHub as the Den's
|
|
116
|
+
owner (`<cli> web owner link` links the account once), approving the
|
|
117
|
+
computer with the code its page shows (`<cli> web approve <code>`, or
|
|
118
|
+
Settings › Access › Waiting devices), or a one-time link (`<cli> web
|
|
119
|
+
login`, which waits until it's used; `web open` opens it; Settings ›
|
|
120
|
+
Access pairs a device with a QR code). The rail's chip shows who you are.
|
|
116
121
|
- **Web UI and CLI** over the same state.
|
|
117
122
|
|
|
118
123
|
## Strategies
|