mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-12 10:18:52 +00:00
fix(api,codegen): ARCH-001-A — Phase 1 Orval codegen + 2 new CI guards (large diff)
Sprint 5 unified-master-audit closure. Pre-fix:
- api/openapi.yaml: 7,788 LOC of hand-authored spec.
- web/src/api/generated/: directory did NOT exist (the Phase-5
scaffolding never had its first generation run).
- scripts/ci-guards/openapi-codegen-drift.sh: skip-when-absent
(line 33-39 — informational scaffold).
- api/openapi.yaml info.version: '2.0.0', latest tag: v2.1.7
(a 7-version drift between spec and ship).
Net effect: every new route required three coordinated edits (Go
handler, openapi.yaml, frontend client.ts), payload-level breaking
changes shipped unnoticed, and downstream API client integration
cost was permanent.
Phase 1 fix (the audit's literal scope):
1. **Run Orval**, commit the generated tree. 316 files / ~1.8 MB
under web/src/api/generated/, tags-split layout (one directory
per OpenAPI tag), TanStack Query client mode. All output routes
through web/src/api/mutator.ts which delegates to the existing
fetchJSON in client.ts so auth/CSRF/401-event semantics stay
in one place.
2. **Fix two spec defects** the first orval run surfaced:
- YAML duplicate-key bug at L77-89 — SCEP's description was
misplaced under OIDC. Restored to its own tag entry.
- Missing #/components/schemas/Error referenced by three
operations. Aliased to the existing ErrorResponse schema.
3. **Flip the codegen-drift guard from skip-when-absent to
hard-gate.** A missing generated/ directory now fails the
build with an actionable restore command. The existing
regenerate-and-diff path stays as before.
4. **New openapi-version-tag-parity CI guard.** Asserts
openapi.yaml info.version equals the latest v* git tag. Falls
back to api.github.com when the local clone is shallow.
Bumped openapi.yaml info.version 2.0.0 → 2.1.7 in the same
commit so the new guard greens out.
5. **CI workflow** updated to fetch tags on the frontend job's
checkout so the parity guard reads them locally (the GH API
fallback still works but adds a network round-trip).
Verified locally:
- openapi-codegen-drift.sh: clean (re-generation produces
byte-identical tree to what's tracked).
- openapi-version-tag-parity.sh: clean (2.1.7 == v2.1.7).
- tsc --noEmit: exit 0 across the entire frontend (the
generated tree's responseType field threaded through the
mutator's CertctlFetchOptions cleanly).
- Existing Vitest suite: 141/141 pass on the three sampled
suites (AuthProvider + client + IssuerHierarchyPage).
Follow-on work (NOT in this commit):
- Per-consumer migration: pages flip from client.ts imports to
generated/ imports one at a time. Both styles share fetchJSON
semantics, so the migration is incremental.
- Server-side oapi-codegen handler stubs (Phase 2 from the
audit's fix language) — separate sprint.
Closes ARCH-001-A.
This commit is contained in:
@@ -0,0 +1,387 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* certctl API
|
||||
* Certificate lifecycle management platform API. Manages certificates, issuers,
|
||||
deployment targets, agents, jobs, policies, profiles, teams, owners, agent groups,
|
||||
audit events, notifications, and observability metrics.
|
||||
|
||||
All endpoints under `/api/v1/` require authentication by default (configurable via
|
||||
`CERTCTL_AUTH_TYPE`). Use `Bearer {api_key}` in the Authorization header.
|
||||
|
||||
Paginated list endpoints accept `page` (default 1) and `per_page` (default 50, max 500)
|
||||
query parameters and return a standard envelope with `data`, `total`, `page`, and `per_page`.
|
||||
|
||||
* OpenAPI spec version: 2.1.7
|
||||
*/
|
||||
import {
|
||||
useMutation,
|
||||
useQuery
|
||||
} from '@tanstack/react-query';
|
||||
import type {
|
||||
DataTag,
|
||||
DefinedInitialDataOptions,
|
||||
DefinedUseQueryResult,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UndefinedInitialDataOptions,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult
|
||||
} from '@tanstack/react-query';
|
||||
|
||||
import type {
|
||||
ApprovalRequest,
|
||||
ApproveApprovalRequest200,
|
||||
ApproveApprovalRequestBody,
|
||||
InternalErrorResponse,
|
||||
ListApprovalRequests200,
|
||||
ListApprovalRequestsParams,
|
||||
NotFoundResponse,
|
||||
RejectApprovalRequest200,
|
||||
RejectApprovalRequestBody
|
||||
} from '.././model';
|
||||
|
||||
import { certctlFetch } from '../../mutator';
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Rank 7 issuance approval-workflow primitive. Returns paginated approval
|
||||
requests, optionally filtered by ?state= (pending/approved/rejected/expired),
|
||||
?certificate_id=, or ?requested_by=. Empty filters return the unfiltered
|
||||
list (default page=1, per_page=50).
|
||||
|
||||
* @summary List approval requests
|
||||
*/
|
||||
export const listApprovalRequests = (
|
||||
params?: ListApprovalRequestsParams,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
|
||||
|
||||
return certctlFetch<ListApprovalRequests200>(
|
||||
{url: `/api/v1/approvals`, method: 'GET',
|
||||
params, signal
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const getListApprovalRequestsQueryKey = (params?: ListApprovalRequestsParams,) => {
|
||||
return [
|
||||
`/api/v1/approvals`, ...(params ? [params]: [])
|
||||
] as const;
|
||||
}
|
||||
|
||||
|
||||
export const getListApprovalRequestsQueryOptions = <TData = Awaited<ReturnType<typeof listApprovalRequests>>, TError = InternalErrorResponse>(params?: ListApprovalRequestsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listApprovalRequests>>, TError, TData>>, }
|
||||
) => {
|
||||
|
||||
const {query: queryOptions} = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getListApprovalRequestsQueryKey(params);
|
||||
|
||||
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof listApprovalRequests>>> = ({ signal }) => listApprovalRequests(params, signal);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof listApprovalRequests>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
}
|
||||
|
||||
export type ListApprovalRequestsQueryResult = NonNullable<Awaited<ReturnType<typeof listApprovalRequests>>>
|
||||
export type ListApprovalRequestsQueryError = InternalErrorResponse
|
||||
|
||||
|
||||
export function useListApprovalRequests<TData = Awaited<ReturnType<typeof listApprovalRequests>>, TError = InternalErrorResponse>(
|
||||
params: undefined | ListApprovalRequestsParams, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof listApprovalRequests>>, TError, TData>> & Pick<
|
||||
DefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof listApprovalRequests>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof listApprovalRequests>>
|
||||
> , 'initialData'
|
||||
>, }
|
||||
, queryClient?: QueryClient
|
||||
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
export function useListApprovalRequests<TData = Awaited<ReturnType<typeof listApprovalRequests>>, TError = InternalErrorResponse>(
|
||||
params?: ListApprovalRequestsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listApprovalRequests>>, TError, TData>> & Pick<
|
||||
UndefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof listApprovalRequests>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof listApprovalRequests>>
|
||||
> , 'initialData'
|
||||
>, }
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
export function useListApprovalRequests<TData = Awaited<ReturnType<typeof listApprovalRequests>>, TError = InternalErrorResponse>(
|
||||
params?: ListApprovalRequestsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listApprovalRequests>>, TError, TData>>, }
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
/**
|
||||
* @summary List approval requests
|
||||
*/
|
||||
|
||||
export function useListApprovalRequests<TData = Awaited<ReturnType<typeof listApprovalRequests>>, TError = InternalErrorResponse>(
|
||||
params?: ListApprovalRequestsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listApprovalRequests>>, TError, TData>>, }
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
||||
|
||||
const queryOptions = getListApprovalRequestsQueryOptions(params,options)
|
||||
|
||||
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
|
||||
query.queryKey = queryOptions.queryKey ;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns a single approval request by ID.
|
||||
* @summary Get approval request
|
||||
*/
|
||||
export const getApprovalRequest = (
|
||||
id: string,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
|
||||
|
||||
return certctlFetch<ApprovalRequest>(
|
||||
{url: `/api/v1/approvals/${id}`, method: 'GET', signal
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const getGetApprovalRequestQueryKey = (id?: string,) => {
|
||||
return [
|
||||
`/api/v1/approvals/${id}`
|
||||
] as const;
|
||||
}
|
||||
|
||||
|
||||
export const getGetApprovalRequestQueryOptions = <TData = Awaited<ReturnType<typeof getApprovalRequest>>, TError = NotFoundResponse | InternalErrorResponse>(id: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApprovalRequest>>, TError, TData>>, }
|
||||
) => {
|
||||
|
||||
const {query: queryOptions} = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetApprovalRequestQueryKey(id);
|
||||
|
||||
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getApprovalRequest>>> = ({ signal }) => getApprovalRequest(id, signal);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { queryKey, queryFn, enabled: !!(id), ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getApprovalRequest>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
}
|
||||
|
||||
export type GetApprovalRequestQueryResult = NonNullable<Awaited<ReturnType<typeof getApprovalRequest>>>
|
||||
export type GetApprovalRequestQueryError = NotFoundResponse | InternalErrorResponse
|
||||
|
||||
|
||||
export function useGetApprovalRequest<TData = Awaited<ReturnType<typeof getApprovalRequest>>, TError = NotFoundResponse | InternalErrorResponse>(
|
||||
id: string, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApprovalRequest>>, TError, TData>> & Pick<
|
||||
DefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof getApprovalRequest>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof getApprovalRequest>>
|
||||
> , 'initialData'
|
||||
>, }
|
||||
, queryClient?: QueryClient
|
||||
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
export function useGetApprovalRequest<TData = Awaited<ReturnType<typeof getApprovalRequest>>, TError = NotFoundResponse | InternalErrorResponse>(
|
||||
id: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApprovalRequest>>, TError, TData>> & Pick<
|
||||
UndefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof getApprovalRequest>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof getApprovalRequest>>
|
||||
> , 'initialData'
|
||||
>, }
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
export function useGetApprovalRequest<TData = Awaited<ReturnType<typeof getApprovalRequest>>, TError = NotFoundResponse | InternalErrorResponse>(
|
||||
id: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApprovalRequest>>, TError, TData>>, }
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
/**
|
||||
* @summary Get approval request
|
||||
*/
|
||||
|
||||
export function useGetApprovalRequest<TData = Awaited<ReturnType<typeof getApprovalRequest>>, TError = NotFoundResponse | InternalErrorResponse>(
|
||||
id: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApprovalRequest>>, TError, TData>>, }
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
||||
|
||||
const queryOptions = getGetApprovalRequestQueryOptions(id,options)
|
||||
|
||||
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
|
||||
query.queryKey = queryOptions.queryKey ;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Transitions a pending request to approved AND transitions the linked
|
||||
Job from AwaitingApproval to Pending so the scheduler picks it up.
|
||||
RBAC: the authenticated actor extracted via the auth middleware MUST
|
||||
differ from the request's requested_by — a same-actor self-approval
|
||||
returns HTTP 403 with the substring `two-person integrity` in the
|
||||
body. This is the load-bearing two-person integrity contract;
|
||||
compliance auditors (PCI-DSS 6.4.5, NIST 800-53 SA-15, SOC 2 CC6.1)
|
||||
pattern-match against this code path.
|
||||
|
||||
* @summary Approve a pending approval request
|
||||
*/
|
||||
export const approveApprovalRequest = (
|
||||
id: string,
|
||||
approveApprovalRequestBody?: ApproveApprovalRequestBody,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
|
||||
|
||||
return certctlFetch<ApproveApprovalRequest200>(
|
||||
{url: `/api/v1/approvals/${id}/approve`, method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', },
|
||||
data: approveApprovalRequestBody, signal
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const getApproveApprovalRequestMutationOptions = <TError = void | NotFoundResponse | InternalErrorResponse,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof approveApprovalRequest>>, TError,{id: string;data: ApproveApprovalRequestBody}, TContext>, }
|
||||
): UseMutationOptions<Awaited<ReturnType<typeof approveApprovalRequest>>, TError,{id: string;data: ApproveApprovalRequestBody}, TContext> => {
|
||||
|
||||
const mutationKey = ['approveApprovalRequest'];
|
||||
const {mutation: mutationOptions} = options ?
|
||||
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
||||
options
|
||||
: {...options, mutation: {...options.mutation, mutationKey}}
|
||||
: {mutation: { mutationKey, }};
|
||||
|
||||
|
||||
|
||||
|
||||
const mutationFn: MutationFunction<Awaited<ReturnType<typeof approveApprovalRequest>>, {id: string;data: ApproveApprovalRequestBody}> = (props) => {
|
||||
const {id,data} = props ?? {};
|
||||
|
||||
return approveApprovalRequest(id,data,)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return { mutationFn, ...mutationOptions }}
|
||||
|
||||
export type ApproveApprovalRequestMutationResult = NonNullable<Awaited<ReturnType<typeof approveApprovalRequest>>>
|
||||
export type ApproveApprovalRequestMutationBody = ApproveApprovalRequestBody
|
||||
export type ApproveApprovalRequestMutationError = void | NotFoundResponse | InternalErrorResponse
|
||||
|
||||
/**
|
||||
* @summary Approve a pending approval request
|
||||
*/
|
||||
export const useApproveApprovalRequest = <TError = void | NotFoundResponse | InternalErrorResponse,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof approveApprovalRequest>>, TError,{id: string;data: ApproveApprovalRequestBody}, TContext>, }
|
||||
, queryClient?: QueryClient): UseMutationResult<
|
||||
Awaited<ReturnType<typeof approveApprovalRequest>>,
|
||||
TError,
|
||||
{id: string;data: ApproveApprovalRequestBody},
|
||||
TContext
|
||||
> => {
|
||||
|
||||
const mutationOptions = getApproveApprovalRequestMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
}
|
||||
/**
|
||||
* Transitions a pending request to rejected AND cancels the linked
|
||||
Job. Same-actor RBAC contract as approve. The job's error_message
|
||||
is populated with the supplied note for audit continuity.
|
||||
|
||||
* @summary Reject a pending approval request
|
||||
*/
|
||||
export const rejectApprovalRequest = (
|
||||
id: string,
|
||||
rejectApprovalRequestBody?: RejectApprovalRequestBody,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
|
||||
|
||||
return certctlFetch<RejectApprovalRequest200>(
|
||||
{url: `/api/v1/approvals/${id}/reject`, method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', },
|
||||
data: rejectApprovalRequestBody, signal
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const getRejectApprovalRequestMutationOptions = <TError = void | NotFoundResponse | InternalErrorResponse,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof rejectApprovalRequest>>, TError,{id: string;data: RejectApprovalRequestBody}, TContext>, }
|
||||
): UseMutationOptions<Awaited<ReturnType<typeof rejectApprovalRequest>>, TError,{id: string;data: RejectApprovalRequestBody}, TContext> => {
|
||||
|
||||
const mutationKey = ['rejectApprovalRequest'];
|
||||
const {mutation: mutationOptions} = options ?
|
||||
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
||||
options
|
||||
: {...options, mutation: {...options.mutation, mutationKey}}
|
||||
: {mutation: { mutationKey, }};
|
||||
|
||||
|
||||
|
||||
|
||||
const mutationFn: MutationFunction<Awaited<ReturnType<typeof rejectApprovalRequest>>, {id: string;data: RejectApprovalRequestBody}> = (props) => {
|
||||
const {id,data} = props ?? {};
|
||||
|
||||
return rejectApprovalRequest(id,data,)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return { mutationFn, ...mutationOptions }}
|
||||
|
||||
export type RejectApprovalRequestMutationResult = NonNullable<Awaited<ReturnType<typeof rejectApprovalRequest>>>
|
||||
export type RejectApprovalRequestMutationBody = RejectApprovalRequestBody
|
||||
export type RejectApprovalRequestMutationError = void | NotFoundResponse | InternalErrorResponse
|
||||
|
||||
/**
|
||||
* @summary Reject a pending approval request
|
||||
*/
|
||||
export const useRejectApprovalRequest = <TError = void | NotFoundResponse | InternalErrorResponse,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof rejectApprovalRequest>>, TError,{id: string;data: RejectApprovalRequestBody}, TContext>, }
|
||||
, queryClient?: QueryClient): UseMutationResult<
|
||||
Awaited<ReturnType<typeof rejectApprovalRequest>>,
|
||||
TError,
|
||||
{id: string;data: RejectApprovalRequestBody},
|
||||
TContext
|
||||
> => {
|
||||
|
||||
const mutationOptions = getRejectApprovalRequestMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user