1151 lines
32 KiB
TypeScript
1151 lines
32 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;
|
|
tags?: Record<string, string>;
|
|
}
|
|
|
|
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;
|
|
insecure?: boolean;
|
|
}
|
|
|
|
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 {
|
|
offline_threshold_minutes: number;
|
|
offline_channel_ids: string[] | null;
|
|
}
|
|
|
|
export interface SecretsSettings {
|
|
read_token_set: boolean;
|
|
rotated_at?: string;
|
|
}
|
|
|
|
export interface Settings {
|
|
alerts: AlertSettings;
|
|
secrets: SecretsSettings;
|
|
workflow_log_retention_days?: number | null;
|
|
local_login_enabled?: boolean;
|
|
}
|
|
|
|
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[];
|
|
target_tags?: Record<string, string>;
|
|
steps: WorkflowStepRef[];
|
|
schedule?: Schedule;
|
|
next_run_at?: string;
|
|
last_run_at?: string;
|
|
last_skipped?: Skip;
|
|
}
|
|
|
|
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[];
|
|
}
|
|
|
|
export type Role = "owner" | "admin" | "member";
|
|
|
|
/** The session as returned by GET /auth/me mirrors auth.Session on the server. */
|
|
export interface SessionUser {
|
|
user_id: string;
|
|
instance_id: string;
|
|
role: Role;
|
|
email: string;
|
|
name: string;
|
|
}
|
|
|
|
export interface Instance {
|
|
instance_id: string;
|
|
name: string;
|
|
slug: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface MeResponse {
|
|
user: SessionUser;
|
|
instance: Instance | null;
|
|
}
|
|
|
|
export interface BootstrapStatus {
|
|
needs_setup: boolean;
|
|
instance_name?: string;
|
|
}
|
|
|
|
export interface BootstrapResponse {
|
|
instance: Instance;
|
|
slug: string;
|
|
instance_id: string;
|
|
}
|
|
|
|
export interface InstanceUser {
|
|
user_id: string;
|
|
instance_id: string;
|
|
email: string;
|
|
role: Role;
|
|
// "hq" means the row was projected from a Vantage HQ account. Its role,
|
|
// password and existence belong to HQ; this instance refuses to change them.
|
|
auth_source: "local" | "oidc" | "hq";
|
|
hq_user_id?: string;
|
|
created_at: string;
|
|
last_login?: string;
|
|
}
|
|
|
|
export interface OrgUserInput {
|
|
email: string;
|
|
password: string;
|
|
role: Role;
|
|
}
|
|
|
|
export interface PublicProvider {
|
|
id: string;
|
|
name: string;
|
|
preset: string;
|
|
}
|
|
|
|
export interface ProvidersResponse {
|
|
local_enabled: boolean;
|
|
providers: PublicProvider[];
|
|
}
|
|
|
|
export interface AuthProvider {
|
|
provider_id: string;
|
|
instance_id: string;
|
|
name: string;
|
|
kind: "oidc" | "oauth2";
|
|
preset: string;
|
|
issuer: string;
|
|
client_id: string;
|
|
scopes: string[];
|
|
enabled: boolean;
|
|
callback_notice: boolean;
|
|
order: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
client_secret_set: boolean;
|
|
callback_url: string;
|
|
}
|
|
|
|
export interface AuthPreset {
|
|
id: string;
|
|
label: string;
|
|
kind: "oidc" | "oauth2";
|
|
input_label: string;
|
|
input_hint: string;
|
|
}
|
|
|
|
export interface AuthProviderInput {
|
|
name: string;
|
|
preset: string;
|
|
issuer_input?: string;
|
|
client_id: string;
|
|
client_secret: string;
|
|
enabled: boolean;
|
|
}
|
|
|
|
export interface AuthProviderUpdate {
|
|
name?: string;
|
|
issuer_input?: string;
|
|
client_id?: string;
|
|
client_secret?: string;
|
|
enabled?: boolean;
|
|
order?: number;
|
|
}
|
|
|
|
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(() => "");
|
|
|
|
let message = text || res.statusText || `HTTP ${res.status}`;
|
|
try {
|
|
const body = JSON.parse(text);
|
|
if (body?.error) message = body.error;
|
|
} catch {}
|
|
throw new ApiError(res.status, message);
|
|
}
|
|
|
|
if (res.status === 204) {
|
|
return undefined as T;
|
|
}
|
|
|
|
return res.json();
|
|
}
|
|
|
|
async function authRequest<T>(path: string, options?: RequestInit): Promise<T> {
|
|
const res = await fetch(path, {
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json", ...options?.headers },
|
|
...options,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
let message = `HTTP ${res.status}`;
|
|
try {
|
|
const body = await res.json();
|
|
if (body?.error) message = body.error;
|
|
} catch {}
|
|
throw new ApiError(res.status, message);
|
|
}
|
|
|
|
if (res.status === 204) {
|
|
return undefined as T;
|
|
}
|
|
|
|
return res.json();
|
|
}
|
|
|
|
export const auth = {
|
|
bootstrapStatus(): Promise<BootstrapStatus> {
|
|
return authRequest<BootstrapStatus>("/auth/bootstrap-status");
|
|
},
|
|
|
|
bootstrap(input: { instance_name: string; email: string; password: string }): Promise<BootstrapResponse> {
|
|
return authRequest<BootstrapResponse>("/auth/bootstrap", {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
});
|
|
},
|
|
|
|
login(email: string, password: string): Promise<{ ok: boolean }> {
|
|
return authRequest<{ ok: boolean }>("/auth/login", {
|
|
method: "POST",
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
},
|
|
|
|
logout(): Promise<void> {
|
|
return authRequest<void>("/auth/logout", { method: "POST" });
|
|
},
|
|
|
|
me(): Promise<MeResponse> {
|
|
return authRequest<MeResponse>("/auth/me");
|
|
},
|
|
|
|
/** Unauthenticated: what the login page draws itself from. */
|
|
providers(): Promise<ProvidersResponse> {
|
|
return authRequest<ProvidersResponse>("/auth/providers");
|
|
},
|
|
|
|
/** Where a provider button sends the browser. */
|
|
ssoStartUrl(providerId: string): string {
|
|
return `/auth/oidc/${providerId}/start`;
|
|
},
|
|
};
|
|
|
|
export const api = {
|
|
listInstanceUsers(): Promise<InstanceUser[]> {
|
|
return request<InstanceUser[]>("/instance/users");
|
|
},
|
|
|
|
createInstanceUser(input: OrgUserInput): Promise<InstanceUser> {
|
|
return request<InstanceUser>("/instance/users", { method: "POST", body: JSON.stringify(input) });
|
|
},
|
|
|
|
updateInstanceUserRole(userId: string, role: Role): Promise<{ ok: boolean }> {
|
|
return request<{ ok: boolean }>(`/instance/users/${userId}/role`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({ role }),
|
|
});
|
|
},
|
|
|
|
deleteInstanceUser(userId: string): Promise<void> {
|
|
return request<void>(`/instance/users/${userId}`, { method: "DELETE" });
|
|
},
|
|
|
|
listAuthPresets(): Promise<AuthPreset[]> {
|
|
return request<AuthPreset[]>("/auth/presets");
|
|
},
|
|
|
|
listAuthProviders(): Promise<AuthProvider[]> {
|
|
return request<AuthProvider[]>("/auth/providers");
|
|
},
|
|
|
|
createAuthProvider(input: AuthProviderInput): Promise<AuthProvider> {
|
|
return request<AuthProvider>("/auth/providers", { method: "POST", body: JSON.stringify(input) });
|
|
},
|
|
|
|
updateAuthProvider(id: string, input: AuthProviderUpdate): Promise<{ saved: boolean }> {
|
|
return request<{ saved: boolean }>(`/auth/providers/${id}`, { method: "PUT", body: JSON.stringify(input) });
|
|
},
|
|
|
|
deleteAuthProvider(id: string): Promise<{ deleted: boolean }> {
|
|
return request<{ deleted: boolean }>(`/auth/providers/${id}`, { method: "DELETE" });
|
|
},
|
|
|
|
testAuthProvider(id: string): Promise<{ ok: boolean; message: string }> {
|
|
return request<{ ok: boolean; message: string }>(`/auth/providers/${id}/test`, { method: "POST" });
|
|
},
|
|
|
|
ackAuthProviderNotice(id: string): Promise<{ acknowledged: boolean }> {
|
|
return request<{ acknowledged: boolean }>(`/auth/providers/${id}/ack-notice`, { method: "POST" });
|
|
},
|
|
|
|
listServers(tags?: Record<string, string>): Promise<Server[]> {
|
|
const params = Object.entries(tags ?? {}).map(([k, v]) => `tag=${encodeURIComponent(`${k}:${v}`)}`);
|
|
return request<Server[]>(`/servers${params.length ? `?${params.join("&")}` : ""}`);
|
|
},
|
|
|
|
listKnownTags(): Promise<Record<string, string[]>> {
|
|
return request<Record<string, string[]>>("/servers/tags");
|
|
},
|
|
|
|
setServerTags(serverId: string, tags: Record<string, string>): Promise<{ tags: Record<string, string> }> {
|
|
return request<{ tags: Record<string, string> }>(`/servers/${serverId}/tags`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({ tags }),
|
|
});
|
|
},
|
|
|
|
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`;
|
|
},
|
|
|
|
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`);
|
|
},
|
|
|
|
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",
|
|
});
|
|
},
|
|
|
|
listAuditEvents(limit?: number): Promise<AuditEvent[]> {
|
|
const qs = limit ? `?limit=${limit}` : "";
|
|
return request<AuditEvent[]>(`/audit${qs}`);
|
|
},
|
|
|
|
getSettings(): Promise<Settings> {
|
|
return request<Settings>("/settings");
|
|
},
|
|
|
|
saveSettings(settings: { alerts: AlertSettings; workflow_log_retention_days?: number | null; local_login_enabled?: boolean }): 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" });
|
|
},
|
|
|
|
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" });
|
|
},
|
|
|
|
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" });
|
|
},
|
|
|
|
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",
|
|
});
|
|
},
|
|
|
|
connectConsole(body: ConsoleConnectRequest): Promise<ConsoleConnectResponse> {
|
|
return request<ConsoleConnectResponse>("/console/connect", {
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
});
|
|
},
|
|
|
|
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");
|
|
},
|
|
|
|
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`);
|
|
},
|
|
|
|
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`;
|
|
},
|
|
|
|
setWorkflowSchedule(workflowId: string, schedule: Schedule): Promise<{ schedule: Schedule; next_run_at: string | null }> {
|
|
return request(`/workflows/${workflowId}/schedule`, { method: "PUT", body: JSON.stringify(schedule) });
|
|
},
|
|
|
|
previewSchedule(workflowId: string, cron: string, tz: string): Promise<{ occurrences: string[] }> {
|
|
return request(`/workflows/${workflowId}/schedule/preview?cron=${encodeURIComponent(cron)}&tz=${encodeURIComponent(tz)}`);
|
|
},
|
|
};
|
|
|
|
export type LicenseState = "valid" | "expired" | "invalid";
|
|
|
|
export interface LicenseInfo {
|
|
instance_id: string;
|
|
state: LicenseState;
|
|
reason?: string;
|
|
tier?: string;
|
|
support_level?: string;
|
|
expires_at?: string;
|
|
days_remaining?: number;
|
|
limits: {
|
|
max_servers: number;
|
|
max_monitors: number;
|
|
max_secret_groups: number;
|
|
max_channels: number;
|
|
audit_retention_days: number;
|
|
};
|
|
features: Record<string, boolean>;
|
|
usage: {
|
|
servers: number;
|
|
monitors: number;
|
|
secret_groups: number;
|
|
channels: number;
|
|
};
|
|
source: string;
|
|
/** "cloud" | "self_hosted". A cloud instance's licence is managed in HQ. */
|
|
deployment: string;
|
|
}
|
|
|
|
export type Severity = "critical" | "high" | "medium" | "low" | "unknown";
|
|
export type FindingState = "open" | "fixed" | "accepted";
|
|
|
|
export interface Acceptance {
|
|
by: string;
|
|
reason: string;
|
|
until: string;
|
|
at: string;
|
|
}
|
|
|
|
export interface VulnFinding {
|
|
id: string;
|
|
server_id: string;
|
|
cve_id: string;
|
|
package_name: string;
|
|
installed_version: string;
|
|
/** Absent means no vendor fix is published — a real state, not missing data. */
|
|
fixed_in?: string;
|
|
severity: Severity;
|
|
cvss_score?: number;
|
|
title?: string;
|
|
references?: string[];
|
|
state: FindingState;
|
|
first_seen: string;
|
|
last_seen: string;
|
|
fixed_at?: string;
|
|
accepted?: Acceptance;
|
|
}
|
|
|
|
/** One CVE across every server it affects. The board groups by CVE because the
|
|
* same CVE on forty servers is one decision, not forty rows. */
|
|
export interface VulnGroup {
|
|
cve_id: string;
|
|
severity: Severity;
|
|
title?: string;
|
|
server_count: number;
|
|
findings: VulnFinding[];
|
|
}
|
|
|
|
export interface VulnSummary {
|
|
counts: Partial<Record<Severity, number>>;
|
|
db_version?: number;
|
|
pulled_at?: string;
|
|
last_full_scan_at?: string;
|
|
last_error?: string;
|
|
}
|
|
|
|
export interface InstalledPackage {
|
|
name: string;
|
|
version: string;
|
|
epoch?: number;
|
|
arch: string;
|
|
source_name?: string;
|
|
}
|
|
|
|
export interface ServerPackages {
|
|
server_id: string;
|
|
os: { family: string; version_id: string; arch: string };
|
|
hash: string;
|
|
packages: InstalledPackage[];
|
|
collected_at: string;
|
|
scan_pending: boolean;
|
|
scanned_at?: string;
|
|
/** "ok" | "unsupported". Unsupported must never read as "clean". */
|
|
status: string;
|
|
db_version: number;
|
|
}
|
|
|
|
export interface PackageHit {
|
|
server_id: string;
|
|
name: string;
|
|
version: string;
|
|
}
|
|
|
|
export interface VulnAlertRule {
|
|
id: string;
|
|
name: string;
|
|
enabled: boolean;
|
|
min_severity: Severity;
|
|
tags?: Record<string, string>;
|
|
channel_ids: string[];
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface VulnAlertRuleInput {
|
|
name: string;
|
|
enabled: boolean;
|
|
min_severity: Severity;
|
|
tags?: Record<string, string>;
|
|
channel_ids: string[];
|
|
}
|
|
|
|
export const vulnerabilities = {
|
|
list(params?: { severity?: string; state?: string; server?: string; tags?: Record<string, string> }): Promise<VulnGroup[]> {
|
|
const q = new URLSearchParams();
|
|
if (params?.severity) q.set("severity", params.severity);
|
|
if (params?.state) q.set("state", params.state);
|
|
if (params?.server) q.set("server", params.server);
|
|
for (const [k, v] of Object.entries(params?.tags ?? {})) q.append("tag", `${k}:${v}`);
|
|
const qs = q.toString();
|
|
return request<VulnGroup[]>(`/vulnerabilities${qs ? `?${qs}` : ""}`);
|
|
},
|
|
|
|
summary(): Promise<VulnSummary> {
|
|
return request<VulnSummary>("/vulnerabilities/summary");
|
|
},
|
|
|
|
rescan(): Promise<{ queued: number }> {
|
|
return request<{ queued: number }>("/vulnerabilities/rescan", { method: "POST" });
|
|
},
|
|
|
|
accept(id: string, reason: string, until: string): Promise<VulnFinding> {
|
|
return request<VulnFinding>(`/vulnerabilities/${id}/accept`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ reason, until }),
|
|
});
|
|
},
|
|
|
|
unaccept(id: string): Promise<VulnFinding> {
|
|
return request<VulnFinding>(`/vulnerabilities/${id}/accept`, { method: "DELETE" });
|
|
},
|
|
|
|
forServer(serverId: string): Promise<VulnFinding[]> {
|
|
return request<VulnFinding[]>(`/servers/${serverId}/vulnerabilities`);
|
|
},
|
|
|
|
packagesForServer(serverId: string): Promise<ServerPackages | { reported: false }> {
|
|
return request<ServerPackages | { reported: false }>(`/servers/${serverId}/packages`);
|
|
},
|
|
|
|
searchPackages(name: string): Promise<PackageHit[]> {
|
|
return request<PackageHit[]>(`/packages/search?name=${encodeURIComponent(name)}`);
|
|
},
|
|
|
|
listRules(): Promise<VulnAlertRule[]> {
|
|
return request<VulnAlertRule[]>("/vuln-rules");
|
|
},
|
|
|
|
createRule(input: VulnAlertRuleInput): Promise<VulnAlertRule> {
|
|
return request<VulnAlertRule>("/vuln-rules", { method: "POST", body: JSON.stringify(input) });
|
|
},
|
|
|
|
updateRule(id: string, input: VulnAlertRuleInput): Promise<{ status: string }> {
|
|
return request<{ status: string }>(`/vuln-rules/${id}`, { method: "PUT", body: JSON.stringify(input) });
|
|
},
|
|
|
|
deleteRule(id: string): Promise<{ status: string }> {
|
|
return request<{ status: string }>(`/vuln-rules/${id}`, { method: "DELETE" });
|
|
},
|
|
};
|
|
|
|
export type WorkloadKind = "container" | "unit";
|
|
export type WorkloadAction = "start" | "stop" | "restart";
|
|
|
|
/** One container or one systemd unit.
|
|
*
|
|
* `state` is deliberately not a shared vocabulary across the two kinds:
|
|
* containers report running/exited/paused/restarting/created, units report
|
|
* active/inactive/failed/activating. A failed unit and an exited container
|
|
* mean different things. */
|
|
export interface Workload {
|
|
kind: WorkloadKind;
|
|
id: string;
|
|
name: string;
|
|
state: string;
|
|
health?: string;
|
|
image?: string;
|
|
stack?: string;
|
|
ports?: string[];
|
|
restarts?: number;
|
|
started_at?: string;
|
|
protected: boolean;
|
|
}
|
|
|
|
export interface ServerWorkloads {
|
|
server_id: string;
|
|
hash?: string;
|
|
workloads: Workload[];
|
|
collected_at?: string;
|
|
/** false with no error means "Docker not in use here", which is not a
|
|
* fault. With an error it means installed but not responding. */
|
|
docker_ok: boolean;
|
|
docker_error?: string;
|
|
systemd_ok: boolean;
|
|
systemd_error?: string;
|
|
}
|
|
|
|
export interface WorkloadHit {
|
|
server_id: string;
|
|
workload: Workload;
|
|
}
|
|
|
|
export const workloads = {
|
|
forServer(serverId: string): Promise<ServerWorkloads> {
|
|
return request<ServerWorkloads>(`/servers/${serverId}/workloads`);
|
|
},
|
|
|
|
refresh(serverId: string): Promise<{ message: string }> {
|
|
return request<{ message: string }>(`/servers/${serverId}/workloads/refresh`, { method: "POST" });
|
|
},
|
|
|
|
control(serverId: string, kind: WorkloadKind, id: string, action: WorkloadAction): Promise<{ message: string }> {
|
|
return request<{ message: string }>(
|
|
`/servers/${serverId}/workloads/${encodeURIComponent(id)}/action`,
|
|
{ method: "POST", body: JSON.stringify({ kind, action }) },
|
|
);
|
|
},
|
|
|
|
logs(serverId: string, kind: WorkloadKind, id: string, tail = 500): Promise<{ text: string; truncated: boolean }> {
|
|
return request<{ text: string; truncated: boolean }>(
|
|
`/servers/${serverId}/workloads/${encodeURIComponent(id)}/logs?kind=${kind}&tail=${tail}`,
|
|
);
|
|
},
|
|
|
|
search(params?: { image?: string; stack?: string; state?: string }): Promise<WorkloadHit[]> {
|
|
const q = new URLSearchParams();
|
|
if (params?.image) q.set("image", params.image);
|
|
if (params?.stack) q.set("stack", params.stack);
|
|
if (params?.state) q.set("state", params.state);
|
|
const qs = q.toString();
|
|
return request<WorkloadHit[]>(`/workloads${qs ? `?${qs}` : ""}`);
|
|
},
|
|
};
|
|
|
|
// `request` already prefixes /api, so these paths do not repeat it.
|
|
export const licence = {
|
|
get(): Promise<LicenseInfo> {
|
|
return request<LicenseInfo>("/license");
|
|
},
|
|
put(blob: string): Promise<{ state: LicenseState; tier: string; expires_at?: string }> {
|
|
return request("/license", { method: "POST", body: JSON.stringify({ blob }) });
|
|
},
|
|
};
|
|
|
|
export interface Schedule {
|
|
enabled: boolean;
|
|
cron: string;
|
|
tz: string;
|
|
}
|
|
|
|
export interface Skip {
|
|
reason: string;
|
|
due: string;
|
|
at: string;
|
|
}
|