Phase 5 reconciliation: the audit's headline framing 'ARCH-H1 = 62-route
OpenAPI gap' was a measurement scoping error. Every one of the 209
unique router routes is already accounted for — 154 in api/openapi.yaml,
55 in api/openapi-handler-exceptions.yaml. The existing
openapi-handler-parity.sh CI guard already enforces this and passes
clean today. The audit subtracted operation-count from route-count
without accounting for the documented exceptions YAML.
Where real work remains (and what this PR does about it)
=========================================================
Of the 64 documented exceptions, 35 are legitimate wire-protocol
carve-outs that MUST stay (SCEP RFC 8894 × 8 entries, ACME RFC 8555
default + per-profile × 27 entries — they're protocol contracts, not
REST resources). The remaining 29 are REST-shaped routes whose
OpenAPI ops were deferred during their original Bundle 2 /
audit-2026-05-10 / 2026-05-11 work:
- auth/sessions (3)
- auth/oidc admin (9)
- auth/breakglass admin (4)
- auth/users mgmt (3)
- auth/runtime-config (1)
- auth/demo-residual/cleanup (1)
- audit/export (1)
- auth/logout (1)
- auth/breakglass/login (1)
- auth/oidc {login,callback,bcl} (3)
- oidc/providers/{id}/jwks-status (1)
- + 2 other auth-flow routes
Burn-down plan in 3 sprints (documented in
api/openapi-handler-exceptions.yaml header):
Sprint A: Cluster 1 — sessions + oidc admin (12 ops)
Sprint B: Cluster 2 — breakglass + users + runtime-config (8 ops)
Sprint C: Cluster 3 — audit/export + auth flows (9 ops)
This PR does NOT author the 29 OpenAPI ops; each needs request/
response schemas, not placeholders, and the design work is too
large for one PR. The reconciliation here is documentation + a CI
guard that will fail any future schema-drift, plus the scaffolding
needed for sub-phase 5b.
Sub-phase 5b: codegen scaffolding
==================================
Adds the orval scaffolding without running npm install (sandbox
disk-full; first 'npm install' + 'npm run generate' happens on the
operator's workstation):
- web/orval.config.ts — codegen config emits react-query hooks
from api/openapi.yaml into web/src/api/generated/
- web/package.json — adds orval@^7.0.0 devDep + 'generate' npm script
- web/CODEGEN.md — operator-facing migration doc:
first-time setup, per-consumer migration pattern, burn-down plan,
CI-guard rules
- scripts/ci-guards/openapi-codegen-drift.sh — blocks the build
when api/openapi.yaml changes but web/src/api/generated/ wasn't
regenerated alongside. Currently no-op (the directory doesn't
exist yet); activates from the first 'npm run generate' run.
The legacy web/src/api/client.ts stays in tree per the phase prompt's
'do not delete in same PR as codegen' rule. Consumers migrate one
page at a time as their OpenAPI ops land; client.ts deletion is a
SEPARATE follow-up PR after the last consumer migrates.
Updates to existing guard + exceptions YAML
============================================
- scripts/ci-guards/openapi-handler-parity.sh header rewritten
with the Phase 5 reconciliation numbers (220/158/64/0) and the
wire-protocol vs REST-deferred classification.
- api/openapi-handler-exceptions.yaml header rewritten with the
35/29 split + the 3-sprint burn-down plan. Each exception entry
is unchanged; the header now documents which entries are
permanent (wire-protocol) vs temporary (REST-deferred).
Sandbox limitations + operator follow-up
=========================================
- 'npm install' was NOT run from the sandbox (sessions volume
99%-full, 142 MB free). The operator runs 'cd web && npm install'
on their workstation; this lands orval@^7.0.0 in node_modules,
then 'cd web && npm run generate' produces the initial
web/src/api/generated/ tree.
- First per-consumer migration (suggested: web/src/pages/AuthSettings
or one of the operator-decision pages) lands in a follow-up PR
after npm install completes.
- The 29-op OpenAPI burn-down is a 2-sprint effort tracked under
ARCH-H1 in cowork/certctl-architecture-diligence-audit.html.
All CI guards (openapi-handler-parity, openapi-codegen-drift, plus
every existing guard) verified clean by running each individually.
Closes:
- cowork/certctl-architecture-diligence-audit.html#fix-ARCH-H1
(reconciliation: gap is 0 with exceptions accounted for; burn-down
plan documented for follow-up sprints)
- cowork/certctl-architecture-diligence-audit.html#fix-ARCH-M6
(codegen scaffolding shipped; client.ts deletion follows in a
subsequent PR after consumers migrate)
3.9 KiB
Generated API Client
Last reviewed: 2026-05-13
Phase 5 of the certctl architecture diligence remediation introduced
orval-based code generation for the frontend API client. The
hand-rolled web/src/api/client.ts (1,396 lines, 161 exported
functions) is staged to be retired in favor of the generated
TanStack-Query-shaped surface emitted by orval from
api/openapi.yaml.
Where things live
| Path | What |
|---|---|
api/openapi.yaml |
Source of truth — 158 operations |
api/openapi-handler-exceptions.yaml |
64 routes intentionally NOT in OpenAPI (35 wire-protocol carve-outs + 29 REST-deferred) |
web/orval.config.ts |
Codegen config — emits react-query hooks |
web/src/api/generated/ |
Output tree (regenerated, git-tracked) |
web/src/api/client.ts |
Legacy hand-rolled client (TO BE DELETED in follow-up PR) |
web/src/api/mutator.ts |
Fetch wrapper used by the generated client (CSRF, auth) |
scripts/ci-guards/openapi-handler-parity.sh |
Verifies every router route is in OpenAPI OR exceptions |
scripts/ci-guards/openapi-codegen-drift.sh |
Blocks the build when openapi.yaml changes but generated/ wasn't regenerated |
First-time setup
Run from the repo root:
cd web
npm install # installs orval as a devDep
npm run generate # regenerates web/src/api/generated/
git add web/src/api/generated/ web/src/api/mutator.ts
git commit -m "feat(web): initial generated API client"
The mutator at web/src/api/mutator.ts is operator-authored (orval
references it from orval.config.ts); it must export a
certctlFetch<T>(config: AxiosRequestConfig): Promise<T> function
that the generated code calls for every HTTP request. The mutator is
where CSRF + bearer-token + retry policy + 401-redirect logic lives
in one place.
Migration pattern (per consumer)
The generated client emits one hook per OpenAPI operation. Migrate consumers one page at a time; the hand-rolled client and generated client coexist until the last consumer migrates.
// Legacy (web/src/api/client.ts → web/src/pages/CertificatesPage.tsx):
import { useQuery } from '@tanstack/react-query';
import { getCertificates } from '../api/client';
const certs = useQuery({
queryKey: ['certificates'],
queryFn: getCertificates,
});
// Generated (web/src/api/generated/certificates/certificates.ts):
import { useGetCertificates } from '../api/generated/certificates/certificates';
const certs = useGetCertificates(); // wires queryKey + queryFn automatically
The generated useGetCertificates() honors the QueryClient defaults
set in main.tsx (frontend-design-audit Phase 2 TQ-H2 / TQ-M1 tier
model). Per-call overrides (staleTime, refetchInterval, etc.) pass
through as the second argument:
const certs = useGetCertificates({
query: { staleTime: STALE_TIME.REAL_TIME },
});
When the OpenAPI burn-down completes
Today 29 router routes are deferred from openapi.yaml (see the
"REST-shaped" group in api/openapi-handler-exceptions.yaml). The
generated client only covers what's in openapi.yaml — those 29
routes still go through web/src/api/client.ts until their OpenAPI
ops land. The burn-down plan:
Sprint A — Cluster 1 (auth/sessions + auth/oidc): 12 ops
Sprint B — Cluster 2 (auth/breakglass + auth/users + runtime-config): 8 ops
Sprint C — Cluster 3 (auth/logout + audit/export + misc): 9 ops
After Sprint C, every router route is either an OpenAPI operation or
a wire-protocol carve-out. The last consumer migrates off
web/src/api/client.ts and the file gets deleted in a follow-up PR.
CI guards
openapi-handler-parity.shblocks any new router route that isn't inopenapi.yamlAND isn't in the exceptions YAML.openapi-codegen-drift.shblocks anyopenapi.yamlchange that doesn't regenerateweb/src/api/generated/alongside.
Both run automatically as part of the per-PR CI guard sweep at
.github/workflows/ci.yml.