feat: add ctx.hosts create/update to the SDK, host-metrics drops proxmox

This commit is contained in:
LukeGus committed 2026-09-23 13:06:06 -05:00
1 parent f9283ec83f
commit 218c5088e7
20 files changed
+194 -1329

No files matched your search

+74
View File
@@ -409,6 +409,56 @@ export interface PluginHostSummary {
authType: string;
}
/**
* A host record wide enough for a plugin that creates or updates hosts on the
* user's behalf (import, discovery, sync), decrypted for that user. Unlike
* PluginHostSummary this carries the fields a plugin needs to set up a
* connectable host, not just what a list view shows. Still no secret auth
* material (password, key, vault tokens): a plugin that creates a host picks
* an authType and, for "credential", a credentialId it does not need to see
* the contents of.
*/
export interface PluginHostRecord {
id: number;
userId: string;
name: string | null;
ip: string;
port: number;
username: string;
authType: string;
credentialId?: number | null;
overrideCredentialUsername?: boolean | null;
connectionType?: string | null;
tags: string | null;
folder: string | null;
jumpHosts?: unknown;
enableSsh?: boolean | null;
enableRdp?: boolean | null;
enableTerminal?: boolean | null;
enableFileManager?: boolean | null;
enableTunnel?: boolean | null;
enableDocker?: boolean | null;
createdAt?: string | null;
updatedAt?: string | null;
[key: string]: unknown;
}
/** Fields a plugin may set when creating a host it will own or manage. */
export type PluginHostCreateInput = Partial<
Omit<PluginHostRecord, "id" | "userId">
> & {
name: string;
ip: string;
port: number;
username: string;
authType: string;
};
/** Fields a plugin may change on a host it already created or was granted access to. */
export type PluginHostUpdateInput = Partial<
Omit<PluginHostRecord, "id" | "userId">
>;
/** The same shape canAccessHost returns internally, without secrets. */
export interface PluginHostAccess {
hasAccess: boolean;
@@ -462,6 +512,30 @@ export interface PluginHosts {
hostId: number,
level: PluginHostShareLevel,
) => Promise<PluginHostAccess>;
/**
* Creates a host owned by the acting user, encrypted the same way the host
* editor's own create route does it. Needs hosts:write. For a plugin that
* imports or discovers hosts on the user's behalf (B5's proxmox discovery).
*/
create: (host: PluginHostCreateInput) => Promise<PluginHostRecord>;
/**
* Updates a host the acting user owns. Refuses a host it does not own
* (sharing a write onto someone else's host goes through share(), not
* update()). Needs hosts:write.
*/
update: (
hostId: number,
patch: PluginHostUpdateInput,
) => Promise<PluginHostRecord | null>;
/**
* Every host the acting user owns, decrypted and wide (PluginHostRecord,
* not the narrow list() summary). For a plugin that scans its own hosts in
* the background (auto-sync) and needs fields list() does not carry, such
* as credentialId or jumpHosts. Needs hosts:write, matching create/update:
* this is the same "full host detail" surface a create/update caller needs
* to read back, not a wider read grant than hosts:read gives via list/get.
*/
listOwned: () => Promise<PluginHostRecord[]>;
/**
* Grants access to a host the caller manages, snapshotting shared secrets
* for each target the same way the host editor's own share action does.
+3
View File
@@ -20,6 +20,7 @@ export type PluginColumnType =
| "varchar"
| "integer"
| "bigint"
| "real"
| "boolean"
| "timestamp"
| "json"
@@ -77,6 +78,8 @@ export const varchar = (length: number = KEY_LENGTH) =>
export const integer = () => builder({ type: "integer" });
export const bigint = () => builder({ type: "bigint" });
/** Floating point. A plain number on every engine. Never indexable. */
export const real = () => builder({ type: "real" });
export const boolean = () => builder({ type: "boolean" });
/** Stored as text on every engine. See src/backend/database/repositories/sql-timestamp.ts. */
+3
View File
@@ -62,6 +62,9 @@ function columnType(
return dialect === "mysql" ? "int" : "integer";
case "bigint":
return dialect === "sqlite" ? "integer" : "bigint";
case "real":
if (dialect === "sqlite") return "real";
return dialect === "mysql" ? "double" : "double precision";
case "boolean":
return dialect === "sqlite" ? "integer" : "boolean";
case "varchar":
+4
View File
@@ -12,6 +12,7 @@ import {
sqliteTable,
text as sqliteText,
integer as sqliteInteger,
real as sqliteReal,
index as sqliteIndex,
uniqueIndex as sqliteUniqueIndex,
type AnySQLiteColumn,
@@ -55,6 +56,9 @@ export function buildTable(
case "refHost":
built = sqliteInteger(name);
break;
case "real":
built = sqliteReal(name);
break;
case "boolean":
built = sqliteInteger(name, { mode: "boolean" });
break;
+44
View File
@@ -24,6 +24,9 @@ import type {
PluginSshAuthProvider,
PluginSshHost,
PluginHostSummary,
PluginHostRecord,
PluginHostCreateInput,
PluginHostUpdateInput,
PluginHostAccess,
PluginHostShareResult,
PluginShareableUser,
@@ -174,6 +177,8 @@ export function createFakeContext(
const hostsById = new Map<number, PluginHostSummary>(
(options.hosts ?? []).map((h) => [h.id, h]),
);
const hostRecordsById = new Map<number, PluginHostRecord>();
let nextHostId = Math.max(0, ...(options.hosts ?? []).map((h) => h.id)) + 1;
const services = new Map<string, object>();
const auth: FakeAuthRegistrations = {
sshAuthProviders: [],
@@ -421,6 +426,33 @@ export function createFakeContext(
permissionLevel: "manage",
};
},
create: async (
host: PluginHostCreateInput,
): Promise<PluginHostRecord> => {
const id = nextHostId++;
const record: PluginHostRecord = {
tags: null,
folder: null,
...host,
id,
userId: actor ?? "unknown",
};
hostRecordsById.set(id, record);
return record;
},
update: async (
hostId: number,
patch: PluginHostUpdateInput,
): Promise<PluginHostRecord | null> => {
const existing = hostRecordsById.get(hostId);
if (!existing) return null;
const updated = { ...existing, ...patch };
hostRecordsById.set(hostId, updated);
return updated;
},
listOwned: async (): Promise<PluginHostRecord[]> => [
...hostRecordsById.values(),
],
share: async (
hostId,
targets,
@@ -706,6 +738,18 @@ export function createMockCtx(
require("hosts:read");
return ctx.hosts.checkAccess(hostId, level);
},
create: async (host) => {
require("hosts:write");
return ctx.hosts.create(host);
},
update: async (hostId, patch) => {
require("hosts:write");
return ctx.hosts.update(hostId, patch);
},
listOwned: async () => {
require("hosts:write");
return ctx.hosts.listOwned();
},
share: async (hostId, targets, permissionLevel, durationHours) => {
require("hosts:write");
return ctx.hosts.share(hostId, targets, permissionLevel, durationHours);