* 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:
authored and GitHub committed 2026-10-20 16:03:29 -05:00
1 parent aff29a9581
commit a89ccc6b07
577 files changed
+76479 -101831

No files matched your search

+397 -2
View File
@@ -14,7 +14,10 @@ import {
PluginCapabilityError,
} from "./backend.js";
import type {
PluginAuditEntry,
PluginAuditFilters,
PluginContext,
PluginDesktopLocal,
PluginDisposables,
PluginLogger,
} from "./backend.js";
@@ -65,12 +68,19 @@ import type {
PluginSshKeyCredential,
PluginSshKeyCredentialInput,
} from "./backend.js";
import type { SyncEntityRegistration } from "./backend.js";
import type { PluginSyncHost, SyncEntityRegistration } from "./backend.js";
import type { PluginDatabase } from "./backend.js";
import type {
PluginExternalClientRequest,
PluginProtocolTarget,
PluginHostProbeTarget,
PluginHostStatusEntry,
PluginActivityProvider,
PluginHostLockProvider,
PluginAuthPolicyProvider,
PluginCredentialSummary,
PluginHostShareGrant,
PluginHostStatusProvider,
PluginNotification,
PluginNotificationChannel,
PluginNotifyHub,
@@ -94,6 +104,8 @@ export interface FakeScheduledJob {
export interface FakeContextOptions {
pluginId?: string;
/** What ctx.sync.host answers, for a sync provider's tests. */
syncHost?: Partial<PluginSyncHost>;
manifest?: Partial<PluginManifest>;
/** Acting user returned by currentActor and used by asUser. */
actor?: string;
@@ -101,6 +113,8 @@ export interface FakeContextOptions {
settings?: Record<string, unknown>;
/** Seed for the core settings ctx.settings.readCore can reach. */
coreSettings?: Record<string, string>;
/** What ctx.files.dataDir() answers. Defaults to /tmp/<pluginId>. */
dataDir?: string;
/** What ctx.ssh.connect and withConnection hand back as the client. */
sshClient?: unknown;
/** A real database behind ctx.db, usually createTestDb().database. */
@@ -132,10 +146,22 @@ export interface FakeContextOptions {
protocolTargets?: Record<string, PluginProtocolTarget>;
/** What ctx.credentials.listSshKeys answers. */
sshKeyCredentials?: PluginSshKeyCredential[];
/** What ctx.credentials.list answers. */
credentialSummaries?: PluginCredentialSummary[];
/** What ctx.desktop.available() answers. Defaults to false. */
desktopAvailable?: boolean;
/** What ctx.desktop.local is. Calls throw when this is not given. */
desktopLocal?: PluginDesktopLocal;
/** The stored audit lines ctx.audit.list and exportBatches serve. */
auditEntries?: PluginAuditEntry[];
/** What ctx.hosts.status.get and check answer, by host id. */
hostStatuses?: Record<number, PluginHostStatusEntry>;
/** What ctx.hosts.probeTargets answers. */
probeTargets?: PluginHostProbeTarget[];
/** Host ids ctx.hosts.sessionActive answers true for. */
activeSessions?: number[];
/** What ctx.network.tcpProbe and ctx.ssh.probe answer. Defaults to false. */
reachable?: (target: string | number, port?: number) => boolean;
/** The acting user's channels, what ctx.notify.channels answers. */
notificationChannels?: PluginNotificationChannel[];
/**
@@ -172,6 +198,8 @@ export interface FakeContextOptions {
baseUrl?: string;
/** What ctx.auth.countLinkedUsers answers, by provider. Defaults to 0. */
linkedUsers?: Record<string, number>;
/** Second factors from other plugins that ctx.auth.listSecondFactors adds. */
secondFactors?: { id: string; pluginId: string }[];
/** Failures before ctx.auth.loginRateLimit locks a key. Defaults to 5. */
loginAttemptLimit?: number;
/** What ctx.users.getLocale answers, by user id. A missing user answers null. */
@@ -251,6 +279,8 @@ export interface FakeAuthRegistrations {
loginFailures: Map<string, number>;
/** Every ctx.auth.issueUserToken call, in order. */
userTokens: Array<{ userId: string; purpose: string; ttlSeconds?: number }>;
/** Providers passed to ctx.auth.policy.provide that are still registered. */
policies: PluginAuthPolicyProvider[];
}
export interface FakePluginContext {
@@ -263,6 +293,12 @@ export interface FakePluginContext {
) => Promise<Record<string, string>>;
/** Everything the plugin registered through ctx.disposables. */
disposals: Array<() => void | Promise<void>>;
/** Providers registered through ctx.hosts.status.provide. */
hostStatusProviders: PluginHostStatusProvider[];
/** Every provider passed to ctx.hosts.activity.provide that is still registered. */
activityProviders: PluginActivityProvider[];
/** Every provider passed to ctx.hosts.locks.provide that is still registered. */
hostLockProviders: PluginHostLockProvider[];
/** Topics emitted through ctx.events.emit, in order. */
emitted: Array<{ topic: string; payload: unknown }>;
/** Backing store behind ctx.kv. */
@@ -295,6 +331,8 @@ export interface FakePluginContext {
permissionLevel: string;
durationHours?: number;
}>;
/** Every ctx.hosts.setShares call, in order. */
hostShareSets: Array<{ hostId: number; grants: PluginHostShareGrant[] }>;
/** Everything registered through ctx.auth. */
auth: FakeAuthRegistrations;
/** Every ctx.desktop.openIsolatedWindow call, in order. */
@@ -338,6 +376,8 @@ export interface FakePluginContext {
challenges: Map<string, string>;
renewers: number;
};
/** Calls every ctx.audit.subscribe handler, like a line core just accepted. */
emitAudit: (entry: PluginAuditEntry) => void;
/** Every ctx.audit.record entry, in order. */
audits: Array<{ action: string; success: boolean; [key: string]: unknown }>;
/** Every ctx.hosts.recordActivity call, in order. */
@@ -464,7 +504,14 @@ function applyHostInput(
const {
tags,
pluginSettings: _settings,
pluginEnable: _pluginEnable,
forceKeyboardInteractive: _fki,
password: _password,
key: _key,
keyPassword: _keyPassword,
keyType: _keyType,
sudoPassword: _sudoPassword,
protocolAuth: _protocolAuth,
...rest
} = input;
const next: PluginHostRecord = { ...record };
@@ -491,6 +538,9 @@ export function createFakeContext(
const pluginId = options.pluginId ?? "test-plugin";
const disposals: Array<() => void | Promise<void>> = [];
const emitted: Array<{ topic: string; payload: unknown }> = [];
const hostStatusProviders: PluginHostStatusProvider[] = [];
const activityProviders: PluginActivityProvider[] = [];
const hostLockProviders: PluginHostLockProvider[] = [];
const kv = new Map<string, unknown>();
const tables: PluginTableDefinition[] = [];
const syncEntities: SyncEntityRegistration[] = [];
@@ -508,6 +558,7 @@ export function createFakeContext(
}>();
const sshConnections: FakePluginContext["sshConnections"] = [];
const hostShares: FakePluginContext["hostShares"] = [];
const hostShareSets: FakePluginContext["hostShareSets"] = [];
const desktopWindows: FakePluginContext["desktopWindows"] = [];
const externalClientLaunches: FakePluginContext["externalClientLaunches"] =
[];
@@ -531,6 +582,7 @@ export function createFakeContext(
};
const secretStore = new Map<string, string>();
const audits: FakePluginContext["audits"] = [];
const auditSubscribers = new Set<(entry: PluginAuditEntry) => void>();
const activities: FakePluginContext["activities"] = [];
const trackedSessions: number[] = [];
const interactions: FakePluginContext["interactions"] = [];
@@ -579,6 +631,7 @@ export function createFakeContext(
revokedSessions: [],
loginFailures: new Map(),
userTokens: [],
policies: [],
};
const sshClient = options.sshClient ?? {};
let actor = options.actor;
@@ -665,7 +718,7 @@ export function createFakeContext(
files: {
// A fake path; a test that needs a real folder uses the OS temp dir
// itself, not this double.
dataDir: async () => `/tmp/${pluginId}`,
dataDir: async () => options.dataDir ?? `/tmp/${pluginId}`,
},
db: options.db
@@ -705,6 +758,12 @@ export function createFakeContext(
registerEntity: (entity) => {
syncEntities.push(entity);
},
provide: () => () => {},
get host() {
if (!options.syncHost)
throw new Error("Pass syncHost to use ctx.sync.host");
return options.syncHost as PluginSyncHost;
},
},
registry: {
@@ -902,6 +961,27 @@ export function createFakeContext(
disposables,
hosts: {
importMany: async (rows) => {
const created = rows.map(() => nextHostId++);
return {
success: created.length,
updated: 0,
skipped: 0,
failed: 0,
errors: [],
createdHostIds: created,
};
},
importSshConfig: async () => ({
success: 0,
updated: 0,
skipped: 0,
failed: 0,
errors: [],
createdHostIds: [],
}),
exportMany: async () => ({ hosts: [] }),
registerImportNormalizer: () => () => {},
list: async () => [...hostsById.values()],
get: async (hostId) => hostsById.get(hostId) ?? null,
checkAccess: async (hostId): Promise<PluginHostAccess> => {
@@ -956,6 +1036,10 @@ export function createFakeContext(
hostShares.push({ hostId, targets, permissionLevel, durationHours });
return { hostId, shared: true };
},
setShares: async (hostId, grants) => {
hostShareSets.push({ hostId, grants: [...grants] });
return { hostId, shared: true };
},
listUsers: async () => options.shareableUsers ?? [],
listRoles: async () => options.shareableRoles ?? [],
trackSession: (hostId) => {
@@ -971,7 +1055,42 @@ export function createFakeContext(
recordActivity: async (hostId, type, hostName) => {
activities.push({ hostId, type, hostName });
},
probeTargets: async (filter) =>
(options.probeTargets ?? []).filter(
(target) => !filter?.hostIds || filter.hostIds.includes(target.id),
),
sessionActive: (hostId) =>
options.activeSessions?.includes(hostId) ?? false,
activity: {
provide: (provider) => {
activityProviders.push(provider);
return () => {
const at = activityProviders.indexOf(provider);
if (at >= 0) activityProviders.splice(at, 1);
};
},
},
folders: {
ensure: async () => {},
removeEmpty: async () => true,
},
locks: {
provide: (provider) => {
hostLockProviders.push(provider);
return () => {
const at = hostLockProviders.indexOf(provider);
if (at >= 0) hostLockProviders.splice(at, 1);
};
},
},
status: {
provide: (provider) => {
hostStatusProviders.push(provider);
return () => {
const at = hostStatusProviders.indexOf(provider);
if (at >= 0) hostStatusProviders.splice(at, 1);
};
},
get: async (hostId) => hostStatuses.get(hostId) ?? null,
check: async (hostId) => hostStatuses.get(hostId) ?? null,
reportLogin: (hostId, outcome) => {
@@ -996,12 +1115,46 @@ export function createFakeContext(
},
audit: {
subscribe: (handler) => {
auditSubscribers.add(handler);
return () => {
auditSubscribers.delete(handler);
};
},
list: async (listOptions) => {
const matching = filterAuditEntries(
options.auditEntries ?? [],
listOptions?.filters,
).reverse();
const offset = listOptions?.offset ?? 0;
return {
entries: matching.slice(offset, offset + (listOptions?.limit ?? 50)),
total: matching.length,
};
},
exportBatches: async function* (filters, batchSize) {
const matching = filterAuditEntries(
options.auditEntries ?? [],
filters,
);
const size = batchSize ?? 500;
for (let at = 0; at < matching.length; at += size) {
yield matching.slice(at, at + size);
}
},
record: async (entry) => {
audits.push({ ...entry });
},
},
network: {
tcpProbe: async (host, port) => options.reachable?.(host, port) ?? false,
},
ssh: {
transports: { provide: () => () => {} },
probe: async (hostId, probeOptions) =>
options.reachable?.(hostId, probeOptions?.port) ?? false,
connect: async (host, connectOptions) => {
sshConnections.push({ host, options: connectOptions });
return {
@@ -1013,6 +1166,16 @@ export function createFakeContext(
dispose: () => {},
};
},
exec: async (host) => {
sshConnections.push({ host });
return {
stdout: "",
stderr: "",
code: 0,
truncated: false,
timedOut: false,
};
},
withConnection: async (host, connectOptions, fn) => {
sshConnections.push({ host, pool: connectOptions.pool });
return fn(sshClient as never);
@@ -1153,6 +1316,22 @@ export function createFakeContext(
expiresAt: new Date(Date.now() + ttl * 1000).toISOString(),
};
},
policy: {
provide: (provider) => {
auth.policies.push(provider);
return () => {
const at = auth.policies.indexOf(provider);
if (at >= 0) auth.policies.splice(at, 1);
};
},
},
listSecondFactors: () => [
...auth.secondFactors.map((factor) => ({
id: factor.id,
pluginId,
})),
...(options.secondFactors ?? []),
],
},
users: {
@@ -1169,9 +1348,11 @@ export function createFakeContext(
return { success: true };
},
available: () => options.desktopAvailable ?? false,
local: options.desktopLocal ?? unavailableDesktopLocal(),
},
credentials: {
list: async () => [...(options.credentialSummaries ?? [])],
listSshKeys: async () => [
...(options.sshKeyCredentials ?? []),
...createdSshKeys.map((key) => ({
@@ -1195,6 +1376,13 @@ export function createFakeContext(
credentialReads.push({ hostId, protocol });
return options.protocolTargets?.[`${hostId}:${protocol}`] ?? null;
},
publicKey: async (credentialId) =>
(options.sshKeyCredentials ?? []).find((k) => k.id === credentialId)
?.publicKey ?? null,
resolveKeyRefs: async (key, passphrase) => ({
key,
passphrase: passphrase ?? undefined,
}),
registerSecretResolver: (scheme, resolve) => {
auth.secretResolvers.set(scheme, resolve);
disposals.push(() => {
@@ -1345,6 +1533,9 @@ export function createFakeContext(
return errors;
},
disposals,
hostStatusProviders,
activityProviders,
hostLockProviders,
emitted,
kv,
tables,
@@ -1355,6 +1546,7 @@ export function createFakeContext(
coreSettings,
sshConnections,
hostShares,
hostShareSets,
auth,
desktopWindows,
externalClientLaunches,
@@ -1368,6 +1560,9 @@ export function createFakeContext(
tls,
secretStore,
audits,
emitAudit: (entry) => {
for (const handler of auditSubscribers) handler(entry);
},
activities,
trackedSessions,
interactions,
@@ -1400,6 +1595,8 @@ export function createFakeContext(
export interface MockContextOptions {
pluginId?: string;
/** What ctx.sync.host answers, for a sync provider's tests. */
syncHost?: Partial<PluginSyncHost>;
manifest?: Partial<PluginManifest>;
/** Capabilities the plugin is granted. Anything else throws. */
capabilities?: string[];
@@ -1409,6 +1606,8 @@ export interface MockContextOptions {
settings?: Record<string, unknown>;
/** Seed for the core settings readCore can reach. */
coreSettings?: Record<string, string>;
/** What ctx.files.dataDir() answers. */
dataDir?: string;
/** Acting user returned by currentActor and used by asUser. */
actor?: string;
/** What ctx.ssh.connect and withConnection hand back as the client. */
@@ -1432,10 +1631,22 @@ export interface MockContextOptions {
protocolTargets?: Record<string, PluginProtocolTarget>;
/** What ctx.credentials.listSshKeys serves. */
sshKeyCredentials?: PluginSshKeyCredential[];
/** What ctx.credentials.list serves. */
credentialSummaries?: PluginCredentialSummary[];
/** What ctx.desktop.available() answers. */
desktopAvailable?: boolean;
/** What ctx.desktop.local is. See FakeContextOptions. */
desktopLocal?: PluginDesktopLocal;
/** See FakeContextOptions. */
auditEntries?: PluginAuditEntry[];
/** What ctx.hosts.status answers. See FakeContextOptions. */
hostStatuses?: Record<number, PluginHostStatusEntry>;
/** What ctx.hosts.probeTargets answers. */
probeTargets?: PluginHostProbeTarget[];
/** Host ids ctx.hosts.sessionActive answers true for. */
activeSessions?: number[];
/** What ctx.network.tcpProbe and ctx.ssh.probe answer. */
reachable?: (target: string | number, port?: number) => boolean;
/** The acting user's channels, what ctx.notify.channels answers. */
notificationChannels?: PluginNotificationChannel[];
/**
@@ -1460,6 +1671,8 @@ export interface MockContextOptions {
/** See FakeContextOptions. */
loginAttemptLimit?: number;
/** See FakeContextOptions. */
secondFactors?: FakeContextOptions["secondFactors"];
/** See FakeContextOptions. */
locales?: Record<string, string>;
}
@@ -1481,6 +1694,63 @@ export interface MockPluginContext extends FakePluginContext {
* (users:impersonate on issueUserToken), and users:read on ctx.users. Reading a plugin's own settings is deliberately
* ungated. As the SDK grows a member, add its gate here in the same shape.
*/
function filterAuditEntries(
entries: PluginAuditEntry[],
filters: PluginAuditFilters | undefined,
): PluginAuditEntry[] {
return entries.filter(
(entry) =>
(filters?.userId === undefined || entry.userId === filters.userId) &&
(filters?.action === undefined || entry.action === filters.action) &&
(filters?.resourceType === undefined ||
entry.resourceType === filters.resourceType) &&
(filters?.success === undefined || entry.success === filters.success) &&
(filters?.startDate === undefined ||
entry.timestamp >= filters.startDate) &&
(filters?.endDate === undefined || entry.timestamp <= filters.endDate),
);
}
function gateDesktopLocal(
target: PluginDesktopLocal,
check: () => void,
): PluginDesktopLocal {
const wrap = (value: unknown): unknown => {
if (typeof value === "function") {
return (...args: unknown[]) => {
check();
return (value as (...a: unknown[]) => unknown)(...args);
};
}
if (value && typeof value === "object") {
return new Proxy(value as object, {
get: (obj, key) => wrap((obj as Record<string | symbol, unknown>)[key]),
});
}
return value;
};
return new Proxy(target, {
get: (obj, key) =>
key === "available"
? obj.available
: wrap((obj as unknown as Record<string | symbol, unknown>)[key]),
});
}
function unavailableDesktopLocal(): PluginDesktopLocal {
const fail = (): never => {
throw new Error("ctx.desktop.local needs the desktopLocal test option");
};
const deep = (): unknown =>
new Proxy(() => fail(), { get: () => deep(), apply: () => fail() });
return {
available: () => false,
info: fail,
fs: deep(),
pty: deep(),
} as unknown as PluginDesktopLocal;
}
export function createMockCtx(
options: MockContextOptions = {},
): MockPluginContext {
@@ -1489,9 +1759,11 @@ export function createMockCtx(
const base = createFakeContext({
pluginId: options.pluginId,
syncHost: options.syncHost,
actor: options.actor,
settings: options.settings,
coreSettings: options.coreSettings,
dataDir: options.dataDir,
sshClient: options.sshClient,
db: options.db,
router: options.router,
@@ -1503,8 +1775,14 @@ export function createMockCtx(
services: options.services,
protocolTargets: options.protocolTargets,
sshKeyCredentials: options.sshKeyCredentials,
credentialSummaries: options.credentialSummaries,
desktopAvailable: options.desktopAvailable,
desktopLocal: options.desktopLocal,
auditEntries: options.auditEntries,
hostStatuses: options.hostStatuses,
probeTargets: options.probeTargets,
activeSessions: options.activeSessions,
reachable: options.reachable,
notificationChannels: options.notificationChannels,
fetch: options.fetch,
process: options.process,
@@ -1515,6 +1793,7 @@ export function createMockCtx(
baseUrl: options.baseUrl,
linkedUsers: options.linkedUsers,
loginAttemptLimit: options.loginAttemptLimit,
secondFactors: options.secondFactors,
locales: options.locales,
manifest: {
capabilities: options.capabilities ?? [],
@@ -1650,6 +1929,23 @@ export function createMockCtx(
},
hosts: {
importMany: async (rows, importOptions) => {
require("hosts:write");
if (importOptions?.credentials?.length) require("credentials:write");
return ctx.hosts.importMany(rows, importOptions);
},
importSshConfig: async (content, importOptions) => {
require("hosts:write");
return ctx.hosts.importSshConfig(content, importOptions);
},
exportMany: async (exportOptions) => {
require("hosts:export");
return ctx.hosts.exportMany(exportOptions);
},
registerImportNormalizer: (normalizer) => {
require("hosts:write");
return ctx.hosts.registerImportNormalizer(normalizer);
},
list: async () => {
require("hosts:read");
return ctx.hosts.list();
@@ -1682,6 +1978,10 @@ export function createMockCtx(
require("hosts:write");
return ctx.hosts.share(hostId, targets, permissionLevel, durationHours);
},
setShares: async (hostId, grants) => {
require("hosts:write");
return ctx.hosts.setShares(hostId, grants);
},
listUsers: async () => {
require("hosts:write");
return ctx.hosts.listUsers();
@@ -1698,7 +1998,42 @@ export function createMockCtx(
require("hosts:read");
return ctx.hosts.recordActivity(hostId, type, hostName);
},
probeTargets: async (filter) => {
require("hosts:read");
return ctx.hosts.probeTargets(filter);
},
sessionActive: (hostId) => {
require("hosts:read");
return ctx.hosts.sessionActive(hostId);
},
activity: {
provide: (provider) => {
require("hosts:activity-provide");
return ctx.hosts.activity.provide(provider);
},
},
folders: {
ensure: async (name, style) => {
require("hosts:write");
return ctx.hosts.folders.ensure(name, style);
},
removeEmpty: async (name) => {
require("hosts:write");
return ctx.hosts.folders.removeEmpty(name);
},
},
locks: {
provide: (provider) => {
require("hosts:lock");
return ctx.hosts.locks.provide(provider);
},
},
status: {
provide: (provider) => {
require("hosts:read");
require("hosts:status-provide");
return ctx.hosts.status.provide(provider);
},
get: async (hostId) => {
require("hosts:read");
return ctx.hosts.status.get(hostId);
@@ -1720,11 +2055,26 @@ export function createMockCtx(
ssh: {
...ctx.ssh,
transports: {
provide: (provider) => {
require("ssh:transport");
return ctx.ssh.transports.provide(provider);
},
},
probe: async (hostId, probeOptions) => {
require("ssh:connect");
return ctx.ssh.probe(hostId, probeOptions);
},
connect: async (host, connectOptions) => {
require("ssh:connect");
require("credentials:use");
return ctx.ssh.connect(host, connectOptions);
},
exec: async (host, command, execOptions) => {
require("ssh:connect");
require("credentials:use");
return ctx.ssh.exec(host, command, execOptions);
},
withConnection: async (host, connectOptions, fn) => {
require("ssh:connect");
require("credentials:use");
@@ -1811,6 +2161,13 @@ export function createMockCtx(
require("users:impersonate");
return ctx.auth.issueUserToken(userId, tokenOptions);
},
policy: {
provide: (provider) => {
require("auth:policy");
return ctx.auth.policy.provide(provider);
},
},
listSecondFactors: () => ctx.auth.listSecondFactors(),
},
users: {
@@ -1820,6 +2177,22 @@ export function createMockCtx(
},
},
audit: {
record: (entry) => ctx.audit.record(entry),
subscribe: (handler) => {
require("audit:read");
return ctx.audit.subscribe(handler);
},
list: async (listOptions) => {
require("audit:read");
return ctx.audit.list(listOptions);
},
exportBatches: (filters, batchSize) => {
require("audit:read");
return ctx.audit.exportBatches(filters, batchSize);
},
},
desktop: {
openIsolatedWindow: async (request) => {
require("desktop:window");
@@ -1830,9 +2203,16 @@ export function createMockCtx(
return ctx.desktop.launchExternalClient(request);
},
available: () => ctx.desktop.available(),
local: gateDesktopLocal(ctx.desktop.local, () =>
require("desktop:local"),
),
},
credentials: {
list: async () => {
require("credentials:use");
return ctx.credentials.list();
},
listSshKeys: async () => {
require("credentials:use");
return ctx.credentials.listSshKeys();
@@ -1851,6 +2231,14 @@ export function createMockCtx(
}
return ctx.credentials.resolveHostProtocol(hostId, protocol);
},
publicKey: async (credentialId) => {
require("credentials:use");
return ctx.credentials.publicKey(credentialId);
},
resolveKeyRefs: async (key, passphrase) => {
require("credentials:read");
return ctx.credentials.resolveKeyRefs(key, passphrase);
},
registerSecretResolver: (scheme, resolve) => {
require("auth:provide");
ctx.credentials.registerSecretResolver(scheme, resolve);
@@ -1877,6 +2265,13 @@ export function createMockCtx(
return ctx.fetch(url, init);
},
network: {
tcpProbe: async (host, port, probeOptions) => {
require("network:probe");
return ctx.network.tcpProbe(host, port, probeOptions);
},
},
process: {
run: async (file, args, runOptions) => {
require("process:spawn");