654 lines
16 KiB
TypeScript
654 lines
16 KiB
TypeScript
export type ServerStatus = "pending" | "active" | "offline";
|
|
export type KeySource = "uploaded" | "generated";
|
|
|
|
export interface PackageUpdate {
|
|
name: string;
|
|
current_version?: string;
|
|
new_version: string;
|
|
}
|
|
|
|
export interface Inventory {
|
|
cpu: { model?: string; cores?: number; usage_pct: number; load1?: number };
|
|
memory: { total_bytes: number; used_bytes: number };
|
|
swap_total_bytes: number;
|
|
swap_used_bytes: number;
|
|
partitions?: { device: string; mountpoint: string; fstype?: string; total_bytes: number; used_bytes: number }[];
|
|
kernel?: string;
|
|
metrics_at?: string;
|
|
static_at?: string;
|
|
}
|
|
|
|
export interface Server {
|
|
id: string;
|
|
server_id: string;
|
|
hostname: string;
|
|
ip_address: string;
|
|
os_info: string;
|
|
status: ServerStatus;
|
|
agent_version?: string;
|
|
last_seen: string;
|
|
created_at: string;
|
|
available_updates?: PackageUpdate[];
|
|
updates_checked_at?: string;
|
|
console_protocols?: string[];
|
|
inventory?: Inventory;
|
|
}
|
|
|
|
export type MonitorType = "http" | "tcp" | "icmp" | "tls";
|
|
export type MonitorStatus = "up" | "down" | "pending";
|
|
|
|
export interface MonitorTarget {
|
|
url?: string;
|
|
host?: string;
|
|
port?: number;
|
|
method?: string;
|
|
expected_status?: number;
|
|
keyword?: string;
|
|
tls_warn_days?: number;
|
|
}
|
|
|
|
export interface MonitorState {
|
|
status: MonitorStatus;
|
|
last_check_at?: string;
|
|
latency_ms: number;
|
|
message?: string;
|
|
cert_expiry_at?: string;
|
|
fails: number;
|
|
}
|
|
|
|
export interface Monitor {
|
|
monitor_id: string;
|
|
name: string;
|
|
type: MonitorType;
|
|
target: MonitorTarget;
|
|
interval_sec: number;
|
|
runner: string; // "server" or a server_id
|
|
retries: number;
|
|
enabled: boolean;
|
|
channel_ids?: string[];
|
|
state: MonitorState;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface MonitorInput {
|
|
name: string;
|
|
type: MonitorType;
|
|
target: MonitorTarget;
|
|
interval_sec: number;
|
|
runner: string;
|
|
retries: number;
|
|
enabled: boolean;
|
|
channel_ids?: string[];
|
|
}
|
|
|
|
export interface Incident {
|
|
incident_id: string;
|
|
monitor_id: string;
|
|
started_at: string;
|
|
resolved_at?: string;
|
|
cause?: string;
|
|
}
|
|
|
|
export interface Rollup {
|
|
monitor_id: string;
|
|
period_start: string;
|
|
checks: number;
|
|
up_count: number;
|
|
sum_latency: number;
|
|
}
|
|
|
|
export type ChannelType = "webhook" | "smtp" | "discord" | "slack" | "telegram";
|
|
|
|
export interface NotificationChannel {
|
|
channel_id: string;
|
|
name: string;
|
|
type: ChannelType;
|
|
config: Record<string, string>;
|
|
enabled: boolean;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ChannelInput {
|
|
name: string;
|
|
type: ChannelType;
|
|
config: Record<string, string>;
|
|
enabled: boolean;
|
|
}
|
|
|
|
export interface ConsoleConnectRequest {
|
|
server_id: string;
|
|
protocol: string;
|
|
key_id?: string;
|
|
rdp_username?: string;
|
|
rdp_password?: string;
|
|
ssh_username?: string;
|
|
}
|
|
|
|
export interface ConsoleConnectResponse {
|
|
session_id: string;
|
|
token: string;
|
|
ws_path: string;
|
|
}
|
|
|
|
export interface Key {
|
|
id: string;
|
|
key_id: string;
|
|
label: string;
|
|
public_key: string;
|
|
fingerprint: string;
|
|
source: KeySource;
|
|
generated_by_server_id?: string;
|
|
has_private_key: boolean;
|
|
has_passphrase?: boolean;
|
|
created_at: string;
|
|
assigned_count?: number;
|
|
}
|
|
|
|
export interface Assignment {
|
|
id: string;
|
|
key_id: string;
|
|
server_id: string;
|
|
assigned_at: string;
|
|
revoked_at: string | null;
|
|
}
|
|
|
|
export interface AuditEvent {
|
|
id: string;
|
|
event_type: string;
|
|
actor: string;
|
|
server_id?: string;
|
|
key_id?: string;
|
|
details: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface AlertSettings {
|
|
enabled: boolean;
|
|
webhook_url: string;
|
|
offline_threshold_minutes: number;
|
|
}
|
|
|
|
export interface EmailSettings {
|
|
enabled: boolean;
|
|
smtp_host: string;
|
|
smtp_port: number;
|
|
username: string;
|
|
password: string;
|
|
from_addr: string;
|
|
to_addrs: string[];
|
|
use_tls: boolean;
|
|
}
|
|
|
|
export interface SecretsSettings {
|
|
read_token_set: boolean;
|
|
rotated_at?: string;
|
|
}
|
|
|
|
export interface Settings {
|
|
alerts: AlertSettings;
|
|
email: EmailSettings;
|
|
secrets: SecretsSettings;
|
|
workflow_log_retention_days?: number | null;
|
|
}
|
|
|
|
export interface SecretGroupSummary {
|
|
group: string;
|
|
key_count: number;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface Secret {
|
|
group: string;
|
|
key: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface NewServerResponse {
|
|
server_id: string;
|
|
pre_reg_token: string;
|
|
install_command: string;
|
|
install_command_ps: string;
|
|
}
|
|
|
|
export interface GenerateKeyOptions {
|
|
label: string;
|
|
key_type: "ed25519" | "rsa" | "ecdsa";
|
|
key_size?: number;
|
|
passphrase?: string;
|
|
comment?: string;
|
|
}
|
|
|
|
export interface KeyWithAssignments extends Key {
|
|
assignments: (Assignment & { server: Server })[];
|
|
}
|
|
|
|
export interface ServerWithKeys extends Server {
|
|
keys: (Assignment & { key: Key })[];
|
|
}
|
|
|
|
export interface InputParam {
|
|
name: string;
|
|
default: string;
|
|
description: string;
|
|
}
|
|
|
|
export interface WorkflowStep {
|
|
step_id: string;
|
|
name: string;
|
|
description: string;
|
|
interpreter: "bash" | "powershell";
|
|
script: string;
|
|
declared_outputs: string[];
|
|
declared_inputs: InputParam[];
|
|
secret_refs: string[];
|
|
source?: "user" | "default";
|
|
slug?: string;
|
|
}
|
|
|
|
export interface WorkflowStepRef {
|
|
step_id?: string;
|
|
inline?: WorkflowStep;
|
|
order: number;
|
|
on_failure: "stop" | "continue" | "retry";
|
|
max_retries: number;
|
|
overrides?: { script?: string; secret_refs?: string[] };
|
|
inputs?: Record<string, string>;
|
|
}
|
|
|
|
export interface Workflow {
|
|
workflow_id: string;
|
|
name: string;
|
|
target_server_ids: string[];
|
|
steps: WorkflowStepRef[];
|
|
}
|
|
|
|
export interface StepRun {
|
|
order: number;
|
|
name: string;
|
|
status: string;
|
|
attempts: number;
|
|
exit_code: number;
|
|
log_offset: number;
|
|
output_env: Record<string, string>;
|
|
started_at?: string;
|
|
finished_at?: string;
|
|
}
|
|
|
|
export interface ServerRun {
|
|
server_id: string;
|
|
hostname: string;
|
|
status: string;
|
|
run_env: Record<string, string>;
|
|
steps: StepRun[];
|
|
started_at?: string;
|
|
finished_at?: string;
|
|
}
|
|
|
|
export interface WorkflowRun {
|
|
run_id: string;
|
|
workflow_id: string;
|
|
name: string;
|
|
status: string;
|
|
triggered_by: string;
|
|
started_at: string;
|
|
finished_at?: string;
|
|
server_runs: ServerRun[];
|
|
}
|
|
|
|
class ApiError extends Error {
|
|
constructor(
|
|
public status: number,
|
|
message: string
|
|
) {
|
|
super(message);
|
|
this.name = "ApiError";
|
|
}
|
|
}
|
|
|
|
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
|
const res = await fetch(`/api${path}`, {
|
|
credentials: "include",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...options?.headers,
|
|
},
|
|
...options,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => res.statusText);
|
|
throw new ApiError(res.status, text || `HTTP ${res.status}`);
|
|
}
|
|
|
|
if (res.status === 204) {
|
|
return undefined as T;
|
|
}
|
|
|
|
return res.json();
|
|
}
|
|
|
|
export const api = {
|
|
// Servers
|
|
listServers(): Promise<Server[]> {
|
|
return request<Server[]>("/servers");
|
|
},
|
|
|
|
getServer(serverId: string): Promise<ServerWithKeys> {
|
|
return request<ServerWithKeys>(`/servers/${serverId}`);
|
|
},
|
|
|
|
createServer(): Promise<NewServerResponse> {
|
|
return request<NewServerResponse>("/servers/new", { method: "POST" });
|
|
},
|
|
|
|
deleteServer(serverId: string): Promise<void> {
|
|
return request<void>(`/servers/${serverId}`, { method: "DELETE" });
|
|
},
|
|
|
|
generateKeyForServer(serverId: string, opts: GenerateKeyOptions): Promise<{ command_id: string }> {
|
|
return request<{ command_id: string }>(`/servers/${serverId}/generate-key`, {
|
|
method: "POST",
|
|
body: JSON.stringify(opts),
|
|
});
|
|
},
|
|
|
|
getUpdateCommand(osInfo?: string): string {
|
|
if (osInfo && osInfo.toLowerCase().includes("windows")) {
|
|
return `irm "${window.location.origin}/update.ps1" | iex`;
|
|
}
|
|
return `curl -fsSL "${window.location.origin}/update" | bash`;
|
|
},
|
|
|
|
// Monitors
|
|
listMonitors(): Promise<Monitor[]> {
|
|
return request<Monitor[]>("/monitors");
|
|
},
|
|
|
|
getMonitor(monitorId: string): Promise<Monitor> {
|
|
return request<Monitor>(`/monitors/${monitorId}`);
|
|
},
|
|
|
|
createMonitor(input: MonitorInput): Promise<Monitor> {
|
|
return request<Monitor>("/monitors", { method: "POST", body: JSON.stringify(input) });
|
|
},
|
|
|
|
updateMonitor(monitorId: string, input: Partial<MonitorInput>): Promise<void> {
|
|
return request<void>(`/monitors/${monitorId}`, { method: "PUT", body: JSON.stringify(input) });
|
|
},
|
|
|
|
deleteMonitor(monitorId: string): Promise<void> {
|
|
return request<void>(`/monitors/${monitorId}`, { method: "DELETE" });
|
|
},
|
|
|
|
getMonitorIncidents(monitorId: string): Promise<Incident[]> {
|
|
return request<Incident[]>(`/monitors/${monitorId}/incidents`);
|
|
},
|
|
|
|
getMonitorUptime(monitorId: string): Promise<Rollup[]> {
|
|
return request<Rollup[]>(`/monitors/${monitorId}/uptime`);
|
|
},
|
|
|
|
// Notification channels
|
|
listChannels(): Promise<NotificationChannel[]> {
|
|
return request<NotificationChannel[]>("/channels");
|
|
},
|
|
|
|
createChannel(input: ChannelInput): Promise<NotificationChannel> {
|
|
return request<NotificationChannel>("/channels", { method: "POST", body: JSON.stringify(input) });
|
|
},
|
|
|
|
updateChannel(channelId: string, input: Partial<ChannelInput>): Promise<void> {
|
|
return request<void>(`/channels/${channelId}`, { method: "PUT", body: JSON.stringify(input) });
|
|
},
|
|
|
|
deleteChannel(channelId: string): Promise<void> {
|
|
return request<void>(`/channels/${channelId}`, { method: "DELETE" });
|
|
},
|
|
|
|
testChannel(channelId: string): Promise<{ status: string }> {
|
|
return request<{ status: string }>(`/channels/${channelId}/test`, { method: "POST" });
|
|
},
|
|
|
|
getLatestAgentVersion(): Promise<{ version: string }> {
|
|
return request<{ version: string }>("/agent/latest-version");
|
|
},
|
|
|
|
updateAgent(serverId: string): Promise<{ message: string; version: string }> {
|
|
return request<{ message: string; version: string }>(`/servers/${serverId}/update-agent`, {
|
|
method: "POST",
|
|
});
|
|
},
|
|
|
|
applyUpdates(serverId: string): Promise<{ message: string }> {
|
|
return request<{ message: string }>(`/servers/${serverId}/apply-updates`, {
|
|
method: "POST",
|
|
});
|
|
},
|
|
|
|
// Audit
|
|
listAuditEvents(limit?: number): Promise<AuditEvent[]> {
|
|
const qs = limit ? `?limit=${limit}` : "";
|
|
return request<AuditEvent[]>(`/audit${qs}`);
|
|
},
|
|
|
|
// Settings
|
|
getSettings(): Promise<Settings> {
|
|
return request<Settings>("/settings");
|
|
},
|
|
|
|
saveSettings(settings: {
|
|
alerts: AlertSettings;
|
|
email: EmailSettings;
|
|
workflow_log_retention_days?: number | null;
|
|
}): Promise<{ saved: boolean }> {
|
|
return request<{ saved: boolean }>("/settings", {
|
|
method: "PUT",
|
|
body: JSON.stringify(settings),
|
|
});
|
|
},
|
|
|
|
rotateSecretsToken(): Promise<{ token: string }> {
|
|
return request<{ token: string }>("/settings/secrets-token", { method: "POST" });
|
|
},
|
|
|
|
// Secrets
|
|
listSecretGroups(): Promise<SecretGroupSummary[]> {
|
|
return request<SecretGroupSummary[]>("/secrets");
|
|
},
|
|
|
|
createSecretGroup(group: string, values: Record<string, string>): Promise<{ group: string }> {
|
|
return request<{ group: string }>("/secrets", {
|
|
method: "POST",
|
|
body: JSON.stringify({ group, values }),
|
|
});
|
|
},
|
|
|
|
getSecretGroup(group: string): Promise<{ group: string; secrets: Secret[] }> {
|
|
return request<{ group: string; secrets: Secret[] }>(`/secrets/${encodeURIComponent(group)}`);
|
|
},
|
|
|
|
putSecrets(group: string, values: Record<string, string>): Promise<{ saved: boolean }> {
|
|
return request<{ saved: boolean }>(`/secrets/${encodeURIComponent(group)}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(values),
|
|
});
|
|
},
|
|
|
|
revealSecret(group: string, key: string): Promise<{ value: string }> {
|
|
return request<{ value: string }>(`/secrets/${encodeURIComponent(group)}/reveal`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ key }),
|
|
});
|
|
},
|
|
|
|
deleteSecret(group: string, key: string): Promise<void> {
|
|
return request<void>(`/secrets/${encodeURIComponent(group)}/${encodeURIComponent(key)}`, {
|
|
method: "DELETE",
|
|
});
|
|
},
|
|
|
|
deleteSecretGroup(group: string): Promise<void> {
|
|
return request<void>(`/secrets/${encodeURIComponent(group)}`, { method: "DELETE" });
|
|
},
|
|
|
|
// Keys
|
|
listKeys(): Promise<Key[]> {
|
|
return request<Key[]>("/keys");
|
|
},
|
|
|
|
getKey(keyId: string): Promise<KeyWithAssignments> {
|
|
return request<KeyWithAssignments>(`/keys/${keyId}`);
|
|
},
|
|
|
|
uploadKey(label: string, public_key: string, private_key?: string, passphrase?: string): Promise<Key> {
|
|
return request<Key>("/keys", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
label,
|
|
public_key,
|
|
private_key: private_key || undefined,
|
|
passphrase: passphrase || undefined,
|
|
}),
|
|
});
|
|
},
|
|
|
|
getPrivateKey(keyId: string): Promise<{ private_key: string }> {
|
|
return request<{ private_key: string }>(`/keys/${keyId}/private-key`);
|
|
},
|
|
|
|
deleteKey(keyId: string): Promise<void> {
|
|
return request<void>(`/keys/${keyId}`, { method: "DELETE" });
|
|
},
|
|
|
|
// Assignments
|
|
assignKey(keyId: string, serverId: string): Promise<Assignment> {
|
|
return request<Assignment>(`/keys/${keyId}/assign`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ server_id: serverId }),
|
|
});
|
|
},
|
|
|
|
revokeKey(keyId: string, serverId: string): Promise<void> {
|
|
return request<void>(`/keys/${keyId}/assign/${serverId}`, {
|
|
method: "DELETE",
|
|
});
|
|
},
|
|
|
|
// Console
|
|
connectConsole(body: ConsoleConnectRequest): Promise<ConsoleConnectResponse> {
|
|
return request<ConsoleConnectResponse>("/console/connect", {
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
});
|
|
},
|
|
|
|
// Steps
|
|
listSteps(): Promise<WorkflowStep[]> {
|
|
return request<WorkflowStep[]>("/steps");
|
|
},
|
|
|
|
createStep(s: Partial<WorkflowStep>): Promise<WorkflowStep> {
|
|
return request<WorkflowStep>("/steps", {
|
|
method: "POST",
|
|
body: JSON.stringify(s),
|
|
});
|
|
},
|
|
|
|
updateStep(stepId: string, s: Partial<WorkflowStep>): Promise<WorkflowStep> {
|
|
return request<WorkflowStep>(`/steps/${stepId}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(s),
|
|
});
|
|
},
|
|
|
|
deleteStep(stepId: string): Promise<void> {
|
|
return request<void>(`/steps/${stepId}`, { method: "DELETE" });
|
|
},
|
|
|
|
exportStepUrl(stepId: string): string {
|
|
return `/api/steps/${stepId}/export`;
|
|
},
|
|
|
|
importStep(doc: unknown): Promise<WorkflowStep> {
|
|
return request<WorkflowStep>("/steps/import", {
|
|
method: "POST",
|
|
body: JSON.stringify(doc),
|
|
});
|
|
},
|
|
|
|
parseStep(doc: unknown): Promise<WorkflowStep> {
|
|
return request<WorkflowStep>("/steps/parse", {
|
|
method: "POST",
|
|
body: JSON.stringify(doc),
|
|
});
|
|
},
|
|
|
|
seedDefaults(): Promise<{ created: number; updated: number }> {
|
|
return request<{ created: number; updated: number }>("/steps/seed-defaults", {
|
|
method: "POST",
|
|
});
|
|
},
|
|
|
|
stepUsage(): Promise<Record<string, number>> {
|
|
return request<Record<string, number>>("/steps/usage");
|
|
},
|
|
|
|
// Workflows
|
|
listWorkflows(): Promise<Workflow[]> {
|
|
return request<Workflow[]>("/workflows");
|
|
},
|
|
|
|
getWorkflow(workflowId: string): Promise<Workflow> {
|
|
return request<Workflow>(`/workflows/${workflowId}`);
|
|
},
|
|
|
|
createWorkflow(w: Partial<Workflow>): Promise<Workflow> {
|
|
return request<Workflow>("/workflows", {
|
|
method: "POST",
|
|
body: JSON.stringify(w),
|
|
});
|
|
},
|
|
|
|
updateWorkflow(workflowId: string, w: Partial<Workflow>): Promise<Workflow> {
|
|
return request<Workflow>(`/workflows/${workflowId}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(w),
|
|
});
|
|
},
|
|
|
|
deleteWorkflow(workflowId: string): Promise<void> {
|
|
return request<void>(`/workflows/${workflowId}`, { method: "DELETE" });
|
|
},
|
|
|
|
runWorkflow(workflowId: string): Promise<{ run_id: string }> {
|
|
return request<{ run_id: string }>(`/workflows/${workflowId}/run`, {
|
|
method: "POST",
|
|
});
|
|
},
|
|
|
|
listRuns(workflowId: string): Promise<WorkflowRun[]> {
|
|
return request<WorkflowRun[]>(`/workflows/${workflowId}/runs`);
|
|
},
|
|
|
|
// Runs
|
|
getRun(runId: string): Promise<WorkflowRun> {
|
|
return request<WorkflowRun>(`/runs/${runId}`);
|
|
},
|
|
|
|
cancelRun(runId: string): Promise<void> {
|
|
return request<void>(`/runs/${runId}/cancel`, { method: "POST" });
|
|
},
|
|
|
|
async getServerRunLog(runId: string, serverId: string): Promise<string> {
|
|
const res = await fetch(`/api/runs/${runId}/servers/${serverId}/logs`, {
|
|
credentials: "include",
|
|
});
|
|
if (!res.ok) throw new Error("no logs");
|
|
return res.text();
|
|
},
|
|
|
|
serverRunLogStreamUrl(runId: string, serverId: string): string {
|
|
return `/api/runs/${runId}/servers/${serverId}/logs/stream`;
|
|
},
|
|
};
|