Initial project snapshot
This commit is contained in:
@@ -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)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user