feat(dev): switch dev tokens to a real admin account with configurable duration

Requested: dev-token.sh/.ps1 tokens should last 3h by default, with an
argument to override the duration.

Server-verified finding: STALWART_RECOVERY_ADMIN (the break-glass
account docker-compose.yml and the scripts used) always issues OAuth
tokens with a fixed 1h expiry, regardless of the server's configured
accessTokenExpiry — confirmed against the live container, including
after changing the setting and restarting. Confirmed x:ApiKey objects,
by contrast, support an arbitrary expiresAt set per request, and their
secret works directly as a bearer token.

Add scripts/dev-server-init.sh (+ .ps1): a one-time, idempotent setup
step that completes the server's bootstrap wizard (default domain, no
TLS certificate request), creates a real "devadmin" admin account, and
sets the server's default OAuth token lifetime to 3h.

Rework dev-token.sh/.ps1 to authenticate as devadmin and create an
x:ApiKey with a caller-supplied expiry (`dev-token.sh 1800` for 30
minutes, defaults to 10800s/3h) instead of running the OAuth PKCE flow
against the recovery account. Verified end-to-end against a fresh
container, including a real browser session against the running WebUI.

Also includes an incidental package-lock.json sync (was still pinned to
v1.0.8 / stale dependency ranges from before the upstream merge).
This commit is contained in:
Steven RYDELL
2026-08-01 18:32:13 +02:00
parent f4c8f8f21c
commit adca6d8730
9 changed files with 725 additions and 191 deletions
+48 -18
View File
@@ -39,27 +39,55 @@ npm run dev:server:down # stop it (add `-v` via `docker compose down -v` to al
The server takes a couple of seconds to come up; `docker compose logs stalwart`
will show `Network listener started ... localPort = 8080` once it's ready.
## 2. Get an access token
## 2. Initialize the server (first time only)
The WebUI normally authenticates through an OAuth flow in the browser, but
for local development it's simpler to skip that and use a bearer token
directly via `VITE_ACCESS_TOKEN` (see `.env.development`).
The container starts in Stalwart's bootstrap mode, which only allows
signing in as the break-glass `STALWART_RECOVERY_ADMIN` account — real
accounts and most settings aren't usable yet. Run once per fresh volume:
```bash
# Windows / PowerShell
pwsh ./scripts/dev-token.ps1
pwsh ./scripts/dev-server-init.ps1
# Linux / macOS / any POSIX shell (including most AI agent sandboxes)
bash ./scripts/dev-token.sh
bash ./scripts/dev-server-init.sh
```
Both scripts log in as the dev container's admin account, run the full
OAuth PKCE flow against it, and write the resulting token to
`.env.development.local` (gitignored, never committed). Tokens expire
after 1 hour — re-run the script and restart `npm run dev` if the UI
starts returning 401s.
This completes the bootstrap wizard (default domain `example.org`, no TLS
certificate request — safe for local/offline use), creates a real
`devadmin@example.org` admin account, and sets the server's default OAuth
access token lifetime to 3 hours. It's idempotent — safe to re-run, it
no-ops once the server is already bootstrapped. You only need to re-run it
after `docker compose down -v` (which wipes the volumes).
## 3. Run the WebUI
## 3. Get an access token
For local development it's simpler to skip interactive login and use a
bearer token directly via `VITE_ACCESS_TOKEN` (see `.env.development`).
```bash
# Windows / PowerShell
pwsh ./scripts/dev-token.ps1 # 3 hour token (server default)
pwsh ./scripts/dev-token.ps1 -DurationSeconds 1800 # custom duration (30 min)
# Linux / macOS / any POSIX shell (including most AI agent sandboxes)
bash ./scripts/dev-token.sh # 3 hour token
bash ./scripts/dev-token.sh 1800 # custom duration (30 min)
```
Both scripts authenticate as the `devadmin` account created in step 2 and
create a Stalwart API key with the requested expiry (default 3 hours,
overridable per invocation — this is a genuine per-request duration, not
a global setting), then write its secret to `.env.development.local`
(gitignored, never committed) as `VITE_ACCESS_TOKEN`. Re-run the script
and restart `npm run dev` once the token expires (the UI starts returning
401s).
The `STALWART_RECOVERY_ADMIN` account is intentionally not used here: it's
a break-glass credential and its tokens always expire in a fixed 1 hour
regardless of server configuration, so it can't honor a custom duration.
## 4. Run the WebUI
```bash
npm install # first time only
@@ -69,7 +97,7 @@ npm run dev
Open `http://localhost:5173`. You should land directly in the admin panel
(no login screen) since `VITE_ACCESS_TOKEN` is set.
## 4. Verify your change
## 5. Verify your change
```bash
npm run typecheck
@@ -91,15 +119,17 @@ behavior):
npm run dev:server:down
docker compose down -v # also removes the stalwart-etc/stalwart-data volumes
npm run dev:server
bash ./scripts/dev-server-init.sh # re-run: fresh volume needs bootstrapping again
```
## Notes for AI agents
- This whole workflow (steps 13) is scriptable end-to-end without a
browser: `npm run dev:server`, then `bash scripts/dev-token.sh`, then
the app is reachable at `http://localhost:5173` with
`VITE_ACCESS_TOKEN` already set. Verify backend connectivity directly
with `curl`, e.g. `curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/jmap/session`.
- This whole workflow (steps 14) is scriptable end-to-end without a
browser: `npm run dev:server`, then `bash scripts/dev-server-init.sh`
(first time only), then `bash scripts/dev-token.sh`, then the app is
reachable at `http://localhost:5173` with `VITE_ACCESS_TOKEN` already
set. Verify backend connectivity directly with `curl`, e.g.
`curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/jmap/session`.
- Read [AGENTS.md](AGENTS.md) before touching anything under `src/`
the schema-fidelity rule applies to all development, local test server
or not.