feat: frontend audit fixes, README accuracy pass, doc updates

Frontend audit (10 categories): lifecycle fields in types, new API
functions (CRL, OCSP, deployments, updateIssuer/Target, getPolicy),
issuer/owner/profile filters on CertificatesPage, last_renewal_at
column, error_message column on JobsPage, full crypto policy UI on
ProfilesPage (key algorithms, EKUs, SAN patterns), key info + CA
badge on DiscoveryPage, edit modal on TargetDetailPage, tags field
on certificate creation, darwin→macOS mapping on AgentFleetPage.
211 Vitest tests passing.

README accuracy: test counts (1300+ Go, 211 frontend), page count
(24), demo data (32 certs, 7 issuers, 180 days), endpoint count
(97), MCP tools (80), CLI subcommands (10), moved shipped items
out of "Coming in v2.1.0".

Docs: architecture.md diagrams updated (Vault PKI, DigiCert,
Traefik, Caddy added), features.md Vault/DigiCert status updated.
Version bumped to v2.0.20. cli binary removed from git tracking.
Testing guide Part 41 added (12 auto + 9 manual tests).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Shankar
2026-03-30 22:10:45 -04:00
parent 45531ebbba
commit 8dc68381e7
16 changed files with 638 additions and 52 deletions
+55
View File
@@ -83,6 +83,12 @@ import {
getIssuer,
getTarget,
getPrometheusMetrics,
getCertificateDeployments,
getCRL,
getOCSPStatus,
updateIssuer,
updateTarget,
getPolicy,
} from './client';
// Mock global fetch
@@ -1150,4 +1156,53 @@ describe('API Client', () => {
expect(init.headers['Authorization']).toBe('Bearer prom-key');
});
});
describe('Frontend Audit: New API Functions', () => {
it('getCertificateDeployments sends GET with cert ID', async () => {
mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0 }));
await getCertificateDeployments('mc-1');
expect(mockFetch.mock.calls[0][0]).toContain('/api/v1/certificates/mc-1/deployments');
});
it('getCRL sends GET to /crl', async () => {
mockFetch.mockReturnValueOnce(mockJsonResponse({ entries: [], total: 0 }));
await getCRL();
expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/crl');
});
it('getOCSPStatus sends GET with issuer and serial', async () => {
const buf = new ArrayBuffer(8);
mockFetch.mockReturnValueOnce(
Promise.resolve({
ok: true,
status: 200,
arrayBuffer: () => Promise.resolve(buf),
} as Response)
);
await getOCSPStatus('iss-local', 'ABC123');
expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/ocsp/iss-local/ABC123');
});
it('updateIssuer sends PUT with data', async () => {
mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'iss-1', name: 'Updated' }));
await updateIssuer('iss-1', { name: 'Updated' });
const [url, init] = mockFetch.mock.calls[0];
expect(url).toBe('/api/v1/issuers/iss-1');
expect(init.method).toBe('PUT');
});
it('updateTarget sends PUT with data', async () => {
mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 't-1', name: 'Updated' }));
await updateTarget('t-1', { name: 'Updated' });
const [url, init] = mockFetch.mock.calls[0];
expect(url).toBe('/api/v1/targets/t-1');
expect(init.method).toBe('PUT');
});
it('getPolicy sends GET with policy ID', async () => {
mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'pol-1', name: 'Test' }));
await getPolicy('pol-1');
expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/policies/pol-1');
});
});
});
+29
View File
@@ -122,6 +122,26 @@ export const exportCertificatePKCS12 = (id: string, password: string = '') => {
});
};
// Certificate Deployments
export const getCertificateDeployments = (id: string, params: Record<string, string> = {}) => {
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
return fetchJSON<PaginatedResponse<Job>>(`${BASE}/certificates/${id}/deployments?${qs}`);
};
// CRL / OCSP
export const getCRL = () =>
fetchJSON<{ version: number; entries: unknown[]; total: number; generated_at: string }>(`${BASE}/crl`);
export const getOCSPStatus = (issuerId: string, serial: string) => {
const headers: Record<string, string> = {};
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
return fetch(`${BASE}/ocsp/${issuerId}/${serial}`, { headers })
.then(r => {
if (!r.ok) throw new Error(`OCSP request failed: ${r.status}`);
return r.arrayBuffer();
});
};
// Agents
export const getAgents = (params: Record<string, string> = {}) => {
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
@@ -170,6 +190,9 @@ export const createPolicy = (data: Partial<PolicyRule>) =>
export const updatePolicy = (id: string, data: Partial<PolicyRule>) =>
fetchJSON<PolicyRule>(`${BASE}/policies/${id}`, { method: 'PUT', body: JSON.stringify(data) });
export const getPolicy = (id: string) =>
fetchJSON<PolicyRule>(`${BASE}/policies/${id}`);
export const deletePolicy = (id: string) =>
fetchJSON<{ message: string }>(`${BASE}/policies/${id}`, { method: 'DELETE' });
@@ -188,6 +211,9 @@ export const createIssuer = (data: Partial<Issuer>) =>
export const testIssuerConnection = (id: string) =>
fetchJSON<{ message: string }>(`${BASE}/issuers/${id}/test`, { method: 'POST' });
export const updateIssuer = (id: string, data: Partial<Issuer>) =>
fetchJSON<Issuer>(`${BASE}/issuers/${id}`, { method: 'PUT', body: JSON.stringify(data) });
export const deleteIssuer = (id: string) =>
fetchJSON<{ message: string }>(`${BASE}/issuers/${id}`, { method: 'DELETE' });
@@ -200,6 +226,9 @@ export const getTargets = (params: Record<string, string> = {}) => {
export const createTarget = (data: Partial<Target>) =>
fetchJSON<Target>(`${BASE}/targets`, { method: 'POST', body: JSON.stringify(data) });
export const updateTarget = (id: string, data: Partial<Target>) =>
fetchJSON<Target>(`${BASE}/targets/${id}`, { method: 'PUT', body: JSON.stringify(data) });
export const deleteTarget = (id: string) =>
fetchJSON<{ message: string }>(`${BASE}/targets/${id}`, { method: 'DELETE' });
+7
View File
@@ -18,7 +18,10 @@ export interface Certificate {
expires_at: string;
revoked_at?: string;
revocation_reason?: string;
target_ids?: string[];
tags: Record<string, string>;
last_renewal_at?: string;
last_deployment_at?: string;
created_at: string;
updated_at: string;
}
@@ -45,6 +48,8 @@ export interface CertificateVersion {
csr_pem: string;
not_before: string;
not_after: string;
key_algorithm?: string;
key_size?: number;
created_at: string;
}
@@ -138,6 +143,7 @@ export interface Issuer {
/** Backend returns enabled boolean; status is derived from this */
enabled: boolean;
created_at: string;
updated_at?: string;
}
export interface Target {
@@ -149,6 +155,7 @@ export interface Target {
config: Record<string, unknown>;
status: string;
created_at: string;
updated_at?: string;
}
export interface KeyAlgorithmRule {