@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,43 @@
1
+ /**
2
+ * Settings field validation, shared by core and the frontend.
3
+ *
4
+ * Both the HTTP routes and the form renderer call these, so "what counts as a
5
+ * valid value" is decided once. A field the manifest never declared has no
6
+ * entry here and is rejected rather than stored, which is what stops a PUT
7
+ * from writing arbitrary keys into a plugin's namespace.
8
+ */
9
+ import type { PluginSettingsField } from "./manifest.js";
10
+ /** The permission an admin-scope field requires when it names none itself. */
11
+ export declare const ADMIN_SETTINGS_PERMISSION = "admin.plugins.manage";
12
+ /** What a secret looks like on the wire. The value never leaves the server. */
13
+ export interface RedactedSecret {
14
+ set: boolean;
15
+ }
16
+ export declare function isRedactedSecret(value: unknown): value is RedactedSecret;
17
+ /**
18
+ * Turns whatever arrived on the wire into the type the field declares.
19
+ *
20
+ * Forms send everything as strings, so a number field that refused "30" would
21
+ * reject every value the UI can produce. Anything genuinely unconvertible is
22
+ * left alone for validateSettingValue to reject with a readable message.
23
+ */
24
+ export declare function coerceSettingValue(field: PluginSettingsField, value: unknown): unknown;
25
+ /**
26
+ * Returns an error message, or null when the value is acceptable.
27
+ *
28
+ * Messages are plain English rather than i18n keys: they name the offending
29
+ * field and the rule it broke, and the UI shows them beside the input.
30
+ */
31
+ export declare function validateSettingValue(field: PluginSettingsField, value: unknown): string | null;
32
+ /** The declared defaults for a scope, used when no row has been written. */
33
+ export declare function defaultsFor(fields?: readonly PluginSettingsField[]): Record<string, unknown>;
34
+ /** Looks a field up by key, for a caller holding only the declared list. */
35
+ export declare function findSettingsField(fields: readonly PluginSettingsField[], key: string): PluginSettingsField | undefined;
36
+ /**
37
+ * Whether a field should be shown or accepted, given the rest of the scope.
38
+ *
39
+ * A field declaring `requires` is inert while its named boolean is off, so the
40
+ * UI hides it and a write to it is pointless rather than wrong.
41
+ */
42
+ export declare function isFieldActive(field: PluginSettingsField, values: Record<string, unknown>): boolean;
43
+ export type { PluginSettingsField } from "./manifest.js";
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Settings field validation, shared by core and the frontend.
3
+ *
4
+ * Both the HTTP routes and the form renderer call these, so "what counts as a
5
+ * valid value" is decided once. A field the manifest never declared has no
6
+ * entry here and is rejected rather than stored, which is what stops a PUT
7
+ * from writing arbitrary keys into a plugin's namespace.
8
+ */
9
+ /** The permission an admin-scope field requires when it names none itself. */
10
+ export const ADMIN_SETTINGS_PERMISSION = "admin.plugins.manage";
11
+ export function isRedactedSecret(value) {
12
+ return (typeof value === "object" &&
13
+ value !== null &&
14
+ !Array.isArray(value) &&
15
+ Object.keys(value).length === 1 &&
16
+ typeof value.set === "boolean");
17
+ }
18
+ /**
19
+ * Turns whatever arrived on the wire into the type the field declares.
20
+ *
21
+ * Forms send everything as strings, so a number field that refused "30" would
22
+ * reject every value the UI can produce. Anything genuinely unconvertible is
23
+ * left alone for validateSettingValue to reject with a readable message.
24
+ */
25
+ export function coerceSettingValue(field, value) {
26
+ if (value === null || value === undefined)
27
+ return value;
28
+ switch (field.type) {
29
+ case "boolean":
30
+ if (typeof value === "boolean")
31
+ return value;
32
+ if (value === "true")
33
+ return true;
34
+ if (value === "false")
35
+ return false;
36
+ return value;
37
+ case "number": {
38
+ if (typeof value === "number")
39
+ return value;
40
+ if (typeof value === "string" && value.trim() !== "") {
41
+ const parsed = Number(value);
42
+ return Number.isNaN(parsed) ? value : parsed;
43
+ }
44
+ return value;
45
+ }
46
+ case "multiselect":
47
+ return Array.isArray(value) ? value : value === "" ? [] : value;
48
+ case "string":
49
+ case "secret":
50
+ case "textarea":
51
+ case "select":
52
+ return typeof value === "number" || typeof value === "boolean"
53
+ ? String(value)
54
+ : value;
55
+ default:
56
+ return value;
57
+ }
58
+ }
59
+ /**
60
+ * Returns an error message, or null when the value is acceptable.
61
+ *
62
+ * Messages are plain English rather than i18n keys: they name the offending
63
+ * field and the rule it broke, and the UI shows them beside the input.
64
+ */
65
+ export function validateSettingValue(field, value) {
66
+ // Clearing a field is always allowed; the default takes over on read.
67
+ if (value === null || value === undefined)
68
+ return null;
69
+ switch (field.type) {
70
+ case "boolean":
71
+ return typeof value === "boolean"
72
+ ? null
73
+ : `"${field.key}" must be true or false`;
74
+ case "number": {
75
+ if (typeof value !== "number" || !Number.isFinite(value)) {
76
+ return `"${field.key}" must be a number`;
77
+ }
78
+ if (field.min !== undefined && value < field.min) {
79
+ return `"${field.key}" must be at least ${field.min}`;
80
+ }
81
+ if (field.max !== undefined && value > field.max) {
82
+ return `"${field.key}" must be at most ${field.max}`;
83
+ }
84
+ return null;
85
+ }
86
+ case "select": {
87
+ if (typeof value !== "string") {
88
+ return `"${field.key}" must be a string`;
89
+ }
90
+ const allowed = (field.options ?? []).map((option) => option.value);
91
+ return allowed.includes(value)
92
+ ? null
93
+ : `"${field.key}" must be one of: ${allowed.join(", ")}`;
94
+ }
95
+ case "multiselect": {
96
+ if (!Array.isArray(value)) {
97
+ return `"${field.key}" must be an array`;
98
+ }
99
+ const allowed = (field.options ?? []).map((option) => option.value);
100
+ for (const entry of value) {
101
+ if (typeof entry !== "string" || !allowed.includes(entry)) {
102
+ return `"${field.key}" may only contain: ${allowed.join(", ")}`;
103
+ }
104
+ }
105
+ return null;
106
+ }
107
+ case "json": {
108
+ if (typeof value === "string") {
109
+ try {
110
+ JSON.parse(value);
111
+ return null;
112
+ }
113
+ catch {
114
+ return `"${field.key}" must be valid JSON`;
115
+ }
116
+ }
117
+ // Already-parsed JSON is fine as long as it survives a round trip.
118
+ try {
119
+ JSON.stringify(value);
120
+ return null;
121
+ }
122
+ catch {
123
+ return `"${field.key}" must be valid JSON`;
124
+ }
125
+ }
126
+ case "secret":
127
+ // A redacted echo is a no-op rather than a value, handled by the caller.
128
+ if (isRedactedSecret(value))
129
+ return null;
130
+ return typeof value === "string"
131
+ ? null
132
+ : `"${field.key}" must be a string`;
133
+ case "string":
134
+ case "textarea":
135
+ return typeof value === "string"
136
+ ? null
137
+ : `"${field.key}" must be a string`;
138
+ case "custom":
139
+ // The component owns its own shape; core only stores what it sends.
140
+ return null;
141
+ default:
142
+ return `"${field.key}" has an unknown type`;
143
+ }
144
+ }
145
+ /** The declared defaults for a scope, used when no row has been written. */
146
+ export function defaultsFor(fields = []) {
147
+ const defaults = {};
148
+ for (const field of fields) {
149
+ if (field.default !== undefined)
150
+ defaults[field.key] = field.default;
151
+ }
152
+ return defaults;
153
+ }
154
+ /** Looks a field up by key, for a caller holding only the declared list. */
155
+ export function findSettingsField(fields = [], key) {
156
+ return fields.find((field) => field.key === key);
157
+ }
158
+ /**
159
+ * Whether a field should be shown or accepted, given the rest of the scope.
160
+ *
161
+ * A field declaring `requires` is inert while its named boolean is off, so the
162
+ * UI hides it and a write to it is pointless rather than wrong.
163
+ */
164
+ export function isFieldActive(field, values) {
165
+ if (!field.requires)
166
+ return true;
167
+ return values[field.requires] === true;
168
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * OpenSSH certificate auth for ssh2, which has no native support for it.
3
+ *
4
+ * Grafts the certificate onto the parsed key, converts ECDSA signatures from
5
+ * DER to SSH wire format, and patches Protocol.authPK so the signature
6
+ * wrapper carries the base algorithm (OpenSSH's sshkey_check_sigtype wants
7
+ * that). Pure: no core state, it only touches the config and client it gets.
8
+ *
9
+ * Used by core key auth with a CA-signed certificate and by every plugin that
10
+ * issues short-lived certificates (opkssh, step-ca, vault).
11
+ */
12
+ import type { Client, ConnectConfig } from "ssh2";
13
+ export interface CertificateCredentials {
14
+ /** OpenSSH or PEM private key. */
15
+ privateKey: Buffer | string;
16
+ /** The certificate line, e.g. "ssh-ed25519-cert-v01@openssh.com AAAA...". */
17
+ certificate: string;
18
+ passphrase?: string;
19
+ }
20
+ /**
21
+ * Sets up certificate publickey auth on an ssh2 client. Call before
22
+ * `client.connect(config)`; it sets `config.authHandler` and
23
+ * `config.username`.
24
+ */
25
+ export declare function applyCertificateAuth(config: ConnectConfig, client: Client, credentials: CertificateCredentials, username: string): Promise<void>;
@@ -0,0 +1,234 @@
1
+ /**
2
+ * OpenSSH certificate auth for ssh2, which has no native support for it.
3
+ *
4
+ * Grafts the certificate onto the parsed key, converts ECDSA signatures from
5
+ * DER to SSH wire format, and patches Protocol.authPK so the signature
6
+ * wrapper carries the base algorithm (OpenSSH's sshkey_check_sigtype wants
7
+ * that). Pure: no core state, it only touches the config and client it gets.
8
+ *
9
+ * Used by core key auth with a CA-signed certificate and by every plugin that
10
+ * issues short-lived certificates (opkssh, step-ca, vault).
11
+ */
12
+ // Grafts an OpenSSH certificate onto an already-parsed private key object and
13
+ // patches the ssh2 client so that certificate-based publickey auth succeeds.
14
+ async function applyCertToConnection(config, client, privKey, certStr) {
15
+ // Extract cert type and blob from the stored certificate
16
+ const certParts = certStr.trim().split(/\s+/);
17
+ if (certParts.length < 2) {
18
+ throw new Error("Invalid certificate format: expected '<type> <base64>' string");
19
+ }
20
+ const certType = certParts[0];
21
+ const certBlob = Buffer.from(certParts[1], "base64");
22
+ // Graft cert type and blob onto the parsed private key
23
+ privKey.type = certType;
24
+ const pubSSHSym = Object.getOwnPropertySymbols(privKey).find((s) => String(s) === "Symbol(Public key SSH)");
25
+ if (!pubSSHSym) {
26
+ throw new Error("Cannot find public SSH symbol on parsed key; ssh2 internals may have changed");
27
+ }
28
+ privKey[pubSSHSym] = certBlob;
29
+ // Wrap sign() for ECDSA cert keys (DER → SSH wire format)
30
+ if (privKey.type.startsWith("ecdsa-")) {
31
+ const origSign = privKey.sign.bind(privKey);
32
+ privKey.sign = (data, algo) => {
33
+ const sigAlgo = algo?.includes("-cert-")
34
+ ? algo.replace(/-cert-v\d+@openssh\.com$/, "")
35
+ : algo;
36
+ const sig = origSign(data, sigAlgo);
37
+ if (sig instanceof Error || sig[0] !== 0x30)
38
+ return sig;
39
+ // Convert DER-encoded ECDSA signature to SSH wire format
40
+ try {
41
+ let pos = 2;
42
+ if (sig[1] & 0x80)
43
+ pos += sig[1] & 0x7f;
44
+ pos++;
45
+ const rLen = sig[pos++];
46
+ const r = sig.subarray(pos, pos + rLen);
47
+ pos += rLen + 1;
48
+ const sLen = sig[pos++];
49
+ const s = sig.subarray(pos, pos + sLen);
50
+ const out = Buffer.allocUnsafe(4 + r.length + 4 + s.length);
51
+ out.writeUInt32BE(r.length, 0);
52
+ r.copy(out, 4);
53
+ out.writeUInt32BE(s.length, 4 + r.length);
54
+ s.copy(out, 4 + r.length + 4);
55
+ return out;
56
+ }
57
+ catch {
58
+ return sig;
59
+ }
60
+ };
61
+ }
62
+ // Set up authHandler to bypass ssh2's cert type rejection
63
+ let certAuthAttempted = false;
64
+ const authHandler = (methodsLeft, _partialSuccess, callback) => {
65
+ const next = callback;
66
+ if (!certAuthAttempted &&
67
+ (!methodsLeft || methodsLeft.includes("publickey"))) {
68
+ certAuthAttempted = true;
69
+ next({
70
+ type: "publickey",
71
+ username: config.username,
72
+ key: privKey,
73
+ });
74
+ }
75
+ else {
76
+ next(false);
77
+ }
78
+ };
79
+ config.authHandler = authHandler;
80
+ // Monkey-patch Protocol.authPK after connect() to fix the signature
81
+ // wrapper algorithm for cert types.
82
+ const baseAlgo = certType.replace(/-cert-v\d+@openssh\.com$/, "");
83
+ const origConnect = client.connect.bind(client);
84
+ const patchedClient = client;
85
+ patchedClient.connect = (cfg) => {
86
+ const connectedClient = origConnect(cfg);
87
+ const proto = patchedClient._protocol;
88
+ if (!proto)
89
+ return connectedClient;
90
+ const origAuthPK = proto.authPK.bind(proto);
91
+ proto.authPK = (user, pubKey, keyAlgo, cbSign) => {
92
+ const isCertAuth = !!cbSign && pubKey?.type?.includes("-cert-");
93
+ if (!isCertAuth) {
94
+ return origAuthPK(user, pubKey, keyAlgo, cbSign);
95
+ }
96
+ // Signed auth with cert type: rebuild packet with base algo in
97
+ // the signature wrapper. keyAlgo may be undefined for ECDSA.
98
+ const certAlgo = keyAlgo || pubKey.type;
99
+ const pubSSH = pubKey.getPublicSSH();
100
+ const sessionID = proto._kex.sessionID;
101
+ const sesLen = sessionID.length;
102
+ const userLen = Buffer.byteLength(user);
103
+ const certAlgoLen = Buffer.byteLength(certAlgo);
104
+ const baseAlgoLen = Buffer.byteLength(baseAlgo);
105
+ const pubKeyLen = pubSSH.length;
106
+ // Build data to sign (uses the cert algo, which the server verifies against)
107
+ const sigDataLen = 4 +
108
+ sesLen +
109
+ 1 +
110
+ 4 +
111
+ userLen +
112
+ 4 +
113
+ 14 +
114
+ 4 +
115
+ 9 +
116
+ 1 +
117
+ 4 +
118
+ certAlgoLen +
119
+ 4 +
120
+ pubKeyLen;
121
+ const sigData = Buffer.allocUnsafe(sigDataLen);
122
+ let sp = 0;
123
+ sigData.writeUInt32BE(sesLen, sp);
124
+ sp += 4;
125
+ sessionID.copy(sigData, sp);
126
+ sp += sesLen;
127
+ sigData[sp++] = 50; // SSH_MSG_USERAUTH_REQUEST
128
+ sigData.writeUInt32BE(userLen, sp);
129
+ sp += 4;
130
+ sigData.write(user, sp, userLen, "utf8");
131
+ sp += userLen;
132
+ sigData.writeUInt32BE(14, sp);
133
+ sp += 4;
134
+ sigData.write("ssh-connection", sp, 14, "utf8");
135
+ sp += 14;
136
+ sigData.writeUInt32BE(9, sp);
137
+ sp += 4;
138
+ sigData.write("publickey", sp, 9, "utf8");
139
+ sp += 9;
140
+ sigData[sp++] = 1; // TRUE
141
+ sigData.writeUInt32BE(certAlgoLen, sp);
142
+ sp += 4;
143
+ sigData.write(certAlgo, sp, certAlgoLen, "utf8");
144
+ sp += certAlgoLen;
145
+ sigData.writeUInt32BE(pubKeyLen, sp);
146
+ sp += 4;
147
+ pubSSH.copy(sigData, sp);
148
+ cbSign(sigData, (signature) => {
149
+ const sigLen = signature.length;
150
+ const payloadLen = 1 +
151
+ 4 +
152
+ userLen +
153
+ 4 +
154
+ 14 +
155
+ 4 +
156
+ 9 +
157
+ 1 +
158
+ 4 +
159
+ certAlgoLen +
160
+ 4 +
161
+ pubKeyLen +
162
+ 4 +
163
+ 4 +
164
+ baseAlgoLen +
165
+ 4 +
166
+ sigLen;
167
+ const packet = proto._packetRW.write.alloc(payloadLen);
168
+ let pp = proto._packetRW.write.allocStart;
169
+ packet[pp] = 50; // SSH_MSG_USERAUTH_REQUEST
170
+ packet.writeUInt32BE(userLen, ++pp);
171
+ pp += 4;
172
+ packet.write(user, pp, userLen, "utf8");
173
+ pp += userLen;
174
+ packet.writeUInt32BE(14, pp);
175
+ pp += 4;
176
+ packet.write("ssh-connection", pp, 14, "utf8");
177
+ pp += 14;
178
+ packet.writeUInt32BE(9, pp);
179
+ pp += 4;
180
+ packet.write("publickey", pp, 9, "utf8");
181
+ pp += 9;
182
+ packet[pp++] = 1; // TRUE
183
+ // Header: cert type
184
+ packet.writeUInt32BE(certAlgoLen, pp);
185
+ pp += 4;
186
+ packet.write(certAlgo, pp, certAlgoLen, "utf8");
187
+ pp += certAlgoLen;
188
+ // Public key blob
189
+ packet.writeUInt32BE(pubKeyLen, pp);
190
+ pp += 4;
191
+ pubSSH.copy(packet, pp);
192
+ pp += pubKeyLen;
193
+ // Signature wrapper: base algo (NOT cert type)
194
+ packet.writeUInt32BE(4 + baseAlgoLen + 4 + sigLen, pp);
195
+ pp += 4;
196
+ packet.writeUInt32BE(baseAlgoLen, pp);
197
+ pp += 4;
198
+ packet.write(baseAlgo, pp, baseAlgoLen, "utf8");
199
+ pp += baseAlgoLen;
200
+ packet.writeUInt32BE(sigLen, pp);
201
+ pp += 4;
202
+ signature.copy(packet, pp);
203
+ proto._authsQueue.push("publickey");
204
+ proto._debug?.("Outbound: Sending USERAUTH_REQUEST (publickey)");
205
+ const finalized = proto._packetRW.write.finalize(packet);
206
+ proto._cipher.encrypt(finalized);
207
+ });
208
+ };
209
+ return connectedClient;
210
+ };
211
+ }
212
+ /**
213
+ * Sets up certificate publickey auth on an ssh2 client. Call before
214
+ * `client.connect(config)`; it sets `config.authHandler` and
215
+ * `config.username`.
216
+ */
217
+ export async function applyCertificateAuth(config, client, credentials, username) {
218
+ const { createRequire } = await import("node:module");
219
+ const esmRequire = createRequire(import.meta.url);
220
+ const { utils: { parseKey }, } = esmRequire("ssh2");
221
+ config.username = username;
222
+ const keyBuf = Buffer.isBuffer(credentials.privateKey)
223
+ ? credentials.privateKey
224
+ : Buffer.from(credentials.privateKey, "utf8");
225
+ const parsed = credentials.passphrase
226
+ ? parseKey(keyBuf, credentials.passphrase)
227
+ : parseKey(keyBuf);
228
+ if (parsed instanceof Error || !parsed) {
229
+ const reason = parsed instanceof Error ? parsed.message : "unknown error";
230
+ throw new Error(`Failed to parse private key for certificate auth: ${reason}`);
231
+ }
232
+ const privKey = (Array.isArray(parsed) ? parsed[0] : parsed);
233
+ await applyCertToConnection(config, client, privKey, credentials.certificate);
234
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A host's SSH connection options, as core's connect pipeline reads them
3
+ * (`ssh_data.ssh_options`). Shared by the backend and frontend host records.
4
+ */
5
+ export interface HostSshOptions {
6
+ /** Seconds between keepalive packets. */
7
+ keepaliveInterval?: number;
8
+ /** Missed keepalives before the connection is dropped. */
9
+ keepaliveCountMax?: number;
10
+ /** Offer older key exchange, cipher and MAC algorithms. On unless false. */
11
+ allowLegacyAlgorithms?: boolean;
12
+ /** Agent auth: the agent socket to use instead of the default. */
13
+ agentSocketPath?: string | null;
14
+ /** Agent auth: the key comment or fingerprint to offer first. */
15
+ agentIdentity?: string | null;
16
+ /** Forward the agent (or the host's key) into the session. */
17
+ agentForwarding?: boolean;
18
+ /** Variables exported into an interactive session. */
19
+ environmentVariables?: Array<{
20
+ key: string;
21
+ value: string;
22
+ }>;
23
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Turns a plugin's table definition into the Drizzle table it is queried
3
+ * through.
4
+ *
5
+ * One table object for every dialect. The sqlite-core definitions already
6
+ * encode correctly on each engine at query time, so the repositories use them
7
+ * everywhere; only DDL genuinely differs, and that lives in the SDK's ddl
8
+ * entry so the CLI and the server share one mapping.
9
+ */
10
+ import type { PluginTableDefinition } from "./db.js";
11
+ /**
12
+ * Builds the queryable table object.
13
+ *
14
+ * Always sqlite-core, on every dialect. drizzle's three table classes share no
15
+ * base class, and the query-builder surface a plugin uses is identical across
16
+ * them.
17
+ */
18
+ export declare function buildTable(pluginId: string, definition: PluginTableDefinition): unknown;
19
+ /**
20
+ * A minimal stand-in for a core table, for tests that join against users,
21
+ * ssh_data, roles or user_roles without core's schema. Only what
22
+ * createTestDb's stubs declare. The property name is camelCased and mapped
23
+ * to its snake_case column the same way buildTable does, so e.g. a stub's
24
+ * "displayName" property reads the real "display_name" SQL column.
25
+ */
26
+ export declare function buildRefTable(name: string, columns: Record<string, "text" | "integer" | "boolean">): unknown;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Turns a plugin's table definition into the Drizzle table it is queried
3
+ * through.
4
+ *
5
+ * One table object for every dialect. The sqlite-core definitions already
6
+ * encode correctly on each engine at query time, so the repositories use them
7
+ * everywhere; only DDL genuinely differs, and that lives in the SDK's ddl
8
+ * entry so the CLI and the server share one mapping.
9
+ */
10
+ import { sqliteTable, text as sqliteText, integer as sqliteInteger, real as sqliteReal, index as sqliteIndex, uniqueIndex as sqliteUniqueIndex, } from "drizzle-orm/sqlite-core";
11
+ import { sql } from "drizzle-orm";
12
+ import { prefixedTableName } from "./db.js";
13
+ import { columnName } from "./ddl.js";
14
+ /**
15
+ * Builds the queryable table object.
16
+ *
17
+ * Always sqlite-core, on every dialect. drizzle's three table classes share no
18
+ * base class, and the query-builder surface a plugin uses is identical across
19
+ * them.
20
+ */
21
+ export function buildTable(pluginId, definition) {
22
+ const physical = prefixedTableName(pluginId, definition.name);
23
+ const columns = {};
24
+ for (const [property, column] of Object.entries(definition.columns)) {
25
+ const name = columnName(property, column);
26
+ let built;
27
+ switch (column.type) {
28
+ case "id":
29
+ built = sqliteInteger(name).primaryKey({ autoIncrement: true });
30
+ break;
31
+ case "integer":
32
+ case "bigint":
33
+ case "refHost":
34
+ built = sqliteInteger(name);
35
+ break;
36
+ case "real":
37
+ built = sqliteReal(name);
38
+ break;
39
+ case "boolean":
40
+ built = sqliteInteger(name, { mode: "boolean" });
41
+ break;
42
+ default:
43
+ built = sqliteText(name);
44
+ break;
45
+ }
46
+ if (column.type !== "id") {
47
+ if (column.primaryKey)
48
+ built = built.primaryKey();
49
+ if (column.notNull)
50
+ built = built.notNull();
51
+ if (column.unique)
52
+ built = built.unique();
53
+ if (column.defaultNow) {
54
+ built = built.default(sql `CURRENT_TIMESTAMP`);
55
+ }
56
+ else if (column.defaultValue !== undefined) {
57
+ built = built.default(column.defaultValue);
58
+ }
59
+ }
60
+ columns[property] = built;
61
+ }
62
+ return sqliteTable(physical, columns, (table) => definition.indexes.map((entry) => {
63
+ const make = entry.unique ? sqliteUniqueIndex : sqliteIndex;
64
+ const target = table;
65
+ return make(entry.name).on(...entry.columns.map((c) => target[c]));
66
+ }));
67
+ }
68
+ /**
69
+ * A minimal stand-in for a core table, for tests that join against users,
70
+ * ssh_data, roles or user_roles without core's schema. Only what
71
+ * createTestDb's stubs declare. The property name is camelCased and mapped
72
+ * to its snake_case column the same way buildTable does, so e.g. a stub's
73
+ * "displayName" property reads the real "display_name" SQL column.
74
+ */
75
+ export function buildRefTable(name, columns) {
76
+ const built = {};
77
+ for (const [property, type] of Object.entries(columns)) {
78
+ const columnSqlName = columnName(property);
79
+ built[property] =
80
+ type === "boolean"
81
+ ? sqliteInteger(columnSqlName, { mode: "boolean" })
82
+ : type === "integer"
83
+ ? sqliteInteger(columnSqlName)
84
+ : sqliteText(columnSqlName);
85
+ }
86
+ return sqliteTable(name, built);
87
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Keeps a test server's random port off the fetch spec's blocked list.
3
+ *
4
+ * Tests start servers on port 0 and call them with fetch, which refuses
5
+ * ports like 6000 or 6667 with "bad port". Linux never hands those out as
6
+ * ephemeral ports, but Windows can (its dynamic range may start at 1024), so
7
+ * a server that lands on one closes and listens again.
8
+ */
9
+ export declare const FETCH_BLOCKED_PORTS: Set<number>;
10
+ export declare function installSafePorts(): void;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Keeps a test server's random port off the fetch spec's blocked list.
3
+ *
4
+ * Tests start servers on port 0 and call them with fetch, which refuses
5
+ * ports like 6000 or 6667 with "bad port". Linux never hands those out as
6
+ * ephemeral ports, but Windows can (its dynamic range may start at 1024), so
7
+ * a server that lands on one closes and listens again.
8
+ */
9
+ import net from "node:net";
10
+ export const FETCH_BLOCKED_PORTS = new Set([
11
+ 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79,
12
+ 87, 95, 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137,
13
+ 139, 143, 161, 179, 389, 427, 465, 512, 513, 514, 515, 526, 530, 531, 532,
14
+ 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993, 995, 1719, 1720, 1723,
15
+ 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668, 6669,
16
+ 6679, 6697, 10080,
17
+ ]);
18
+ function wantsRandomPort(args) {
19
+ const first = args[0];
20
+ if (first === 0 || first === "0")
21
+ return true;
22
+ if (first && typeof first === "object" && !Array.isArray(first)) {
23
+ const port = first.port;
24
+ return port === 0 || port === "0";
25
+ }
26
+ return false;
27
+ }
28
+ const PATCHED = Symbol.for("termix.safePorts");
29
+ export function installSafePorts() {
30
+ const proto = net.Server.prototype;
31
+ if (proto[PATCHED])
32
+ return;
33
+ proto[PATCHED] = true;
34
+ const listen = proto.listen;
35
+ proto.listen = function (...args) {
36
+ if (wantsRandomPort(args))
37
+ retryOnBlockedPort(this, args);
38
+ return listen.apply(this, args);
39
+ };
40
+ }
41
+ function retryOnBlockedPort(server, args) {
42
+ const emit = server.emit;
43
+ // The caller's callback is already a pending "listening" listener, so a
44
+ // retry leaves it out and it fires once, on the good port.
45
+ const retryArgs = args.filter((arg) => typeof arg !== "function");
46
+ server.emit = ((event, ...rest) => {
47
+ if (event === "listening") {
48
+ const address = server.address();
49
+ if (address &&
50
+ typeof address === "object" &&
51
+ FETCH_BLOCKED_PORTS.has(address.port)) {
52
+ server.close(() => server.listen(...retryArgs));
53
+ return true;
54
+ }
55
+ server.emit = emit;
56
+ }
57
+ return emit.call(server, event, ...rest);
58
+ });
59
+ }