Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,9 @@ you can run `pnpm dev` headlessly (no `Stdin is not a tty`) and then read the st
`backlog browser` on 6430) needs a socat relay. That doc also has the real port table,
verify commands, and the cleanup recipe.

The committed dev config targets `*.ymbihq.local` on the `42xxx`/`52xxx` port range — see
[frontend/CLAUDE.md](frontend/CLAUDE.md) and edit the `.env` files for local-only dev.
The dev stack runs on **`http://localhost`** (`42xxx`/`52xxx` ports, IPv4 binds, so the same
URLs work in Orca's and in the Windows browser). Use `localhost`, never a LAN/WSL hostname:
OIDC login needs a secure context (HTTPS or localhost) — see [frontend/CLAUDE.md](frontend/CLAUDE.md).
Details: [DEVELOPMENT.md](DEVELOPMENT.md) (dev), [DEPLOYMENT.md](DEPLOYMENT.md) (Docker),
[KEYCLOAK.md](KEYCLOAK.md) / [OIDC.md](OIDC.md) (auth).

Expand Down
40 changes: 28 additions & 12 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,20 @@ pnpm dev

### Dev stack ports

The committed dev config targets `*.ymbihq.local` on the `42xxx`/`52xxx` range
The dev stack runs on **`localhost`** on the `42xxx`/`52xxx` range
(`backend/.env`, `frontend/.env`, `frontend/vite.config.ts`) — **not** the 3000/5173
defaults.
defaults. Everything binds IPv4 `0.0.0.0`, so WSL2 forwards each port to Windows and the
same `http://localhost:PORT` URLs work in Orca's browser **and** in any Windows browser.

**Use `localhost`, not a LAN/WSL hostname.** OIDC login (PKCE) needs `crypto.subtle`, which
browsers only expose in a secure context — HTTPS or `http://localhost`. Over plain HTTP on
any other name (e.g. the old `wsl.ymbihq.local`) the app shows *"Login requires a secure
context"*. Production must be served over HTTPS.

| Service | URL | Bound by |
|---|---|---|
| Frontend (Vite) | http://wsl.ymbihq.local:52080 | `frontend/vite.config.ts` (`host: '0.0.0.0'`) |
| Backend (Fastify) | http://wsl.ymbihq.local:52081 — Swagger at `/api/docs` | `backend/.env` `PORT`/`HOST` |
| Frontend (Vite) | http://localhost:52080 | `frontend/vite.config.ts` (`host: '0.0.0.0'`) |
| Backend (Fastify) | http://localhost:52081 — Swagger at `/api/docs` | `backend/.env` `PORT`/`HOST` |
| Cosmian KMS | http://localhost:42998 | `kms/docker-compose.yml` |
| Keycloak | http://localhost:42997 (admin/admin) | `keycloak/docker-compose.yml` |
| Backlog.md web UI | http://localhost:6430 | `backlog/config.yml` `defaultPort` |
Expand Down Expand Up @@ -104,9 +110,19 @@ serving fine — probe the HTTP endpoints, don't trust the healthcheck label.

### WSL2 / Windows reachability gotcha

The Orca browser and the user resolve `localhost` on **Windows**, and WSL2 only forwards
**IPv4 (`0.0.0.0`)** binds. A server on the IPv6 wildcard (`*:PORT` in `ss`) works from
inside WSL but gives `ERR_EMPTY_RESPONSE` / a `chrome-error://` page in the browser.
The Orca browser and the user resolve `localhost` on **Windows**. How a Windows
`localhost:PORT` reaches WSL depends on the machine:

- **WSL default (`localhostForwarding=true`)**: WSL2 forwards **IPv4 (`0.0.0.0`)** binds
automatically. A server on the IPv6 wildcard (`*:PORT` in `ss`) works from inside WSL
but gives `ERR_EMPTY_RESPONSE` / a `chrome-error://` page in the browser.
- **d0 (`localhostForwarding=false` in `%USERPROFILE%\.wslconfig`, on purpose)**: ports are
exposed with `netsh portproxy` rules instead. The dev stack's browser-facing ports are
forwarded loopback-only (`127.0.0.1:PORT → wsl.ymbihq.local:PORT`, not reachable from the
LAN) by `C:\tools\private\add-pki-manager-dev-ports.ps1` (elevated, additive):
**52080** frontend, **52081** API, **42997** Keycloak, **52091** backlog relay. A new
browser-facing port needs a rule there too. Check with
`netsh.exe interface portproxy show v4tov4`.

- Backend and frontend already bind `0.0.0.0` — fine as-is.
- **`backlog browser` binds the IPv6 wildcard on 6430** and has no `--host` flag, so it
Expand Down Expand Up @@ -180,7 +196,7 @@ HOST=0.0.0.0
NODE_ENV=development

# Frontend URL for CORS
FRONTEND_URL=http://wsl.ymbihq.local:52080/
FRONTEND_URL=http://localhost:52080

# Database
DATABASE_PATH=./data/pki.db
Expand All @@ -192,7 +208,7 @@ KMS_URL=http://localhost:42998
ALLOW_UNAUTHENTICATED_SSH_CA=true

# CRL Distribution Point
CRL_DISTRIBUTION_URL=http://wsl.ymbihq.local:52081/crl
CRL_DISTRIBUTION_URL=http://localhost:52081/crl
```

With no `OIDC_ISSUER`/`OIDC_AUDIENCE` set the backend runs **fully unauthenticated** and
Expand All @@ -202,7 +218,7 @@ when the Keycloak container is up. See [OIDC.md](OIDC.md) to enable it.
### Frontend (`frontend/.env`)

```env
VITE_API_URL=http://wsl.ymbihq.local:52081/trpc
VITE_API_URL=http://localhost:52081/trpc
```

## Development Servers
Expand All @@ -223,8 +239,8 @@ Use this when you can't give mprocs a TTY (scripts, agents, CI-ish runs):
|----------|-----------|---------|-----|
| 1 | `kms/` | `docker compose up -d` | http://localhost:42998 |
| 2 | `keycloak/` | `docker compose up -d` | http://localhost:42997 |
| 3 | `backend/` | `pnpm dev` | http://wsl.ymbihq.local:52081 |
| 4 | `frontend/` | `pnpm dev` | http://wsl.ymbihq.local:52080 |
| 3 | `backend/` | `pnpm dev` | http://localhost:52081 |
| 4 | `frontend/` | `pnpm dev` | http://localhost:52080 |
| 5 | repo root | `backlog browser --no-open` | http://localhost:6430 |

## Available Scripts
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,8 @@ cd backend && pnpm db:migrate
pnpm dev # from root
```

- **Frontend**: http://localhost:5173
- **Backend API**: http://localhost:3000
- **Frontend**: http://localhost:52080
- **Backend API**: http://localhost:52081
- **Cosmian KMS**: http://localhost:42998
- **Keycloak**: http://localhost:42997 (admin/admin)

Expand Down
6 changes: 3 additions & 3 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Server Configuration
PORT=3000
PORT=52081
HOST=0.0.0.0
NODE_ENV=development

# Frontend URL for CORS
FRONTEND_URL=http://localhost:5173
FRONTEND_URL=http://localhost:52080

# Database
DATABASE_PATH=./data/pki.db
Expand All @@ -16,7 +16,7 @@ KMS_URL=http://localhost:42998
# CRL Distribution Point URL (for X.509 certificates)
# Format: http://your-domain.com/crl/{caId}.crl
# Example: http://crl.example.com/crl/{caId}.crl
CRL_DISTRIBUTION_URL=http://localhost:3000/crl
CRL_DISTRIBUTION_URL=http://localhost:52081/crl

# =============================================================================
# OIDC Authentication Configuration
Expand Down
2 changes: 1 addition & 1 deletion backend/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ trusts only its own zone's user CAs (`ssh-host-krl.service` narrows the composed
## Gotchas

- **KMS is a hard dependency** for almost all CA/cert paths; unreachable → most ops fail
(`CAKmsInconsistencyError`). `getKMSService()` defaults `KMS_URL` to `http://wsl.ymbihq.local:42998`.
(`CAKmsInconsistencyError`). `getKMSService()` defaults `KMS_URL` to `http://localhost:42998`.
- KMS `certify` modes: pass a `publicKeyId` to certify a KMS-held key (CA creation, manual
issuance); pass a `csr` + `preserveCsrKey` to sign a CSR's **own** public key (the external
`/sign` path — the CA key stays in the KMS). The CSR goes in the KMIP `CertificateRequestValue`
Expand Down
2 changes: 1 addition & 1 deletion backend/src/kms/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,7 @@ let kmsServiceInstance: KMSService | null = null;
*/
export function getKMSService(): KMSService {
if (!kmsServiceInstance) {
const kmsUrl = process.env.KMS_URL || 'http://wsl.ymbihq.local:42998';
const kmsUrl = process.env.KMS_URL || 'http://localhost:42998';

kmsServiceInstance = new KMSService({
kmsUrl,
Expand Down
2 changes: 1 addition & 1 deletion backend/src/kms/spike-ssh-ecies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const KMS_URL = (process.env.KMS_URL || 'http://wsl.ymbihq.local:42998').replace(/\/$/, '');
const KMS_URL = (process.env.KMS_URL || 'http://localhost:42998').replace(/\/$/, '');
const COSMIAN = process.env.COSMIAN_BIN || 'cosmian';
const cosmian = (args: string[]) => execFileSync(COSMIAN, ['--kms-url', KMS_URL, 'kms', ...args], { encoding: 'utf8' });

Expand Down
2 changes: 1 addition & 1 deletion backend/src/kms/spike-ssh-sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { mkdtempSync, writeFileSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const KMS_URL = (process.env.KMS_URL || "http://wsl.ymbihq.local:42998").replace(/\/$/, "");
const KMS_URL = (process.env.KMS_URL || "http://localhost:42998").replace(/\/$/, "");
const COSMIAN_BIN = process.env.COSMIAN_BIN || "cosmian";

type Probe = { path: string; ok: boolean; detail: string };
Expand Down
2 changes: 1 addition & 1 deletion frontend/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Backend API URL
VITE_API_URL=http://localhost:3000/trpc
VITE_API_URL=http://localhost:52081/trpc

# =============================================================================
# OIDC Authentication Configuration
Expand Down
22 changes: 12 additions & 10 deletions frontend/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

React 19 SPA: **TanStack Router** (file-based) + **TanStack Query** + a **tRPC v11** client
that imports the backend's `AppRouter` type for end-to-end type safety. **OIDC** auth
(oidc-client-ts + react-oidc-context, Keycloak-shaped), **Tailwind 4**, **Vite 7**, Vitest +
(oidc-client-ts + react-oidc-context, provider-agnostic), **Tailwind 4**, **Vite 7**, Vitest +
RTL. See the root `CLAUDE.md` for repo-wide rules.

## Commands
Expand Down Expand Up @@ -50,11 +50,15 @@ runtime `public/config.json` (fetched at startup; `apiUrl` + optional `oidc` blo
with placeholder `{"apiUrl":"__API_URL__"}` substituted at deploy time. **OIDC is optional**
— enabled only when an authority is set; otherwise the app runs unauthenticated.

The library's PKCE/`signinRedirect` is unreliable over non-HTTPS, so `src/lib/auth/` has a
**hand-rolled fallback**: a manual `/protocol/openid-connect/auth` URL with self-generated
`state`/`nonce`; `callback.tsx` falls back to a manual token POST → `localStorage`;
`token.ts` `getAccessToken()` tries the library `UserManager` then the manual token. Silent
renew uses `public/silent-renew.html` (loads oidc-client-ts from a **CDN**).
The whole flow is the library's (PR #7): endpoints come from OIDC discovery, login/logout are
`signinRedirect`/`signoutRedirect`, PKCE + state/nonce are validated by oidc-client-ts. One
**shared `UserManager`** (`token.ts` `getUserManager()`) feeds both `AuthProvider` (via its
`userManager` prop) and the tRPC `getAccessToken()`, which renews on demand (`signinSilent`,
single-flight, 60 s buffer). The session lives in **`localStorage`** (`userStore`), key
`oidc.user:<authority>:<clientId>`. **Requires a secure context** (HTTPS or
`http://localhost`): `AuthProvider` shows an explicit error otherwise. There is no
non-HTTPS fallback any more — don't reintroduce one. Silent renew prefers the refresh
token; the iframe fallback uses `public/silent-renew.html` (loads oidc-client-ts from a **CDN**).

## Styling

Expand All @@ -64,9 +68,7 @@ Tailwind **4** via PostCSS (no `tailwind.config.js`; tokens are CSS-first in `sr

## Gotchas

- **Dev port `52080`**; `vite.config.ts` `allowedHosts` = `wsl.ymbihq.local`, `localhost`,
`.ymbihq.local` (wildcard) — other hostnames are blocked. Committed `.env` points at
`wsl.ymbihq.local` hosts; change it for local-only dev.
- **Dev port `52080`, open it as `http://localhost:52080`.** `vite.config.ts` has no
`allowedHosts`, so only localhost/IPs are accepted, and only localhost is a secure context.
- `build:docker`/`tsconfig.build.json` **relax strictness** — run `npm run typecheck` to catch real type errors.
- Two token stores coexist (library `UserManager` + manual `localStorage`) — check both when debugging auth.
- `silent-renew.html` pulls oidc-client-ts from a public CDN — silent renew breaks in air-gapped envs.
Loading
Loading