@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.
Files changed (236) hide show
  1. package/README.md +52 -0
  2. package/cli/commands/build.mjs +246 -0
  3. package/cli/commands/migrations.mjs +248 -0
  4. package/cli/commands/pack.mjs +42 -0
  5. package/cli/commands/sign.mjs +83 -0
  6. package/cli/commands/test.mjs +33 -0
  7. package/cli/commands/validate.mjs +215 -0
  8. package/cli/index.mjs +65 -0
  9. package/cli/lib/changelog.mjs +126 -0
  10. package/cli/lib/externals.mjs +46 -0
  11. package/cli/lib/plugin-dir.mjs +29 -0
  12. package/cli/lib/signing.mjs +86 -0
  13. package/cli/lib/static-url-imports.mjs +54 -0
  14. package/cli/lib/tmxplug.mjs +106 -0
  15. package/cli/patches/guacamole-common-js-flush.cjs +76 -0
  16. package/cli/patches/xterm-android-ime.cjs +130 -0
  17. package/client.d.ts +13 -0
  18. package/dist/backend.d.ts +1777 -0
  19. package/dist/backend.js +84 -0
  20. package/dist/capabilities.d.ts +38 -0
  21. package/dist/capabilities.js +71 -0
  22. package/dist/db.d.ts +112 -0
  23. package/dist/db.js +189 -0
  24. package/dist/ddl.d.ts +76 -0
  25. package/dist/ddl.js +678 -0
  26. package/dist/desktop.d.ts +346 -0
  27. package/dist/desktop.js +5 -0
  28. package/dist/frontend.d.ts +1206 -0
  29. package/dist/frontend.js +388 -0
  30. package/dist/host/chunk-F3SDZDDG.js +6017 -0
  31. package/dist/host/chunk-XGWE7BTJ.js +90 -0
  32. package/dist/host/chunk-XH5NXL6Y.js +56314 -0
  33. package/dist/host/chunk-YC2MAKSP.js +14 -0
  34. package/dist/host/linked-server-FZGWQW32.js +12 -0
  35. package/dist/host/rbac-api-6KDMBVDZ.js +52 -0
  36. package/dist/host/testing-host.js +414 -0
  37. package/dist/host/theme.css +91 -0
  38. package/dist/host/types/src/types/auth-protocols.d.ts +10 -0
  39. package/dist/host/types/src/types/connection-log.d.ts +13 -0
  40. package/dist/host/types/src/types/credential-sidebar-preferences.d.ts +38 -0
  41. package/dist/host/types/src/types/host-defaults.d.ts +60 -0
  42. package/dist/host/types/src/types/host-sidebar-preferences.d.ts +55 -0
  43. package/dist/host/types/src/types/index.d.ts +298 -0
  44. package/dist/host/types/src/types/keybindings.d.ts +27 -0
  45. package/dist/host/types/src/types/ui-preferences.d.ts +102 -0
  46. package/dist/host/types/src/types/ui-types.d.ts +260 -0
  47. package/dist/host/types/src/ui/api/admin-user-data-api.d.ts +11 -0
  48. package/dist/host/types/src/ui/api/auth-methods-api.d.ts +63 -0
  49. package/dist/host/types/src/ui/api/credential-sidebar-preferences-api.d.ts +3 -0
  50. package/dist/host/types/src/ui/api/credentials-api.d.ts +56 -0
  51. package/dist/host/types/src/ui/api/dashboard-api.d.ts +23 -0
  52. package/dist/host/types/src/ui/api/external-account-api.d.ts +8 -0
  53. package/dist/host/types/src/ui/api/host-sidebar-preferences-api.d.ts +3 -0
  54. package/dist/host/types/src/ui/api/host-status-api.d.ts +11 -0
  55. package/dist/host/types/src/ui/api/open-tabs-api.d.ts +74 -0
  56. package/dist/host/types/src/ui/api/plugins-api.d.ts +128 -0
  57. package/dist/host/types/src/ui/api/rbac-api.d.ts +167 -0
  58. package/dist/host/types/src/ui/api/settings-api.d.ts +22 -0
  59. package/dist/host/types/src/ui/api/ssh-host-management-api.d.ts +65 -0
  60. package/dist/host/types/src/ui/api/sync-api.d.ts +93 -0
  61. package/dist/host/types/src/ui/api/system-status-api.d.ts +49 -0
  62. package/dist/host/types/src/ui/api/ui-preferences-api.d.ts +8 -0
  63. package/dist/host/types/src/ui/api/user-management-api.d.ts +80 -0
  64. package/dist/host/types/src/ui/auth/Auth.d.ts +12 -0
  65. package/dist/host/types/src/ui/auth/auth-bits.d.ts +11 -0
  66. package/dist/host/types/src/ui/auth/silent-signin.d.ts +2 -0
  67. package/dist/host/types/src/ui/components/FullScreenAppWrapper.d.ts +9 -0
  68. package/dist/host/types/src/ui/components/alert-dialog.d.ts +14 -0
  69. package/dist/host/types/src/ui/components/alert.d.ts +9 -0
  70. package/dist/host/types/src/ui/components/badge.d.ts +9 -0
  71. package/dist/host/types/src/ui/components/button.d.ts +13 -0
  72. package/dist/host/types/src/ui/components/card-grid/CardGridCanvas.d.ts +22 -0
  73. package/dist/host/types/src/ui/components/card-grid/layout-utils.d.ts +26 -0
  74. package/dist/host/types/src/ui/components/card-grid/types.d.ts +29 -0
  75. package/dist/host/types/src/ui/components/card-masonry.d.ts +16 -0
  76. package/dist/host/types/src/ui/components/card.d.ts +11 -0
  77. package/dist/host/types/src/ui/components/charts/BarSeries.d.ts +16 -0
  78. package/dist/host/types/src/ui/components/charts/LineChart.d.ts +17 -0
  79. package/dist/host/types/src/ui/components/charts/RadialGauge.d.ts +18 -0
  80. package/dist/host/types/src/ui/components/charts/Sparkline.d.ts +22 -0
  81. package/dist/host/types/src/ui/components/charts/StatRow.d.ts +15 -0
  82. package/dist/host/types/src/ui/components/charts/geometry.d.ts +20 -0
  83. package/dist/host/types/src/ui/components/charts/index.d.ts +4 -0
  84. package/dist/host/types/src/ui/components/checkbox.d.ts +4 -0
  85. package/dist/host/types/src/ui/components/connection/ConnectionGate.d.ts +56 -0
  86. package/dist/host/types/src/ui/components/connection/ConnectionLogPanel.d.ts +9 -0
  87. package/dist/host/types/src/ui/components/connection/ConnectionScreen.d.ts +34 -0
  88. package/dist/host/types/src/ui/components/connection/connection-status.d.ts +1 -0
  89. package/dist/host/types/src/ui/components/data-view.d.ts +32 -0
  90. package/dist/host/types/src/ui/components/dialog.d.ts +17 -0
  91. package/dist/host/types/src/ui/components/dropdown-menu.d.ts +29 -0
  92. package/dist/host/types/src/ui/components/empty-state.d.ts +19 -0
  93. package/dist/host/types/src/ui/components/form-fields.d.ts +115 -0
  94. package/dist/host/types/src/ui/components/input.d.ts +3 -0
  95. package/dist/host/types/src/ui/components/kbd.d.ts +12 -0
  96. package/dist/host/types/src/ui/components/label.d.ts +4 -0
  97. package/dist/host/types/src/ui/components/manager-card.d.ts +25 -0
  98. package/dist/host/types/src/ui/components/meter.d.ts +14 -0
  99. package/dist/host/types/src/ui/components/metric-card.d.ts +18 -0
  100. package/dist/host/types/src/ui/components/panel-layout.d.ts +83 -0
  101. package/dist/host/types/src/ui/components/password-input.d.ts +4 -0
  102. package/dist/host/types/src/ui/components/popover.d.ts +7 -0
  103. package/dist/host/types/src/ui/components/scroll-area.d.ts +5 -0
  104. package/dist/host/types/src/ui/components/section-card.d.ts +29 -0
  105. package/dist/host/types/src/ui/components/select.d.ts +15 -0
  106. package/dist/host/types/src/ui/components/select2.d.ts +8 -0
  107. package/dist/host/types/src/ui/components/separator.d.ts +4 -0
  108. package/dist/host/types/src/ui/components/sheet.d.ts +14 -0
  109. package/dist/host/types/src/ui/components/sidebar.d.ts +69 -0
  110. package/dist/host/types/src/ui/components/skeleton.d.ts +2 -0
  111. package/dist/host/types/src/ui/components/slider.d.ts +4 -0
  112. package/dist/host/types/src/ui/components/sonner.d.ts +4 -0
  113. package/dist/host/types/src/ui/components/surface/prompt-styles.d.ts +4 -0
  114. package/dist/host/types/src/ui/components/surface/surface-scope.d.ts +79 -0
  115. package/dist/host/types/src/ui/components/switch.d.ts +4 -0
  116. package/dist/host/types/src/ui/components/tab-strip.d.ts +19 -0
  117. package/dist/host/types/src/ui/components/tabs.d.ts +7 -0
  118. package/dist/host/types/src/ui/components/textarea.d.ts +4 -0
  119. package/dist/host/types/src/ui/components/theme-provider.d.ts +14 -0
  120. package/dist/host/types/src/ui/components/tooltip.d.ts +7 -0
  121. package/dist/host/types/src/ui/contexts/BrandingContext.d.ts +15 -0
  122. package/dist/host/types/src/ui/contexts/UiPreferencesContext.d.ts +38 -0
  123. package/dist/host/types/src/ui/contexts/branding-document.d.ts +10 -0
  124. package/dist/host/types/src/ui/dashboard/dashboard-cards-registry.d.ts +30 -0
  125. package/dist/host/types/src/ui/hooks/use-action-slot.d.ts +10 -0
  126. package/dist/host/types/src/ui/hooks/use-adaptive-polling.d.ts +5 -0
  127. package/dist/host/types/src/ui/hooks/use-mobile.d.ts +1 -0
  128. package/dist/host/types/src/ui/hooks/use-permissions.d.ts +29 -0
  129. package/dist/host/types/src/ui/hooks/use-status-color-scheme.d.ts +12 -0
  130. package/dist/host/types/src/ui/hooks/use-sync-status.d.ts +4 -0
  131. package/dist/host/types/src/ui/hooks/useSshAuthProviders.d.ts +17 -0
  132. package/dist/host/types/src/ui/i18n/i18n.d.ts +52 -0
  133. package/dist/host/types/src/ui/lib/ServerStatusContext.d.ts +33 -0
  134. package/dist/host/types/src/ui/lib/adaptive-polling.d.ts +29 -0
  135. package/dist/host/types/src/ui/lib/adaptive-resource-budget.d.ts +39 -0
  136. package/dist/host/types/src/ui/lib/app-keyboard-shortcuts.d.ts +8 -0
  137. package/dist/host/types/src/ui/lib/base-path.d.ts +1 -0
  138. package/dist/host/types/src/ui/lib/clipboard-provider.d.ts +9 -0
  139. package/dist/host/types/src/ui/lib/clipboard.d.ts +20 -0
  140. package/dist/host/types/src/ui/lib/connection-origin.d.ts +47 -0
  141. package/dist/host/types/src/ui/lib/db-health-monitor.d.ts +34 -0
  142. package/dist/host/types/src/ui/lib/device-id.d.ts +1 -0
  143. package/dist/host/types/src/ui/lib/electron.d.ts +1 -0
  144. package/dist/host/types/src/ui/lib/embedded-server-status.d.ts +15 -0
  145. package/dist/host/types/src/ui/lib/error-message.d.ts +9 -0
  146. package/dist/host/types/src/ui/lib/frontend-logger.d.ts +67 -0
  147. package/dist/host/types/src/ui/lib/global-shortcut-handler.d.ts +3 -0
  148. package/dist/host/types/src/ui/lib/host-defaults-context.d.ts +48 -0
  149. package/dist/host/types/src/ui/lib/hosts-request-cache.d.ts +9 -0
  150. package/dist/host/types/src/ui/lib/keybinding-match.d.ts +3 -0
  151. package/dist/host/types/src/ui/lib/linked-server.d.ts +10 -0
  152. package/dist/host/types/src/ui/lib/local-adaptive-engine.d.ts +40 -0
  153. package/dist/host/types/src/ui/lib/permissions.d.ts +16 -0
  154. package/dist/host/types/src/ui/lib/plugin-i18n.d.ts +11 -0
  155. package/dist/host/types/src/ui/lib/plugin-icon.d.ts +11 -0
  156. package/dist/host/types/src/ui/lib/plugin-transport.d.ts +50 -0
  157. package/dist/host/types/src/ui/lib/registry.d.ts +29 -0
  158. package/dist/host/types/src/ui/lib/rem.d.ts +7 -0
  159. package/dist/host/types/src/ui/lib/remote-server-api.d.ts +11 -0
  160. package/dist/host/types/src/ui/lib/server-status-store.d.ts +37 -0
  161. package/dist/host/types/src/ui/lib/sync-events.d.ts +2 -0
  162. package/dist/host/types/src/ui/lib/sync-status.d.ts +6 -0
  163. package/dist/host/types/src/ui/lib/tab-jump-hotkey.d.ts +4 -0
  164. package/dist/host/types/src/ui/lib/theme.d.ts +21 -0
  165. package/dist/host/types/src/ui/lib/ttl-request-cache.d.ts +10 -0
  166. package/dist/host/types/src/ui/lib/usage-color.d.ts +5 -0
  167. package/dist/host/types/src/ui/lib/utils.d.ts +2 -0
  168. package/dist/host/types/src/ui/lib/visible-interval.d.ts +5 -0
  169. package/dist/host/types/src/ui/lib/widget-title.d.ts +8 -0
  170. package/dist/host/types/src/ui/lib/ws-auth.d.ts +1 -0
  171. package/dist/host/types/src/ui/main-axios.d.ts +170 -0
  172. package/dist/host/types/src/ui/plugin-host/PluginViewPlaceholder.d.ts +13 -0
  173. package/dist/host/types/src/ui/plugin-host/app.d.ts +18 -0
  174. package/dist/host/types/src/ui/plugin-host/auth-registry.d.ts +50 -0
  175. package/dist/host/types/src/ui/plugin-host/bridge.d.ts +26 -0
  176. package/dist/host/types/src/ui/plugin-host/component-registry.d.ts +24 -0
  177. package/dist/host/types/src/ui/plugin-host/desktop.d.ts +7 -0
  178. package/dist/host/types/src/ui/plugin-host/extension-registry.d.ts +16 -0
  179. package/dist/host/types/src/ui/plugin-host/host-scope.d.ts +26 -0
  180. package/dist/host/types/src/ui/plugin-host/loader.d.ts +52 -0
  181. package/dist/host/types/src/ui/plugin-host/plugin-store.d.ts +40 -0
  182. package/dist/host/types/src/ui/plugin-host/scope.d.ts +20 -0
  183. package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +99 -0
  184. package/dist/host/types/src/ui/plugin-host/shell-bridge.d.ts +67 -0
  185. package/dist/host/types/src/ui/plugin-host/view-ownership.d.ts +31 -0
  186. package/dist/host/types/src/ui/settings/HostPluginSections.d.ts +31 -0
  187. package/dist/host/types/src/ui/settings/SettingsFields.d.ts +22 -0
  188. package/dist/host/types/src/ui/settings/host-feature-tabs.d.ts +11 -0
  189. package/dist/host/types/src/ui/settings/settings-components.d.ts +32 -0
  190. package/dist/host/types/src/ui/settings/settings-fields-util.d.ts +11 -0
  191. package/dist/host/types/src/ui/shell/ActionSlot.d.ts +41 -0
  192. package/dist/host/types/src/ui/shell/TabContext.d.ts +30 -0
  193. package/dist/host/types/src/ui/shell/action-registry.d.ts +99 -0
  194. package/dist/host/types/src/ui/shell/keybinding-registry.d.ts +62 -0
  195. package/dist/host/types/src/ui/shell/palette-registry.d.ts +55 -0
  196. package/dist/host/types/src/ui/shell/panel-registry.d.ts +24 -0
  197. package/dist/host/types/src/ui/shell/tab-registry.d.ts +92 -0
  198. package/dist/host/types/src/ui/sidebar/HostManagerTabs.d.ts +65 -0
  199. package/dist/host/types/src/ui/sidebar/host-contributions.d.ts +72 -0
  200. package/dist/host/types/src/ui/sidebar/host-defaults/catalog.d.ts +17 -0
  201. package/dist/host/types/src/ui/sidebar/host-protocols.d.ts +49 -0
  202. package/dist/host/types/src/ui/sidebar/quick-connect-host.d.ts +21 -0
  203. package/dist/host/types/src/ui/sidebar/rail-items.d.ts +103 -0
  204. package/dist/host/types/src/ui/ssh/connection-log/ConnectionLogContext.d.ts +17 -0
  205. package/dist/host/types/src/ui/ssh/dialogs/BrowserSignInDialog.d.ts +18 -0
  206. package/dist/host/types/src/ui/ssh/dialogs/HostKeyVerificationDialog.d.ts +17 -0
  207. package/dist/host/types/src/ui/ssh/dialogs/PassphraseDialog.d.ts +15 -0
  208. package/dist/host/types/src/ui/ssh/dialogs/SSHAuthDialog.d.ts +20 -0
  209. package/dist/host/types/src/ui/ssh/dialogs/TOTPDialog.d.ts +13 -0
  210. package/dist/host/ui.d.ts +1 -0
  211. package/dist/host/ui.js +31754 -0
  212. package/dist/host/use-permissions-NULZL43J.js +18 -0
  213. package/dist/host-commands.d.ts +89 -0
  214. package/dist/host-commands.js +387 -0
  215. package/dist/manifest.d.ts +461 -0
  216. package/dist/manifest.js +1499 -0
  217. package/dist/settings.d.ts +43 -0
  218. package/dist/settings.js +168 -0
  219. package/dist/ssh-certs.d.ts +25 -0
  220. package/dist/ssh-certs.js +234 -0
  221. package/dist/ssh-options.d.ts +23 -0
  222. package/dist/ssh-options.js +1 -0
  223. package/dist/table-builder.d.ts +26 -0
  224. package/dist/table-builder.js +87 -0
  225. package/dist/testing/safe-ports.d.ts +10 -0
  226. package/dist/testing/safe-ports.js +59 -0
  227. package/dist/testing/setup.d.ts +7 -0
  228. package/dist/testing/setup.js +146 -0
  229. package/dist/testing.d.ts +537 -0
  230. package/dist/testing.js +1451 -0
  231. package/dist/vitest-preset.d.ts +54 -0
  232. package/dist/vitest-preset.js +96 -0
  233. package/package.json +143 -0
  234. package/schema/changelog.schema.json +67 -0
  235. package/schema/manifest.schema.json +1160 -0
  236. 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";