feat(dev): add one-command local Stalwart test server + workflow docs

Add docker-compose.yml: a disposable Stalwart instance with a fixed dev
admin account (STALWART_RECOVERY_ADMIN), matching the credentials
scripts/dev-token.ps1 already expected. Wired up via new npm scripts
(dev:server, dev:server:down, dev:server:logs).

scripts/dev-token.ps1 was previously untracked (the whole scripts/
directory was gitignored) even though it's part of the documented dev
workflow — un-ignored it, and added scripts/dev-token.sh, a POSIX
equivalent for non-Windows shells and AI agents without PowerShell.

New DEVELOPMENT.md documents the full loop end-to-end (start server,
get a token, run the dev server, verify), written so it's actionable by
both humans and AI coding agents without needing a browser. Linked from
AGENTS.md (Commands) and README.md (Getting started).

Verified manually: docker compose up brings the server to a healthy
state, /api/auth + /auth/token issue a working bearer token, and
/jmap/session returns 200 with it end-to-end.
This commit is contained in:
Steven RYDELL
2026-08-01 18:05:43 +02:00
parent 38abba39f8
commit f4c8f8f21c
8 changed files with 277 additions and 4 deletions
+60
View File
@@ -0,0 +1,60 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Generates a fresh OAuth access token from the local Stalwart dev container
and writes it to .env.development.local (gitignored).
.DESCRIPTION
Local development only. Requires the dev container from docker-compose.yml
(`docker compose up -d`) to be running. Tokens expire after 1 hour; re-run
this script and restart "npm run dev" when the UI starts returning 401s.
The credentials below belong to the disposable local Stalwart container.
See DEVELOPMENT.md for the full workflow. Non-Windows shells (and AI
agents without PowerShell) can use scripts/dev-token.sh instead.
#>
param(
[string]$ApiBaseUrl = "http://localhost:8080",
[string]$AccountName = "admin@example.org",
[string]$AccountSecret = "c8321iEscHDy0GWV"
)
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
# PKCE pair (S256)
$chars = (48..57) + (65..90) + (97..122)
$verifier = -join ($chars | Get-Random -Count 64 | ForEach-Object { [char]$_ })
$sha = [System.Security.Cryptography.SHA256]::Create()
$challenge = [Convert]::ToBase64String($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($verifier))).Replace('+', '-').Replace('/', '_').TrimEnd('=')
$redirectUri = "http://localhost:3005/oauth/callback"
$authPayload = @{
type = "authCode"
accountName = $AccountName
accountSecret = $AccountSecret
clientId = "stalwart-webui"
redirectUri = $redirectUri
scope = "openid email profile offline_access"
state = [guid]::NewGuid().ToString("N")
codeChallenge = $challenge
codeChallengeMethod = "S256"
} | ConvertTo-Json -Compress
$auth = Invoke-RestMethod -Uri "$ApiBaseUrl/api/auth" -Method Post -ContentType "application/json" -Body $authPayload -TimeoutSec 15
if ($auth.type -ne "authenticated" -or -not $auth.client_code) {
throw "Unexpected /api/auth response: $($auth | ConvertTo-Json -Compress)"
}
$tokenBody = "grant_type=authorization_code&code=$($auth.client_code)&code_verifier=$verifier&client_id=stalwart-webui&redirect_uri=$([uri]::EscapeDataString($redirectUri))"
$token = Invoke-RestMethod -Uri "$ApiBaseUrl/auth/token" -Method Post -ContentType "application/x-www-form-urlencoded" -Body $tokenBody -TimeoutSec 15
$envPath = Join-Path $root ".env.development.local"
@"
# Generated by scripts/dev-token.ps1 - gitignored, do not commit.
# Empty base URL: API calls stay same-origin and go through the Vite proxy.
VITE_API_BASE_URL=
VITE_ACCESS_TOKEN=$($token.access_token)
"@ | Set-Content -Path $envPath -Encoding ascii
Write-Host "Token written to $envPath (expires in $($token.expires_in)s). Restart 'npm run dev' to pick it up."