20260617 增加RPKI Explorer前端

This commit is contained in:
2026-06-17 17:29:20 +08:00
parent 6ef2c98890
commit 56f0d10dc6
28 changed files with 7362 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
export interface ApiEnvelope<T> {
data: T;
page: { nextCursor: string | null; limit: number } | null;
meta: { runId: string | null; schemaVersion: number };
}
export class ApiError extends Error {
constructor(
message: string,
readonly status: number
) {
super(message);
this.name = "ApiError";
}
}
export type JsonQuery = Record<string, string | number | boolean | null | undefined>;
export function withQuery(path: string, query?: JsonQuery): string {
if (!query) {
return path;
}
const search = new URLSearchParams();
for (const [key, value] of Object.entries(query)) {
if (value === undefined || value === null) {
continue;
}
search.set(key, String(value));
}
const queryString = search.toString();
return queryString ? `${path}?${queryString}` : path;
}
export async function getJson<T>(path: string, query?: JsonQuery): Promise<ApiEnvelope<T>> {
const response = await fetch(withQuery(path, query));
const body = (await response.json().catch(() => ({}))) as { error?: string };
if (!response.ok) {
throw new ApiError(body.error ?? `Request failed: ${response.status}`, response.status);
}
return normalizeEnvelope<T>(body);
}
export async function postJson<T>(path: string, body?: unknown): Promise<ApiEnvelope<T>> {
const response = await fetch(path, {
body: JSON.stringify(body ?? {}),
headers: { "content-type": "application/json" },
method: "POST"
});
const responseBody = (await response.json().catch(() => ({}))) as { error?: string };
if (!response.ok) {
throw new ApiError(responseBody.error ?? `Request failed: ${response.status}`, response.status);
}
return normalizeEnvelope<T>(responseBody);
}
export async function getBinary(path: string): Promise<Blob> {
const response = await fetch(path);
if (!response.ok) {
const body = (await response.json().catch(() => ({}))) as { error?: string };
throw new ApiError(body.error ?? `Request failed: ${response.status}`, response.status);
}
return response.blob();
}
export function normalizeEnvelope<T>(value: unknown): ApiEnvelope<T> {
if (typeof value !== "object" || value === null) {
return {
data: value as T,
page: null,
meta: { runId: null, schemaVersion: 1 }
};
}
const record = value as {
data?: T;
page?: { nextCursor?: string | null; limit?: number } | null;
meta?: { runId?: string | null; schemaVersion?: number } | null;
};
return {
data: record.data as T,
page: record.page
? {
nextCursor: record.page.nextCursor ?? null,
limit: record.page.limit ?? 0
}
: null,
meta: {
runId: record.meta?.runId ?? null,
schemaVersion: record.meta?.schemaVersion ?? 1
}
};
}
export function normalizeApiError(error: unknown): Error {
if (error instanceof ApiError) {
return error;
}
if (error instanceof Error) {
return error;
}
return new Error("Unknown API error");
}

View File

@@ -0,0 +1,301 @@
import { getBinary, getJson, postJson } from "./client";
export interface ServiceInfo {
service: string;
version: number;
}
export interface RunRecord {
schemaVersion: number;
runId: string;
runSeq: number | null;
runDir: string;
validationTime: string | null;
syncMode: string | null;
startedAt: string | null;
finishedAt: string | null;
wallMs: number | null;
artifactPaths: Record<string, string>;
counts: {
publicationPoints: number;
objects: number;
freshObjects: number;
cachedObjects: number;
rejectedObjects: number;
freshRejectedObjects: number;
cachedRejectedObjects: number;
trustAnchors: number;
vrps: number;
aspas: number;
warnings: number;
};
indexStatus: string;
indexError: string | null;
}
export interface RepositoryRecord {
schemaVersion: number;
runId: string;
repoId: string;
uri: string;
host: string;
transport: string;
publicationPoints: number;
objects: number;
rejectedObjects: number;
downloadBytes: number | null;
syncDurationMsTotal: number;
phases: Record<string, number>;
terminalStates: Record<string, number>;
}
export interface PublicationPointRecord {
schemaVersion: number;
runId: string;
ppId: string;
repoId: string;
nodeId: number | null;
parentNodeId: number | null;
rsyncBaseUri: string | null;
manifestRsyncUri: string | null;
publicationPointRsyncUri: string | null;
rrdpNotificationUri: string | null;
source: string | null;
repoSyncSource: string | null;
repoSyncPhase: string | null;
repoSyncDurationMs: number | null;
repoSyncError: string | null;
repoTerminalState: string | null;
thisUpdate: string | null;
nextUpdate: string | null;
verifiedAt: string | null;
objects: number;
rejectedObjects: number;
warnings: number;
}
export interface ObjectInstanceRecord {
schemaVersion: number;
runId: string;
objectInstanceId: string;
uri: string;
uriHash: string;
sha256: string;
objectType: string;
result: string;
detailSummary: string | null;
repoId: string;
ppId: string;
sourceSection: string;
rejected: boolean;
rejectReason: string | null;
}
export interface ObjectProjectionRecord {
schemaVersion?: number;
objectType?: string;
sha256?: string;
uri?: string;
parseStatus?: string;
errorSummary?: string | null;
projection?: unknown;
}
export interface ValidationIssueRecord {
stage?: string;
severity?: string;
reasonCode?: string;
summary?: string;
}
export interface ObjectValidationRecord {
objectInstanceId: string;
uri: string;
sha256: string;
objectType: string;
finalStatus: string;
auditResult: string;
detailSummary: string | null;
rejected: boolean;
rejectReason: string | null;
sourceSection: string;
explainAvailable: boolean;
authoritative: boolean;
fileValidation: {
status: string;
stage: string;
issues: ValidationIssueRecord[];
detailSummary: string | null;
};
chainValidation: {
status: string;
stage: string;
issues: ValidationIssueRecord[];
note?: string;
edgesPage?: { nextCursor: string | null; limit: number };
};
}
export interface ChainEdgeRecord {
relation: string;
fromUri: string;
toUri: string;
toObjectInstanceId: string | null;
toSha256: string | null;
status: string;
evidence: unknown;
}
export interface ValidationExplainRecord {
schemaVersion: number;
explainVersion: number;
runId: string;
objectInstanceId: string;
uri: string;
sha256: string;
objectType: string;
finalStatus: string;
auditResult: string;
detailSummary: string | null;
authoritative: boolean;
explainMode: string;
generatedAt: string;
parsevalidate: unknown;
chainvalidate: unknown;
chainEdges: ChainEdgeRecord[];
}
export interface ObjectUriIndexRecord {
runId: string;
uri: string;
sha256: string;
objectInstanceId: string;
repoId: string;
ppId: string;
}
export interface ExportJobRecord {
schemaVersion: number;
jobId: string;
runId: string;
scope: string;
repoId: string | null;
ppId: string | null;
status: string;
createdAt: string;
finishedAt: string | null;
outputPath: string | null;
objectCount: number;
bytesWritten: number;
error: string | null;
}
export type CountsByKey = Record<string, number>;
export async function getServiceInfo() {
return getJson<ServiceInfo>("/api/v1");
}
export async function getLatestRun() {
return getJson<RunRecord>("/api/v1/latest_run");
}
export async function getRunSummary(runId: string) {
return getJson<Record<string, number>>(`/api/v1/runs/${runId}/summary`);
}
export async function getStatsObjectTypes(runId: string) {
return getJson<CountsByKey>(`/api/v1/runs/${runId}/stats/object-types`);
}
export async function getStatsValidation(runId: string) {
return getJson<CountsByKey>(`/api/v1/runs/${runId}/stats/validation`);
}
export async function getStatsReasons(runId: string) {
return getJson<CountsByKey>(`/api/v1/runs/${runId}/stats/reasons`);
}
export async function getStatsDownloads(runId: string) {
return getJson<Record<string, unknown>>(`/api/v1/runs/${runId}/stats/downloads`);
}
export async function listRepos(runId: string, limit = 5, cursor?: string | null) {
return getJson<RepositoryRecord[]>(`/api/v1/runs/${runId}/repos`, { limit, cursor: cursor ?? undefined });
}
export async function listPublicationPointsForRepo(runId: string, repoId: string, limit = 50, cursor?: string | null) {
return getJson<PublicationPointRecord[]>(`/api/v1/runs/${runId}/repos/${repoId}/publication-points`, {
limit,
cursor: cursor ?? undefined
});
}
export async function listObjectsForPublicationPoint(runId: string, ppId: string, limit = 50, cursor?: string | null) {
return getJson<ObjectInstanceRecord[]>(`/api/v1/runs/${runId}/publication-points/${ppId}/objects`, {
limit,
cursor: cursor ?? undefined
});
}
export async function getObject(runId: string, objectInstanceId: string) {
return getJson<ObjectInstanceRecord>(`/api/v1/runs/${runId}/objects/${objectInstanceId}`);
}
export async function getObjectByUri(runId: string, uri: string) {
return getJson<ObjectUriIndexRecord>(`/api/v1/runs/${runId}/objects/by-uri`, { uri });
}
export async function getObjectParsed(runId: string, objectInstanceId: string) {
return getJson<ObjectProjectionRecord | null>(`/api/v1/runs/${runId}/objects/${objectInstanceId}/parsed`);
}
export async function getObjectValidation(runId: string, objectInstanceId: string) {
return getJson<ObjectValidationRecord>(`/api/v1/runs/${runId}/objects/${objectInstanceId}/validation`);
}
export async function getObjectChain(runId: string, objectInstanceId: string) {
return getJson<ChainEdgeRecord[]>(`/api/v1/runs/${runId}/objects/${objectInstanceId}/chain`);
}
export async function listManifestFiles(runId: string, objectInstanceId: string, limit = 50, cursor?: string | null) {
return getJson<unknown[]>(`/api/v1/runs/${runId}/objects/${objectInstanceId}/parsed/manifest-files`, {
limit,
cursor: cursor ?? undefined
});
}
export async function listRevokedCertificates(runId: string, objectInstanceId: string, limit = 50, cursor?: string | null) {
return getJson<unknown[]>(`/api/v1/runs/${runId}/objects/${objectInstanceId}/parsed/revoked-certs`, {
limit,
cursor: cursor ?? undefined
});
}
export async function explainObjectValidation(runId: string, objectInstanceId: string, forceRefresh = false) {
return postJson<ValidationExplainRecord>(`/api/v1/runs/${runId}/objects/${objectInstanceId}/validation/explain`, {
forceRefresh
});
}
export async function downloadRawObject(runId: string, objectInstanceId: string) {
return getBinary(`/api/v1/runs/${runId}/objects/${objectInstanceId}/raw`);
}
export async function createObjectSetExport(runId: string, objectInstanceIds: string[]) {
return postJson<ExportJobRecord>(`/api/v1/runs/${runId}/exports`, {
objectInstanceIds,
scope: "object_set"
});
}
export async function createPublicationPointExport(runId: string, ppId: string) {
return postJson<ExportJobRecord>(`/api/v1/runs/${runId}/exports`, {
ppId,
scope: "publication_point"
});
}
export async function getExportJob(runId: string, jobId: string) {
return getJson<ExportJobRecord>(`/api/v1/runs/${runId}/exports/${jobId}`);
}