@termix-ssh/plugin-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/cli/commands/build.mjs +246 -0
- package/cli/commands/migrations.mjs +248 -0
- package/cli/commands/pack.mjs +42 -0
- package/cli/commands/sign.mjs +83 -0
- package/cli/commands/test.mjs +33 -0
- package/cli/commands/validate.mjs +215 -0
- package/cli/index.mjs +65 -0
- package/cli/lib/changelog.mjs +126 -0
- package/cli/lib/externals.mjs +46 -0
- package/cli/lib/plugin-dir.mjs +29 -0
- package/cli/lib/signing.mjs +86 -0
- package/cli/lib/static-url-imports.mjs +54 -0
- package/cli/lib/tmxplug.mjs +106 -0
- package/cli/patches/guacamole-common-js-flush.cjs +76 -0
- package/cli/patches/xterm-android-ime.cjs +130 -0
- package/client.d.ts +13 -0
- package/dist/backend.d.ts +1777 -0
- package/dist/backend.js +84 -0
- package/dist/capabilities.d.ts +38 -0
- package/dist/capabilities.js +71 -0
- package/dist/db.d.ts +112 -0
- package/dist/db.js +189 -0
- package/dist/ddl.d.ts +76 -0
- package/dist/ddl.js +678 -0
- package/dist/desktop.d.ts +346 -0
- package/dist/desktop.js +5 -0
- package/dist/frontend.d.ts +1206 -0
- package/dist/frontend.js +388 -0
- package/dist/host/chunk-F3SDZDDG.js +6017 -0
- package/dist/host/chunk-XGWE7BTJ.js +90 -0
- package/dist/host/chunk-XH5NXL6Y.js +56314 -0
- package/dist/host/chunk-YC2MAKSP.js +14 -0
- package/dist/host/linked-server-FZGWQW32.js +12 -0
- package/dist/host/rbac-api-6KDMBVDZ.js +52 -0
- package/dist/host/testing-host.js +414 -0
- package/dist/host/theme.css +91 -0
- package/dist/host/types/src/types/auth-protocols.d.ts +10 -0
- package/dist/host/types/src/types/connection-log.d.ts +13 -0
- package/dist/host/types/src/types/credential-sidebar-preferences.d.ts +38 -0
- package/dist/host/types/src/types/host-defaults.d.ts +60 -0
- package/dist/host/types/src/types/host-sidebar-preferences.d.ts +55 -0
- package/dist/host/types/src/types/index.d.ts +298 -0
- package/dist/host/types/src/types/keybindings.d.ts +27 -0
- package/dist/host/types/src/types/ui-preferences.d.ts +102 -0
- package/dist/host/types/src/types/ui-types.d.ts +260 -0
- package/dist/host/types/src/ui/api/admin-user-data-api.d.ts +11 -0
- package/dist/host/types/src/ui/api/auth-methods-api.d.ts +63 -0
- package/dist/host/types/src/ui/api/credential-sidebar-preferences-api.d.ts +3 -0
- package/dist/host/types/src/ui/api/credentials-api.d.ts +56 -0
- package/dist/host/types/src/ui/api/dashboard-api.d.ts +23 -0
- package/dist/host/types/src/ui/api/external-account-api.d.ts +8 -0
- package/dist/host/types/src/ui/api/host-sidebar-preferences-api.d.ts +3 -0
- package/dist/host/types/src/ui/api/host-status-api.d.ts +11 -0
- package/dist/host/types/src/ui/api/open-tabs-api.d.ts +74 -0
- package/dist/host/types/src/ui/api/plugins-api.d.ts +128 -0
- package/dist/host/types/src/ui/api/rbac-api.d.ts +167 -0
- package/dist/host/types/src/ui/api/settings-api.d.ts +22 -0
- package/dist/host/types/src/ui/api/ssh-host-management-api.d.ts +65 -0
- package/dist/host/types/src/ui/api/sync-api.d.ts +93 -0
- package/dist/host/types/src/ui/api/system-status-api.d.ts +49 -0
- package/dist/host/types/src/ui/api/ui-preferences-api.d.ts +8 -0
- package/dist/host/types/src/ui/api/user-management-api.d.ts +80 -0
- package/dist/host/types/src/ui/auth/Auth.d.ts +12 -0
- package/dist/host/types/src/ui/auth/auth-bits.d.ts +11 -0
- package/dist/host/types/src/ui/auth/silent-signin.d.ts +2 -0
- package/dist/host/types/src/ui/components/FullScreenAppWrapper.d.ts +9 -0
- package/dist/host/types/src/ui/components/alert-dialog.d.ts +14 -0
- package/dist/host/types/src/ui/components/alert.d.ts +9 -0
- package/dist/host/types/src/ui/components/badge.d.ts +9 -0
- package/dist/host/types/src/ui/components/button.d.ts +13 -0
- package/dist/host/types/src/ui/components/card-grid/CardGridCanvas.d.ts +22 -0
- package/dist/host/types/src/ui/components/card-grid/layout-utils.d.ts +26 -0
- package/dist/host/types/src/ui/components/card-grid/types.d.ts +29 -0
- package/dist/host/types/src/ui/components/card-masonry.d.ts +16 -0
- package/dist/host/types/src/ui/components/card.d.ts +11 -0
- package/dist/host/types/src/ui/components/charts/BarSeries.d.ts +16 -0
- package/dist/host/types/src/ui/components/charts/LineChart.d.ts +17 -0
- package/dist/host/types/src/ui/components/charts/RadialGauge.d.ts +18 -0
- package/dist/host/types/src/ui/components/charts/Sparkline.d.ts +22 -0
- package/dist/host/types/src/ui/components/charts/StatRow.d.ts +15 -0
- package/dist/host/types/src/ui/components/charts/geometry.d.ts +20 -0
- package/dist/host/types/src/ui/components/charts/index.d.ts +4 -0
- package/dist/host/types/src/ui/components/checkbox.d.ts +4 -0
- package/dist/host/types/src/ui/components/connection/ConnectionGate.d.ts +56 -0
- package/dist/host/types/src/ui/components/connection/ConnectionLogPanel.d.ts +9 -0
- package/dist/host/types/src/ui/components/connection/ConnectionScreen.d.ts +34 -0
- package/dist/host/types/src/ui/components/connection/connection-status.d.ts +1 -0
- package/dist/host/types/src/ui/components/data-view.d.ts +32 -0
- package/dist/host/types/src/ui/components/dialog.d.ts +17 -0
- package/dist/host/types/src/ui/components/dropdown-menu.d.ts +29 -0
- package/dist/host/types/src/ui/components/empty-state.d.ts +19 -0
- package/dist/host/types/src/ui/components/form-fields.d.ts +115 -0
- package/dist/host/types/src/ui/components/input.d.ts +3 -0
- package/dist/host/types/src/ui/components/kbd.d.ts +12 -0
- package/dist/host/types/src/ui/components/label.d.ts +4 -0
- package/dist/host/types/src/ui/components/manager-card.d.ts +25 -0
- package/dist/host/types/src/ui/components/meter.d.ts +14 -0
- package/dist/host/types/src/ui/components/metric-card.d.ts +18 -0
- package/dist/host/types/src/ui/components/panel-layout.d.ts +83 -0
- package/dist/host/types/src/ui/components/password-input.d.ts +4 -0
- package/dist/host/types/src/ui/components/popover.d.ts +7 -0
- package/dist/host/types/src/ui/components/scroll-area.d.ts +5 -0
- package/dist/host/types/src/ui/components/section-card.d.ts +29 -0
- package/dist/host/types/src/ui/components/select.d.ts +15 -0
- package/dist/host/types/src/ui/components/select2.d.ts +8 -0
- package/dist/host/types/src/ui/components/separator.d.ts +4 -0
- package/dist/host/types/src/ui/components/sheet.d.ts +14 -0
- package/dist/host/types/src/ui/components/sidebar.d.ts +69 -0
- package/dist/host/types/src/ui/components/skeleton.d.ts +2 -0
- package/dist/host/types/src/ui/components/slider.d.ts +4 -0
- package/dist/host/types/src/ui/components/sonner.d.ts +4 -0
- package/dist/host/types/src/ui/components/surface/prompt-styles.d.ts +4 -0
- package/dist/host/types/src/ui/components/surface/surface-scope.d.ts +79 -0
- package/dist/host/types/src/ui/components/switch.d.ts +4 -0
- package/dist/host/types/src/ui/components/tab-strip.d.ts +19 -0
- package/dist/host/types/src/ui/components/tabs.d.ts +7 -0
- package/dist/host/types/src/ui/components/textarea.d.ts +4 -0
- package/dist/host/types/src/ui/components/theme-provider.d.ts +14 -0
- package/dist/host/types/src/ui/components/tooltip.d.ts +7 -0
- package/dist/host/types/src/ui/contexts/BrandingContext.d.ts +15 -0
- package/dist/host/types/src/ui/contexts/UiPreferencesContext.d.ts +38 -0
- package/dist/host/types/src/ui/contexts/branding-document.d.ts +10 -0
- package/dist/host/types/src/ui/dashboard/dashboard-cards-registry.d.ts +30 -0
- package/dist/host/types/src/ui/hooks/use-action-slot.d.ts +10 -0
- package/dist/host/types/src/ui/hooks/use-adaptive-polling.d.ts +5 -0
- package/dist/host/types/src/ui/hooks/use-mobile.d.ts +1 -0
- package/dist/host/types/src/ui/hooks/use-permissions.d.ts +29 -0
- package/dist/host/types/src/ui/hooks/use-status-color-scheme.d.ts +12 -0
- package/dist/host/types/src/ui/hooks/use-sync-status.d.ts +4 -0
- package/dist/host/types/src/ui/hooks/useSshAuthProviders.d.ts +17 -0
- package/dist/host/types/src/ui/i18n/i18n.d.ts +52 -0
- package/dist/host/types/src/ui/lib/ServerStatusContext.d.ts +33 -0
- package/dist/host/types/src/ui/lib/adaptive-polling.d.ts +29 -0
- package/dist/host/types/src/ui/lib/adaptive-resource-budget.d.ts +39 -0
- package/dist/host/types/src/ui/lib/app-keyboard-shortcuts.d.ts +8 -0
- package/dist/host/types/src/ui/lib/base-path.d.ts +1 -0
- package/dist/host/types/src/ui/lib/clipboard-provider.d.ts +9 -0
- package/dist/host/types/src/ui/lib/clipboard.d.ts +20 -0
- package/dist/host/types/src/ui/lib/connection-origin.d.ts +47 -0
- package/dist/host/types/src/ui/lib/db-health-monitor.d.ts +34 -0
- package/dist/host/types/src/ui/lib/device-id.d.ts +1 -0
- package/dist/host/types/src/ui/lib/electron.d.ts +1 -0
- package/dist/host/types/src/ui/lib/embedded-server-status.d.ts +15 -0
- package/dist/host/types/src/ui/lib/error-message.d.ts +9 -0
- package/dist/host/types/src/ui/lib/frontend-logger.d.ts +67 -0
- package/dist/host/types/src/ui/lib/global-shortcut-handler.d.ts +3 -0
- package/dist/host/types/src/ui/lib/host-defaults-context.d.ts +48 -0
- package/dist/host/types/src/ui/lib/hosts-request-cache.d.ts +9 -0
- package/dist/host/types/src/ui/lib/keybinding-match.d.ts +3 -0
- package/dist/host/types/src/ui/lib/linked-server.d.ts +10 -0
- package/dist/host/types/src/ui/lib/local-adaptive-engine.d.ts +40 -0
- package/dist/host/types/src/ui/lib/permissions.d.ts +16 -0
- package/dist/host/types/src/ui/lib/plugin-i18n.d.ts +11 -0
- package/dist/host/types/src/ui/lib/plugin-icon.d.ts +11 -0
- package/dist/host/types/src/ui/lib/plugin-transport.d.ts +50 -0
- package/dist/host/types/src/ui/lib/registry.d.ts +29 -0
- package/dist/host/types/src/ui/lib/rem.d.ts +7 -0
- package/dist/host/types/src/ui/lib/remote-server-api.d.ts +11 -0
- package/dist/host/types/src/ui/lib/server-status-store.d.ts +37 -0
- package/dist/host/types/src/ui/lib/sync-events.d.ts +2 -0
- package/dist/host/types/src/ui/lib/sync-status.d.ts +6 -0
- package/dist/host/types/src/ui/lib/tab-jump-hotkey.d.ts +4 -0
- package/dist/host/types/src/ui/lib/theme.d.ts +21 -0
- package/dist/host/types/src/ui/lib/ttl-request-cache.d.ts +10 -0
- package/dist/host/types/src/ui/lib/usage-color.d.ts +5 -0
- package/dist/host/types/src/ui/lib/utils.d.ts +2 -0
- package/dist/host/types/src/ui/lib/visible-interval.d.ts +5 -0
- package/dist/host/types/src/ui/lib/widget-title.d.ts +8 -0
- package/dist/host/types/src/ui/lib/ws-auth.d.ts +1 -0
- package/dist/host/types/src/ui/main-axios.d.ts +170 -0
- package/dist/host/types/src/ui/plugin-host/PluginViewPlaceholder.d.ts +13 -0
- package/dist/host/types/src/ui/plugin-host/app.d.ts +18 -0
- package/dist/host/types/src/ui/plugin-host/auth-registry.d.ts +50 -0
- package/dist/host/types/src/ui/plugin-host/bridge.d.ts +26 -0
- package/dist/host/types/src/ui/plugin-host/component-registry.d.ts +24 -0
- package/dist/host/types/src/ui/plugin-host/desktop.d.ts +7 -0
- package/dist/host/types/src/ui/plugin-host/extension-registry.d.ts +16 -0
- package/dist/host/types/src/ui/plugin-host/host-scope.d.ts +26 -0
- package/dist/host/types/src/ui/plugin-host/loader.d.ts +52 -0
- package/dist/host/types/src/ui/plugin-host/plugin-store.d.ts +40 -0
- package/dist/host/types/src/ui/plugin-host/scope.d.ts +20 -0
- package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +99 -0
- package/dist/host/types/src/ui/plugin-host/shell-bridge.d.ts +67 -0
- package/dist/host/types/src/ui/plugin-host/view-ownership.d.ts +31 -0
- package/dist/host/types/src/ui/settings/HostPluginSections.d.ts +31 -0
- package/dist/host/types/src/ui/settings/SettingsFields.d.ts +22 -0
- package/dist/host/types/src/ui/settings/host-feature-tabs.d.ts +11 -0
- package/dist/host/types/src/ui/settings/settings-components.d.ts +32 -0
- package/dist/host/types/src/ui/settings/settings-fields-util.d.ts +11 -0
- package/dist/host/types/src/ui/shell/ActionSlot.d.ts +41 -0
- package/dist/host/types/src/ui/shell/TabContext.d.ts +30 -0
- package/dist/host/types/src/ui/shell/action-registry.d.ts +99 -0
- package/dist/host/types/src/ui/shell/keybinding-registry.d.ts +62 -0
- package/dist/host/types/src/ui/shell/palette-registry.d.ts +55 -0
- package/dist/host/types/src/ui/shell/panel-registry.d.ts +24 -0
- package/dist/host/types/src/ui/shell/tab-registry.d.ts +92 -0
- package/dist/host/types/src/ui/sidebar/HostManagerTabs.d.ts +65 -0
- package/dist/host/types/src/ui/sidebar/host-contributions.d.ts +72 -0
- package/dist/host/types/src/ui/sidebar/host-defaults/catalog.d.ts +17 -0
- package/dist/host/types/src/ui/sidebar/host-protocols.d.ts +49 -0
- package/dist/host/types/src/ui/sidebar/quick-connect-host.d.ts +21 -0
- package/dist/host/types/src/ui/sidebar/rail-items.d.ts +103 -0
- package/dist/host/types/src/ui/ssh/connection-log/ConnectionLogContext.d.ts +17 -0
- package/dist/host/types/src/ui/ssh/dialogs/BrowserSignInDialog.d.ts +18 -0
- package/dist/host/types/src/ui/ssh/dialogs/HostKeyVerificationDialog.d.ts +17 -0
- package/dist/host/types/src/ui/ssh/dialogs/PassphraseDialog.d.ts +15 -0
- package/dist/host/types/src/ui/ssh/dialogs/SSHAuthDialog.d.ts +20 -0
- package/dist/host/types/src/ui/ssh/dialogs/TOTPDialog.d.ts +13 -0
- package/dist/host/ui.d.ts +1 -0
- package/dist/host/ui.js +31754 -0
- package/dist/host/use-permissions-NULZL43J.js +18 -0
- package/dist/host-commands.d.ts +89 -0
- package/dist/host-commands.js +387 -0
- package/dist/manifest.d.ts +461 -0
- package/dist/manifest.js +1499 -0
- package/dist/settings.d.ts +43 -0
- package/dist/settings.js +168 -0
- package/dist/ssh-certs.d.ts +25 -0
- package/dist/ssh-certs.js +234 -0
- package/dist/ssh-options.d.ts +23 -0
- package/dist/ssh-options.js +1 -0
- package/dist/table-builder.d.ts +26 -0
- package/dist/table-builder.js +87 -0
- package/dist/testing/safe-ports.d.ts +10 -0
- package/dist/testing/safe-ports.js +59 -0
- package/dist/testing/setup.d.ts +7 -0
- package/dist/testing/setup.js +146 -0
- package/dist/testing.d.ts +537 -0
- package/dist/testing.js +1451 -0
- package/dist/vitest-preset.d.ts +54 -0
- package/dist/vitest-preset.js +96 -0
- package/package.json +143 -0
- package/schema/changelog.schema.json +67 -0
- package/schema/manifest.schema.json +1160 -0
- package/tsconfig.plugin.json +31 -0
|
@@ -0,0 +1,1777 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The backend contract: what a plugin's activate(ctx) receives.
|
|
3
|
+
*
|
|
4
|
+
* These are types only. Core builds the real object in
|
|
5
|
+
* src/backend/plugins/ctx.ts and injects it at activation, so the SDK stays a
|
|
6
|
+
* description of the contract rather than an implementation a plugin could
|
|
7
|
+
* reach around.
|
|
8
|
+
*
|
|
9
|
+
* ctx.ssh is the part of step B's host surface that A8 needed early, so plugin
|
|
10
|
+
* transports could leave core's internals behind.
|
|
11
|
+
*/
|
|
12
|
+
import type { HostSshOptions } from "./ssh-options.js";
|
|
13
|
+
export type { HostSshOptions } from "./ssh-options.js";
|
|
14
|
+
import type { PluginManifest, PluginSettingsField } from "./manifest.js";
|
|
15
|
+
import type { PluginTableDefinition } from "./db.js";
|
|
16
|
+
/**
|
|
17
|
+
* Thrown when a plugin calls a guarded member without the capability.
|
|
18
|
+
*
|
|
19
|
+
* Two things must be true: the manifest declares it, and it is granted. A
|
|
20
|
+
* grant for something the manifest never declared is ignored, so widening a
|
|
21
|
+
* plugin's reach always needs a new manifest the user can see.
|
|
22
|
+
*/
|
|
23
|
+
export declare class PluginCapabilityError extends Error {
|
|
24
|
+
readonly code = "EPLUGINCAP";
|
|
25
|
+
readonly pluginId: string;
|
|
26
|
+
readonly capability: string;
|
|
27
|
+
constructor(pluginId: string, capability: string);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Thrown by a login method or second factor to refuse a sign-in with a
|
|
31
|
+
* message the user can see. `code` is a stable id the login screen can map to
|
|
32
|
+
* its own text; `status` is the HTTP status for form methods.
|
|
33
|
+
*/
|
|
34
|
+
export declare class LoginMethodError extends Error {
|
|
35
|
+
readonly status: number;
|
|
36
|
+
readonly code?: string;
|
|
37
|
+
constructor(message: string, status?: number, code?: string);
|
|
38
|
+
}
|
|
39
|
+
export interface PluginLogger {
|
|
40
|
+
debug: (message: string) => void;
|
|
41
|
+
info: (message: string) => void;
|
|
42
|
+
warn: (message: string) => void;
|
|
43
|
+
error: (message: string, error?: Error) => void;
|
|
44
|
+
}
|
|
45
|
+
export interface PluginEvents {
|
|
46
|
+
/** Topics are namespaced to plugin.<id>.* unless the plugin holds events:core. */
|
|
47
|
+
emit: (topic: string, payload: unknown) => void;
|
|
48
|
+
on: (topic: string, listener: (payload: unknown) => void) => () => void;
|
|
49
|
+
}
|
|
50
|
+
/** Small key/value state owned by this plugin. Requires kv:own. */
|
|
51
|
+
export interface PluginKeyValue {
|
|
52
|
+
get: (key: string) => Promise<unknown>;
|
|
53
|
+
set: (key: string, value: unknown) => Promise<void>;
|
|
54
|
+
delete: (key: string) => Promise<boolean>;
|
|
55
|
+
list: () => Promise<string[]>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A folder on disk this plugin owns, for state too large for ctx.kv (session
|
|
59
|
+
* recordings, uploaded files). Requires files:own.
|
|
60
|
+
*/
|
|
61
|
+
export interface PluginFiles {
|
|
62
|
+
/**
|
|
63
|
+
* The plugin's own folder under DATA_DIR, created if it does not exist yet.
|
|
64
|
+
* Always the same path for this plugin; a plugin lays out its own
|
|
65
|
+
* subdirectories underneath it.
|
|
66
|
+
*/
|
|
67
|
+
dataDir: () => Promise<string>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The plugin's own tables.
|
|
71
|
+
*
|
|
72
|
+
* `define` registers a table definition and returns the Drizzle table object
|
|
73
|
+
* to query through `db`. Both require db:own.
|
|
74
|
+
*
|
|
75
|
+
* Scoped by name: every table carries the p_<id>_ prefix, and `refs` exposes
|
|
76
|
+
* users, ssh_data, roles and user_roles (behind db:core-refs) so a plugin can
|
|
77
|
+
* join against them. In-process code could reach around
|
|
78
|
+
* all of this. The capability, the prefix, lint and review are the contract,
|
|
79
|
+
* not a sandbox.
|
|
80
|
+
*/
|
|
81
|
+
export interface PluginDatabase {
|
|
82
|
+
/** Registers a definition and returns its queryable table object. */
|
|
83
|
+
define: <T = unknown>(definition: PluginTableDefinition) => Promise<T>;
|
|
84
|
+
/** Drizzle handle, scoped to this plugin's tables. */
|
|
85
|
+
client: <T = unknown>() => Promise<T>;
|
|
86
|
+
/**
|
|
87
|
+
* Core tables a plugin may join against: users, hosts, roles, userRoles.
|
|
88
|
+
* Needs db:core-refs. Treat them as read-only.
|
|
89
|
+
*/
|
|
90
|
+
refs: <T = unknown>() => Promise<T>;
|
|
91
|
+
/**
|
|
92
|
+
* Flushes writes to disk. Call it after every write: on SQLite the database
|
|
93
|
+
* lives in memory and is saved to its encrypted file only when asked, so a
|
|
94
|
+
* write without it can be lost on restart. A no-op on Postgres and MySQL.
|
|
95
|
+
*/
|
|
96
|
+
persist: (options?: {
|
|
97
|
+
/**
|
|
98
|
+
* Mark the database dirty and let the debounced save pick it up, rather
|
|
99
|
+
* than saving now. For frequent, low-value writes such as samples, where
|
|
100
|
+
* losing a couple of seconds on a crash is fine.
|
|
101
|
+
*/
|
|
102
|
+
lazy?: boolean;
|
|
103
|
+
}) => Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* The engine the server runs on. Table objects encode the same on all
|
|
106
|
+
* three, but MySQL has no RETURNING, so portable code reads a written row
|
|
107
|
+
* back by its key rather than relying on .returning().
|
|
108
|
+
*/
|
|
109
|
+
readonly dialect: "sqlite" | "postgres" | "mysql";
|
|
110
|
+
}
|
|
111
|
+
/** A row's shape on the wire, before it is written to a table. */
|
|
112
|
+
export type SyncRow = Record<string, unknown>;
|
|
113
|
+
export interface SyncEntityReference {
|
|
114
|
+
/**
|
|
115
|
+
* Where a local numeric id lives. A plain column ("credentialId"), or a
|
|
116
|
+
* path into a JSON column: "jumpHosts[].hostId" walks every item of the
|
|
117
|
+
* jumpHosts array, "layout.panes[].hostId" walks nested objects. A JSON
|
|
118
|
+
* column may be stored as a string or as parsed JSON.
|
|
119
|
+
*/
|
|
120
|
+
field: string;
|
|
121
|
+
/**
|
|
122
|
+
* Wire field holding the portable id, e.g. "credentialSyncId". Only for a
|
|
123
|
+
* plain column; a JSON path is rewritten in place.
|
|
124
|
+
*/
|
|
125
|
+
syncField?: string;
|
|
126
|
+
/** The entity the id points at. */
|
|
127
|
+
entityType: string;
|
|
128
|
+
/** How a JSON path stores the local id. Defaults to "number". */
|
|
129
|
+
idType?: "number" | "string";
|
|
130
|
+
}
|
|
131
|
+
export interface SyncEntityPermissions {
|
|
132
|
+
/** Checked before a pushed row that does not exist yet is written. */
|
|
133
|
+
create?: string;
|
|
134
|
+
/** Checked before a pushed change to an existing row is written. */
|
|
135
|
+
update?: string;
|
|
136
|
+
/** Checked before a pushed delete runs. */
|
|
137
|
+
delete?: string;
|
|
138
|
+
}
|
|
139
|
+
export interface SyncWriteEvent {
|
|
140
|
+
/** The local id of the row that was written. */
|
|
141
|
+
id: number | null;
|
|
142
|
+
userId: string;
|
|
143
|
+
/** The row as it arrived, before references were resolved. */
|
|
144
|
+
wire: SyncRow;
|
|
145
|
+
created: boolean;
|
|
146
|
+
/** Another entity's syncId to its local id here, for ids serialize put on the wire. */
|
|
147
|
+
resolveId?: (entityType: string, syncId: string) => Promise<number | null>;
|
|
148
|
+
}
|
|
149
|
+
export interface SyncEntityRegistration {
|
|
150
|
+
/** Stable wire name. Never change it: records on both sides match on it. */
|
|
151
|
+
type: string;
|
|
152
|
+
/** The table, as returned by ctx.db.define. */
|
|
153
|
+
table: unknown;
|
|
154
|
+
/** Column holding the owning user id. Defaults to "userId". */
|
|
155
|
+
userColumn?: string;
|
|
156
|
+
/** Fields stored encrypted with the owner's data key. Plaintext on the wire. */
|
|
157
|
+
encryptedFields?: readonly string[];
|
|
158
|
+
/** Local-id to sync-id translations applied on the way out and back. */
|
|
159
|
+
references?: readonly SyncEntityReference[];
|
|
160
|
+
/** Lower sorts first. Reference targets must sort before their referrers. */
|
|
161
|
+
order?: number;
|
|
162
|
+
/**
|
|
163
|
+
* Fields that stay on this side: never sent, never overwritten by an
|
|
164
|
+
* inbound row, and ignored when deciding whether a row changed.
|
|
165
|
+
*/
|
|
166
|
+
readOnlyFields?: readonly string[];
|
|
167
|
+
/** One row per user rather than many, keyed on the owner. */
|
|
168
|
+
singleton?: boolean;
|
|
169
|
+
/**
|
|
170
|
+
* Names core uses when its own rows point at this entity without knowing
|
|
171
|
+
* which plugin provides it. Nothing in core uses one today.
|
|
172
|
+
*/
|
|
173
|
+
answersTo?: readonly string[];
|
|
174
|
+
/**
|
|
175
|
+
* Rows this returns false for are left out of sync in both directions, for
|
|
176
|
+
* state that belongs to one install (a per-device "last session").
|
|
177
|
+
*/
|
|
178
|
+
shouldSync?: (row: SyncRow) => boolean;
|
|
179
|
+
/** RBAC permissions a pushed change needs on the server. */
|
|
180
|
+
permissions?: SyncEntityPermissions;
|
|
181
|
+
/**
|
|
182
|
+
* Runs after `references` on the way out, for anything they cannot
|
|
183
|
+
* express. Gets the row with references already translated.
|
|
184
|
+
*/
|
|
185
|
+
serialize?: (row: SyncRow, resolveSyncId: (entityType: string, id: number) => Promise<string | null>) => Promise<SyncRow>;
|
|
186
|
+
/** Runs after `references` on the way in. */
|
|
187
|
+
deserialize?: (row: SyncRow, resolveId: (entityType: string, syncId: string) => Promise<number | null>) => Promise<SyncRow>;
|
|
188
|
+
/** Runs after a synced row was written on this side. */
|
|
189
|
+
afterWrite?: (event: SyncWriteEvent) => Promise<void>;
|
|
190
|
+
/**
|
|
191
|
+
* Replaces the plain delete of a synced row, for rows with cleanup of their
|
|
192
|
+
* own. Gets the stored row.
|
|
193
|
+
*/
|
|
194
|
+
remove?: (row: SyncRow, userId: string) => Promise<void>;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Adds an entity to sync between a linked desktop and its server.
|
|
198
|
+
*
|
|
199
|
+
* Deletes need nothing extra: core notices a registered row is gone and
|
|
200
|
+
* sends the delete itself.
|
|
201
|
+
*/
|
|
202
|
+
export interface PluginSync {
|
|
203
|
+
registerEntity: (entity: SyncEntityRegistration) => void;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* A plain key/value hand-off between plugins. `provide` and `revoke` only take
|
|
207
|
+
* keys under the plugin's own id (`<pluginId>.something`); anyone may
|
|
208
|
+
* `consume`.
|
|
209
|
+
*/
|
|
210
|
+
export interface PluginRegistry {
|
|
211
|
+
provide: <T>(key: string, value: T) => void;
|
|
212
|
+
consume: <T>(key: string) => T | undefined;
|
|
213
|
+
revoke: (key: string, value?: unknown) => boolean;
|
|
214
|
+
}
|
|
215
|
+
export interface PluginServices {
|
|
216
|
+
/**
|
|
217
|
+
* `name` registers one of several providers of the same service, for a
|
|
218
|
+
* service keyed by something the caller picks (sessions.live by session
|
|
219
|
+
* type). It must be listed in the manifest's `provides[].names`.
|
|
220
|
+
*/
|
|
221
|
+
provide: <T extends object>(service: string, implementation: T, options?: {
|
|
222
|
+
name?: string;
|
|
223
|
+
}) => void;
|
|
224
|
+
/**
|
|
225
|
+
* The service must be listed in the manifest's `requires` (or be one this
|
|
226
|
+
* plugin provides). `provider` picks a named provider; omitted, the unnamed
|
|
227
|
+
* one. A `userId` other than the acting user needs users:impersonate.
|
|
228
|
+
*/
|
|
229
|
+
get: <T extends object>(service: string, options?: {
|
|
230
|
+
userId?: string;
|
|
231
|
+
provider?: string;
|
|
232
|
+
}) => T;
|
|
233
|
+
/** Names of the providers running now ("" for an unnamed one). */
|
|
234
|
+
providers: (service: string) => string[];
|
|
235
|
+
}
|
|
236
|
+
export interface PluginSecrets {
|
|
237
|
+
/**
|
|
238
|
+
* The acting user's own copy of a secret this plugin stored, decrypted, or
|
|
239
|
+
* null when nothing is stored. Needs secrets:own and an actor. Not audited:
|
|
240
|
+
* a plugin reads its keys on every request that uses them.
|
|
241
|
+
*/
|
|
242
|
+
get: (key: string) => Promise<string | null>;
|
|
243
|
+
/**
|
|
244
|
+
* Stores a secret for the acting user, encrypted at rest. Null clears it.
|
|
245
|
+
* Needs secrets:own and an actor. Audited without the value.
|
|
246
|
+
*/
|
|
247
|
+
set: (key: string, value: string | null) => Promise<void>;
|
|
248
|
+
/** Removes the acting user's secret. Needs secrets:own. Audited. */
|
|
249
|
+
delete: (key: string) => Promise<void>;
|
|
250
|
+
/**
|
|
251
|
+
* Encrypts a value with the installation key, for a plugin that keeps a
|
|
252
|
+
* secret in its own table and has to read it without an acting user (a
|
|
253
|
+
* second factor checked during login). Needs secrets:own. Not audited.
|
|
254
|
+
*/
|
|
255
|
+
seal: (value: string) => Promise<string>;
|
|
256
|
+
/** Reverses seal(). Null when the value cannot be decrypted. */
|
|
257
|
+
unseal: (sealed: string) => Promise<string | null>;
|
|
258
|
+
offer: (key: string, resolve: (userId: string) => Promise<string | null> | string | null) => void;
|
|
259
|
+
withdraw: (key: string) => boolean;
|
|
260
|
+
getShared: (pluginId: string, key: string, options?: {
|
|
261
|
+
userId?: string;
|
|
262
|
+
}) => Promise<string | null>;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Express types, structurally. The SDK does not depend on express: a plugin
|
|
266
|
+
* brings its own copy, and a nominal type from core's would not match it.
|
|
267
|
+
*/
|
|
268
|
+
export interface PluginRequestLike {
|
|
269
|
+
method: string;
|
|
270
|
+
url: string;
|
|
271
|
+
path: string;
|
|
272
|
+
headers: Record<string, unknown>;
|
|
273
|
+
[key: string]: unknown;
|
|
274
|
+
}
|
|
275
|
+
export interface PluginResponseLike {
|
|
276
|
+
status: (code: number) => PluginResponseLike;
|
|
277
|
+
json: (body: unknown) => unknown;
|
|
278
|
+
[key: string]: unknown;
|
|
279
|
+
}
|
|
280
|
+
export type PluginNextFunction = (error?: unknown) => void;
|
|
281
|
+
export type PluginMiddleware = (req: PluginRequestLike, res: PluginResponseLike, next: PluginNextFunction) => void;
|
|
282
|
+
export interface PluginRouterOptions {
|
|
283
|
+
/**
|
|
284
|
+
* Paths served without authentication, relative to the plugin's mount point
|
|
285
|
+
* and matched exactly ("/callback", not "/plugin-api/<id>/callback"). A
|
|
286
|
+
* ":name" segment matches one segment, and a trailing "/*" matches any
|
|
287
|
+
* number of further segments, for a proxied page ("/chooser/:id/*").
|
|
288
|
+
*
|
|
289
|
+
* For the handful of routes an unauthenticated third party has to reach: an
|
|
290
|
+
* OIDC callback, an inbound webhook. Every entry is audited when the router
|
|
291
|
+
* is registered and every request that uses one is logged, because "this
|
|
292
|
+
* plugin opened a hole in auth" is exactly the thing an operator should be
|
|
293
|
+
* able to find later.
|
|
294
|
+
*/
|
|
295
|
+
public?: readonly string[];
|
|
296
|
+
/** Body size limit for JSON and urlencoded bodies. Defaults to "1mb". */
|
|
297
|
+
bodyLimit?: string;
|
|
298
|
+
/**
|
|
299
|
+
* Skips the built-in body parsers entirely, for a router that parses its own
|
|
300
|
+
* (multer, a raw body for signature checking, a proxied stream).
|
|
301
|
+
*/
|
|
302
|
+
rawBody?: boolean;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* HTTP routes, served at /plugin-api/<id>/ by the main backend server.
|
|
306
|
+
*
|
|
307
|
+
* No plugin gets its own port or nginx block. Core runs auth, the actor, the
|
|
308
|
+
* enabled check, body limits and an error wrapper in front of every route, so
|
|
309
|
+
* a plugin's router only has to handle its own paths.
|
|
310
|
+
*/
|
|
311
|
+
export interface PluginHttp {
|
|
312
|
+
/**
|
|
313
|
+
* Returns an Express Router mounted at /plugin-api/<id>/. Call it once in
|
|
314
|
+
* activate and register routes on the result; it is unmounted automatically
|
|
315
|
+
* on deactivate.
|
|
316
|
+
*/
|
|
317
|
+
router: <T = unknown>(options?: PluginRouterOptions) => T;
|
|
318
|
+
/**
|
|
319
|
+
* The public base URL a request came in on: origin plus the install's base
|
|
320
|
+
* path, no trailing slash. Honours forwarded headers from a trusted proxy,
|
|
321
|
+
* BASE_PATH and EXTERNAL_FORCE_HTTPS. For building absolute callback URLs.
|
|
322
|
+
*/
|
|
323
|
+
baseUrl: (req: unknown) => string;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Core RBAC, as a plugin sees it.
|
|
327
|
+
*
|
|
328
|
+
* Every method takes either a short name this plugin declares
|
|
329
|
+
* ("services.use"), which is prefixed with the plugin id, or a full id
|
|
330
|
+
* belonging to another plugin or a core group, which is used as given. That is
|
|
331
|
+
* what makes a cross-plugin check expressible without letting a plugin gate its
|
|
332
|
+
* own routes on someone else's authority.
|
|
333
|
+
*/
|
|
334
|
+
export interface PluginRbac {
|
|
335
|
+
/** Whether the user this call is running as holds `permission`. */
|
|
336
|
+
has: (permission: string) => Promise<boolean>;
|
|
337
|
+
/** Whether `userId` holds `permission`. */
|
|
338
|
+
hasFor: (userId: string, permission: string) => Promise<boolean>;
|
|
339
|
+
/**
|
|
340
|
+
* Middleware that rejects a request whose user lacks `permission`.
|
|
341
|
+
*
|
|
342
|
+
* A plugin may only require a permission it declares in
|
|
343
|
+
* contributes.permissions, so a route cannot gate on admin.users.manage and
|
|
344
|
+
* borrow someone else's authority.
|
|
345
|
+
*/
|
|
346
|
+
require: (permission: string) => PluginMiddleware;
|
|
347
|
+
}
|
|
348
|
+
export interface PluginWebSocketConnection {
|
|
349
|
+
/** The authenticated user for this socket. */
|
|
350
|
+
readonly userId: string;
|
|
351
|
+
/** The upgrade request, for query parameters and headers. */
|
|
352
|
+
readonly request: unknown;
|
|
353
|
+
/** The ws WebSocket. Binary frames and backpressure work as usual. */
|
|
354
|
+
readonly socket: unknown;
|
|
355
|
+
/** The client address, behind a trusted proxy when one is configured. */
|
|
356
|
+
readonly clientIp: string;
|
|
357
|
+
/** The public origin the request came in on, for browser redirects. */
|
|
358
|
+
readonly requestOrigin: string;
|
|
359
|
+
/**
|
|
360
|
+
* Whether the user's data key is still unlocked. False for a guest and
|
|
361
|
+
* once a session expires, so a long-lived socket can refuse work it can no
|
|
362
|
+
* longer decrypt for.
|
|
363
|
+
*/
|
|
364
|
+
isDataUnlocked: () => boolean;
|
|
365
|
+
}
|
|
366
|
+
export type PluginWebSocketHandler = (connection: PluginWebSocketConnection) => void | Promise<void>;
|
|
367
|
+
export interface PluginWebSocketOptions {
|
|
368
|
+
/**
|
|
369
|
+
* Serves the upgrade without core authentication. The handler gets an empty
|
|
370
|
+
* userId and must authenticate the socket itself, which is what a share-link
|
|
371
|
+
* guest or a token-in-query protocol needs.
|
|
372
|
+
*/
|
|
373
|
+
public?: boolean;
|
|
374
|
+
/**
|
|
375
|
+
* With `public`, still resolves a token when the client sent one, so one
|
|
376
|
+
* route can serve signed-in users and a guest arriving with a token of its
|
|
377
|
+
* own. The handler gets the user id, or an empty one for a guest.
|
|
378
|
+
*/
|
|
379
|
+
optionalAuth?: boolean;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* WebSocket routes, served at /plugin-ws/<id>/<path> through the main server's
|
|
383
|
+
* upgrade event. Every socket a plugin opened is closed on deactivate.
|
|
384
|
+
*/
|
|
385
|
+
export interface PluginWebSockets {
|
|
386
|
+
route: (path: string, handler: PluginWebSocketHandler, options?: PluginWebSocketOptions) => void;
|
|
387
|
+
/**
|
|
388
|
+
* Hands the raw upgrade to the plugin, for a library that insists on owning
|
|
389
|
+
* its own WebSocket server (guacamole-lite attaches this way). Auth and the
|
|
390
|
+
* actor still run first.
|
|
391
|
+
*/
|
|
392
|
+
upgrade: (path: string, handler: (request: unknown, socket: unknown, head: unknown, userId: string) => void, options?: PluginWebSocketOptions) => void;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Anything the runtime should undo when the plugin deactivates. Everything
|
|
396
|
+
* registered through ctx is tracked automatically; this is for resources a
|
|
397
|
+
* plugin creates itself, such as a server or an interval.
|
|
398
|
+
*/
|
|
399
|
+
export interface PluginDisposables {
|
|
400
|
+
add: (dispose: () => void | Promise<void>) => void;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* The plugin's own settings, as declared in contributes.settings.
|
|
404
|
+
*
|
|
405
|
+
* Reading and writing a plugin's own settings needs no capability: the
|
|
406
|
+
* manifest already says which fields exist, and a plugin that could not read
|
|
407
|
+
* its own configuration would be useless. Only readCore, which reaches outside
|
|
408
|
+
* the plugin's namespace, is gated on settings:read-core.
|
|
409
|
+
*
|
|
410
|
+
* Values are validated against the declared field on every write, so a key the
|
|
411
|
+
* manifest never declared is rejected rather than stored. Secret fields are
|
|
412
|
+
* encrypted at rest and returned decrypted here; the HTTP surface never sends
|
|
413
|
+
* them to a browser.
|
|
414
|
+
*/
|
|
415
|
+
export interface PluginSettings {
|
|
416
|
+
/** Install-wide admin settings. */
|
|
417
|
+
get: <T = unknown>(key: string) => Promise<T | undefined>;
|
|
418
|
+
set: (key: string, value: unknown) => Promise<void>;
|
|
419
|
+
/** Per-user settings. */
|
|
420
|
+
getUser: <T = unknown>(userId: string, key: string) => Promise<T | undefined>;
|
|
421
|
+
setUser: (userId: string, key: string, value: unknown) => Promise<void>;
|
|
422
|
+
/**
|
|
423
|
+
* Per-host settings. A host follows its host defaults (server, user,
|
|
424
|
+
* folder) for every key it has not set itself, and the value read here is
|
|
425
|
+
* already the resolved one.
|
|
426
|
+
*/
|
|
427
|
+
getHost: <T = unknown>(hostId: number | string, key: string) => Promise<T | undefined>;
|
|
428
|
+
/**
|
|
429
|
+
* The value as one user sees it. Only a `personal` field differs from
|
|
430
|
+
* getHost: on a host shared with that user and following its defaults, it
|
|
431
|
+
* resolves against the user's own defaults rather than the owner's.
|
|
432
|
+
*/
|
|
433
|
+
getHostFor: <T = unknown>(hostId: number | string, userId: string, key: string) => Promise<T | undefined>;
|
|
434
|
+
/**
|
|
435
|
+
* What a user's hosts get for a host field when nothing more specific
|
|
436
|
+
* sets it: their own defaults, then the server's, then the manifest
|
|
437
|
+
* default. For something with no host, such as a quick connect.
|
|
438
|
+
*/
|
|
439
|
+
getHostDefault: <T = unknown>(userId: string, key: string) => Promise<T | undefined>;
|
|
440
|
+
/**
|
|
441
|
+
* Writes a host value, which makes it the host's own rather than
|
|
442
|
+
* following its defaults. `{ inherit: true }` hands the key back to the
|
|
443
|
+
* defaults instead and ignores `value`.
|
|
444
|
+
*/
|
|
445
|
+
setHost: (hostId: number | string, key: string, value: unknown, options?: {
|
|
446
|
+
inherit?: boolean;
|
|
447
|
+
}) => Promise<void>;
|
|
448
|
+
/**
|
|
449
|
+
* Every host that saved a value for one of this plugin's host fields, across
|
|
450
|
+
* all users, with the host's owner. For a background job that must know
|
|
451
|
+
* which hosts to act on, and as whom, before any request is in flight.
|
|
452
|
+
* Requires hosts:read. Secret fields cannot be listed.
|
|
453
|
+
*/
|
|
454
|
+
listHostValues: <T = unknown>(key: string) => Promise<Array<{
|
|
455
|
+
hostId: number;
|
|
456
|
+
userId: string;
|
|
457
|
+
value: T;
|
|
458
|
+
}>>;
|
|
459
|
+
/** Every value in one scope, with declared defaults merged in. */
|
|
460
|
+
getAll: (scope: "admin" | "user" | "host", scopeId?: string | number) => Promise<Record<string, unknown>>;
|
|
461
|
+
/**
|
|
462
|
+
* Fires when a key in this plugin's settings changes, from either the ctx
|
|
463
|
+
* API or the HTTP routes. Disposed automatically on deactivate.
|
|
464
|
+
*/
|
|
465
|
+
onChange: (key: string, listener: (value: unknown) => void) => () => void;
|
|
466
|
+
/**
|
|
467
|
+
* Checks a save from the settings screen or host editor before anything is
|
|
468
|
+
* written. Return field key to message for what is wrong; nothing means the
|
|
469
|
+
* save goes ahead. Disposed automatically on deactivate.
|
|
470
|
+
*/
|
|
471
|
+
onValidate: (scope: "admin" | "user" | "host", validator: (values: Record<string, unknown>,
|
|
472
|
+
/**
|
|
473
|
+
* `defaults` is set when the values are a host defaults level being
|
|
474
|
+
* saved rather than one host, so there is no hostId.
|
|
475
|
+
*/
|
|
476
|
+
context: {
|
|
477
|
+
hostId?: number;
|
|
478
|
+
defaults?: {
|
|
479
|
+
level: "admin" | "user" | "folder";
|
|
480
|
+
};
|
|
481
|
+
}) => Record<string, string> | void | Promise<Record<string, string> | void>) => () => void;
|
|
482
|
+
/**
|
|
483
|
+
* Reads a core server setting from a small documented allowlist.
|
|
484
|
+
* Requires the settings:read-core capability.
|
|
485
|
+
*/
|
|
486
|
+
readCore: (key: string) => Promise<string | null>;
|
|
487
|
+
}
|
|
488
|
+
/** A host's non-secret fields, for listing, tag matching and display. */
|
|
489
|
+
export interface PluginHostSummary {
|
|
490
|
+
id: number;
|
|
491
|
+
userId: string;
|
|
492
|
+
name: string | null;
|
|
493
|
+
ip: string;
|
|
494
|
+
port: number;
|
|
495
|
+
username: string;
|
|
496
|
+
/** Comma-separated, as ssh_data stores them. Split on "," to match tags. */
|
|
497
|
+
tags: string | null;
|
|
498
|
+
folder: string | null;
|
|
499
|
+
authType: string;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* A host as ctx.hosts hands it to a plugin that creates, updates or scans
|
|
503
|
+
* hosts on the user's behalf (import, discovery, sync), decrypted for that
|
|
504
|
+
* user. Every field is listed here and core builds it field by field, so a
|
|
505
|
+
* core column a plugin needs has to be added to this contract first. Carries
|
|
506
|
+
* no secret auth material (password, key, vault tokens): a plugin that
|
|
507
|
+
* creates a host picks an authType and, for "credential", a credentialId it
|
|
508
|
+
* does not need to see the contents of.
|
|
509
|
+
*/
|
|
510
|
+
export interface PluginHostRecord {
|
|
511
|
+
id: number;
|
|
512
|
+
userId: string;
|
|
513
|
+
/** Stable across a desktop and the server it syncs with. */
|
|
514
|
+
syncId: string | null;
|
|
515
|
+
name: string | null;
|
|
516
|
+
ip: string;
|
|
517
|
+
port: number;
|
|
518
|
+
username: string;
|
|
519
|
+
authType: string;
|
|
520
|
+
credentialId: number | null;
|
|
521
|
+
overrideCredentialUsername: boolean;
|
|
522
|
+
/** "ssh", or the id of the plugin protocol a host without SSH uses. */
|
|
523
|
+
connectionType: string;
|
|
524
|
+
/** Comma-separated, as ssh_data stores them. */
|
|
525
|
+
tags: string | null;
|
|
526
|
+
folder: string | null;
|
|
527
|
+
parentHostId: number | null;
|
|
528
|
+
pin: boolean;
|
|
529
|
+
notes: string | null;
|
|
530
|
+
jumpHosts: PluginHostJumpHost[];
|
|
531
|
+
enableSsh: boolean;
|
|
532
|
+
sshPort: number | null;
|
|
533
|
+
statusCheckEnabled: boolean;
|
|
534
|
+
/** Seconds between status checks; null follows the global setting. */
|
|
535
|
+
statusCheckInterval: number | null;
|
|
536
|
+
connectionOrigin: "local" | "remote" | null;
|
|
537
|
+
/** Keepalive, legacy algorithms, agent and environment options. */
|
|
538
|
+
sshOptions: HostSshOptions;
|
|
539
|
+
/**
|
|
540
|
+
* The calling plugin's own host settings, secrets redacted. Another
|
|
541
|
+
* plugin's settings are that plugin's to hand out, through a service.
|
|
542
|
+
*/
|
|
543
|
+
pluginSettings: Record<string, unknown>;
|
|
544
|
+
/** Core's last reachability check, or null before the first one. */
|
|
545
|
+
status: PluginHostStatusEntry | null;
|
|
546
|
+
/** Desktop only: kept on this device, never synced to the server. */
|
|
547
|
+
localOnly: boolean;
|
|
548
|
+
createdAt: string | null;
|
|
549
|
+
updatedAt: string | null;
|
|
550
|
+
}
|
|
551
|
+
export interface PluginHostJumpHost {
|
|
552
|
+
hostId: number;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* The fields a plugin may set when it creates a host. A key not listed here
|
|
556
|
+
* is refused with an error rather than dropped. Host settings go in
|
|
557
|
+
* `pluginSettings`, keyed by the plugin that declares them, and are
|
|
558
|
+
* validated against that plugin's manifest: an undeclared key is refused, and
|
|
559
|
+
* settings for a plugin that is not running are skipped and logged.
|
|
560
|
+
*/
|
|
561
|
+
export interface PluginHostCreateInput {
|
|
562
|
+
name: string;
|
|
563
|
+
ip: string;
|
|
564
|
+
port: number;
|
|
565
|
+
username: string;
|
|
566
|
+
authType: string;
|
|
567
|
+
credentialId?: number | null;
|
|
568
|
+
overrideCredentialUsername?: boolean;
|
|
569
|
+
connectionType?: string;
|
|
570
|
+
/** Comma-separated or a list. */
|
|
571
|
+
tags?: string | string[] | null;
|
|
572
|
+
folder?: string | null;
|
|
573
|
+
pin?: boolean;
|
|
574
|
+
notes?: string | null;
|
|
575
|
+
jumpHosts?: PluginHostJumpHost[] | null;
|
|
576
|
+
enableSsh?: boolean;
|
|
577
|
+
sshPort?: number | null;
|
|
578
|
+
statusCheckEnabled?: boolean;
|
|
579
|
+
statusCheckInterval?: number | null;
|
|
580
|
+
forceKeyboardInteractive?: boolean;
|
|
581
|
+
sshOptions?: HostSshOptions | null;
|
|
582
|
+
/** Host settings by plugin id, the shape a Termix export carries. */
|
|
583
|
+
pluginSettings?: Record<string, Record<string, unknown>>;
|
|
584
|
+
}
|
|
585
|
+
/** Fields a plugin may change on a host the acting user owns. */
|
|
586
|
+
export type PluginHostUpdateInput = Partial<PluginHostCreateInput>;
|
|
587
|
+
/** Every key PluginHostCreateInput and PluginHostUpdateInput accept. */
|
|
588
|
+
export declare const PLUGIN_HOST_INPUT_KEYS: readonly ["name", "ip", "port", "username", "authType", "credentialId", "overrideCredentialUsername", "connectionType", "tags", "folder", "pin", "notes", "jumpHosts", "enableSsh", "sshPort", "statusCheckEnabled", "statusCheckInterval", "forceKeyboardInteractive", "sshOptions", "pluginSettings"];
|
|
589
|
+
/** The same shape canAccessHost returns internally, without secrets. */
|
|
590
|
+
export interface PluginHostAccess {
|
|
591
|
+
hasAccess: boolean;
|
|
592
|
+
isOwner: boolean;
|
|
593
|
+
isShared: boolean;
|
|
594
|
+
permissionLevel?: "connect" | "view" | "edit" | "manage";
|
|
595
|
+
expiresAt?: string | null;
|
|
596
|
+
}
|
|
597
|
+
export type PluginHostShareLevel = "connect" | "view" | "edit" | "manage";
|
|
598
|
+
export interface PluginShareTarget {
|
|
599
|
+
type: "user" | "role";
|
|
600
|
+
id: string | number;
|
|
601
|
+
}
|
|
602
|
+
export interface PluginHostShareResult {
|
|
603
|
+
hostId: number;
|
|
604
|
+
shared: boolean;
|
|
605
|
+
reason?: string;
|
|
606
|
+
}
|
|
607
|
+
/** A user or role a plugin can offer as a share target. No secrets, no roles' permission lists. */
|
|
608
|
+
export interface PluginShareableUser {
|
|
609
|
+
id: string;
|
|
610
|
+
username: string;
|
|
611
|
+
}
|
|
612
|
+
export interface PluginShareableRole {
|
|
613
|
+
id: number;
|
|
614
|
+
name: string;
|
|
615
|
+
displayName: string | null;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Hosts the acting user can see, and the RBAC + sharing operations a plugin
|
|
619
|
+
* needs to build a feature (like fleets) on top of hosts it does not own.
|
|
620
|
+
*
|
|
621
|
+
* list/get/checkAccess need hosts:read. share, and the user/role pickers a
|
|
622
|
+
* share UI needs, need hosts:write: sharing changes who can reach a host,
|
|
623
|
+
* which is a write on that host's access, even though the plugin owns
|
|
624
|
+
* neither the host nor the grant.
|
|
625
|
+
*/
|
|
626
|
+
export interface PluginHosts {
|
|
627
|
+
/** Every host the acting user owns or was shared, without secrets. */
|
|
628
|
+
list: () => Promise<PluginHostSummary[]>;
|
|
629
|
+
/** One host the acting user can see, or null if it does not exist or they cannot. */
|
|
630
|
+
get: (hostId: number) => Promise<PluginHostSummary | null>;
|
|
631
|
+
/** The same access rule ctx.ssh.connect enforces, but naming the level, not just yes/no. */
|
|
632
|
+
checkAccess: (hostId: number, level: PluginHostShareLevel) => Promise<PluginHostAccess>;
|
|
633
|
+
/**
|
|
634
|
+
* Creates a host owned by the acting user, encrypted the same way the host
|
|
635
|
+
* editor's own create route does it. Needs hosts:write. For a plugin that
|
|
636
|
+
* imports or discovers hosts on the user's behalf (B5's proxmox discovery).
|
|
637
|
+
*/
|
|
638
|
+
create: (host: PluginHostCreateInput) => Promise<PluginHostRecord>;
|
|
639
|
+
/**
|
|
640
|
+
* Updates a host the acting user owns. Refuses a host it does not own
|
|
641
|
+
* (sharing a write onto someone else's host goes through share(), not
|
|
642
|
+
* update()). Needs hosts:write.
|
|
643
|
+
*/
|
|
644
|
+
update: (hostId: number, patch: PluginHostUpdateInput) => Promise<PluginHostRecord | null>;
|
|
645
|
+
/**
|
|
646
|
+
* Deletes a host the acting user owns, the same way the host editor's
|
|
647
|
+
* delete does: its access grants, activity and plugin settings go with it
|
|
648
|
+
* and remote sync gets a tombstone. Needs hosts:write, and the acting user
|
|
649
|
+
* needs the core hosts.delete permission. False when the host does not
|
|
650
|
+
* exist or is not theirs.
|
|
651
|
+
*/
|
|
652
|
+
delete: (hostId: number) => Promise<boolean>;
|
|
653
|
+
/**
|
|
654
|
+
* Every host the acting user owns, decrypted and wide (PluginHostRecord,
|
|
655
|
+
* not the narrow list() summary). For a plugin that scans its own hosts in
|
|
656
|
+
* the background (auto-sync) and needs fields list() does not carry, such
|
|
657
|
+
* as credentialId or jumpHosts. Needs hosts:write, matching create/update:
|
|
658
|
+
* this is the same "full host detail" surface a create/update caller needs
|
|
659
|
+
* to read back, not a wider read grant than hosts:read gives via list/get.
|
|
660
|
+
*/
|
|
661
|
+
listOwned: () => Promise<PluginHostRecord[]>;
|
|
662
|
+
/**
|
|
663
|
+
* Grants access to a host the caller manages, snapshotting shared secrets
|
|
664
|
+
* for each target the same way the host editor's own share action does.
|
|
665
|
+
* Refuses a host the caller does not hold "manage" on.
|
|
666
|
+
*/
|
|
667
|
+
share: (hostId: number, targets: PluginShareTarget[], permissionLevel: PluginHostShareLevel, durationHours?: number) => Promise<PluginHostShareResult>;
|
|
668
|
+
/** Users the caller may pick as a share target, for building a picker UI. */
|
|
669
|
+
listUsers: () => Promise<PluginShareableUser[]>;
|
|
670
|
+
/** Non-system roles the caller may pick as a share target. */
|
|
671
|
+
listRoles: () => Promise<PluginShareableRole[]>;
|
|
672
|
+
/**
|
|
673
|
+
* Marks a live session on a host, for the online indicator. Counted with
|
|
674
|
+
* every other feature's sessions; call the returned function once when the
|
|
675
|
+
* session ends. Needs hosts:read.
|
|
676
|
+
*/
|
|
677
|
+
trackSession: (hostId: number) => () => void;
|
|
678
|
+
/**
|
|
679
|
+
* Adds a recent activity entry for the acting user, rate limited and
|
|
680
|
+
* refused for a host they cannot reach, like the dashboard's own route.
|
|
681
|
+
* Needs hosts:read.
|
|
682
|
+
*/
|
|
683
|
+
recordActivity: (hostId: number, type: string, hostName: string) => Promise<void>;
|
|
684
|
+
/**
|
|
685
|
+
* Core's reachability status, the one behind the host list's status dot.
|
|
686
|
+
* Needs hosts:read.
|
|
687
|
+
*/
|
|
688
|
+
status: PluginHostStatusApi;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* online: the port answers, a session is open or a login worked. offline: the
|
|
692
|
+
* port does not answer. "reachable" is no longer sent and stays only so older
|
|
693
|
+
* plugins still type check.
|
|
694
|
+
*/
|
|
695
|
+
export type PluginHostStatus = "online" | "reachable" | "offline";
|
|
696
|
+
export interface PluginHostStatusEntry {
|
|
697
|
+
status: PluginHostStatus;
|
|
698
|
+
lastChecked: string;
|
|
699
|
+
/** @deprecated No longer sent. */
|
|
700
|
+
reason?: "host_key_changed";
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Core checks every host with status checks on by opening a TCP connection to
|
|
704
|
+
* it. A plugin that logs in to hosts can report a working login, which marks
|
|
705
|
+
* the host online right away.
|
|
706
|
+
*/
|
|
707
|
+
export interface PluginHostStatusApi {
|
|
708
|
+
/**
|
|
709
|
+
* The last known status, or null when core has not checked the host yet or
|
|
710
|
+
* the acting user cannot reach it.
|
|
711
|
+
*/
|
|
712
|
+
get: (hostId: number) => Promise<PluginHostStatusEntry | null>;
|
|
713
|
+
/** Checks the host now unless core checked it recently. */
|
|
714
|
+
check: (hostId: number) => Promise<PluginHostStatusEntry | null>;
|
|
715
|
+
/**
|
|
716
|
+
* Reports a login attempt. Only a working login changes the status; a
|
|
717
|
+
* failed one is ignored. Not audited: pollers call it every sample.
|
|
718
|
+
*/
|
|
719
|
+
reportLogin: (hostId: number, outcome: {
|
|
720
|
+
ok: boolean;
|
|
721
|
+
hostKeyChanged?: boolean;
|
|
722
|
+
}) => void;
|
|
723
|
+
/**
|
|
724
|
+
* Tells core which port proves a host of this connection type is up, for
|
|
725
|
+
* protocols whose port lives in the plugin's own host settings. Core falls
|
|
726
|
+
* back to the host's port. Removed on deactivate.
|
|
727
|
+
*/
|
|
728
|
+
registerPort: (connectionType: string, resolve: (hostId: number) => number | undefined | Promise<number | undefined>) => () => void;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* A host as the SSH pipeline reads it: resolved by core, with secrets filled
|
|
732
|
+
* in for the acting user. Plugins get one from core and hand it back; they
|
|
733
|
+
* should not build one from scratch.
|
|
734
|
+
*/
|
|
735
|
+
export interface PluginSshHost {
|
|
736
|
+
id: number;
|
|
737
|
+
ip: string;
|
|
738
|
+
port: number;
|
|
739
|
+
username: string;
|
|
740
|
+
userId?: string | null;
|
|
741
|
+
authType?: string | null;
|
|
742
|
+
sshOptions?: HostSshOptions | null;
|
|
743
|
+
[key: string]: unknown;
|
|
744
|
+
}
|
|
745
|
+
/** Picks keepalive and timeout defaults. "plugin" is the generic one. */
|
|
746
|
+
/** What a connection is for, as a free label. Auth providers see it. */
|
|
747
|
+
export type PluginSshPurpose = string;
|
|
748
|
+
/**
|
|
749
|
+
* Keepalive, timeout and environment defaults for a kind of connection.
|
|
750
|
+
* terminal: an interactive shell. session: a long-lived browsing session.
|
|
751
|
+
* stream: a long transfer or console. forward: port forwarding. background
|
|
752
|
+
* (the default): short exec work such as polling.
|
|
753
|
+
*/
|
|
754
|
+
export type PluginSshProfile = "terminal" | "session" | "stream" | "forward" | "background";
|
|
755
|
+
export type PluginSshAuthOutcome = {
|
|
756
|
+
status: "ready";
|
|
757
|
+
} | {
|
|
758
|
+
status: "interaction-required";
|
|
759
|
+
/** Transports send "<interaction>_auth_required", e.g. "opkssh". */
|
|
760
|
+
interaction: string;
|
|
761
|
+
message: string;
|
|
762
|
+
/** Extra boolean HTTP transports put in their 401 body. */
|
|
763
|
+
flag?: string;
|
|
764
|
+
} | {
|
|
765
|
+
status: "error";
|
|
766
|
+
code: "missing-secret" | "invalid-key" | "passphrase-required" | "provider-missing" | "failed";
|
|
767
|
+
message: string;
|
|
768
|
+
} | {
|
|
769
|
+
/** Try once more with these config changes. */
|
|
770
|
+
status: "retry";
|
|
771
|
+
patch: Record<string, unknown>;
|
|
772
|
+
message: string;
|
|
773
|
+
};
|
|
774
|
+
export type PluginSshBannerDecision = {
|
|
775
|
+
/** Hold the connect timeout open and wait for the out-of-band step. */
|
|
776
|
+
action: "hold";
|
|
777
|
+
timeoutMs: number;
|
|
778
|
+
message: string;
|
|
779
|
+
details?: Record<string, unknown>;
|
|
780
|
+
} | {
|
|
781
|
+
/** The out-of-band step finished; resume the normal connect timeout. */
|
|
782
|
+
action: "release";
|
|
783
|
+
details?: Record<string, unknown>;
|
|
784
|
+
};
|
|
785
|
+
export interface PluginKeyboardInteractivePrompt {
|
|
786
|
+
prompt: string;
|
|
787
|
+
echo?: boolean;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* A round the user finishes in a browser: open a URL, confirm a code there,
|
|
791
|
+
* then continue. Transports show one generic dialog for it and name their
|
|
792
|
+
* messages after `id` (the terminal sends "<id>_auth_required" and waits for
|
|
793
|
+
* "<id>_auth_continue").
|
|
794
|
+
*/
|
|
795
|
+
export interface PluginBrowserSignInRound {
|
|
796
|
+
kind: "browser";
|
|
797
|
+
/** The keyboard-interactive handler that claimed the round. */
|
|
798
|
+
id: string;
|
|
799
|
+
/** Product name for the dialog title. */
|
|
800
|
+
label: string;
|
|
801
|
+
url: string | null;
|
|
802
|
+
/** A code to compare in the browser, or "N/A". */
|
|
803
|
+
code: string;
|
|
804
|
+
instructions: string;
|
|
805
|
+
}
|
|
806
|
+
export type PluginKeyboardInteractiveDecision = {
|
|
807
|
+
kind: "auto";
|
|
808
|
+
responses: string[];
|
|
809
|
+
} | PluginBrowserSignInRound | {
|
|
810
|
+
kind: "totp";
|
|
811
|
+
promptIndex: number;
|
|
812
|
+
} | {
|
|
813
|
+
kind: "input";
|
|
814
|
+
promptIndex: number;
|
|
815
|
+
isPush: boolean;
|
|
816
|
+
};
|
|
817
|
+
export type PluginSshPromptRequest = {
|
|
818
|
+
kind: "totp";
|
|
819
|
+
prompt: string;
|
|
820
|
+
retry: boolean;
|
|
821
|
+
} | {
|
|
822
|
+
kind: "input";
|
|
823
|
+
prompt: string;
|
|
824
|
+
echo: boolean;
|
|
825
|
+
isPush: boolean;
|
|
826
|
+
} | PluginBrowserSignInRound;
|
|
827
|
+
/** What a keyboard-interactive handler's detect may return. */
|
|
828
|
+
export type PluginKeyboardInteractiveDetection = Exclude<PluginKeyboardInteractiveDecision, PluginBrowserSignInRound> | Omit<PluginBrowserSignInRound, "id" | "label">;
|
|
829
|
+
/**
|
|
830
|
+
* Claims keyboard-interactive rounds on any host, whatever its auth type,
|
|
831
|
+
* for a server that speaks its own prompt style (a bastion asking for a
|
|
832
|
+
* browser sign-in). Handlers run before core's TOTP and push detection.
|
|
833
|
+
*
|
|
834
|
+
* `settings` is this plugin's host-scope settings for the host, with manifest
|
|
835
|
+
* defaults filled in. Both hooks are synchronous; they run inside ssh2's
|
|
836
|
+
* keyboard-interactive callback.
|
|
837
|
+
*/
|
|
838
|
+
export interface PluginKeyboardInteractiveHandler {
|
|
839
|
+
/** Listed in contributes.auth.keyboardInteractive. */
|
|
840
|
+
id: string;
|
|
841
|
+
/** Product name shown when a browser round is claimed. */
|
|
842
|
+
label: string;
|
|
843
|
+
detect: (round: {
|
|
844
|
+
name: string;
|
|
845
|
+
instructions: string;
|
|
846
|
+
prompts: PluginKeyboardInteractivePrompt[];
|
|
847
|
+
}, host: PluginSshHost, settings: Record<string, unknown>) => PluginKeyboardInteractiveDetection | null;
|
|
848
|
+
/** Answer password prompts with the stored password instead of asking. */
|
|
849
|
+
autoAnswerPasswords?: (host: PluginSshHost, settings: Record<string, unknown>) => boolean;
|
|
850
|
+
}
|
|
851
|
+
/** Answers keyboard-interactive prompts. Resolve null to give up. */
|
|
852
|
+
export interface PluginSshPromptChannel {
|
|
853
|
+
ask: (request: PluginSshPromptRequest) => Promise<string | null>;
|
|
854
|
+
}
|
|
855
|
+
export interface PluginSshConnectOptions {
|
|
856
|
+
purpose?: PluginSshPurpose;
|
|
857
|
+
/** Keepalive and timeout defaults. Defaults to "background". */
|
|
858
|
+
profile?: PluginSshProfile;
|
|
859
|
+
/** Overall time to reach "ready". Defaults to 30s. */
|
|
860
|
+
timeoutMs?: number;
|
|
861
|
+
/** Without one, the stored password answers password prompts. */
|
|
862
|
+
prompt?: PluginSshPromptChannel;
|
|
863
|
+
/** Config fields to force, e.g. a longer readyTimeout. */
|
|
864
|
+
overrides?: Record<string, unknown>;
|
|
865
|
+
/**
|
|
866
|
+
* An already-open stream to the host, such as a forwardOut channel through
|
|
867
|
+
* another host. The transport step is skipped: no port knocking, proxy,
|
|
868
|
+
* jump hosts or DNS lookup. Host key checks and auth still run.
|
|
869
|
+
*/
|
|
870
|
+
sock?: unknown;
|
|
871
|
+
}
|
|
872
|
+
export interface PluginSshConnection<Client = unknown> {
|
|
873
|
+
/** An ssh2 Client, already "ready". */
|
|
874
|
+
client: Client;
|
|
875
|
+
jumpClient: Client | null;
|
|
876
|
+
/** The host as core resolved it: ip, port, username, name and the rest. */
|
|
877
|
+
host: PluginSshHost;
|
|
878
|
+
/** Ends the connection and its jump chain. Also runs on deactivate. */
|
|
879
|
+
dispose: () => void;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* SSH through core's one connect pipeline: host resolution, auth providers,
|
|
883
|
+
* jump hosts, proxies, host key checks, keepalive and port knocking.
|
|
884
|
+
*
|
|
885
|
+
* Every call needs ssh:connect and credentials:use, runs as the acting user
|
|
886
|
+
* (or the host's owner for background work on a resolved host) and writes an
|
|
887
|
+
* audit line per new connection. Connections still open on deactivate are
|
|
888
|
+
* closed.
|
|
889
|
+
*/
|
|
890
|
+
export interface PluginSsh {
|
|
891
|
+
connect: <Client = unknown>(host: number | PluginSshHost, options?: PluginSshConnectOptions) => Promise<PluginSshConnection<Client>>;
|
|
892
|
+
/** Borrows a pooled connection. The pool key is `<pool>:<owner>:<address>`. */
|
|
893
|
+
withConnection: <T, Client = unknown>(host: number | PluginSshHost, options: PluginSshConnectOptions & {
|
|
894
|
+
pool: string;
|
|
895
|
+
}, fn: (client: Client) => Promise<T>) => Promise<T>;
|
|
896
|
+
/**
|
|
897
|
+
* A ready client at the end of a jump host chain, each hop resolved and
|
|
898
|
+
* authenticated through the pipeline. For forwarding to something that is
|
|
899
|
+
* not an SSH server (a remote desktop port behind a bastion).
|
|
900
|
+
*/
|
|
901
|
+
jumpChain: <Client = unknown>(jumpHosts: Array<{
|
|
902
|
+
hostId: number;
|
|
903
|
+
}>, options?: {
|
|
904
|
+
/** Background work: resolve the hops as this host's owner. */
|
|
905
|
+
forHost?: PluginSshHost;
|
|
906
|
+
/** Answers a hop asking for TOTP or other input. */
|
|
907
|
+
prompt?: PluginSshPromptChannel;
|
|
908
|
+
}) => Promise<PluginSshConnection<Client>>;
|
|
909
|
+
/** The pool key withConnection would use, for clearing it. */
|
|
910
|
+
poolKey: (pool: string, host: PluginSshHost) => string;
|
|
911
|
+
/**
|
|
912
|
+
* Closes this plugin's pooled connections to a host, for when its details
|
|
913
|
+
* changed. Only keys this plugin created are touched.
|
|
914
|
+
*/
|
|
915
|
+
dropPooled: (pool: string, hostId: number) => void;
|
|
916
|
+
/**
|
|
917
|
+
* The host as the acting user may connect to it: RBAC, owner-key
|
|
918
|
+
* decryption, shared-host overrides and external secret references
|
|
919
|
+
* resolved, secrets included. By sync id when given, since a numeric id
|
|
920
|
+
* only names a host on the database that issued it. Null when the host is
|
|
921
|
+
* not on this server or the user cannot reach it.
|
|
922
|
+
*/
|
|
923
|
+
resolveHost: (hostId: number, options?: {
|
|
924
|
+
syncId?: string | null;
|
|
925
|
+
}) => Promise<PluginSshHost | null>;
|
|
926
|
+
/**
|
|
927
|
+
* Lower level: fills an ssh2 config for a client the plugin drives itself,
|
|
928
|
+
* for transports with their own prompt flow. Never connects.
|
|
929
|
+
*/
|
|
930
|
+
prepare: (host: PluginSshHost, options: {
|
|
931
|
+
purpose?: PluginSshPurpose;
|
|
932
|
+
/** Keepalive and timeout defaults. Defaults to "background". */
|
|
933
|
+
profile?: PluginSshProfile;
|
|
934
|
+
client: unknown;
|
|
935
|
+
/** Server-side host id when it differs from host.id. */
|
|
936
|
+
serverHostId?: number;
|
|
937
|
+
log?: (level: "info" | "warning" | "error", message: string) => void;
|
|
938
|
+
/** A person is at the keyboard, so providers may prompt. */
|
|
939
|
+
interactive?: boolean;
|
|
940
|
+
/**
|
|
941
|
+
* The ws socket the host key verifier asks on when a key is new or has
|
|
942
|
+
* changed. Without one a changed key is refused.
|
|
943
|
+
*/
|
|
944
|
+
hostKeySocket?: unknown;
|
|
945
|
+
}) => Promise<PluginSshPrepared>;
|
|
946
|
+
/** Lower level: port knocking, jump hosts or proxy; sets config.sock. */
|
|
947
|
+
openTransport: (host: PluginSshHost, config: Record<string, unknown>, options?: {
|
|
948
|
+
/** False when the caller already resolved DNS, or a jump host will. */
|
|
949
|
+
resolveDns?: boolean;
|
|
950
|
+
log?: (level: "info" | "warning" | "error", message: string) => void;
|
|
951
|
+
/** Answers a jump host asking for TOTP or other input. */
|
|
952
|
+
prompt?: PluginSshPromptChannel;
|
|
953
|
+
}) => Promise<{
|
|
954
|
+
jumpClient: unknown | null;
|
|
955
|
+
via: string;
|
|
956
|
+
}>;
|
|
957
|
+
/**
|
|
958
|
+
* Starts the browser step behind an `<interaction>_auth_required` message.
|
|
959
|
+
* The provider for the host's own auth type wins, else the first provider
|
|
960
|
+
* that owns the interaction. Rejects with PluginSshInteractionError when
|
|
961
|
+
* none can.
|
|
962
|
+
*/
|
|
963
|
+
startInteraction: (interaction: string, request: {
|
|
964
|
+
hostId: number;
|
|
965
|
+
socket: unknown;
|
|
966
|
+
requestOrigin: string;
|
|
967
|
+
payload?: Record<string, unknown>;
|
|
968
|
+
}) => Promise<void>;
|
|
969
|
+
/** Cancels a pending browser step on every provider that owns it. */
|
|
970
|
+
cancelInteraction: (interaction: string, request: {
|
|
971
|
+
hostId?: number;
|
|
972
|
+
requestId?: string;
|
|
973
|
+
}) => Promise<void>;
|
|
974
|
+
/** What a keyboard-interactive round is. Pure; no capability needed. */
|
|
975
|
+
classifyKeyboardInteractive: (round: {
|
|
976
|
+
name: string;
|
|
977
|
+
instructions: string;
|
|
978
|
+
prompts: PluginKeyboardInteractivePrompt[];
|
|
979
|
+
}, host: PluginSshHost) => PluginKeyboardInteractiveDecision;
|
|
980
|
+
/** Stored password for password prompts, empty for the rest. */
|
|
981
|
+
autoResponses: (prompts: PluginKeyboardInteractivePrompt[], password: string | null | undefined) => string[];
|
|
982
|
+
/** Whether an auth type carries a secret (drives "auth_required" flows). */
|
|
983
|
+
requiresSecret: (authType: string) => boolean;
|
|
984
|
+
/** Whether an auth type can connect unattended, for polling. */
|
|
985
|
+
supportsBackground: (authType: string) => boolean;
|
|
986
|
+
}
|
|
987
|
+
/** What ctx.ssh.prepare built, plus the provider hooks bound to it. */
|
|
988
|
+
export interface PluginSshPrepared {
|
|
989
|
+
config: Record<string, unknown>;
|
|
990
|
+
outcome: PluginSshAuthOutcome;
|
|
991
|
+
/** The auth type whose provider prepared it, null when none exists. */
|
|
992
|
+
authType: string | null;
|
|
993
|
+
/** The provider's banner hook, bound to this host and connection. */
|
|
994
|
+
onBanner?: (banner: string) => PluginSshBannerDecision | undefined;
|
|
995
|
+
/** The provider's auth failure hook, bound to this host and connection. */
|
|
996
|
+
onAuthFailed?: (context: {
|
|
997
|
+
error: Error;
|
|
998
|
+
retries: number;
|
|
999
|
+
canRetry: boolean;
|
|
1000
|
+
methodNotAvailable: boolean;
|
|
1001
|
+
}) => PluginSshAuthOutcome | undefined;
|
|
1002
|
+
}
|
|
1003
|
+
/** Thrown by ctx.ssh.startInteraction when no provider can start it. */
|
|
1004
|
+
export declare class PluginSshInteractionError extends Error {
|
|
1005
|
+
constructor(message: string);
|
|
1006
|
+
}
|
|
1007
|
+
export interface PluginSshAuthEnv {
|
|
1008
|
+
/** The ssh2 Client about to connect; certificate auth patches it. */
|
|
1009
|
+
client: unknown;
|
|
1010
|
+
userId: string;
|
|
1011
|
+
hostId: number;
|
|
1012
|
+
purpose: string;
|
|
1013
|
+
interactive: boolean;
|
|
1014
|
+
log: (level: "info" | "warning" | "error", message: string) => void;
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* An SSH auth type. Its `type` is what ssh_data.auth_type stores, and it must
|
|
1018
|
+
* be listed in the manifest's contributes.auth.sshAuthTypes.
|
|
1019
|
+
*/
|
|
1020
|
+
export interface PluginSshAuthProvider {
|
|
1021
|
+
type: string;
|
|
1022
|
+
labelKey: string;
|
|
1023
|
+
descriptionKey?: string;
|
|
1024
|
+
/** Host and credential editor fields, in the settings field schema. */
|
|
1025
|
+
fields?: PluginSettingsField[];
|
|
1026
|
+
/** Also offered as a stored credential type. */
|
|
1027
|
+
credentialType?: boolean;
|
|
1028
|
+
/** Needs a browser sign-in or a person at the keyboard. */
|
|
1029
|
+
needsUserInteraction?: boolean;
|
|
1030
|
+
/** Carries a secret, so a shared host needs one resolved per recipient. */
|
|
1031
|
+
requiresSecret?: boolean;
|
|
1032
|
+
/** Can connect unattended. Defaults to true. */
|
|
1033
|
+
supportsBackground?: boolean;
|
|
1034
|
+
/**
|
|
1035
|
+
* Works for a host that was never saved (nothing in host settings, no host
|
|
1036
|
+
* id to key a cache on), so Quick Connect offers it. Default false.
|
|
1037
|
+
*/
|
|
1038
|
+
quickConnect?: boolean;
|
|
1039
|
+
/** Interaction name its outcomes use, for startInteraction routing. */
|
|
1040
|
+
interaction?: string;
|
|
1041
|
+
connectOptions?: (host: PluginSshHost, purpose: string) => Record<string, unknown>;
|
|
1042
|
+
/** Fills the ssh2 config for this host. Never connects. */
|
|
1043
|
+
prepare: (config: Record<string, unknown>, host: PluginSshHost, env: PluginSshAuthEnv) => Promise<PluginSshAuthOutcome>;
|
|
1044
|
+
/** Claims keyboard-interactive rounds for hosts of this type. */
|
|
1045
|
+
onKeyboardInteractive?: (round: {
|
|
1046
|
+
name: string;
|
|
1047
|
+
instructions: string;
|
|
1048
|
+
prompts: PluginKeyboardInteractivePrompt[];
|
|
1049
|
+
}, host: PluginSshHost) => PluginKeyboardInteractiveDecision | null;
|
|
1050
|
+
/** Synchronous: decide on a retry before the socket closes. */
|
|
1051
|
+
onAuthFailed?: (host: PluginSshHost, env: PluginSshAuthEnv, context: {
|
|
1052
|
+
error: Error;
|
|
1053
|
+
retries: number;
|
|
1054
|
+
canRetry: boolean;
|
|
1055
|
+
methodNotAvailable: boolean;
|
|
1056
|
+
}) => PluginSshAuthOutcome | undefined;
|
|
1057
|
+
/**
|
|
1058
|
+
* Claims an ssh2 "banner" event during the handshake, for a server that
|
|
1059
|
+
* holds the connection open pending an out-of-band step (Tailscale SSH
|
|
1060
|
+
* check mode). A transport that can show this sends `<type>_check_required`
|
|
1061
|
+
* / `<type>_check_completed` over its socket, with `details` merged in.
|
|
1062
|
+
* Return nothing to leave the banner unhandled.
|
|
1063
|
+
*/
|
|
1064
|
+
onBanner?: (banner: string, host: PluginSshHost, env: PluginSshAuthEnv) => PluginSshBannerDecision | undefined;
|
|
1065
|
+
/** Starts the browser step behind an interaction-required outcome. */
|
|
1066
|
+
startInteraction?: (request: {
|
|
1067
|
+
userId: string;
|
|
1068
|
+
hostId: number;
|
|
1069
|
+
host: {
|
|
1070
|
+
name?: string | null;
|
|
1071
|
+
ip: string;
|
|
1072
|
+
username: string;
|
|
1073
|
+
};
|
|
1074
|
+
socket: unknown;
|
|
1075
|
+
requestOrigin: string;
|
|
1076
|
+
payload: Record<string, unknown>;
|
|
1077
|
+
}) => Promise<void>;
|
|
1078
|
+
/** Cancels a pending browser step. */
|
|
1079
|
+
cancelInteraction?: (request: {
|
|
1080
|
+
userId: string;
|
|
1081
|
+
hostId?: number;
|
|
1082
|
+
requestId?: string;
|
|
1083
|
+
}) => void | Promise<void>;
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Who a login method says the user is. Core finds or provisions the user,
|
|
1087
|
+
* runs second factors and issues the session; a method never does.
|
|
1088
|
+
*/
|
|
1089
|
+
export type PluginVerifiedIdentity = {
|
|
1090
|
+
kind: "user";
|
|
1091
|
+
userId: string;
|
|
1092
|
+
/** The method already proved a second factor (a verified passkey). */
|
|
1093
|
+
mfaSatisfied?: boolean;
|
|
1094
|
+
/** Password logins unlock the user's data key with it. */
|
|
1095
|
+
password?: string;
|
|
1096
|
+
/** Redirect methods: where the browser goes back to. */
|
|
1097
|
+
returnTo?: string;
|
|
1098
|
+
rememberMe?: boolean;
|
|
1099
|
+
} | {
|
|
1100
|
+
kind: "external";
|
|
1101
|
+
/** Stable provider id, e.g. an SSO provider row id. */
|
|
1102
|
+
provider: string;
|
|
1103
|
+
/** The provider's id for the user. At most 255 characters. */
|
|
1104
|
+
subject: string;
|
|
1105
|
+
email?: string | null;
|
|
1106
|
+
name?: string | null;
|
|
1107
|
+
groups?: string[];
|
|
1108
|
+
/** Provider says the user is an admin (admin group, LDAP group). */
|
|
1109
|
+
isAdmin?: boolean;
|
|
1110
|
+
/** Allowed-users list to check before provisioning or signing in. */
|
|
1111
|
+
allowedUsers?: string | null;
|
|
1112
|
+
mfaSatisfied?: boolean;
|
|
1113
|
+
/** Redirect methods: where the browser goes back to. */
|
|
1114
|
+
returnTo?: string;
|
|
1115
|
+
rememberMe?: boolean;
|
|
1116
|
+
/**
|
|
1117
|
+
* Provider groups mapped to Termix roles. Core adds the desired roles
|
|
1118
|
+
* and removes managed roles the user no longer maps to, on every login.
|
|
1119
|
+
* Roles outside `managed` are never touched.
|
|
1120
|
+
*/
|
|
1121
|
+
roles?: {
|
|
1122
|
+
desired: string[];
|
|
1123
|
+
managed: string[];
|
|
1124
|
+
};
|
|
1125
|
+
/**
|
|
1126
|
+
* Stored on the session so `ctx.auth.revokeSessions` can find it later,
|
|
1127
|
+
* for an identity provider's back-channel logout.
|
|
1128
|
+
*/
|
|
1129
|
+
logoutClaims?: {
|
|
1130
|
+
providerId?: number | null;
|
|
1131
|
+
sub?: string | null;
|
|
1132
|
+
sid?: string | null;
|
|
1133
|
+
};
|
|
1134
|
+
/**
|
|
1135
|
+
* The identifier a 2.8 install kept for the user. Only for a plugin
|
|
1136
|
+
* that took over a 2.8 login method: core also checks `identifier`
|
|
1137
|
+
* against `allowedUsers`. `providerRowId` is no longer read.
|
|
1138
|
+
*/
|
|
1139
|
+
legacy?: {
|
|
1140
|
+
identifier: string;
|
|
1141
|
+
providerRowId?: number | null;
|
|
1142
|
+
};
|
|
1143
|
+
/**
|
|
1144
|
+
* The key this attempt was counted under with
|
|
1145
|
+
* `ctx.auth.loginRateLimit.recordFailure`. Cleared on success.
|
|
1146
|
+
*/
|
|
1147
|
+
rateLimitKey?: string;
|
|
1148
|
+
};
|
|
1149
|
+
/** Request shape a login method sees. Structural, like PluginRequestLike. */
|
|
1150
|
+
export interface PluginLoginRequest {
|
|
1151
|
+
body: Record<string, unknown>;
|
|
1152
|
+
query: Record<string, unknown>;
|
|
1153
|
+
headers: Record<string, unknown>;
|
|
1154
|
+
ip?: string;
|
|
1155
|
+
[key: string]: unknown;
|
|
1156
|
+
}
|
|
1157
|
+
export interface PluginLoginInstance {
|
|
1158
|
+
/** Passed back to start/verify as instanceId, e.g. an SSO provider id. */
|
|
1159
|
+
id: string;
|
|
1160
|
+
label: string;
|
|
1161
|
+
enabled: boolean;
|
|
1162
|
+
/**
|
|
1163
|
+
* Provider type shown to 2.8 clients by the old `GET /users/sso-providers`
|
|
1164
|
+
* route ("oidc", "github", "google", "ldap"). Leave unset otherwise.
|
|
1165
|
+
*/
|
|
1166
|
+
type?: string;
|
|
1167
|
+
/**
|
|
1168
|
+
* Redirect methods: the login page starts this instance without a click,
|
|
1169
|
+
* for a server that signs everyone in through one provider. The first
|
|
1170
|
+
* instance that asks wins.
|
|
1171
|
+
*/
|
|
1172
|
+
autoStart?: boolean;
|
|
1173
|
+
}
|
|
1174
|
+
export interface PluginLoginMethod {
|
|
1175
|
+
id: string;
|
|
1176
|
+
labelKey: string;
|
|
1177
|
+
icon?: string;
|
|
1178
|
+
kind: "redirect" | "form";
|
|
1179
|
+
/**
|
|
1180
|
+
* True for a method that authenticates against something other than a
|
|
1181
|
+
* password or key this server holds: SSO, an external directory, a
|
|
1182
|
+
* federated identity provider. Governs whether the "ask for a second
|
|
1183
|
+
* factor after external logins" admin setting applies to it. Leave unset
|
|
1184
|
+
* for a local method (password, a passkey, a plugin's own local form).
|
|
1185
|
+
*/
|
|
1186
|
+
external?: boolean;
|
|
1187
|
+
/** Enabled instances, shown on the login screen. No secrets. */
|
|
1188
|
+
describe?: () => Promise<PluginLoginInstance[]>;
|
|
1189
|
+
/** Redirect methods: where to send the browser. */
|
|
1190
|
+
start?: (request: PluginLoginRequest, instanceId: string | null) => Promise<{
|
|
1191
|
+
redirectUrl: string;
|
|
1192
|
+
}>;
|
|
1193
|
+
/** Redirect methods: the provider's callback. */
|
|
1194
|
+
callback?: (request: PluginLoginRequest) => Promise<PluginVerifiedIdentity>;
|
|
1195
|
+
/** Form methods: check what the user typed. */
|
|
1196
|
+
verify?: (request: PluginLoginRequest, instanceId: string | null) => Promise<PluginVerifiedIdentity>;
|
|
1197
|
+
}
|
|
1198
|
+
export interface PluginSecondFactor {
|
|
1199
|
+
id: string;
|
|
1200
|
+
labelKey: string;
|
|
1201
|
+
isEnrolled: (userId: string) => Promise<boolean>;
|
|
1202
|
+
/** Anything the client needs before the user answers. */
|
|
1203
|
+
challenge?: (userId: string) => Promise<unknown>;
|
|
1204
|
+
/**
|
|
1205
|
+
* True when the answer is right. Return an object to refuse with a specific
|
|
1206
|
+
* message, for example when the factor had to be reset.
|
|
1207
|
+
*/
|
|
1208
|
+
verify: (userId: string, body: Record<string, unknown>) => Promise<boolean | {
|
|
1209
|
+
ok: false;
|
|
1210
|
+
error: string;
|
|
1211
|
+
code?: string;
|
|
1212
|
+
}>;
|
|
1213
|
+
/** Removes the user's enrolment. Called by the admin reset. */
|
|
1214
|
+
reset?: (userId: string) => Promise<void>;
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Login methods, second factors and SSH auth types. Every register call needs
|
|
1218
|
+
* auth:provide and a matching contributes.auth entry, and is undone on
|
|
1219
|
+
* deactivate. Core owns sessions: a method returns an identity and core does
|
|
1220
|
+
* the rest.
|
|
1221
|
+
*/
|
|
1222
|
+
export interface PluginAuth {
|
|
1223
|
+
registerSshAuthProvider: (provider: PluginSshAuthProvider) => void;
|
|
1224
|
+
/** A host-independent keyboard-interactive handler. */
|
|
1225
|
+
registerKeyboardInteractiveHandler: (handler: PluginKeyboardInteractiveHandler) => void;
|
|
1226
|
+
registerLoginMethod: (method: PluginLoginMethod) => void;
|
|
1227
|
+
registerSecondFactor: (factor: PluginSecondFactor) => void;
|
|
1228
|
+
/**
|
|
1229
|
+
* Marks a user as enrolled in one of this plugin's factors. Core keeps the
|
|
1230
|
+
* row even when the plugin is gone, so it can refuse the login instead of
|
|
1231
|
+
* skipping the factor.
|
|
1232
|
+
*/
|
|
1233
|
+
recordEnrollment: (userId: string, factorId: string) => Promise<void>;
|
|
1234
|
+
removeEnrollment: (userId: string, factorId: string) => Promise<void>;
|
|
1235
|
+
/**
|
|
1236
|
+
* Finishes a redirect login from one of this plugin's own public routes:
|
|
1237
|
+
* runs the registered method's `callback`, then core finds or provisions
|
|
1238
|
+
* the user, runs second factors and redirects the browser back with a
|
|
1239
|
+
* session, exactly like `/users/auth/<methodId>/callback`. The method must
|
|
1240
|
+
* be one this plugin registered.
|
|
1241
|
+
*/
|
|
1242
|
+
completeRedirectLogin: (methodId: string, req: unknown, res: unknown) => Promise<void>;
|
|
1243
|
+
/**
|
|
1244
|
+
* Revokes sessions whose login carried matching `logoutClaims`, for a
|
|
1245
|
+
* back-channel logout. Needs `sub` or `sid`; `providerId` narrows it.
|
|
1246
|
+
* Returns how many sessions ended.
|
|
1247
|
+
*/
|
|
1248
|
+
revokeSessions: (match: {
|
|
1249
|
+
providerId?: number | null;
|
|
1250
|
+
sub?: string | null;
|
|
1251
|
+
sid?: string | null;
|
|
1252
|
+
}) => Promise<number>;
|
|
1253
|
+
/**
|
|
1254
|
+
* Core's login rate limiter, the one password login uses. Keys are kept
|
|
1255
|
+
* apart per plugin. Put the key in the identity's `rateLimitKey` so a
|
|
1256
|
+
* successful login clears it.
|
|
1257
|
+
*/
|
|
1258
|
+
loginRateLimit: {
|
|
1259
|
+
isLocked: (ip: string, key: string) => Promise<{
|
|
1260
|
+
locked: boolean;
|
|
1261
|
+
remainingTime?: number;
|
|
1262
|
+
}>;
|
|
1263
|
+
recordFailure: (ip: string, key: string) => Promise<void>;
|
|
1264
|
+
};
|
|
1265
|
+
/**
|
|
1266
|
+
* How many users are linked to an external identity from `provider` (the
|
|
1267
|
+
* `provider` of the identities this plugin returns). For refusing to
|
|
1268
|
+
* delete a provider people still sign in with.
|
|
1269
|
+
*/
|
|
1270
|
+
countLinkedUsers: (provider: string) => Promise<number>;
|
|
1271
|
+
}
|
|
1272
|
+
export interface PluginOpenIsolatedWindowRequest {
|
|
1273
|
+
/** http(s) only. Refused when it points anywhere else. */
|
|
1274
|
+
url: string;
|
|
1275
|
+
/** Isolated Electron session partition; a fresh one when omitted. */
|
|
1276
|
+
partition?: string;
|
|
1277
|
+
title?: string;
|
|
1278
|
+
/** Present an invalid TLS certificate on this window's own origin only. */
|
|
1279
|
+
ignoreCert?: boolean;
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Opens a desktop window outside the main renderer, for a target a plugin
|
|
1283
|
+
* does not want sharing Termix's own session (a tunnelled or direct web UI).
|
|
1284
|
+
* Electron only: rejects when the server is not running embedded in the
|
|
1285
|
+
* desktop app. Needs desktop:window.
|
|
1286
|
+
*/
|
|
1287
|
+
/**
|
|
1288
|
+
* A host to open in the operating system's own client for a protocol. The
|
|
1289
|
+
* desktop app supports "rdp" (mstsc on Windows) today; any other protocol is
|
|
1290
|
+
* refused.
|
|
1291
|
+
*/
|
|
1292
|
+
export interface PluginExternalClientRequest {
|
|
1293
|
+
protocol: string;
|
|
1294
|
+
host: string;
|
|
1295
|
+
port?: number;
|
|
1296
|
+
username?: string;
|
|
1297
|
+
domain?: string;
|
|
1298
|
+
}
|
|
1299
|
+
export interface PluginDesktop {
|
|
1300
|
+
openIsolatedWindow: (request: PluginOpenIsolatedWindowRequest) => Promise<{
|
|
1301
|
+
success: true;
|
|
1302
|
+
}>;
|
|
1303
|
+
/**
|
|
1304
|
+
* Opens the operating system's own client for a protocol. The password is
|
|
1305
|
+
* never passed; the client asks for it. Desktop app only.
|
|
1306
|
+
*/
|
|
1307
|
+
launchExternalClient: (request: PluginExternalClientRequest) => Promise<{
|
|
1308
|
+
success: boolean;
|
|
1309
|
+
error?: string;
|
|
1310
|
+
}>;
|
|
1311
|
+
/** Whether the server runs embedded in the desktop app, so the calls above can work. */
|
|
1312
|
+
available: () => boolean;
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* A host protocol whose login core keeps next to the host: any id a plugin
|
|
1316
|
+
* declares in contributes.protocols.
|
|
1317
|
+
*/
|
|
1318
|
+
export type PluginHostProtocol = string;
|
|
1319
|
+
/**
|
|
1320
|
+
* What a plugin needs to hand a host's protocol login to something core
|
|
1321
|
+
* does not run (guacd): the address and the plaintext credentials the acting
|
|
1322
|
+
* user is allowed to use. A shared recipient gets the owner's shared
|
|
1323
|
+
* snapshot or their own override, never the owner's raw secret.
|
|
1324
|
+
*/
|
|
1325
|
+
export interface PluginProtocolTarget {
|
|
1326
|
+
host: {
|
|
1327
|
+
id: number;
|
|
1328
|
+
name: string | null;
|
|
1329
|
+
ip: string;
|
|
1330
|
+
/** The host's SSH port, the old fallback when no protocol port is set. */
|
|
1331
|
+
port: number;
|
|
1332
|
+
ownerUserId: string;
|
|
1333
|
+
jumpHosts: Array<{
|
|
1334
|
+
hostId: number;
|
|
1335
|
+
}>;
|
|
1336
|
+
};
|
|
1337
|
+
/** True when the acting user is not the owner. */
|
|
1338
|
+
shared: boolean;
|
|
1339
|
+
auth: {
|
|
1340
|
+
/** "direct", "credential", or "none" when the user is asked at connect time. */
|
|
1341
|
+
authType: string;
|
|
1342
|
+
username: string;
|
|
1343
|
+
password: string;
|
|
1344
|
+
/** The protocol's declared credentialFields, secret ones included; "" when unset. */
|
|
1345
|
+
fields: Record<string, string>;
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
/** One of the acting user's saved SSH key credentials, public half only. */
|
|
1349
|
+
export interface PluginSshKeyCredential {
|
|
1350
|
+
id: number;
|
|
1351
|
+
name: string;
|
|
1352
|
+
username: string | null;
|
|
1353
|
+
/** "<type> <base64>", derived from the private key when none was stored. Null when it cannot be. */
|
|
1354
|
+
publicKey: string | null;
|
|
1355
|
+
}
|
|
1356
|
+
/** A key pair to save as a new credential for the acting user. */
|
|
1357
|
+
export interface PluginSshKeyCredentialInput {
|
|
1358
|
+
name: string;
|
|
1359
|
+
description?: string;
|
|
1360
|
+
username?: string | null;
|
|
1361
|
+
privateKey: string;
|
|
1362
|
+
publicKey: string;
|
|
1363
|
+
/** The key's algorithm token, such as "ssh-ed25519". */
|
|
1364
|
+
keyType: string;
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Plaintext host credentials, for a plugin that has to hand them to another
|
|
1368
|
+
* program. Needs credentials:read, and every call is audited.
|
|
1369
|
+
*/
|
|
1370
|
+
export interface PluginCredentials {
|
|
1371
|
+
/**
|
|
1372
|
+
* The acting user's own SSH key credentials, without any private material.
|
|
1373
|
+
* Needs credentials:use and the core credentials.view permission. Audited.
|
|
1374
|
+
*/
|
|
1375
|
+
listSshKeys: () => Promise<PluginSshKeyCredential[]>;
|
|
1376
|
+
/**
|
|
1377
|
+
* Saves a key pair as a new encrypted credential owned by the acting user
|
|
1378
|
+
* and returns its id. Needs credentials:write, the core credentials.create
|
|
1379
|
+
* permission and an unlocked data key. Audited.
|
|
1380
|
+
*/
|
|
1381
|
+
createSshKey: (input: PluginSshKeyCredentialInput) => Promise<{
|
|
1382
|
+
id: number;
|
|
1383
|
+
}>;
|
|
1384
|
+
/**
|
|
1385
|
+
* The host's login for one of this plugin's own contributes.protocols.
|
|
1386
|
+
* Null when the host does not exist or the acting user cannot connect to
|
|
1387
|
+
* it. Needs credentials:read. Audited.
|
|
1388
|
+
*/
|
|
1389
|
+
resolveHostProtocol: (hostId: number, protocol: PluginHostProtocol) => Promise<PluginProtocolTarget | null>;
|
|
1390
|
+
/**
|
|
1391
|
+
* Registers this plugin as the resolver for "<scheme>://..." references in
|
|
1392
|
+
* a host's secret fields (secret-sources registers "op" for
|
|
1393
|
+
* "op://vault/item/field"). Needs auth:provide, like registerSshAuthProvider.
|
|
1394
|
+
*
|
|
1395
|
+
* `resolve` runs once per host resolution, for the user the field's owner
|
|
1396
|
+
* resolves as (the host owner, or the recipient for a personal override),
|
|
1397
|
+
* and its result is cached briefly by the connect pipeline. A scheme with no
|
|
1398
|
+
* registered resolver fails the connect with a message naming the plugin
|
|
1399
|
+
* that would provide it.
|
|
1400
|
+
*/
|
|
1401
|
+
registerSecretResolver: (scheme: string, resolve: (userId: string, reference: string) => Promise<string>) => void;
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* An audit line under an action name of the plugin's choosing (an SSH login),
|
|
1405
|
+
* for the acting user, next to the plugin_* line every privileged ctx call
|
|
1406
|
+
* already writes.
|
|
1407
|
+
*/
|
|
1408
|
+
export interface PluginAudit {
|
|
1409
|
+
record: (entry: {
|
|
1410
|
+
action: string;
|
|
1411
|
+
resourceType?: string;
|
|
1412
|
+
resourceId?: string;
|
|
1413
|
+
resourceName?: string;
|
|
1414
|
+
details?: string;
|
|
1415
|
+
success: boolean;
|
|
1416
|
+
errorMessage?: string;
|
|
1417
|
+
/**
|
|
1418
|
+
* The HTTP request or WebSocket upgrade this happened on, for the line's
|
|
1419
|
+
* IP address and user agent.
|
|
1420
|
+
*/
|
|
1421
|
+
request?: unknown;
|
|
1422
|
+
}) => Promise<void>;
|
|
1423
|
+
}
|
|
1424
|
+
/** A notification channel a user set up. Its config never leaves the hub. */
|
|
1425
|
+
export interface PluginNotificationChannel {
|
|
1426
|
+
id: number;
|
|
1427
|
+
name: string;
|
|
1428
|
+
/** "webhook", "ntfy", "discord", "email", or whatever the hub supports. */
|
|
1429
|
+
type: string;
|
|
1430
|
+
enabled: boolean;
|
|
1431
|
+
}
|
|
1432
|
+
export type PluginNotificationSeverity = "info" | "success" | "warning" | "critical";
|
|
1433
|
+
/**
|
|
1434
|
+
* Who receives an alert. Left out, it goes to the acting user. "admins" and
|
|
1435
|
+
* a permission reach every user who holds it when the alert is sent.
|
|
1436
|
+
*/
|
|
1437
|
+
export type PluginNotificationAudience = {
|
|
1438
|
+
userId: string;
|
|
1439
|
+
} | {
|
|
1440
|
+
permission: string;
|
|
1441
|
+
} | "admins";
|
|
1442
|
+
export interface PluginNotification {
|
|
1443
|
+
title: string;
|
|
1444
|
+
body?: string;
|
|
1445
|
+
/** Defaults to "warning". */
|
|
1446
|
+
severity?: PluginNotificationSeverity;
|
|
1447
|
+
/**
|
|
1448
|
+
* What kind of alert this is, "<pluginId>.<kind>" by convention, e.g.
|
|
1449
|
+
* "acme-ssl.renewal_failed". Users route alerts to channels by it.
|
|
1450
|
+
* Defaults to the sending plugin's id.
|
|
1451
|
+
*/
|
|
1452
|
+
category?: string;
|
|
1453
|
+
audience?: PluginNotificationAudience;
|
|
1454
|
+
/**
|
|
1455
|
+
* Channels to deliver to on top of the recipient's own routing rules.
|
|
1456
|
+
* Only channels the recipient owns are used.
|
|
1457
|
+
*/
|
|
1458
|
+
channelIds?: number[];
|
|
1459
|
+
/** Where the alert points: a tab type to open, or an http(s) URL. */
|
|
1460
|
+
link?: {
|
|
1461
|
+
tab?: string;
|
|
1462
|
+
url?: string;
|
|
1463
|
+
};
|
|
1464
|
+
/** While an unread alert with this key exists, repeats are dropped. */
|
|
1465
|
+
dedupeKey?: string;
|
|
1466
|
+
/**
|
|
1467
|
+
* Extra fields a webhook receiver gets alongside the message. `sourceId`
|
|
1468
|
+
* and `sourceName` name what sent it (an automation, a rule).
|
|
1469
|
+
*/
|
|
1470
|
+
context?: {
|
|
1471
|
+
hostId?: number;
|
|
1472
|
+
hostName?: string;
|
|
1473
|
+
sourceId?: number | string;
|
|
1474
|
+
sourceName?: string;
|
|
1475
|
+
triggerType?: string;
|
|
1476
|
+
value?: unknown;
|
|
1477
|
+
threshold?: unknown;
|
|
1478
|
+
[key: string]: unknown;
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
export interface PluginNotifyResult {
|
|
1482
|
+
/** Users the alert reached. */
|
|
1483
|
+
recipients: number;
|
|
1484
|
+
/** Channel deliveries that succeeded, across every recipient. */
|
|
1485
|
+
delivered: number;
|
|
1486
|
+
failures: Array<{
|
|
1487
|
+
channelId: number;
|
|
1488
|
+
name: string;
|
|
1489
|
+
error: string;
|
|
1490
|
+
}>;
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* The plugin that stores alerts and delivers them to channels. Core resolves
|
|
1494
|
+
* the audience and hands the hub a list of user ids; it never sees a channel.
|
|
1495
|
+
*/
|
|
1496
|
+
export interface PluginNotifyHub {
|
|
1497
|
+
deliver: (input: {
|
|
1498
|
+
/** The plugin that sent the alert. */
|
|
1499
|
+
source: string;
|
|
1500
|
+
recipients: string[];
|
|
1501
|
+
notification: PluginNotification;
|
|
1502
|
+
}) => Promise<PluginNotifyResult>;
|
|
1503
|
+
channels: (userId: string) => Promise<PluginNotificationChannel[]>;
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Alerts. Every plugin sends through here and the hub plugin decides where
|
|
1507
|
+
* they land. Needs notify:send. With no hub running, send reaches nobody and
|
|
1508
|
+
* channels answers an empty list, so callers never have to check.
|
|
1509
|
+
*/
|
|
1510
|
+
export interface PluginNotify {
|
|
1511
|
+
/** The acting user's channels, without their config. Needs an actor. */
|
|
1512
|
+
channels: () => Promise<PluginNotificationChannel[]>;
|
|
1513
|
+
/**
|
|
1514
|
+
* Sends an alert. With no audience it goes to the acting user, so a call
|
|
1515
|
+
* with neither is refused. One channel failing does not stop the others.
|
|
1516
|
+
*/
|
|
1517
|
+
send: (notification: PluginNotification) => Promise<PluginNotifyResult>;
|
|
1518
|
+
/**
|
|
1519
|
+
* Makes this plugin the hub. Needs notify:hub. Only one hub runs at a time;
|
|
1520
|
+
* a second one is refused. Revoked on deactivate.
|
|
1521
|
+
*/
|
|
1522
|
+
serve: (hub: PluginNotifyHub) => () => void;
|
|
1523
|
+
}
|
|
1524
|
+
export interface PluginFetchInit {
|
|
1525
|
+
/** Return redirect responses without following them, or reject them (default). */
|
|
1526
|
+
redirect?: "error" | "manual";
|
|
1527
|
+
method?: string;
|
|
1528
|
+
headers?: Record<string, string>;
|
|
1529
|
+
body?: string;
|
|
1530
|
+
/**
|
|
1531
|
+
* Aborts the request when no response headers arrive within this long.
|
|
1532
|
+
* Defaults to 30 seconds. A streamed body may also sit idle between chunks
|
|
1533
|
+
* for this long, never less than 5 minutes.
|
|
1534
|
+
*/
|
|
1535
|
+
timeoutMs?: number;
|
|
1536
|
+
/**
|
|
1537
|
+
* Exact private or loopback hosts this request may reach. Everything else
|
|
1538
|
+
* private is refused, and redirects are never followed.
|
|
1539
|
+
*/
|
|
1540
|
+
allowPrivateHosts?: readonly string[];
|
|
1541
|
+
/** Aborts the request, and a streamed body, when the caller gives up. */
|
|
1542
|
+
signal?: AbortSignal;
|
|
1543
|
+
/**
|
|
1544
|
+
* TLS for a private CA. `ca` trusts this PEM bundle instead of the system
|
|
1545
|
+
* store. `rejectUnauthorized: false` skips verification, only for fetching
|
|
1546
|
+
* a CA root by fingerprint, where the caller checks the answer itself.
|
|
1547
|
+
*/
|
|
1548
|
+
tls?: {
|
|
1549
|
+
ca?: string;
|
|
1550
|
+
rejectUnauthorized?: boolean;
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Outbound HTTP through core's SSRF guard: http and https only, no embedded
|
|
1555
|
+
* credentials, DNS pinned, private addresses refused unless listed. Needs
|
|
1556
|
+
* network:outbound.
|
|
1557
|
+
*/
|
|
1558
|
+
export type PluginFetch = (url: string, init?: PluginFetchInit) => Promise<Response>;
|
|
1559
|
+
/**
|
|
1560
|
+
* A generic capability check, for a privileged action that has no dedicated
|
|
1561
|
+
* ctx method to wrap it: a plugin bundling its own native dependency to touch
|
|
1562
|
+
* hardware (a serial port, a USB device) is the first caller. Core cannot
|
|
1563
|
+
* mediate that access the way it mediates ctx.ssh or ctx.db, so the
|
|
1564
|
+
* capability here is a declared, reviewable, audited statement of intent
|
|
1565
|
+
* rather than a technical gate. Running programs has its own member,
|
|
1566
|
+
* ctx.process.
|
|
1567
|
+
*/
|
|
1568
|
+
export interface PluginCapabilities {
|
|
1569
|
+
/** Whether this plugin currently holds `capability`. Not audited: for deciding whether to offer something, not for gating an action. */
|
|
1570
|
+
has: (capability: string) => Promise<boolean>;
|
|
1571
|
+
/** Throws PluginCapabilityError if this plugin does not hold `capability`. Audited like every guarded ctx method. */
|
|
1572
|
+
require: (capability: string) => Promise<void>;
|
|
1573
|
+
}
|
|
1574
|
+
export interface PluginProcessOptions {
|
|
1575
|
+
/** Added to a small base environment (PATH, HOME, temp, locale, proxy), never the whole server one. */
|
|
1576
|
+
env?: Record<string, string>;
|
|
1577
|
+
cwd?: string;
|
|
1578
|
+
/** Kills the program with SIGKILL after this long. */
|
|
1579
|
+
timeoutMs?: number;
|
|
1580
|
+
}
|
|
1581
|
+
/** A running program started with ctx.process.run. */
|
|
1582
|
+
export interface PluginProcessHandle {
|
|
1583
|
+
readonly pid: number | undefined;
|
|
1584
|
+
/** Output as UTF-8 text, in the chunks it arrives in. */
|
|
1585
|
+
onStdout: (listener: (chunk: string) => void) => void;
|
|
1586
|
+
onStderr: (listener: (chunk: string) => void) => void;
|
|
1587
|
+
/** Settles when the program ends, or rejects when it cannot start. */
|
|
1588
|
+
readonly exited: Promise<{
|
|
1589
|
+
code: number | null;
|
|
1590
|
+
signal: string | null;
|
|
1591
|
+
}>;
|
|
1592
|
+
kill: (signal?: "SIGTERM" | "SIGKILL") => void;
|
|
1593
|
+
}
|
|
1594
|
+
/** A program a plugin downloads and runs, pinned to one checksum. */
|
|
1595
|
+
export interface PluginBinarySpec {
|
|
1596
|
+
/** File name under the plugin's data dir, e.g. "opkssh-linux-amd64". */
|
|
1597
|
+
name: string;
|
|
1598
|
+
version: string;
|
|
1599
|
+
/** https URL of the release asset. Redirects are followed. */
|
|
1600
|
+
url: string;
|
|
1601
|
+
/** Hex SHA-256 the file must have. */
|
|
1602
|
+
sha256: string;
|
|
1603
|
+
/**
|
|
1604
|
+
* Paths checked before downloading, such as a copy baked into the Docker
|
|
1605
|
+
* image. One whose checksum matches is used in place.
|
|
1606
|
+
*/
|
|
1607
|
+
prebuilt?: readonly string[];
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Programs on the Termix server. Needs process:spawn. Every program still
|
|
1611
|
+
* running is killed on deactivate.
|
|
1612
|
+
*/
|
|
1613
|
+
export interface PluginProcess {
|
|
1614
|
+
/** Starts `file` with `args`, no shell. Audited. */
|
|
1615
|
+
run: (file: string, args: readonly string[], options?: PluginProcessOptions) => Promise<PluginProcessHandle>;
|
|
1616
|
+
/**
|
|
1617
|
+
* Returns the path of a verified executable: a matching prebuilt copy, the
|
|
1618
|
+
* copy downloaded earlier, or a fresh download (which also needs
|
|
1619
|
+
* network:outbound). Throws when the checksum does not match.
|
|
1620
|
+
*/
|
|
1621
|
+
ensureBinary: (spec: PluginBinarySpec) => Promise<string>;
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Timers owned by the plugin. Every timer is cleared on deactivate, a run that
|
|
1625
|
+
* throws is logged instead of crashing the server, and a repeating job never
|
|
1626
|
+
* overlaps itself: a tick that arrives while the last run is still going is
|
|
1627
|
+
* skipped. No capability needed.
|
|
1628
|
+
*/
|
|
1629
|
+
export interface PluginSchedule {
|
|
1630
|
+
/** Runs `fn` every `intervalMs`. Returns a function that stops it. */
|
|
1631
|
+
every: (intervalMs: number, fn: () => void | Promise<void>, options?: {
|
|
1632
|
+
/** Delays the first run by up to this much, so many timers spread out. */
|
|
1633
|
+
jitterMs?: number;
|
|
1634
|
+
/** Also run once straight away. */
|
|
1635
|
+
runNow?: boolean;
|
|
1636
|
+
}) => () => void;
|
|
1637
|
+
/** Runs `fn` once after `delayMs`. Returns a function that cancels it. */
|
|
1638
|
+
after: (delayMs: number, fn: () => void | Promise<void>) => () => void;
|
|
1639
|
+
}
|
|
1640
|
+
/** The certificate core serves, read from its configured paths. */
|
|
1641
|
+
export interface PluginTlsCertificateInfo {
|
|
1642
|
+
subject: string;
|
|
1643
|
+
issuer: string;
|
|
1644
|
+
/** DNS names and IPs from the subject alternative names. */
|
|
1645
|
+
names: string[];
|
|
1646
|
+
notBefore: string;
|
|
1647
|
+
notAfter: string;
|
|
1648
|
+
/** True for core's own first-boot certificate. */
|
|
1649
|
+
selfSigned: boolean;
|
|
1650
|
+
/** SHA-256 fingerprint, colon separated. */
|
|
1651
|
+
fingerprint: string;
|
|
1652
|
+
}
|
|
1653
|
+
export interface PluginTlsStatus {
|
|
1654
|
+
/** Whether Termix is set up to serve HTTPS at all. */
|
|
1655
|
+
enabled: boolean;
|
|
1656
|
+
/** Null when there is no readable certificate. */
|
|
1657
|
+
certificate: PluginTlsCertificateInfo | null;
|
|
1658
|
+
/** The plugin that renews the certificate, when one is registered. */
|
|
1659
|
+
renewal: {
|
|
1660
|
+
pluginId: string;
|
|
1661
|
+
pluginName: string;
|
|
1662
|
+
} | null;
|
|
1663
|
+
}
|
|
1664
|
+
export interface PluginTlsReloadResult {
|
|
1665
|
+
/** Whether the new certificate is being served now. */
|
|
1666
|
+
applied: boolean;
|
|
1667
|
+
message: string;
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* The server certificate. Needs system:tls, and every call is audited. Core
|
|
1671
|
+
* owns serving it; a plugin can only replace it, reload it and answer ACME
|
|
1672
|
+
* http-01 challenges.
|
|
1673
|
+
*/
|
|
1674
|
+
export interface PluginSystem {
|
|
1675
|
+
tlsStatus: () => Promise<PluginTlsStatus>;
|
|
1676
|
+
/**
|
|
1677
|
+
* Replaces the served certificate and key. Throws when either does not
|
|
1678
|
+
* parse, the certificate has expired, or the key does not match it. Does
|
|
1679
|
+
* not reload: call reloadTls() after.
|
|
1680
|
+
*/
|
|
1681
|
+
writeTlsCertificate: (certificatePem: string, privateKeyPem: string) => Promise<PluginTlsCertificateInfo>;
|
|
1682
|
+
/** Serves the certificate on disk without a restart. */
|
|
1683
|
+
reloadTls: () => Promise<PluginTlsReloadResult>;
|
|
1684
|
+
/**
|
|
1685
|
+
* Answers GET /.well-known/acme-challenge/<token> with `content` until the
|
|
1686
|
+
* returned function is called or the plugin is disabled.
|
|
1687
|
+
*/
|
|
1688
|
+
publishHttpChallenge: (token: string, content: string) => Promise<() => void>;
|
|
1689
|
+
/**
|
|
1690
|
+
* Tells core this plugin renews the certificate, so admin settings stop
|
|
1691
|
+
* warning that nothing does. Dropped on deactivate.
|
|
1692
|
+
*/
|
|
1693
|
+
registerTlsRenewer: () => Promise<() => void>;
|
|
1694
|
+
}
|
|
1695
|
+
/** One installed plugin as ctx.plugins.list reports it. */
|
|
1696
|
+
export interface PluginSummary {
|
|
1697
|
+
id: string;
|
|
1698
|
+
version: string;
|
|
1699
|
+
source: "bundled" | "user";
|
|
1700
|
+
/** "active" when running; anything else means it is not. */
|
|
1701
|
+
state: string;
|
|
1702
|
+
}
|
|
1703
|
+
/** The installed plugins. Needs plugins:read. */
|
|
1704
|
+
export interface PluginPlugins {
|
|
1705
|
+
list: () => Promise<PluginSummary[]>;
|
|
1706
|
+
}
|
|
1707
|
+
export interface PluginContext {
|
|
1708
|
+
readonly pluginId: string;
|
|
1709
|
+
readonly manifest: PluginManifest;
|
|
1710
|
+
readonly log: PluginLogger;
|
|
1711
|
+
readonly events: PluginEvents;
|
|
1712
|
+
readonly kv: PluginKeyValue;
|
|
1713
|
+
readonly files: PluginFiles;
|
|
1714
|
+
readonly db: PluginDatabase;
|
|
1715
|
+
readonly sync: PluginSync;
|
|
1716
|
+
readonly registry: PluginRegistry;
|
|
1717
|
+
readonly services: PluginServices;
|
|
1718
|
+
readonly secrets: PluginSecrets;
|
|
1719
|
+
readonly http: PluginHttp;
|
|
1720
|
+
readonly ws: PluginWebSockets;
|
|
1721
|
+
readonly rbac: PluginRbac;
|
|
1722
|
+
readonly settings: PluginSettings;
|
|
1723
|
+
readonly disposables: PluginDisposables;
|
|
1724
|
+
/** A generic capability check, for privileged code no ctx method wraps. */
|
|
1725
|
+
readonly capabilities: PluginCapabilities;
|
|
1726
|
+
/** Hosts the actor can see, and host-sharing operations. Needs hosts:read / hosts:write. */
|
|
1727
|
+
readonly hosts: PluginHosts;
|
|
1728
|
+
/** SSH through core's connect pipeline. Needs ssh:connect and credentials:use. */
|
|
1729
|
+
readonly ssh: PluginSsh;
|
|
1730
|
+
/** Login methods, second factors and SSH auth types. Needs auth:provide. */
|
|
1731
|
+
readonly auth: PluginAuth;
|
|
1732
|
+
/** Opens Electron windows outside the main renderer. Needs desktop:window. */
|
|
1733
|
+
readonly desktop: PluginDesktop;
|
|
1734
|
+
/** Host protocol credentials and the user's saved SSH keys. See PluginCredentials. */
|
|
1735
|
+
readonly credentials: PluginCredentials;
|
|
1736
|
+
/** Audit lines under the plugin's own action names. */
|
|
1737
|
+
readonly audit: PluginAudit;
|
|
1738
|
+
/** Timers that stop on deactivate. */
|
|
1739
|
+
readonly schedule: PluginSchedule;
|
|
1740
|
+
/** Core's notification channels. Needs notify:send. */
|
|
1741
|
+
readonly notify: PluginNotify;
|
|
1742
|
+
/** Outbound HTTP through core's SSRF guard. Needs network:outbound. */
|
|
1743
|
+
readonly fetch: PluginFetch;
|
|
1744
|
+
/** Programs on the Termix server. Needs process:spawn. */
|
|
1745
|
+
readonly process: PluginProcess;
|
|
1746
|
+
/** The server certificate. Needs system:tls. */
|
|
1747
|
+
readonly system: PluginSystem;
|
|
1748
|
+
/** The installed plugins and whether each runs. Needs plugins:read. */
|
|
1749
|
+
readonly plugins: PluginPlugins;
|
|
1750
|
+
/**
|
|
1751
|
+
* Runs `fn` with `userId` as the acting user, for background work that has
|
|
1752
|
+
* no request behind it. Always audited, and every core API inside still
|
|
1753
|
+
* applies that user's RBAC.
|
|
1754
|
+
*
|
|
1755
|
+
* Needs users:impersonate. That and a `userId` option on services.get or
|
|
1756
|
+
* secrets.getShared are the only ways a plugin can name a user.
|
|
1757
|
+
*/
|
|
1758
|
+
asUser: <T>(userId: string, fn: () => Promise<T> | T) => Promise<T>;
|
|
1759
|
+
/** The acting user for the current call, when there is one. */
|
|
1760
|
+
currentActor: () => string | undefined;
|
|
1761
|
+
}
|
|
1762
|
+
export type PluginActivate = (ctx: PluginContext) => void | Promise<void>;
|
|
1763
|
+
export type PluginDeactivate = () => void | Promise<void>;
|
|
1764
|
+
export interface PluginModule {
|
|
1765
|
+
activate: PluginActivate;
|
|
1766
|
+
deactivate?: PluginDeactivate;
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Identity helper that gives a plugin entry its types without importing the
|
|
1770
|
+
* runtime. Everything a plugin creates belongs inside activate: the module is
|
|
1771
|
+
* cached by the ESM loader, so disable-then-enable re-runs activate on the
|
|
1772
|
+
* same module object.
|
|
1773
|
+
*/
|
|
1774
|
+
export declare function definePlugin(plugin: PluginModule): PluginModule;
|
|
1775
|
+
export type { PluginManifest } from "./manifest.js";
|
|
1776
|
+
export type { PluginTableDefinition } from "./db.js";
|
|
1777
|
+
export type { PluginSettingsField, PluginSettingsContribution, PluginSettingsScope, } from "./manifest.js";
|