Server Deploy / deploy (push) Successful in 2m14s
A cloud instance's licence is issued in HQ and written into the control plane by admin/internal/inject, straight to the database. The customer has nothing to paste, so /settings/license offered them a form that could only ever fail — and on an expired cloud instance, failed at the exact moment they were looking for a way out. GET /license now reports `deployment`, and the page swaps the paste form for a short panel saying where the licence comes from, with a link to the portal when HQ_URL is set. That is the same treatment hq-managed members already get in the members table: read-only here, and a pointer to where it is actually managed. POST /license refuses with 409 cloud_managed on a cloud deployment. Hiding a form is a courtesy; this codebase's rule is that the API is the boundary, and the endpoint was reachable regardless of what the page rendered. It cannot break injection, which never goes through HTTP at all. Verified: server builds and vets clean, web builds clean.
824 lines
22 KiB
TypeScript
824 lines
22 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;
|
|
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 {
|
|
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[];
|
|
}
|
|
|
|
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 OrgOIDCConfig {
|
|
issuer?: string;
|
|
client_id?: string;
|
|
enabled: boolean;
|
|
client_secret_set: boolean;
|
|
updated_at?: string;
|
|
}
|
|
|
|
export interface OrgOIDCInput {
|
|
issuer: string;
|
|
client_id: string;
|
|
client_secret: string;
|
|
enabled: boolean;
|
|
}
|
|
|
|
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");
|
|
},
|
|
|
|
/** The URL an admin must register with their OIDC provider. */
|
|
oidcRedirectUrl(): string {
|
|
if (typeof window === "undefined") return "/auth/oidc/callback";
|
|
return `${window.location.origin}/auth/oidc/callback`;
|
|
},
|
|
};
|
|
|
|
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" });
|
|
},
|
|
|
|
getInstanceOIDC(): Promise<OrgOIDCConfig> {
|
|
return request<OrgOIDCConfig>("/instance/oidc");
|
|
},
|
|
|
|
saveInstanceOIDC(input: OrgOIDCInput): Promise<{ saved: boolean }> {
|
|
return request<{ saved: boolean }>("/instance/oidc", { method: "PUT", body: JSON.stringify(input) });
|
|
},
|
|
|
|
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`;
|
|
},
|
|
|
|
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; 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" });
|
|
},
|
|
|
|
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`;
|
|
},
|
|
};
|
|
|
|
export type LicenseState = "valid" | "expired" | "invalid";
|
|
|
|
export interface LicenseInfo {
|
|
instance_id: string;
|
|
state: LicenseState;
|
|
reason?: string;
|
|
tier?: string;
|
|
expires_at?: string;
|
|
days_remaining?: number;
|
|
limits: { max_servers: number; max_secret_groups: number; max_channels: number };
|
|
features: Record<string, boolean>;
|
|
usage: { servers: number; secret_groups: number; channels: number };
|
|
source: string;
|
|
/** "cloud" | "self_hosted". A cloud instance's licence is managed in HQ. */
|
|
deployment: string;
|
|
}
|
|
|
|
// `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 }) });
|
|
},
|
|
};
|