SSH: the REST API now covers the whole CA lifecycle
The SSH service layer was always meant to be reachable from both APIs — typed tRPC for the UI, REST/OpenAPI for automation — but it had quietly drifted. Only 30 of the 52 tRPC procedures had a REST twin.
The gaps were not peripheral. Everything after create a CA (get, import, revoke, rotate, retire) and the entire bulk router were UI-only, so a script, pki-manager-cli or an Ansible play could stand a CA up and then had nowhere to go.
All 22 missing operations are now exposed, each delegating to the same service singleton its tRPC twin calls:
| Area | New endpoints |
|---|---|
| CA | GET /cas/:caId · POST /cas/import · POST /cas/:caId/{revoke,rotate,retire}
|
| Host | GET /hosts/:id · GET /hosts/:id/deploy-bundle · POST /hosts/:id/{revoke,ecies-key,offboard}
|
| Identity | GET /identities · GET /users/certificates · POST /identities/:id/{disable,offboard}
|
| Principal | GET /principals/mappings · GET /principals/stale-hosts · DELETE /principals/:id
|
| Bulk | GET /bulk/expiring · POST /bulk/{renew,revoke}
|
| KRL | POST /cas/:caId/revoke-serial · POST /cas/:caId/revoke-key
|
Bug fixes found while wiring them up
Two services had the same silent no-op that markPushed had in 3.9.5:
deletePrincipal()never checked existence. Deleting an unknown id removed zero rows and returned success. Now returns 404.revokeCurrent()conflated two different failures. "No such host" and "host exists but has no live certificate" shared one message, so a typo'd id surfaced as a state error. Now 404 vs 400 respectively.
Two REST-layer defects the new tests caught:
/principals/mappingsreturned 400 — the service yields aRecord, not an array, against an array response schema.- Fastify validates a declared body schema even when no body is sent, so endpoints whose body is nothing but an optional
reasonrejected a barecurl -X POSTwithbody must be object. The body now defaults to{}. This also fixes the pre-existingPOST /certs/:id/revoke.
The guard against future drift
ssh-rest-parity.test.ts enumerates the live tRPC router (sshRouter._def.procedures) rather than a hand-written list, and asserts every procedure maps to a registered REST operation. Adding a tRPC-only procedure now fails CI with instructions to add the route or record a deliberate exemption. The exemption list is empty, and a test keeps it that way.
UI
/api-docs is readable in dark mode. swagger-ui ships a hardcoded light palette, which left the embedded docs white-on-white against the app theme.
Upgrade notes: no migration, no configuration change, no breaking change — every route is additive, and the two service fixes turn silent successes into correct errors.
Backend: 63 files / 702 tests (was 61/625). Frontend: 52 tests.