mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
v26.10.3 (#1692)
* feat: shell controller, TabStage, layout and theme plugins with repair view * feat: move the classic layout into its own bundled plugin * feat: move color themes into a bundled themes plugin * fix: plugin tests and type checks work with a linked sdk * fix: home tab, layout switch and missing layout fallbacks for looks * chore: add classic layout and themes to the readme * chore: drop example plugin links from the plugin guide * feat: plugin filters by type, paged browse, hide plugins needing newer termix * feat: move desktop, status, import, audit and branding features into plugins * feat: host lock api so a plugin can load hosts from a config file (#1581) * chore: add hosts config to the plugin table * feat: login policy hook for plugins and optional https redirect (#1584) * chore: add login security to the plugin table * feat: api key socket tickets so a master can open remote sessions (#1654) * feat: sync moves to a plugin, desktop gets multiple accounts (#1580, #1654) * chore: add sync to the plugin table * feat: move the sync screens and wizard out of core into the plugin (#1580) * fix: add a neutral linked desktop hook for plugins (#1580) * fix: show the linked account's name and admin flag in the app (#1580) * feat: ssh transport providers, core ssh relay and plugin folders api * feat: support FIDO2 security keys through ssh agent * feat: add companion panels beside host tabs * feat: mTLS client certificates for desktop server links * feat: native web view for plugins in the desktop app * fix: detect the desktop link window by Electron's default user agent Fixes #1675 * fix: changelog for desktop link fix * fix: keep text primary keys in the plugin data export * fix: ship sharp binaries for Windows arm64 builds * feat: client cert on the sync link types (sdk 1.0.18) * chore: update translations * feat: themed title bar for the desktop app (#1690) * chore: changelog for the themed title bar * fix: draw the title bar in the sidebar color * feat: per host layout for the terminal panel bar * chore: sdk 1.0.19 for the panel bar layout * chore: bundle all 43 plugins for 26.10.3 * chore: update translations --------- Co-authored-by: SirHephaistos <60616553+SirHephaistos@users.noreply.github.com> Co-authored-by: Giovanny Aranda <garanda21@gmail.com>
This commit is contained in:
577 files changed
+76479
-101831
No files matched your search
@@ -20,18 +20,27 @@ import type {
|
||||
PluginShareableUser,
|
||||
PluginShareableRole,
|
||||
PluginHostJumpHost,
|
||||
PluginHostShareGrant,
|
||||
} from "@termix-ssh/plugin-sdk/backend";
|
||||
import { PLUGIN_HOST_INPUT_KEYS } from "@termix-ssh/plugin-sdk/backend";
|
||||
import type { PluginManifest } from "@termix-ssh/plugin-sdk/manifest";
|
||||
import { assertCapability, capabilityRefused } from "./permissions.js";
|
||||
import { hostSessionStatus } from "../hosts/host-session-status.js";
|
||||
import { getActor } from "./actor.js";
|
||||
import { hostStatusService } from "../hosts/status/host-status-service.js";
|
||||
import { hostStatus, provideHostStatus } from "./host-status-provider.js";
|
||||
import { provideActivity, recentActivity } from "./activity-provider.js";
|
||||
import { loadProbeTargets } from "./ctx-probe.js";
|
||||
import { createPluginHostImport } from "./ctx-host-import.js";
|
||||
import { parseSshOptions } from "../hosts/ssh-options.js";
|
||||
import { hostTerminalExport } from "../database/routes/host-normalizers.js";
|
||||
import type { DisposableBag } from "./disposables.js";
|
||||
import { pluginEvents, TOPICS } from "./events.js";
|
||||
import { pluginLogger } from "../utils/logger.js";
|
||||
import {
|
||||
assertHostsUnlocked,
|
||||
assertManualCreateAllowed,
|
||||
provideHostLocks,
|
||||
} from "../hosts/locks.js";
|
||||
|
||||
type AuditFn = (
|
||||
action: string,
|
||||
@@ -171,7 +180,7 @@ export function toRecord(
|
||||
connectionOrigin: origin === "local" || origin === "remote" ? origin : null,
|
||||
sshOptions: hostTerminalExport(host).sshOptions,
|
||||
pluginSettings,
|
||||
status: Number.isInteger(id) ? hostStatusService.get(id) : null,
|
||||
status: Number.isInteger(id) ? hostStatus.get(id) : null,
|
||||
localOnly: asFlag(host.localOnly, false),
|
||||
createdAt: (host.createdAt as string | null) ?? null,
|
||||
updatedAt: (host.updatedAt as string | null) ?? null,
|
||||
@@ -204,6 +213,8 @@ void inputKeysComplete;
|
||||
export function toHostWrite(input: PluginHostUpdateInput): {
|
||||
row: Record<string, unknown>;
|
||||
settings: Record<string, Record<string, unknown>>;
|
||||
protocolAuth: Record<string, unknown> | null;
|
||||
pluginEnable: Record<string, boolean>;
|
||||
} {
|
||||
const unknown = Object.keys(input ?? {}).filter(
|
||||
(key) => !INPUT_KEYS.has(key),
|
||||
@@ -265,6 +276,32 @@ export function toHostWrite(input: PluginHostUpdateInput): {
|
||||
? null
|
||||
: JSON.stringify(parseSshOptions(input.sshOptions)),
|
||||
);
|
||||
for (const key of [
|
||||
"password",
|
||||
"key",
|
||||
"keyPassword",
|
||||
"keyType",
|
||||
"sudoPassword",
|
||||
] as const) {
|
||||
const value = input[key];
|
||||
if (value !== undefined && value !== null && typeof value !== "string") {
|
||||
throw new PluginHostInputError(`${key} must be a string`);
|
||||
}
|
||||
set(key, value === undefined ? undefined : value || null);
|
||||
}
|
||||
|
||||
let protocolAuth: Record<string, unknown> | null = null;
|
||||
if (input.protocolAuth !== undefined && input.protocolAuth !== null) {
|
||||
if (
|
||||
typeof input.protocolAuth !== "object" ||
|
||||
Array.isArray(input.protocolAuth)
|
||||
) {
|
||||
throw new PluginHostInputError(
|
||||
"protocolAuth must map protocol ids to their logins",
|
||||
);
|
||||
}
|
||||
protocolAuth = input.protocolAuth as Record<string, unknown>;
|
||||
}
|
||||
|
||||
const settings: Record<string, Record<string, unknown>> = {};
|
||||
if (input.pluginSettings !== undefined && input.pluginSettings !== null) {
|
||||
@@ -285,7 +322,136 @@ export function toHostWrite(input: PluginHostUpdateInput): {
|
||||
settings[pluginId] = values;
|
||||
}
|
||||
}
|
||||
return { row, settings };
|
||||
const pluginEnable: Record<string, boolean> = {};
|
||||
if (input.pluginEnable !== undefined && input.pluginEnable !== null) {
|
||||
if (
|
||||
typeof input.pluginEnable !== "object" ||
|
||||
Array.isArray(input.pluginEnable)
|
||||
) {
|
||||
throw new PluginHostInputError(
|
||||
"pluginEnable must map plugin ids to true or false",
|
||||
);
|
||||
}
|
||||
for (const [pluginId, enabled] of Object.entries(input.pluginEnable)) {
|
||||
if (typeof enabled !== "boolean") {
|
||||
throw new PluginHostInputError(
|
||||
`pluginEnable.${pluginId} must be true or false`,
|
||||
);
|
||||
}
|
||||
pluginEnable[pluginId] = enabled;
|
||||
}
|
||||
}
|
||||
return { row, settings, protocolAuth, pluginEnable };
|
||||
}
|
||||
|
||||
/** Folds pluginEnable into each plugin's settings under its enableKey. */
|
||||
async function foldPluginEnable(
|
||||
settings: Record<string, Record<string, unknown>>,
|
||||
pluginEnable: Record<string, boolean>,
|
||||
): Promise<Record<string, Record<string, unknown>>> {
|
||||
const ids = Object.keys(pluginEnable);
|
||||
if (ids.length === 0) return settings;
|
||||
const { hostSettingsPlugins } =
|
||||
await import("../database/routes/host-plugin-settings.js");
|
||||
const running = hostSettingsPlugins();
|
||||
const out = { ...settings };
|
||||
for (const pluginId of ids) {
|
||||
const manifest = running.find((m) => m.id === pluginId);
|
||||
if (!manifest) {
|
||||
// checkHostPluginSettingsInput skips and logs a plugin that is not running.
|
||||
out[pluginId] = { ...out[pluginId] };
|
||||
continue;
|
||||
}
|
||||
const enableKey = manifest.contributes?.settings?.host?.enableKey;
|
||||
if (!enableKey) {
|
||||
throw new PluginHostInputError(`${pluginId} has no host switch`);
|
||||
}
|
||||
out[pluginId] = { ...out[pluginId], [enableKey]: pluginEnable[pluginId] };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Writes a host's protocol logins; a credential the owner can't use is refused. */
|
||||
async function writeHostProtocolAuth(
|
||||
ownerId: string,
|
||||
hostId: number,
|
||||
protocolAuth: Record<string, unknown> | null,
|
||||
): Promise<void> {
|
||||
if (!protocolAuth) return;
|
||||
const { readProtocolAuthPayload, writeProtocolAuth } =
|
||||
await import("../hosts/protocol-auth/protocol-auth.js");
|
||||
const patch = readProtocolAuthPayload({ protocolAuth });
|
||||
if (!patch) return;
|
||||
const { findUsableCredential } =
|
||||
await import("../hosts/usable-credential.js");
|
||||
for (const [protocol, login] of patch) {
|
||||
const credentialId = Number(login?.credentialId);
|
||||
if (
|
||||
Number.isInteger(credentialId) &&
|
||||
credentialId > 0 &&
|
||||
!(await findUsableCredential(credentialId, ownerId))
|
||||
) {
|
||||
throw new PluginHostInputError(
|
||||
`protocolAuth.${protocol}: credential ${credentialId} not found`,
|
||||
);
|
||||
}
|
||||
}
|
||||
await writeProtocolAuth(ownerId, hostId, patch, { isOwner: true });
|
||||
}
|
||||
|
||||
const SHARE_LEVELS: readonly string[] = ["connect", "view", "edit", "manage"];
|
||||
|
||||
/** Upserts one grant and snapshots the host's secrets for it. */
|
||||
async function grantHostShare(
|
||||
hostId: number,
|
||||
ownerId: string,
|
||||
grantedBy: string,
|
||||
target: PluginShareTarget,
|
||||
permissionLevel: PluginHostShareLevel,
|
||||
expiresAt: string | null,
|
||||
): Promise<number> {
|
||||
const { createCurrentRbacAccessRepository } =
|
||||
await import("../database/repositories/factory.js");
|
||||
const grant = await createCurrentRbacAccessRepository().upsertHostAccess({
|
||||
hostId,
|
||||
grantedBy,
|
||||
permissionLevel,
|
||||
expiresAt,
|
||||
...(target.type === "user"
|
||||
? { targetType: "user" as const, targetUserId: target.id as string }
|
||||
: { targetType: "role" as const, targetRoleId: target.id as number }),
|
||||
});
|
||||
|
||||
const { SharedHostSecretsManager } =
|
||||
await import("../utils/shared-host-secrets-manager.js");
|
||||
const secretsManager = SharedHostSecretsManager.getInstance();
|
||||
try {
|
||||
if (target.type === "user") {
|
||||
await secretsManager.snapshotForUser(
|
||||
grant.id,
|
||||
hostId,
|
||||
target.id as string,
|
||||
ownerId,
|
||||
);
|
||||
} else {
|
||||
await secretsManager.snapshotForRole(
|
||||
grant.id,
|
||||
hostId,
|
||||
target.id as number,
|
||||
ownerId,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// A snapshot failure never blocks the grant: the recipient can
|
||||
// still be prompted, or the owner can retry sharing later.
|
||||
}
|
||||
return grant.id;
|
||||
}
|
||||
|
||||
function hoursFromNow(durationHours: number | undefined): string | null {
|
||||
return durationHours && durationHours > 0
|
||||
? new Date(Date.now() + durationHours * 60 * 60 * 1000).toISOString()
|
||||
: null;
|
||||
}
|
||||
|
||||
function actingUser(): string {
|
||||
@@ -389,6 +555,8 @@ export function createPluginHosts({ manifest, bag, audit }: Deps): PluginHosts {
|
||||
};
|
||||
|
||||
return {
|
||||
...createPluginHostImport({ manifest, bag, audit }),
|
||||
|
||||
list: async () => {
|
||||
await requireRead();
|
||||
const userId = actingUser();
|
||||
@@ -471,6 +639,7 @@ export function createPluginHosts({ manifest, bag, audit }: Deps): PluginHosts {
|
||||
create: async (host: PluginHostCreateInput): Promise<PluginHostRecord> => {
|
||||
try {
|
||||
await requireWrite();
|
||||
assertManualCreateAllowed(pluginId);
|
||||
} catch (error) {
|
||||
await audit("hosts_create", host?.name ?? host?.ip ?? "host", {
|
||||
success: false,
|
||||
@@ -479,8 +648,10 @@ export function createPluginHosts({ manifest, bag, audit }: Deps): PluginHosts {
|
||||
throw error;
|
||||
}
|
||||
const userId = actingUser();
|
||||
const { row, settings } = toHostWrite(host);
|
||||
const writes = await checkSettings(settings);
|
||||
const { row, settings, protocolAuth, pluginEnable } = toHostWrite(host);
|
||||
const writes = await checkSettings(
|
||||
await foldPluginEnable(settings, pluginEnable),
|
||||
);
|
||||
const { createCurrentHostRepository } =
|
||||
await import("../database/repositories/factory.js");
|
||||
const { applyHostDefaultsToWrite, applyDefaultsAfterHostWrite } =
|
||||
@@ -499,6 +670,7 @@ export function createPluginHosts({ manifest, bag, audit }: Deps): PluginHosts {
|
||||
userId,
|
||||
columns,
|
||||
);
|
||||
await writeHostProtocolAuth(userId, created.id, protocolAuth);
|
||||
await applyDefaultsAfterHostWrite(created.id);
|
||||
await writeSettings(created.id, writes);
|
||||
await markSettingsOwn(created.id, writes);
|
||||
@@ -515,6 +687,7 @@ export function createPluginHosts({ manifest, bag, audit }: Deps): PluginHosts {
|
||||
): Promise<PluginHostRecord | null> => {
|
||||
try {
|
||||
await requireWrite();
|
||||
assertHostsUnlocked([hostId], pluginId);
|
||||
} catch (error) {
|
||||
await audit("hosts_update", `host ${hostId}`, {
|
||||
success: false,
|
||||
@@ -523,8 +696,10 @@ export function createPluginHosts({ manifest, bag, audit }: Deps): PluginHosts {
|
||||
throw error;
|
||||
}
|
||||
const userId = actingUser();
|
||||
const { row, settings } = toHostWrite(patch);
|
||||
const writes = await checkSettings(settings);
|
||||
const { row, settings, protocolAuth, pluginEnable } = toHostWrite(patch);
|
||||
const writes = await checkSettings(
|
||||
await foldPluginEnable(settings, pluginEnable),
|
||||
);
|
||||
const { createCurrentHostRepository } =
|
||||
await import("../database/repositories/factory.js");
|
||||
const { applyHostDefaultsToWrite, applyDefaultsAfterHostWrite } =
|
||||
@@ -555,6 +730,7 @@ export function createPluginHosts({ manifest, bag, audit }: Deps): PluginHosts {
|
||||
columns,
|
||||
);
|
||||
if (updated) {
|
||||
await writeHostProtocolAuth(userId, hostId, protocolAuth);
|
||||
await writeSettings(hostId, writes);
|
||||
await markSettingsOwn(hostId, writes);
|
||||
await applyDefaultsAfterHostWrite(hostId, {
|
||||
@@ -579,6 +755,7 @@ export function createPluginHosts({ manifest, bag, audit }: Deps): PluginHosts {
|
||||
delete: async (hostId: number): Promise<boolean> => {
|
||||
try {
|
||||
await requireWrite();
|
||||
assertHostsUnlocked([hostId], pluginId);
|
||||
const userId = actingUser();
|
||||
const { PermissionManager } =
|
||||
await import("../utils/permission-manager.js");
|
||||
@@ -626,6 +803,7 @@ export function createPluginHosts({ manifest, bag, audit }: Deps): PluginHosts {
|
||||
): Promise<PluginHostShareResult> => {
|
||||
try {
|
||||
await requireWrite();
|
||||
assertHostsUnlocked([hostId], pluginId);
|
||||
} catch (error) {
|
||||
await audit("hosts_share", `host ${hostId}`, {
|
||||
success: false,
|
||||
@@ -655,10 +833,8 @@ export function createPluginHosts({ manifest, bag, audit }: Deps): PluginHosts {
|
||||
return result;
|
||||
}
|
||||
|
||||
const {
|
||||
createCurrentHostResolutionRepository,
|
||||
createCurrentRbacAccessRepository,
|
||||
} = await import("../database/repositories/factory.js");
|
||||
const { createCurrentHostResolutionRepository } =
|
||||
await import("../database/repositories/factory.js");
|
||||
const ownerId =
|
||||
(await createCurrentHostResolutionRepository().findHostOwnerId(
|
||||
hostId,
|
||||
@@ -672,56 +848,95 @@ export function createPluginHosts({ manifest, bag, audit }: Deps): PluginHosts {
|
||||
return { hostId, shared: false, reason: "owner" };
|
||||
}
|
||||
|
||||
const expiresAt =
|
||||
durationHours && durationHours > 0
|
||||
? new Date(Date.now() + durationHours * 60 * 60 * 1000).toISOString()
|
||||
: null;
|
||||
|
||||
const rbacAccessRepository = createCurrentRbacAccessRepository();
|
||||
const { SharedHostSecretsManager } =
|
||||
await import("../utils/shared-host-secrets-manager.js");
|
||||
const secretsManager = SharedHostSecretsManager.getInstance();
|
||||
|
||||
const expiresAt = hoursFromNow(durationHours);
|
||||
for (const target of targets) {
|
||||
const grant = await rbacAccessRepository.upsertHostAccess({
|
||||
await grantHostShare(
|
||||
hostId,
|
||||
grantedBy: userId,
|
||||
ownerId,
|
||||
userId,
|
||||
target,
|
||||
permissionLevel,
|
||||
expiresAt,
|
||||
...(target.type === "user"
|
||||
? { targetType: "user" as const, targetUserId: target.id as string }
|
||||
: {
|
||||
targetType: "role" as const,
|
||||
targetRoleId: target.id as number,
|
||||
}),
|
||||
});
|
||||
|
||||
try {
|
||||
if (target.type === "user") {
|
||||
await secretsManager.snapshotForUser(
|
||||
grant.id,
|
||||
hostId,
|
||||
target.id as string,
|
||||
ownerId,
|
||||
);
|
||||
} else {
|
||||
await secretsManager.snapshotForRole(
|
||||
grant.id,
|
||||
hostId,
|
||||
target.id as number,
|
||||
ownerId,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// A snapshot failure never blocks the grant: the recipient can
|
||||
// still be prompted, or the owner can retry sharing later.
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
await audit("hosts_share", `host ${hostId}`, { success: true });
|
||||
return { hostId, shared: true };
|
||||
},
|
||||
|
||||
setShares: async (
|
||||
hostId: number,
|
||||
grants: PluginHostShareGrant[],
|
||||
): Promise<PluginHostShareResult> => {
|
||||
const fail = async (reason: string): Promise<PluginHostShareResult> => {
|
||||
await audit("hosts_set_shares", `host ${hostId}`, {
|
||||
success: false,
|
||||
errorMessage: reason,
|
||||
});
|
||||
return { hostId, shared: false, reason };
|
||||
};
|
||||
try {
|
||||
await requireWrite();
|
||||
assertHostsUnlocked([hostId], pluginId);
|
||||
} catch (error) {
|
||||
await fail(error instanceof Error ? error.message : String(error));
|
||||
throw error;
|
||||
}
|
||||
if (!Array.isArray(grants)) {
|
||||
throw new PluginHostInputError("grants must be a list");
|
||||
}
|
||||
for (const grant of grants) {
|
||||
const type = grant?.target?.type;
|
||||
if (
|
||||
(type !== "user" && type !== "role") ||
|
||||
!SHARE_LEVELS.includes(grant.permissionLevel)
|
||||
) {
|
||||
throw new PluginHostInputError(
|
||||
"Each grant needs a user or role target and a share level",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const userId = actingUser();
|
||||
const {
|
||||
createCurrentHostResolutionRepository,
|
||||
createCurrentRbacAccessRepository,
|
||||
} = await import("../database/repositories/factory.js");
|
||||
const ownerId =
|
||||
await createCurrentHostResolutionRepository().findHostOwnerId(hostId);
|
||||
if (ownerId !== userId) return fail("forbidden");
|
||||
if (
|
||||
grants.some((g) => g.target.type === "user" && g.target.id === userId)
|
||||
) {
|
||||
return fail("owner");
|
||||
}
|
||||
|
||||
const kept = new Set<number>();
|
||||
for (const grant of grants) {
|
||||
kept.add(
|
||||
await grantHostShare(
|
||||
hostId,
|
||||
ownerId,
|
||||
userId,
|
||||
grant.target,
|
||||
grant.permissionLevel,
|
||||
hoursFromNow(grant.durationHours),
|
||||
),
|
||||
);
|
||||
}
|
||||
const repository = createCurrentRbacAccessRepository();
|
||||
for (const existing of await repository.listHostAccess(hostId)) {
|
||||
if (!kept.has(existing.id)) {
|
||||
await repository.revokeHostAccess(existing.id, hostId);
|
||||
}
|
||||
}
|
||||
const { pruneHostGrants } = await import("../database/routes/rbac.js");
|
||||
await pruneHostGrants(hostId);
|
||||
|
||||
await audit("hosts_set_shares", `host ${hostId}`, { success: true });
|
||||
return { hostId, shared: true };
|
||||
},
|
||||
|
||||
listUsers: async (): Promise<PluginShareableUser[]> => {
|
||||
try {
|
||||
await requireWrite();
|
||||
@@ -787,38 +1002,146 @@ export function createPluginHosts({ manifest, bag, audit }: Deps): PluginHosts {
|
||||
await requireRead();
|
||||
const userId = actingUser();
|
||||
if (!(await actorMayReach(hostId))) return;
|
||||
const { recordRecentActivity } =
|
||||
await import("../services/recent-activity.js");
|
||||
await recordRecentActivity(userId, { type, hostId, hostName });
|
||||
await recentActivity.record(userId, { type, hostId, hostName });
|
||||
},
|
||||
|
||||
probeTargets: async (filter) => {
|
||||
await requireRead();
|
||||
const actor = getActor();
|
||||
if (!actor) {
|
||||
throw new Error("ctx.hosts.probeTargets needs an acting user");
|
||||
}
|
||||
const ids = filter?.hostIds?.filter((id) => Number.isInteger(id));
|
||||
return loadProbeTargets(actor, ids);
|
||||
},
|
||||
|
||||
sessionActive: (hostId) => {
|
||||
requireReadSync();
|
||||
return hostSessionStatus.hasActiveSession(hostId);
|
||||
},
|
||||
|
||||
locks: {
|
||||
provide: (provider) => {
|
||||
if (!declared.includes("hosts:lock")) {
|
||||
throw capabilityRefused(pluginId, "hosts:lock");
|
||||
}
|
||||
const remove = provideHostLocks(
|
||||
pluginId,
|
||||
manifest.name || pluginId,
|
||||
provider,
|
||||
);
|
||||
void audit("host_locks_provide", `${pluginId} manages hosts`, {
|
||||
success: true,
|
||||
});
|
||||
const drop = bag?.add(remove, "host lock provider");
|
||||
return () => {
|
||||
drop?.();
|
||||
remove();
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
folders: {
|
||||
ensure: async (name, style) => {
|
||||
await requireWrite();
|
||||
const userId = actingUser();
|
||||
const { createCurrentHostFolderRepository } =
|
||||
await import("../database/repositories/factory.js");
|
||||
await createCurrentHostFolderRepository().upsertMetadata(
|
||||
userId,
|
||||
name,
|
||||
style?.color ?? null,
|
||||
style?.icon ?? null,
|
||||
);
|
||||
},
|
||||
removeEmpty: async (name) => {
|
||||
await requireWrite();
|
||||
const userId = actingUser();
|
||||
const { createCurrentHostFolderRepository } =
|
||||
await import("../database/repositories/factory.js");
|
||||
return createCurrentHostFolderRepository().deleteEmptyFolderRecords(
|
||||
userId,
|
||||
name,
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
activity: {
|
||||
provide: (provider) => {
|
||||
if (!declared.includes("hosts:activity-provide")) {
|
||||
throw capabilityRefused(pluginId, "hosts:activity-provide");
|
||||
}
|
||||
const remove = provideActivity(
|
||||
pluginId,
|
||||
provider as Parameters<typeof provideActivity>[1],
|
||||
);
|
||||
void audit(
|
||||
"host_activity_provide",
|
||||
`${pluginId} stores recent activity`,
|
||||
{
|
||||
success: true,
|
||||
},
|
||||
);
|
||||
const drop = bag?.add(remove, "activity provider");
|
||||
return () => {
|
||||
drop?.();
|
||||
remove();
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Not audited: pollers call these on every sample.
|
||||
status: {
|
||||
provide: (provider) => {
|
||||
requireReadSync();
|
||||
if (!declared.includes("hosts:status-provide")) {
|
||||
throw capabilityRefused(pluginId, "hosts:status-provide");
|
||||
}
|
||||
const remove = provideHostStatus(pluginId, {
|
||||
start: () => provider.start(),
|
||||
stop: () => provider.stop(),
|
||||
get: (hostId) => provider.get(hostId),
|
||||
statusesFor: (userId, requested) =>
|
||||
provider.statusesFor(userId, requested ? [...requested] : null),
|
||||
refresh: (userId) => provider.refresh(userId),
|
||||
check: (hostId) => provider.check(hostId),
|
||||
reportLogin: (hostId, outcome) =>
|
||||
provider.reportLogin(hostId, outcome),
|
||||
registerPort: (type, resolve) => provider.registerPort(type, resolve),
|
||||
getIntervalSeconds: () => provider.getIntervalSeconds(),
|
||||
setIntervalSeconds: (seconds) => provider.setIntervalSeconds(seconds),
|
||||
});
|
||||
void audit("host_status_provide", `${pluginId} provides host status`, {
|
||||
success: true,
|
||||
});
|
||||
const drop = bag?.add(remove, "host status provider");
|
||||
return () => {
|
||||
drop?.();
|
||||
remove();
|
||||
};
|
||||
},
|
||||
get: async (hostId) => {
|
||||
await requireRead();
|
||||
if (!(await actorMayReach(hostId))) return null;
|
||||
return hostStatusService.get(hostId);
|
||||
return hostStatus.get(hostId);
|
||||
},
|
||||
check: async (hostId) => {
|
||||
await requireRead();
|
||||
if (!(await actorMayReach(hostId))) return null;
|
||||
return hostStatusService.check(hostId);
|
||||
return hostStatus.check(hostId);
|
||||
},
|
||||
// Synchronous for the same reason as trackSession.
|
||||
reportLogin: (hostId, outcome) => {
|
||||
requireReadSync();
|
||||
void actorMayReach(hostId)
|
||||
.then((allowed) => {
|
||||
if (allowed) hostStatusService.reportLogin(hostId, outcome);
|
||||
if (allowed) hostStatus.reportLogin(hostId, outcome);
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
registerPort: (connectionType, resolve) => {
|
||||
requireReadSync();
|
||||
const unregister = hostStatusService.registerPort(
|
||||
connectionType,
|
||||
resolve,
|
||||
);
|
||||
const unregister = hostStatus.registerPort(connectionType, resolve);
|
||||
const drop = bag?.add(unregister, `status port for ${connectionType}`);
|
||||
return () => {
|
||||
drop?.();
|
||||
|
||||
Reference in new issue
Block a user