Client journey example
An HTTPS loopback host for driving the TypeScript client
against the real Go server. The page under web/ talks to the API only
through the built @sigil/client. The host is the shared
examples/internal/loopback package (also behind
the Go-plus-React example): core credentials with
verification, reset and email-change mail, the magic-link plugin, the OAuth
plugin bound to a built-in mock OIDC provider, and a development mail preview.
It exists to run the client's journeys in Chromium (AUTH-029) and is not a
deployment pattern: it binds only to loopback, mints a throwaway certificate
per run, approves one fixed mock identity, exposes proofs in its mail preview
and raises abuse budgets so repeated sign-ins in one test run are not refused.
Run from a clean checkout
Requirements: the Go version in go.mod, Node/npm versions in package.json,
Docker for the disposable PostgreSQL server, and an available loopback port.
make pg-up # skip if sigil-ci is already running
npm ci --ignore-scripts --no-audit --no-fund
npm run build --workspace packages/client
make client-example
export SIGIL_DATABASE_URL='postgres://auth_ci:auth_ci@127.0.0.1:55432/auth_ci?sslmode=disable'
./bin/client-example --migrate
Open https://localhost:8443/ (exactly this host; the certificate names
localhost) and accept the throwaway certificate once. Register, select
Check mail, open the verification link, confirm, then sign in with the
password, request a magic link, or use the mock provider. Link mock attaches
the mock identity to the signed-in account (recent authentication required);
Sign in with mock then works; Unlink first account ends every session.
The (deny) button makes the provider return access_denied. Buttons report
the client's typed errors: Failed: unauthenticated (401),
Failed: rate_limited (429), retry in 30s, Failed: network.
Why HTTPS: the OAuth plugin requires HTTPS callback and return URLs, and the
production cookie policy (__Host-sigil_session, Secure) needs an https
origin. Development mode is therefore off; the host serves TLS itself with
a certificate it generates on each start and never writes to disk. The mock
provider's token and JWKS endpoints are served by the same process, and the
engine's HTTP client trusts exactly that certificate.
Flags: --addr (numeric loopback only), --host, --database, --schema,
--key-file (parent directory must exist), --client-dir (built client to
serve under /client/) and --migrate (create the schema and apply core and
plugin migrations; later starts can omit it). The first start writes a random
mode-0600 key to ignored bin/client.key; keep it with the database.
What the page does
web/app.js creates one client with core, magic-link and OAuth operations,
renders client.session into #session (data-status carries the status),
and submits each form as a typed call. Landing pages use the client's
helpers: emailed proofs arrive as /#flow=verify|reset|magic&token=... and
are captured and removed from the address bar by captureFragment; the
provider callback /callback/mock?code=...&state=... is captured by
captureOAuthCallback. Both then show an explicit confirmation button, so
opening a link or landing on the callback completes nothing. The OAuth intent
(sign in or link) is kept in sessionStorage across the provider round trip;
it is not a secret and the server binds the transaction's mode independently.
window.sigil exposes the client for the tests' direct cancellation and
failure checks.
Automated journeys
npx playwright install chromium # Linux: --with-deps
make check-client-browser # or: npm run test:client
The command builds the client and the host, launches Chromium against
PostgreSQL on port 8444 with ignoreHTTPSErrors, creates a fresh
sigil_client_test_<32 hex> schema and drops only that schema at teardown via
examples/browser/testdb. The five journeys are defined once in
tests/journeys.mjs and also run against the React pages of
examples/react. They cover: the password journey with cookie
flags, missing-CSRF rejection, renewal and listing, a second tab signing out
over BroadcastChannel without a request, a live 200 GET /session held until
after sign-out that must not restore state, reset revoking another browser
whose client observes the 401, and revoke-all; the magic-link journey with a
scanner-safe link, cross-tab refetch and a replayed proof; provider sign-in
refused for an unlinked identity, link, sign-in, denial, unlink ending every
session, and the other tab following; typed aborted, network, protocol and
401 outcomes leaving state unchanged; and host/origin rejection. The
PostgreSQL CI job runs the same command after the browser example. Go tests
in the shared host package cover the loopback and host boundaries and the mock
provider's enforcement of client credentials, single-use codes, the registered
callback and S256; this package's tests cover the built-client requirement and
the callback page.