Initial project snapshot
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,609 @@
|
||||
import type {
|
||||
AuditEvent,
|
||||
AuthResult,
|
||||
BootstrapOwnerResult,
|
||||
Cluster,
|
||||
ClusterNodeGroup,
|
||||
ClusterAdminSummary,
|
||||
ClusterAuthorityState,
|
||||
ClusterNode,
|
||||
CreatedJoinToken,
|
||||
FabricEntryPoint,
|
||||
FabricEntryPointNode,
|
||||
FabricEgressPool,
|
||||
FabricEgressPoolNode,
|
||||
FabricTestingFlag,
|
||||
InstallationStatus,
|
||||
JoinRequest,
|
||||
MeshLink,
|
||||
NodeHeartbeat,
|
||||
NodeSyntheticMeshConfig,
|
||||
NodeTelemetryObservation,
|
||||
NodeWorkloadDesiredState,
|
||||
OrganizationAdminSummary,
|
||||
QoSPolicy,
|
||||
RoleAssignment,
|
||||
VPNConnection,
|
||||
VPNConnectionLease,
|
||||
WorkloadStatus,
|
||||
} from "../types";
|
||||
|
||||
export type AdminClientConfig = {
|
||||
baseUrl: string;
|
||||
actorUserId: string;
|
||||
};
|
||||
|
||||
type ApiErrorPayload = {
|
||||
error?: {
|
||||
code?: string;
|
||||
message_key?: string;
|
||||
fallback_message?: string;
|
||||
trace_id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type CreateClusterPayload = {
|
||||
slug: string;
|
||||
name: string;
|
||||
region?: string | null;
|
||||
};
|
||||
|
||||
export type UpdateClusterPayload = {
|
||||
name: string;
|
||||
status: string;
|
||||
region?: string | null;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type CreateVPNConnectionPayload = {
|
||||
organizationId: string;
|
||||
name: string;
|
||||
protocolFamily: string;
|
||||
targetEndpoint: Record<string, unknown>;
|
||||
credentialRef?: string | null;
|
||||
desiredState: string;
|
||||
allowedNodePolicy: Record<string, unknown>;
|
||||
routingUsage: unknown[];
|
||||
routePolicy: Record<string, unknown>;
|
||||
qosPolicy: Record<string, unknown>;
|
||||
placementPolicy: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type CreateFabricEntryPointPayload = {
|
||||
name: string;
|
||||
endpointType: string;
|
||||
publicEndpoint?: string | null;
|
||||
policy?: Record<string, unknown>;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type CreateFabricEgressPoolPayload = {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
routeScope?: Record<string, unknown>;
|
||||
policy?: Record<string, unknown>;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type SetFabricEndpointNodePayload = {
|
||||
status?: string;
|
||||
priority?: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export class AdminApiClient {
|
||||
private readonly baseUrl: string;
|
||||
private readonly actorUserId: string;
|
||||
|
||||
constructor(config: AdminClientConfig) {
|
||||
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
||||
this.actorUserId = config.actorUserId.trim();
|
||||
}
|
||||
|
||||
async login(input: { email: string; password: string; deviceLabel: string; trustDevice: boolean }): Promise<AuthResult> {
|
||||
return this.post<AuthResult>("/auth/login", {
|
||||
email: input.email,
|
||||
password: input.password,
|
||||
device_fingerprint: browserDeviceFingerprint(),
|
||||
device_label: input.deviceLabel,
|
||||
trust_device: input.trustDevice,
|
||||
});
|
||||
}
|
||||
|
||||
async getInstallationStatus(): Promise<InstallationStatus> {
|
||||
const payload = await this.request<{ installation: InstallationStatus }>("/installation/status", {
|
||||
method: "GET",
|
||||
});
|
||||
return payload.installation;
|
||||
}
|
||||
|
||||
async bootstrapOwner(input: {
|
||||
email: string;
|
||||
password: string;
|
||||
activationPayload?: unknown;
|
||||
activationSignature?: string;
|
||||
}): Promise<BootstrapOwnerResult> {
|
||||
return this.post<BootstrapOwnerResult>("/installation/bootstrap-owner", {
|
||||
email: input.email,
|
||||
password: input.password,
|
||||
activation_payload: input.activationPayload,
|
||||
activation_signature: input.activationSignature || "",
|
||||
});
|
||||
}
|
||||
|
||||
async revokeAuthSession(input: { userId: string; authSessionId: string; reason: string }): Promise<void> {
|
||||
await this.post("/auth/sessions/revoke", {
|
||||
user_id: input.userId,
|
||||
auth_session_id: input.authSessionId,
|
||||
reason: input.reason,
|
||||
});
|
||||
}
|
||||
|
||||
async listClusters(): Promise<Cluster[]> {
|
||||
const payload = await this.get<{ clusters: Cluster[] }>("/clusters");
|
||||
return payload.clusters ?? [];
|
||||
}
|
||||
|
||||
async createCluster(input: CreateClusterPayload): Promise<Cluster> {
|
||||
const payload = await this.post<{ cluster: Cluster }>("/clusters/", {
|
||||
actor_user_id: this.actorUserId,
|
||||
slug: input.slug,
|
||||
name: input.name,
|
||||
region: input.region || null,
|
||||
metadata: {},
|
||||
});
|
||||
return payload.cluster;
|
||||
}
|
||||
|
||||
async updateCluster(clusterId: string, input: UpdateClusterPayload): Promise<Cluster> {
|
||||
const payload = await this.put<{ cluster: Cluster }>(`/clusters/${clusterId}`, {
|
||||
actor_user_id: this.actorUserId,
|
||||
name: input.name,
|
||||
status: input.status,
|
||||
region: input.region || null,
|
||||
metadata: input.metadata || {},
|
||||
});
|
||||
return payload.cluster;
|
||||
}
|
||||
|
||||
async listClusterSummaries(): Promise<ClusterAdminSummary[]> {
|
||||
const payload = await this.get<{ cluster_summaries: ClusterAdminSummary[] }>("/cluster-admin-summaries");
|
||||
return payload.cluster_summaries ?? [];
|
||||
}
|
||||
|
||||
async getClusterAuthority(clusterId: string): Promise<ClusterAuthorityState> {
|
||||
const payload = await this.get<{ authority_state: ClusterAuthorityState }>(`/clusters/${clusterId}/authority`);
|
||||
return payload.authority_state;
|
||||
}
|
||||
|
||||
async updateClusterAuthority(
|
||||
clusterId: string,
|
||||
input: { authorityState: string; mutationMode: string; notes?: string | null },
|
||||
): Promise<ClusterAuthorityState> {
|
||||
const payload = await this.put<{ authority_state: ClusterAuthorityState }>(`/clusters/${clusterId}/authority`, {
|
||||
actor_user_id: this.actorUserId,
|
||||
authority_state: input.authorityState,
|
||||
mutation_mode: input.mutationMode,
|
||||
notes: input.notes || null,
|
||||
});
|
||||
return payload.authority_state;
|
||||
}
|
||||
|
||||
async listNodes(clusterId: string): Promise<ClusterNode[]> {
|
||||
const payload = await this.get<{ nodes: ClusterNode[] }>(`/clusters/${clusterId}/nodes`);
|
||||
return payload.nodes ?? [];
|
||||
}
|
||||
|
||||
async listNodeGroups(clusterId: string): Promise<ClusterNodeGroup[]> {
|
||||
const payload = await this.get<{ node_groups: ClusterNodeGroup[] }>(`/clusters/${clusterId}/node-groups`);
|
||||
return payload.node_groups ?? [];
|
||||
}
|
||||
|
||||
async createNodeGroup(
|
||||
clusterId: string,
|
||||
input: { name: string; parentGroupId?: string | null; description?: string | null; sortOrder?: number },
|
||||
): Promise<ClusterNodeGroup> {
|
||||
const payload = await this.post<{ node_group: ClusterNodeGroup }>(`/clusters/${clusterId}/node-groups`, {
|
||||
actor_user_id: this.actorUserId,
|
||||
parent_group_id: input.parentGroupId || null,
|
||||
name: input.name,
|
||||
description: input.description || null,
|
||||
sort_order: input.sortOrder || 0,
|
||||
metadata: {},
|
||||
});
|
||||
return payload.node_group;
|
||||
}
|
||||
|
||||
async assignNodeGroup(clusterId: string, nodeId: string, groupId?: string | null): Promise<ClusterNode> {
|
||||
const payload = await this.put<{ node: ClusterNode }>(`/clusters/${clusterId}/nodes/${nodeId}/group`, {
|
||||
actor_user_id: this.actorUserId,
|
||||
group_id: groupId || null,
|
||||
});
|
||||
return payload.node;
|
||||
}
|
||||
|
||||
async disableMembership(clusterId: string, nodeId: string, reason: string): Promise<void> {
|
||||
await this.post(`/clusters/${clusterId}/nodes/${nodeId}/membership/disable`, {
|
||||
actor_user_id: this.actorUserId,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
|
||||
async attachExistingNode(clusterId: string, nodeId: string, roles: string[]): Promise<ClusterNode> {
|
||||
const payload = await this.post<{ node: ClusterNode }>(`/clusters/${clusterId}/nodes/${nodeId}/membership/attach`, {
|
||||
actor_user_id: this.actorUserId,
|
||||
roles,
|
||||
});
|
||||
return payload.node;
|
||||
}
|
||||
|
||||
async revokeNodeIdentity(clusterId: string, nodeId: string, reason: string): Promise<void> {
|
||||
await this.post(`/clusters/${clusterId}/nodes/${nodeId}/identity/revoke`, {
|
||||
actor_user_id: this.actorUserId,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
|
||||
async listJoinRequests(clusterId: string): Promise<JoinRequest[]> {
|
||||
const payload = await this.get<{ join_requests: JoinRequest[] }>(`/clusters/${clusterId}/join-requests`);
|
||||
return payload.join_requests ?? [];
|
||||
}
|
||||
|
||||
async createJoinToken(clusterId: string, input: { maxUses: number; ttlHours: number; scope: Record<string, unknown> }): Promise<CreatedJoinToken> {
|
||||
const expiresAt = new Date(Date.now() + Math.max(input.ttlHours, 1) * 60 * 60 * 1000).toISOString();
|
||||
const payload = await this.post<{ join_token: CreatedJoinToken }>(`/clusters/${clusterId}/join-tokens`, {
|
||||
actor_user_id: this.actorUserId,
|
||||
scope: input.scope,
|
||||
expires_at: expiresAt,
|
||||
max_uses: Math.max(input.maxUses, 1),
|
||||
});
|
||||
return payload.join_token;
|
||||
}
|
||||
|
||||
async approveJoinRequest(clusterId: string, requestId: string): Promise<void> {
|
||||
await this.post(`/clusters/${clusterId}/join-requests/${requestId}/approve`, {
|
||||
actor_user_id: this.actorUserId,
|
||||
ownership_type: "platform_managed",
|
||||
});
|
||||
}
|
||||
|
||||
async rejectJoinRequest(clusterId: string, requestId: string, reason: string): Promise<void> {
|
||||
await this.post(`/clusters/${clusterId}/join-requests/${requestId}/reject`, {
|
||||
actor_user_id: this.actorUserId,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
|
||||
async listNodeRoles(clusterId: string, nodeId: string): Promise<RoleAssignment[]> {
|
||||
const payload = await this.get<{ role_assignments: RoleAssignment[] }>(
|
||||
`/clusters/${clusterId}/nodes/${nodeId}/roles`,
|
||||
);
|
||||
return payload.role_assignments ?? [];
|
||||
}
|
||||
|
||||
async assignRole(clusterId: string, nodeId: string, role: string, organizationId?: string): Promise<void> {
|
||||
await this.setRoleStatus(clusterId, nodeId, role, "active", organizationId);
|
||||
}
|
||||
|
||||
async setRoleStatus(clusterId: string, nodeId: string, role: string, status: "active" | "disabled" | "revoked", organizationId?: string): Promise<void> {
|
||||
await this.post(`/clusters/${clusterId}/nodes/${nodeId}/roles`, {
|
||||
actor_user_id: this.actorUserId,
|
||||
organization_id: organizationId || null,
|
||||
role,
|
||||
status,
|
||||
policy: {},
|
||||
});
|
||||
}
|
||||
|
||||
async listWorkloadStatuses(clusterId: string, nodeId: string): Promise<WorkloadStatus[]> {
|
||||
const payload = await this.get<{ workload_statuses: WorkloadStatus[] }>(
|
||||
`/clusters/${clusterId}/nodes/${nodeId}/workloads/status`,
|
||||
);
|
||||
return payload.workload_statuses ?? [];
|
||||
}
|
||||
|
||||
async listDesiredWorkloads(clusterId: string, nodeId: string): Promise<NodeWorkloadDesiredState[]> {
|
||||
const payload = await this.get<{ desired_workloads: NodeWorkloadDesiredState[] }>(
|
||||
`/clusters/${clusterId}/nodes/${nodeId}/workloads/desired`,
|
||||
);
|
||||
return payload.desired_workloads ?? [];
|
||||
}
|
||||
|
||||
async listNodeHeartbeats(clusterId: string, nodeId: string, limit = 100): Promise<NodeHeartbeat[]> {
|
||||
const payload = await this.get<{ heartbeats: NodeHeartbeat[] }>(
|
||||
`/clusters/${clusterId}/nodes/${nodeId}/heartbeats?limit=${limit}`,
|
||||
);
|
||||
return payload.heartbeats ?? [];
|
||||
}
|
||||
|
||||
async listNodeTelemetry(clusterId: string, nodeId: string, limit = 120): Promise<NodeTelemetryObservation[]> {
|
||||
const payload = await this.get<{ telemetry: NodeTelemetryObservation[] }>(
|
||||
`/clusters/${clusterId}/nodes/${nodeId}/telemetry?limit=${limit}`,
|
||||
);
|
||||
return payload.telemetry ?? [];
|
||||
}
|
||||
|
||||
async listFabricTestingFlags(): Promise<FabricTestingFlag[]> {
|
||||
const payload = await this.get<{ testing_flags: FabricTestingFlag[] }>("/fabric/testing-flags");
|
||||
return payload.testing_flags ?? [];
|
||||
}
|
||||
|
||||
async updateFabricTestingFlag(input: {
|
||||
scopeType: string;
|
||||
scopeId?: string | null;
|
||||
clusterId?: string | null;
|
||||
enabled: boolean;
|
||||
telemetryEnabled: boolean;
|
||||
syntheticLinksEnabled: boolean;
|
||||
historyRetentionHours: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}): Promise<FabricTestingFlag> {
|
||||
const payload = await this.put<{ testing_flag: FabricTestingFlag }>("/fabric/testing-flags", {
|
||||
actor_user_id: this.actorUserId,
|
||||
scope_type: input.scopeType,
|
||||
scope_id: input.scopeId || null,
|
||||
cluster_id: input.clusterId || null,
|
||||
enabled: input.enabled,
|
||||
telemetry_enabled: input.telemetryEnabled,
|
||||
synthetic_links_enabled: input.syntheticLinksEnabled,
|
||||
history_retention_hours: input.historyRetentionHours,
|
||||
metadata: input.metadata || {},
|
||||
});
|
||||
return payload.testing_flag;
|
||||
}
|
||||
|
||||
async setDesiredWorkload(
|
||||
clusterId: string,
|
||||
nodeId: string,
|
||||
serviceType: string,
|
||||
input: { desiredState: string; runtimeMode: string; version?: string; config: Record<string, unknown>; environment: Record<string, unknown> },
|
||||
): Promise<void> {
|
||||
await this.put(`/clusters/${clusterId}/nodes/${nodeId}/workloads/${serviceType}/desired`, {
|
||||
actor_user_id: this.actorUserId,
|
||||
desired_state: input.desiredState,
|
||||
version: input.version || null,
|
||||
runtime_mode: input.runtimeMode,
|
||||
artifact_ref: null,
|
||||
config: input.config,
|
||||
environment: input.environment,
|
||||
});
|
||||
}
|
||||
|
||||
async listMeshLinks(clusterId: string): Promise<MeshLink[]> {
|
||||
const payload = await this.get<{ mesh_links: MeshLink[] }>(`/clusters/${clusterId}/mesh/links`);
|
||||
return payload.mesh_links ?? [];
|
||||
}
|
||||
|
||||
async getNodeSyntheticMeshConfig(clusterId: string, nodeId: string): Promise<NodeSyntheticMeshConfig> {
|
||||
const payload = await this.get<{ synthetic_mesh_config: NodeSyntheticMeshConfig }>(
|
||||
`/clusters/${clusterId}/nodes/${nodeId}/mesh/synthetic-config`,
|
||||
);
|
||||
return payload.synthetic_mesh_config;
|
||||
}
|
||||
|
||||
async listQoSPolicies(clusterId: string): Promise<QoSPolicy[]> {
|
||||
const payload = await this.get<{ qos_policies: QoSPolicy[] }>(`/clusters/${clusterId}/mesh/qos-policies`);
|
||||
return payload.qos_policies ?? [];
|
||||
}
|
||||
|
||||
async listFabricEntryPoints(clusterId: string): Promise<FabricEntryPoint[]> {
|
||||
const payload = await this.get<{ entry_points: FabricEntryPoint[] }>(`/clusters/${clusterId}/fabric/entry-points`);
|
||||
return payload.entry_points ?? [];
|
||||
}
|
||||
|
||||
async createFabricEntryPoint(clusterId: string, input: CreateFabricEntryPointPayload): Promise<FabricEntryPoint> {
|
||||
const payload = await this.post<{ entry_point: FabricEntryPoint }>(`/clusters/${clusterId}/fabric/entry-points`, {
|
||||
actor_user_id: this.actorUserId,
|
||||
name: input.name,
|
||||
status: "active",
|
||||
endpoint_type: input.endpointType || "client_access",
|
||||
public_endpoint: input.publicEndpoint || null,
|
||||
policy: input.policy || {},
|
||||
metadata: input.metadata || {},
|
||||
});
|
||||
return payload.entry_point;
|
||||
}
|
||||
|
||||
async listFabricEntryPointNodes(clusterId: string, entryPointId: string): Promise<FabricEntryPointNode[]> {
|
||||
const payload = await this.get<{ entry_point_nodes: FabricEntryPointNode[] }>(
|
||||
`/clusters/${clusterId}/fabric/entry-points/${entryPointId}/nodes`,
|
||||
);
|
||||
return payload.entry_point_nodes ?? [];
|
||||
}
|
||||
|
||||
async setFabricEntryPointNode(
|
||||
clusterId: string,
|
||||
entryPointId: string,
|
||||
nodeId: string,
|
||||
input: SetFabricEndpointNodePayload = {},
|
||||
): Promise<FabricEntryPointNode> {
|
||||
const payload = await this.put<{ entry_point_node: FabricEntryPointNode }>(
|
||||
`/clusters/${clusterId}/fabric/entry-points/${entryPointId}/nodes/${nodeId}`,
|
||||
{
|
||||
actor_user_id: this.actorUserId,
|
||||
status: input.status || "active",
|
||||
priority: input.priority || 100,
|
||||
metadata: input.metadata || {},
|
||||
},
|
||||
);
|
||||
return payload.entry_point_node;
|
||||
}
|
||||
|
||||
async listFabricEgressPools(clusterId: string): Promise<FabricEgressPool[]> {
|
||||
const payload = await this.get<{ egress_pools: FabricEgressPool[] }>(`/clusters/${clusterId}/fabric/egress-pools`);
|
||||
return payload.egress_pools ?? [];
|
||||
}
|
||||
|
||||
async createFabricEgressPool(clusterId: string, input: CreateFabricEgressPoolPayload): Promise<FabricEgressPool> {
|
||||
const payload = await this.post<{ egress_pool: FabricEgressPool }>(`/clusters/${clusterId}/fabric/egress-pools`, {
|
||||
actor_user_id: this.actorUserId,
|
||||
name: input.name,
|
||||
status: "active",
|
||||
description: input.description || null,
|
||||
route_scope: input.routeScope || {},
|
||||
policy: input.policy || {},
|
||||
metadata: input.metadata || {},
|
||||
});
|
||||
return payload.egress_pool;
|
||||
}
|
||||
|
||||
async listFabricEgressPoolNodes(clusterId: string, egressPoolId: string): Promise<FabricEgressPoolNode[]> {
|
||||
const payload = await this.get<{ egress_pool_nodes: FabricEgressPoolNode[] }>(
|
||||
`/clusters/${clusterId}/fabric/egress-pools/${egressPoolId}/nodes`,
|
||||
);
|
||||
return payload.egress_pool_nodes ?? [];
|
||||
}
|
||||
|
||||
async setFabricEgressPoolNode(
|
||||
clusterId: string,
|
||||
egressPoolId: string,
|
||||
nodeId: string,
|
||||
input: SetFabricEndpointNodePayload = {},
|
||||
): Promise<FabricEgressPoolNode> {
|
||||
const payload = await this.put<{ egress_pool_node: FabricEgressPoolNode }>(
|
||||
`/clusters/${clusterId}/fabric/egress-pools/${egressPoolId}/nodes/${nodeId}`,
|
||||
{
|
||||
actor_user_id: this.actorUserId,
|
||||
status: input.status || "active",
|
||||
priority: input.priority || 100,
|
||||
metadata: input.metadata || {},
|
||||
},
|
||||
);
|
||||
return payload.egress_pool_node;
|
||||
}
|
||||
|
||||
async listVPNConnections(clusterId: string): Promise<VPNConnection[]> {
|
||||
const payload = await this.get<{ vpn_connections: VPNConnection[] }>(`/clusters/${clusterId}/vpn-connections`);
|
||||
return payload.vpn_connections ?? [];
|
||||
}
|
||||
|
||||
async createVPNConnection(clusterId: string, input: CreateVPNConnectionPayload): Promise<VPNConnection> {
|
||||
const payload = await this.post<{ vpn_connection: VPNConnection }>(`/clusters/${clusterId}/vpn-connections`, {
|
||||
actor_user_id: this.actorUserId,
|
||||
organization_id: input.organizationId,
|
||||
name: input.name,
|
||||
target_endpoint: input.targetEndpoint,
|
||||
protocol_family: input.protocolFamily,
|
||||
credential_ref: input.credentialRef || null,
|
||||
mode: "single_active",
|
||||
desired_state: input.desiredState,
|
||||
allowed_node_policy: input.allowedNodePolicy,
|
||||
routing_usage: input.routingUsage,
|
||||
route_policy: input.routePolicy,
|
||||
qos_policy: input.qosPolicy,
|
||||
placement_policy: input.placementPolicy,
|
||||
metadata: {},
|
||||
});
|
||||
return payload.vpn_connection;
|
||||
}
|
||||
|
||||
async updateVPNConnectionDesiredState(clusterId: string, vpnConnectionId: string, desiredState: string): Promise<VPNConnection> {
|
||||
const payload = await this.put<{ vpn_connection: VPNConnection }>(
|
||||
`/clusters/${clusterId}/vpn-connections/${vpnConnectionId}/desired-state`,
|
||||
{
|
||||
actor_user_id: this.actorUserId,
|
||||
desired_state: desiredState,
|
||||
},
|
||||
);
|
||||
return payload.vpn_connection;
|
||||
}
|
||||
|
||||
async getActiveVPNLease(clusterId: string, vpnConnectionId: string): Promise<VPNConnectionLease | null> {
|
||||
try {
|
||||
const payload = await this.get<{ lease: VPNConnectionLease }>(
|
||||
`/clusters/${clusterId}/vpn-connections/${vpnConnectionId}/leases/active`,
|
||||
);
|
||||
return payload.lease;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async expireStaleVPNLeases(clusterId: string): Promise<VPNConnectionLease[]> {
|
||||
const payload = await this.post<{ expired_leases: VPNConnectionLease[] }>(
|
||||
`/clusters/${clusterId}/vpn-connection-leases/expire-stale`,
|
||||
{
|
||||
actor_user_id: this.actorUserId,
|
||||
},
|
||||
);
|
||||
return payload.expired_leases ?? [];
|
||||
}
|
||||
|
||||
async listAudit(clusterId: string): Promise<AuditEvent[]> {
|
||||
const payload = await this.get<{ audit_events: AuditEvent[] }>(`/clusters/${clusterId}/audit?limit=100`);
|
||||
return payload.audit_events ?? [];
|
||||
}
|
||||
|
||||
async getOrganizationAdminSummary(organizationId: string): Promise<OrganizationAdminSummary> {
|
||||
const payload = await this.get<{ admin_summary: OrganizationAdminSummary }>(
|
||||
`/organizations/${organizationId}/admin-summary`,
|
||||
);
|
||||
return payload.admin_summary;
|
||||
}
|
||||
|
||||
private async get<T>(path: string): Promise<T> {
|
||||
const separator = path.includes("?") ? "&" : "?";
|
||||
return this.request<T>(`${path}${separator}actor_user_id=${encodeURIComponent(this.actorUserId)}`, {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
private async post<T>(path: string, body: unknown): Promise<T> {
|
||||
return this.request<T>(path, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
private async put<T>(path: string, body: unknown): Promise<T> {
|
||||
return this.request<T>(path, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
private async request<T>(path: string, init: RequestInit): Promise<T> {
|
||||
const response = await fetch(`${this.baseUrl}${path}`, init);
|
||||
if (!response.ok) {
|
||||
let message = `Запрос завершился ошибкой HTTP ${response.status}`;
|
||||
try {
|
||||
const payload = (await response.json()) as ApiErrorPayload;
|
||||
message = payload.error?.fallback_message || payload.error?.code || message;
|
||||
} catch {
|
||||
// Keep generic HTTP message if backend did not return JSON.
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
}
|
||||
|
||||
function browserDeviceFingerprint(): string {
|
||||
const key = "rap.webAdmin.deviceFingerprint";
|
||||
const existing = localStorage.getItem(key);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const value = `web-admin-${createBrowserIdentifier()}`;
|
||||
localStorage.setItem(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
function createBrowserIdentifier(): string {
|
||||
if (typeof globalThis.crypto?.randomUUID === "function") {
|
||||
return globalThis.crypto.randomUUID();
|
||||
}
|
||||
|
||||
if (typeof globalThis.crypto?.getRandomValues === "function") {
|
||||
const bytes = new Uint8Array(16);
|
||||
globalThis.crypto.getRandomValues(bytes);
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
||||
}
|
||||
|
||||
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { App } from "./App";
|
||||
import "./styles.css";
|
||||
|
||||
createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,529 @@
|
||||
export type Cluster = {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
status: string;
|
||||
region?: string | null;
|
||||
metadata?: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export type AuthUser = {
|
||||
ID?: string;
|
||||
id?: string;
|
||||
Email?: string;
|
||||
email?: string;
|
||||
};
|
||||
|
||||
export type AuthSession = {
|
||||
ID?: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export type AuthTokens = {
|
||||
access_token: string;
|
||||
access_token_expires_at: string;
|
||||
refresh_token: string;
|
||||
refresh_token_expires_at: string;
|
||||
};
|
||||
|
||||
export type AuthResult = {
|
||||
user: AuthUser;
|
||||
auth_session: AuthSession;
|
||||
tokens: AuthTokens;
|
||||
};
|
||||
|
||||
export type InstallationStatus = {
|
||||
bootstrapped: boolean;
|
||||
authority_state: string;
|
||||
install_id?: string;
|
||||
bootstrapped_owner_email?: string;
|
||||
bootstrapped_at?: string;
|
||||
authority_mode: string;
|
||||
strict_authority: boolean;
|
||||
root_fingerprint?: string;
|
||||
insecure_bootstrap_allowed: boolean;
|
||||
};
|
||||
|
||||
export type BootstrapOwnerResult = {
|
||||
installation: InstallationStatus;
|
||||
user: AuthUser;
|
||||
platform_role: string;
|
||||
};
|
||||
|
||||
export type ClusterAuthorityState = {
|
||||
cluster_id: string;
|
||||
authority_state: string;
|
||||
mutation_mode: string;
|
||||
term: number;
|
||||
notes?: string | null;
|
||||
updated_by_user_id?: string | null;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type ClusterNode = {
|
||||
id: string;
|
||||
owner_organization_id?: string | null;
|
||||
node_key: string;
|
||||
name: string;
|
||||
ownership_type: string;
|
||||
registration_status: string;
|
||||
health_status: string;
|
||||
version_state: string;
|
||||
partition_state: string;
|
||||
reported_version?: string | null;
|
||||
last_seen_at?: string | null;
|
||||
membership_status: string;
|
||||
membership_metadata?: Record<string, unknown>;
|
||||
node_group_id?: string | null;
|
||||
node_group_name?: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export type ClusterNodeGroup = {
|
||||
id: string;
|
||||
cluster_id: string;
|
||||
parent_group_id?: string | null;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
sort_order: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
created_by_user_id?: string | null;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export type JoinRequest = {
|
||||
id: string;
|
||||
cluster_id: string;
|
||||
node_name: string;
|
||||
node_fingerprint: string;
|
||||
public_key?: string;
|
||||
reported_capabilities: Record<string, unknown>;
|
||||
reported_facts: Record<string, unknown>;
|
||||
requested_roles: unknown[];
|
||||
status: string;
|
||||
reviewed_at?: string | null;
|
||||
approved_node_id?: string | null;
|
||||
rejection_reason?: string | null;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
approval_payload?: Record<string, unknown>;
|
||||
approval_signature?: ClusterSignature;
|
||||
};
|
||||
|
||||
export type ClusterSignature = {
|
||||
schema_version: string;
|
||||
algorithm: string;
|
||||
key_fingerprint: string;
|
||||
signature: string;
|
||||
signed_at: string;
|
||||
};
|
||||
|
||||
export type ClusterAuthorityDescriptor = {
|
||||
schema_version: string;
|
||||
cluster_id: string;
|
||||
authority_state: string;
|
||||
key_algorithm: string;
|
||||
public_key: string;
|
||||
public_key_fingerprint: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type CreatedJoinToken = {
|
||||
id: string;
|
||||
cluster_id: string;
|
||||
scope: Record<string, unknown>;
|
||||
expires_at: string;
|
||||
max_uses: number;
|
||||
used_count: number;
|
||||
status: string;
|
||||
created_at: string;
|
||||
revoked_at?: string | null;
|
||||
authority_payload?: Record<string, unknown>;
|
||||
authority_signature?: ClusterSignature;
|
||||
token: string;
|
||||
};
|
||||
|
||||
export type RoleAssignment = {
|
||||
id: string;
|
||||
cluster_id: string;
|
||||
node_id: string;
|
||||
organization_id?: string | null;
|
||||
role: string;
|
||||
status: string;
|
||||
policy?: Record<string, unknown>;
|
||||
assigned_at: string;
|
||||
revoked_at?: string | null;
|
||||
};
|
||||
|
||||
export type AuditEvent = {
|
||||
id: string;
|
||||
cluster_id?: string | null;
|
||||
actor_user_id?: string | null;
|
||||
event_type: string;
|
||||
target_type: string;
|
||||
target_id?: string | null;
|
||||
payload: Record<string, unknown>;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type WorkloadStatus = {
|
||||
id: string;
|
||||
cluster_id: string;
|
||||
node_id: string;
|
||||
service_type: string;
|
||||
reported_state: string;
|
||||
runtime_mode: string;
|
||||
version?: string | null;
|
||||
status_payload?: Record<string, unknown>;
|
||||
observed_at: string;
|
||||
};
|
||||
|
||||
export type NodeWorkloadDesiredState = {
|
||||
cluster_id: string;
|
||||
node_id: string;
|
||||
service_type: string;
|
||||
desired_state: string;
|
||||
version?: string | null;
|
||||
runtime_mode: string;
|
||||
artifact_ref?: string | null;
|
||||
config?: Record<string, unknown>;
|
||||
environment?: Record<string, unknown>;
|
||||
updated_by_user_id?: string | null;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type NodeHeartbeat = {
|
||||
id: string;
|
||||
cluster_id: string;
|
||||
node_id: string;
|
||||
health_status: string;
|
||||
reported_version?: string | null;
|
||||
capabilities?: Record<string, unknown>;
|
||||
service_states?: Record<string, unknown>;
|
||||
metadata?: Record<string, unknown>;
|
||||
observed_at: string;
|
||||
};
|
||||
|
||||
export type FabricTestingFlag = {
|
||||
id: string;
|
||||
scope_type: string;
|
||||
scope_id?: string | null;
|
||||
cluster_id?: string | null;
|
||||
enabled: boolean;
|
||||
telemetry_enabled: boolean;
|
||||
synthetic_links_enabled: boolean;
|
||||
history_retention_hours: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
updated_by_user_id?: string | null;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type NodeTelemetryObservation = {
|
||||
id: string;
|
||||
cluster_id: string;
|
||||
node_id: string;
|
||||
cpu_percent?: number | null;
|
||||
memory_used_bytes?: number | null;
|
||||
memory_total_bytes?: number | null;
|
||||
disk_used_bytes?: number | null;
|
||||
disk_total_bytes?: number | null;
|
||||
network_rx_bytes?: number | null;
|
||||
network_tx_bytes?: number | null;
|
||||
process_count?: number | null;
|
||||
payload?: Record<string, unknown>;
|
||||
observed_at: string;
|
||||
};
|
||||
|
||||
export type MeshLink = {
|
||||
id: string;
|
||||
cluster_id: string;
|
||||
source_node_id: string;
|
||||
target_node_id: string;
|
||||
link_status: string;
|
||||
latency_ms?: number | null;
|
||||
quality_score?: number | null;
|
||||
metadata?: Record<string, unknown>;
|
||||
observed_at: string;
|
||||
};
|
||||
|
||||
export type PeerEndpointCandidate = {
|
||||
endpoint_id: string;
|
||||
node_id: string;
|
||||
transport: string;
|
||||
address: string;
|
||||
address_family?: string | null;
|
||||
reachability: string;
|
||||
nat_type?: string | null;
|
||||
connectivity_mode: string;
|
||||
region?: string | null;
|
||||
priority: number;
|
||||
policy_tags?: string[];
|
||||
last_verified_at?: string | null;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type PeerDirectoryEntry = {
|
||||
node_id: string;
|
||||
route_ids?: string[];
|
||||
endpoint_count: number;
|
||||
candidate_count: number;
|
||||
connectivity_modes?: string[];
|
||||
recovery_seed: boolean;
|
||||
};
|
||||
|
||||
export type PeerRecoverySeed = {
|
||||
node_id: string;
|
||||
endpoint: string;
|
||||
transport: string;
|
||||
connectivity_mode?: string | null;
|
||||
region?: string | null;
|
||||
priority: number;
|
||||
last_verified_at?: string | null;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type PeerRendezvousLease = {
|
||||
lease_id: string;
|
||||
peer_node_id: string;
|
||||
relay_node_id: string;
|
||||
relay_endpoint: string;
|
||||
transport: string;
|
||||
connectivity_mode?: string | null;
|
||||
route_ids?: string[];
|
||||
allowed_channels?: string[];
|
||||
priority: number;
|
||||
control_plane_only: boolean;
|
||||
issued_at: string;
|
||||
expires_at: string;
|
||||
reason?: string | null;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type RendezvousRelayPolicyDecision = {
|
||||
route_id?: string;
|
||||
peer_node_id: string;
|
||||
withdrawn_lease_id?: string;
|
||||
stale_relay_node_id?: string;
|
||||
selected_relay_id?: string;
|
||||
selected_endpoint?: string;
|
||||
score?: number;
|
||||
reason: string;
|
||||
score_reasons?: string[];
|
||||
reporter_node_id?: string;
|
||||
};
|
||||
|
||||
export type RendezvousRelayPolicyReport = {
|
||||
schema_version: string;
|
||||
scoring_mode: string;
|
||||
feedback_max_age_seconds: number;
|
||||
stale_relay_count: number;
|
||||
withdrawn_lease_count: number;
|
||||
replacement_lease_count: number;
|
||||
decisions?: RendezvousRelayPolicyDecision[];
|
||||
};
|
||||
|
||||
export type RoutePathDecision = {
|
||||
decision_id: string;
|
||||
route_id: string;
|
||||
cluster_id: string;
|
||||
local_node_id: string;
|
||||
source_node_id: string;
|
||||
destination_node_id: string;
|
||||
original_hops: string[];
|
||||
effective_hops: string[];
|
||||
previous_hop_id?: string;
|
||||
next_hop_id?: string;
|
||||
local_role: string;
|
||||
selected_relay_id?: string;
|
||||
selected_relay_endpoint?: string;
|
||||
stale_relay_node_id?: string;
|
||||
rendezvous_lease_id?: string;
|
||||
rendezvous_lease_reason?: string;
|
||||
decision_source: string;
|
||||
generation: string;
|
||||
path_score?: number;
|
||||
score_reasons?: string[];
|
||||
control_plane_only: boolean;
|
||||
production_forwarding: boolean;
|
||||
expires_at: string;
|
||||
};
|
||||
|
||||
export type RoutePathDecisionReport = {
|
||||
schema_version: string;
|
||||
decision_mode: string;
|
||||
generation: string;
|
||||
decision_count: number;
|
||||
replacement_decision_count: number;
|
||||
control_plane_only: boolean;
|
||||
production_forwarding: boolean;
|
||||
decisions?: RoutePathDecision[];
|
||||
};
|
||||
|
||||
export type SyntheticMeshRoute = {
|
||||
route_id: string;
|
||||
cluster_id: string;
|
||||
source_node_id: string;
|
||||
destination_node_id: string;
|
||||
hops: string[];
|
||||
allowed_channels: string[];
|
||||
expires_at: string;
|
||||
max_ttl: number;
|
||||
max_hops: number;
|
||||
route_version?: string;
|
||||
policy_version?: string;
|
||||
peer_directory_version?: string;
|
||||
};
|
||||
|
||||
export type NodeSyntheticMeshConfig = {
|
||||
enabled: boolean;
|
||||
schema_version: string;
|
||||
cluster_id: string;
|
||||
local_node_id: string;
|
||||
authority_required: boolean;
|
||||
cluster_authority?: ClusterAuthorityDescriptor;
|
||||
authority_payload?: Record<string, unknown>;
|
||||
authority_signature?: ClusterSignature;
|
||||
config_version?: string;
|
||||
peer_directory_version?: string;
|
||||
policy_version?: string;
|
||||
peer_endpoints: Record<string, string>;
|
||||
peer_endpoint_candidates?: Record<string, PeerEndpointCandidate[]>;
|
||||
peer_directory?: PeerDirectoryEntry[];
|
||||
recovery_seeds?: PeerRecoverySeed[];
|
||||
rendezvous_leases?: PeerRendezvousLease[];
|
||||
rendezvous_relay_policy?: RendezvousRelayPolicyReport;
|
||||
route_path_decisions?: RoutePathDecisionReport;
|
||||
routes: SyntheticMeshRoute[];
|
||||
production_forwarding: boolean;
|
||||
};
|
||||
|
||||
export type FabricEntryPoint = {
|
||||
id: string;
|
||||
cluster_id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
endpoint_type: string;
|
||||
public_endpoint?: string | null;
|
||||
policy?: Record<string, unknown>;
|
||||
metadata?: Record<string, unknown>;
|
||||
created_by_user_id?: string | null;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export type FabricEntryPointNode = {
|
||||
entry_point_id: string;
|
||||
cluster_id: string;
|
||||
node_id: string;
|
||||
status: string;
|
||||
priority: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
added_by_user_id?: string | null;
|
||||
added_at: string;
|
||||
};
|
||||
|
||||
export type FabricEgressPool = {
|
||||
id: string;
|
||||
cluster_id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
description?: string | null;
|
||||
route_scope?: Record<string, unknown>;
|
||||
policy?: Record<string, unknown>;
|
||||
metadata?: Record<string, unknown>;
|
||||
created_by_user_id?: string | null;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export type FabricEgressPoolNode = {
|
||||
egress_pool_id: string;
|
||||
cluster_id: string;
|
||||
node_id: string;
|
||||
status: string;
|
||||
priority: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
added_by_user_id?: string | null;
|
||||
added_at: string;
|
||||
};
|
||||
|
||||
export type QoSPolicy = {
|
||||
id: string;
|
||||
cluster_id: string;
|
||||
service_class: string;
|
||||
priority: number;
|
||||
reliability_mode: string;
|
||||
drop_policy: string;
|
||||
bandwidth_policy?: Record<string, unknown>;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type ClusterAdminSummary = {
|
||||
cluster_id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
status: string;
|
||||
region?: string | null;
|
||||
authority_state: string;
|
||||
mutation_mode: string;
|
||||
cluster_key_algorithm?: string | null;
|
||||
cluster_key_fingerprint?: string | null;
|
||||
node_count: number;
|
||||
healthy_node_count: number;
|
||||
pending_join_count: number;
|
||||
active_role_assignment_count: number;
|
||||
last_node_seen_at?: string | null;
|
||||
};
|
||||
|
||||
export type OrganizationAdminSummary = {
|
||||
organization_id: string;
|
||||
resource_count: number;
|
||||
active_session_count: number;
|
||||
service_endpoints: Array<{ protocol: string; count: number }>;
|
||||
connector_status: Record<string, unknown>;
|
||||
topology_exposure: string;
|
||||
};
|
||||
|
||||
export type VPNConnection = {
|
||||
id: string;
|
||||
cluster_id: string;
|
||||
organization_id: string;
|
||||
name: string;
|
||||
target_endpoint: Record<string, unknown>;
|
||||
protocol_family: string;
|
||||
credential_ref?: string | null;
|
||||
mode: string;
|
||||
desired_state: string;
|
||||
allowed_node_policy: Record<string, unknown>;
|
||||
routing_usage: unknown[];
|
||||
route_policy: Record<string, unknown>;
|
||||
qos_policy: Record<string, unknown>;
|
||||
placement_policy: Record<string, unknown>;
|
||||
status: string;
|
||||
metadata: Record<string, unknown>;
|
||||
created_by_user_id?: string | null;
|
||||
updated_by_user_id?: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type VPNConnectionLease = {
|
||||
id: string;
|
||||
vpn_connection_id: string;
|
||||
cluster_id: string;
|
||||
owner_node_id: string;
|
||||
lease_generation: number;
|
||||
fencing_token: string;
|
||||
status: string;
|
||||
acquired_at: string;
|
||||
renewed_at: string;
|
||||
expires_at: string;
|
||||
released_at?: string | null;
|
||||
fenced_at?: string | null;
|
||||
metadata: Record<string, unknown>;
|
||||
};
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user