Initial project snapshot

This commit is contained in:
2026-04-28 22:29:50 +03:00
commit 8ba0561f4f
365 changed files with 91832 additions and 0 deletions
+609
View File
@@ -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)}`;
}