@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,1499 @@
1
+ /**
2
+ * Manifest v2: the inert JSON description of a plugin.
3
+ *
4
+ * Core reads this without running any plugin code, so everything that decides
5
+ * whether a plugin may load, what it may do and where its UI appears has to be
6
+ * expressible here.
7
+ *
8
+ * Unknown fields are rejected at every level. A typo'd "contribute" or
9
+ * "permissionGroups" used to validate clean and be silently dropped, which
10
+ * made a manifest look like it declared something it did not.
11
+ */
12
+ import semver from "semver";
13
+ import { isKnownCapability } from "./capabilities.js";
14
+ /**
15
+ * The plugin API this build implements. A minor bump adds to the API and
16
+ * never removes; a major bump may break plugins.
17
+ *
18
+ * engine.api is a semver range checked against this ("1" means any 1.x,
19
+ * "^1.2" needs 1.2 or later). engine.termix is checked against the core
20
+ * version by the server.
21
+ */
22
+ export const PLUGIN_API_VERSION = "1.0.0";
23
+ /** The API major, the default engine.api for a new plugin. */
24
+ export const SUPPORTED_PLUGIN_API_VERSION = String(semver.major(PLUGIN_API_VERSION));
25
+ /** Whether this build's plugin API satisfies a manifest's engine.api. */
26
+ export function isApiCompatible(range) {
27
+ return (semver.validRange(range) !== null &&
28
+ semver.satisfies(PLUGIN_API_VERSION, range));
29
+ }
30
+ /**
31
+ * Whether a core version satisfies a manifest's engine.termix. A prerelease
32
+ * core (2.9.0-beta.1) counts as that release.
33
+ */
34
+ export function isTermixCompatible(range, coreVersion) {
35
+ if (!coreVersion || !semver.valid(semver.coerce(coreVersion)))
36
+ return true;
37
+ if (semver.validRange(range) === null)
38
+ return false;
39
+ return semver.satisfies(semver.coerce(coreVersion).version, range);
40
+ }
41
+ export const PLUGIN_CATEGORIES = [
42
+ "Terminal",
43
+ "Files & Transfer",
44
+ "Infrastructure",
45
+ "Monitoring",
46
+ "Networking",
47
+ "Access & Security",
48
+ "Productivity",
49
+ ];
50
+ export const PLUGIN_PLATFORMS = ["linux", "win32", "darwin"];
51
+ export const ACTION_CONTRIBUTION_KINDS = ["button", "component"];
52
+ const OPEN_FROM_VALUES = ["rail", "host-context-menu", "palette"];
53
+ const ID_PATTERN = /^[a-z][a-z0-9-]{1,39}$/;
54
+ /**
55
+ * Ids no plugin may take. A permission is registered as <id>.<name>, so a
56
+ * plugin called "admin" would mint admin.* permissions and could default them
57
+ * onto the user role; the rest are names core or the runtime already uses.
58
+ */
59
+ export const RESERVED_PLUGIN_IDS = [
60
+ "admin",
61
+ "hosts",
62
+ "credentials",
63
+ "core",
64
+ "termix",
65
+ "plugin",
66
+ "plugins",
67
+ "users",
68
+ "system",
69
+ "sdk",
70
+ ];
71
+ /** A preset key is a plain property name, never a prototype one. */
72
+ const PRESET_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/;
73
+ const SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z-.]+)?(\+[0-9A-Za-z-.]+)?$/;
74
+ /** Service names are dotted, e.g. "ssh.transport". */
75
+ const SERVICE_PATTERN = /^[a-z0-9-]+(\.[a-z0-9-]+)+$/;
76
+ const SECRET_KEY_PATTERN = /^[a-z0-9-]+$/;
77
+ /** Action ids name a frontend function, so segments may be camelCase. */
78
+ const ACTION_ID_PATTERN = /^[a-z0-9-]+(\.[a-zA-Z0-9-]+)+$/;
79
+ const HANDLER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
80
+ /**
81
+ * A plugin-relative permission name. Unlike a full id one segment is fine,
82
+ * and underscores are allowed in the first segment too ("manage_providers").
83
+ */
84
+ const PERMISSION_NAME_PATTERN = /^[a-z0-9_-]+(\.[a-z0-9_-]+)*$/;
85
+ /** The roles core seeds. A plugin may only set defaults for these. */
86
+ export const SYSTEM_ROLE_NAMES = ["admin", "user"];
87
+ /**
88
+ * Core permission groups. A plugin permission may not start with one of these,
89
+ * or a manifest could declare admin.users.manage and gate a route on it.
90
+ */
91
+ export const RESERVED_PERMISSION_PREFIXES = [
92
+ "hosts",
93
+ "credentials",
94
+ "admin",
95
+ ];
96
+ /** The id a plugin-relative permission name is registered under. */
97
+ export function qualifyPermission(pluginId, name) {
98
+ return `${pluginId}.${name}`;
99
+ }
100
+ export const SETTINGS_FIELD_TYPES = [
101
+ "boolean",
102
+ "string",
103
+ "number",
104
+ "select",
105
+ "multiselect",
106
+ "secret",
107
+ "textarea",
108
+ "json",
109
+ "custom",
110
+ ];
111
+ export const SETTINGS_SCOPES = ["admin", "user", "host"];
112
+ /** Protocol ids core keeps for itself. */
113
+ export const RESERVED_PROTOCOL_IDS = ["ssh"];
114
+ /** Login keys a declared credential field may not reuse. */
115
+ export const RESERVED_PROTOCOL_FIELD_KEYS = [
116
+ "username",
117
+ "password",
118
+ "authType",
119
+ "credentialId",
120
+ ];
121
+ /** Keybinding action types the shell runs itself; no plugin may declare them. */
122
+ export const CORE_KEYBINDING_ACTIONS = [
123
+ "nextTab",
124
+ "previousTab",
125
+ "openCommandPalette",
126
+ "reconnectSession",
127
+ ];
128
+ export const PLUGIN_DESKTOP_MODES = [
129
+ "mirror",
130
+ "local",
131
+ "server",
132
+ ];
133
+ export const DEFAULT_BACKEND_ENTRY = "dist/backend.js";
134
+ export const DEFAULT_FRONTEND_ENTRY = "dist/frontend.js";
135
+ export const DEFAULT_LOCALES_DIR = "locales";
136
+ const REQUIRED_TOP_LEVEL = [
137
+ "id",
138
+ "name",
139
+ "version",
140
+ "description",
141
+ "author",
142
+ "license",
143
+ "engine",
144
+ "category",
145
+ "capabilities",
146
+ ];
147
+ const ALLOWED_TOP_LEVEL = new Set([
148
+ ...REQUIRED_TOP_LEVEL,
149
+ "repository",
150
+ "icon",
151
+ "dependencies",
152
+ "optionalDependencies",
153
+ "provides",
154
+ "requires",
155
+ "providesSecret",
156
+ "requiresSecret",
157
+ "contributes",
158
+ "backend",
159
+ "frontend",
160
+ "locales",
161
+ "platforms",
162
+ "nativeDependencies",
163
+ "desktop",
164
+ ]);
165
+ const ALLOWED_CONTRIBUTES = new Set([
166
+ "tabs",
167
+ "panels",
168
+ "dashboardCards",
169
+ "guest",
170
+ "guestViews",
171
+ "actions",
172
+ "actionSlots",
173
+ "permissions",
174
+ "settings",
175
+ "hostCapability",
176
+ "http",
177
+ "uiPresets",
178
+ "auth",
179
+ "syncEntities",
180
+ "uses",
181
+ "keybindingActions",
182
+ "protocols",
183
+ ]);
184
+ const ALLOWED_SETTINGS_FIELD = [
185
+ "key",
186
+ "type",
187
+ "labelKey",
188
+ "descriptionKey",
189
+ "placeholderKey",
190
+ "default",
191
+ "options",
192
+ "min",
193
+ "max",
194
+ "requires",
195
+ "permission",
196
+ "group",
197
+ "component",
198
+ "hidden",
199
+ "defaultable",
200
+ "defaultLevels",
201
+ "personal",
202
+ "shareRead",
203
+ "ownerOnly",
204
+ "secretKeys",
205
+ ];
206
+ const SHARE_LEVELS = ["connect", "view", "edit", "manage"];
207
+ /** Settings keys are stored as-is, so they stay short and index-safe. */
208
+ const SETTINGS_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
209
+ function isPlainObject(value) {
210
+ return typeof value === "object" && value !== null && !Array.isArray(value);
211
+ }
212
+ /** Pushes an error for every key that is not in `allowed`. */
213
+ function rejectUnknown(value, allowed, where, errors) {
214
+ const set = Array.isArray(allowed) ? new Set(allowed) : allowed;
215
+ for (const key of Object.keys(value)) {
216
+ if (!set.has(key)) {
217
+ errors.push(`Unknown field "${key}" in ${where}`);
218
+ }
219
+ }
220
+ }
221
+ /**
222
+ * A path inside the plugin folder. Absolute paths, ".." and backslashes are
223
+ * refused so an entry point or asset folder cannot point outside the plugin.
224
+ */
225
+ function requireRelativePath(value, where, errors) {
226
+ const segments = value.split("/");
227
+ if (value.startsWith("/") ||
228
+ /^[A-Za-z]:/.test(value) ||
229
+ value.includes("\\") ||
230
+ value.includes("\0") ||
231
+ segments.includes("..") ||
232
+ segments.every((segment) => segment === "" || segment === ".")) {
233
+ errors.push(`Field "${where}" must be a relative path inside the plugin, got: ${JSON.stringify(value)}`);
234
+ }
235
+ }
236
+ function requireString(value, where, errors) {
237
+ if (typeof value !== "string" || value.trim().length === 0) {
238
+ errors.push(`Field "${where}" must be a non-empty string`);
239
+ return false;
240
+ }
241
+ return true;
242
+ }
243
+ export function validateManifest(manifest) {
244
+ const errors = [];
245
+ if (!isPlainObject(manifest)) {
246
+ return ["Manifest must be a JSON object"];
247
+ }
248
+ const m = manifest;
249
+ rejectUnknown(m, ALLOWED_TOP_LEVEL, "the manifest", errors);
250
+ for (const field of REQUIRED_TOP_LEVEL) {
251
+ if (!(field in m))
252
+ errors.push(`Missing required field: "${field}"`);
253
+ }
254
+ if ("id" in m) {
255
+ if (typeof m.id !== "string" || !ID_PATTERN.test(m.id)) {
256
+ errors.push(`Field "id" must match ${ID_PATTERN} (lowercase, starts with a letter, 2-40 chars), got: ${JSON.stringify(m.id)}`);
257
+ }
258
+ else if (RESERVED_PLUGIN_IDS.includes(m.id)) {
259
+ errors.push(`Field "id" may not be "${m.id}": that name is reserved`);
260
+ }
261
+ }
262
+ if ("name" in m)
263
+ requireString(m.name, "name", errors);
264
+ if ("description" in m)
265
+ requireString(m.description, "description", errors);
266
+ if ("license" in m)
267
+ requireString(m.license, "license", errors);
268
+ if ("version" in m) {
269
+ if (typeof m.version !== "string" || !SEMVER_PATTERN.test(m.version)) {
270
+ errors.push(`Field "version" must be valid semver, got: ${JSON.stringify(m.version)}`);
271
+ }
272
+ }
273
+ if ("repository" in m)
274
+ requireString(m.repository, "repository", errors);
275
+ if ("icon" in m)
276
+ requireString(m.icon, "icon", errors);
277
+ for (const field of ["backend", "frontend", "locales"]) {
278
+ if (field in m && requireString(m[field], field, errors)) {
279
+ requireRelativePath(m[field], field, errors);
280
+ }
281
+ }
282
+ validateAuthor(m.author, errors);
283
+ validateEngine(m.engine, errors);
284
+ validateCategory(m.category, errors);
285
+ validateCapabilities(m.capabilities, errors);
286
+ validatePlatforms(m.platforms, errors);
287
+ validateNativeDependencies(m.nativeDependencies, errors);
288
+ if (m.desktop !== undefined &&
289
+ !PLUGIN_DESKTOP_MODES.includes(m.desktop)) {
290
+ errors.push(`Field "desktop" must be one of: ${PLUGIN_DESKTOP_MODES.join(", ")}`);
291
+ }
292
+ validateDependencyMap(m.dependencies, "dependencies", errors);
293
+ validateDependencyMap(m.optionalDependencies, "optionalDependencies", errors);
294
+ validateProvides(m.provides, errors);
295
+ validateRequires(m.requires, errors);
296
+ validateProvidesSecret(m.providesSecret, errors);
297
+ validateRequiresSecret(m.requiresSecret, errors);
298
+ validateContributes(m.contributes, typeof m.id === "string" ? m.id : undefined, errors);
299
+ return errors;
300
+ }
301
+ function validateAuthor(author, errors) {
302
+ if (author === undefined)
303
+ return;
304
+ if (!isPlainObject(author)) {
305
+ errors.push('Field "author" must be an object');
306
+ return;
307
+ }
308
+ rejectUnknown(author, ["name", "url"], '"author"', errors);
309
+ requireString(author.name, "author.name", errors);
310
+ if ("url" in author)
311
+ requireString(author.url, "author.url", errors);
312
+ }
313
+ function validateEngine(engine, errors) {
314
+ if (engine === undefined)
315
+ return;
316
+ if (!isPlainObject(engine)) {
317
+ errors.push('Field "engine" must be an object');
318
+ return;
319
+ }
320
+ rejectUnknown(engine, ["termix", "api"], '"engine"', errors);
321
+ requireString(engine.termix, "engine.termix", errors);
322
+ if (typeof engine.termix === "string" &&
323
+ semver.validRange(engine.termix) === null) {
324
+ errors.push(`Field "engine.termix" must be a semver range, got: ${JSON.stringify(engine.termix)}`);
325
+ }
326
+ if (typeof engine.api !== "string" ||
327
+ semver.validRange(engine.api) === null) {
328
+ errors.push(`Field "engine.api" must be a semver range such as "1" or "^1.2", got: ${JSON.stringify(engine.api)}`);
329
+ }
330
+ }
331
+ function validateCategory(category, errors) {
332
+ if (category === undefined)
333
+ return;
334
+ if (typeof category !== "string") {
335
+ errors.push('Field "category" must be a string');
336
+ return;
337
+ }
338
+ if (!PLUGIN_CATEGORIES.includes(category)) {
339
+ errors.push(`Field "category" must be one of: ${PLUGIN_CATEGORIES.join(", ")}, got: "${category}"`);
340
+ }
341
+ }
342
+ function validateCapabilities(capabilities, errors) {
343
+ if (capabilities === undefined)
344
+ return;
345
+ if (!Array.isArray(capabilities)) {
346
+ errors.push('Field "capabilities" must be an array of capability ids');
347
+ return;
348
+ }
349
+ const seen = new Set();
350
+ capabilities.forEach((capability, index) => {
351
+ if (typeof capability !== "string") {
352
+ errors.push(`capabilities[${index}] must be a string`);
353
+ return;
354
+ }
355
+ if (!isKnownCapability(capability)) {
356
+ errors.push(`capabilities[${index}] is not a known capability: "${capability}"`);
357
+ return;
358
+ }
359
+ if (seen.has(capability)) {
360
+ errors.push(`capabilities[${index}] duplicates "${capability}"`);
361
+ }
362
+ seen.add(capability);
363
+ });
364
+ }
365
+ function validatePlatforms(platforms, errors) {
366
+ if (platforms === undefined)
367
+ return;
368
+ if (!Array.isArray(platforms) || platforms.length === 0) {
369
+ errors.push('Field "platforms" must be a non-empty array when present');
370
+ return;
371
+ }
372
+ platforms.forEach((platform, index) => {
373
+ if (!PLUGIN_PLATFORMS.includes(platform)) {
374
+ errors.push(`platforms[${index}] must be one of: ${PLUGIN_PLATFORMS.join(", ")}, got: ${JSON.stringify(platform)}`);
375
+ }
376
+ });
377
+ }
378
+ function validateNativeDependencies(value, errors) {
379
+ if (value === undefined)
380
+ return;
381
+ if (!Array.isArray(value) || value.length === 0) {
382
+ errors.push('Field "nativeDependencies" must be a non-empty array when present');
383
+ return;
384
+ }
385
+ value.forEach((entry, index) => {
386
+ if (typeof entry !== "string" || entry.length === 0) {
387
+ errors.push(`nativeDependencies[${index}] must be a non-empty string`);
388
+ }
389
+ });
390
+ }
391
+ function validateDependencyMap(value, field, errors) {
392
+ if (value === undefined)
393
+ return;
394
+ if (!isPlainObject(value)) {
395
+ errors.push(`Field "${field}" must be an object of pluginId -> semver range`);
396
+ return;
397
+ }
398
+ for (const [pluginId, range] of Object.entries(value)) {
399
+ if (!ID_PATTERN.test(pluginId)) {
400
+ errors.push(`${field} key "${pluginId}" is not a valid plugin id`);
401
+ }
402
+ if (typeof range !== "string" || !semver.validRange(range)) {
403
+ errors.push(`${field}["${pluginId}"] must be a valid semver range, got: ${JSON.stringify(range)}`);
404
+ }
405
+ }
406
+ }
407
+ function validateProvides(provides, errors) {
408
+ if (provides === undefined)
409
+ return;
410
+ if (!Array.isArray(provides)) {
411
+ errors.push('Field "provides" must be an array');
412
+ return;
413
+ }
414
+ const seen = new Set();
415
+ provides.forEach((raw, index) => {
416
+ const where = `provides[${index}]`;
417
+ if (!isPlainObject(raw)) {
418
+ errors.push(`${where} must be an object`);
419
+ return;
420
+ }
421
+ rejectUnknown(raw, ["service", "version", "permission", "names"], where, errors);
422
+ if (typeof raw.service !== "string" || !SERVICE_PATTERN.test(raw.service)) {
423
+ errors.push(`${where}.service must be a dotted lowercase name`);
424
+ }
425
+ else {
426
+ if (seen.has(raw.service)) {
427
+ errors.push(`${where}.service duplicates "${raw.service}"`);
428
+ }
429
+ seen.add(raw.service);
430
+ }
431
+ if (typeof raw.version !== "string" || !SEMVER_PATTERN.test(raw.version)) {
432
+ errors.push(`${where}.version must be valid semver`);
433
+ }
434
+ requireString(raw.permission, `${where}.permission`, errors);
435
+ if (raw.names !== undefined) {
436
+ validateNameList(raw.names, `${where}.names`, errors);
437
+ }
438
+ });
439
+ }
440
+ const PROVIDER_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
441
+ function validateSyncEntities(value, errors) {
442
+ const where = "contributes.syncEntities";
443
+ if (!Array.isArray(value) || value.length === 0) {
444
+ errors.push(`${where} must be a non-empty array`);
445
+ return;
446
+ }
447
+ const seen = new Set();
448
+ for (const name of value) {
449
+ if (typeof name !== "string" || !/^[a-zA-Z][a-zA-Z0-9]*$/.test(name)) {
450
+ errors.push(`${where} entries must be letters and digits`);
451
+ continue;
452
+ }
453
+ if (seen.has(name))
454
+ errors.push(`${where} duplicates "${name}"`);
455
+ seen.add(name);
456
+ }
457
+ }
458
+ function validateUses(value, errors) {
459
+ const where = "contributes.uses";
460
+ if (!Array.isArray(value) || value.length === 0) {
461
+ errors.push(`${where} must be a non-empty array`);
462
+ return;
463
+ }
464
+ const seen = new Set();
465
+ for (const id of value) {
466
+ if (typeof id !== "string" ||
467
+ !/^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+$/.test(id)) {
468
+ errors.push(`${where} entries must be dotted ids such as "terminal.open"`);
469
+ continue;
470
+ }
471
+ if (seen.has(id))
472
+ errors.push(`${where} duplicates "${id}"`);
473
+ seen.add(id);
474
+ }
475
+ }
476
+ function validateNameList(value, where, errors) {
477
+ if (!Array.isArray(value) || value.length === 0) {
478
+ errors.push(`${where} must be a non-empty array`);
479
+ return;
480
+ }
481
+ const seen = new Set();
482
+ for (const name of value) {
483
+ if (typeof name !== "string" || !PROVIDER_NAME_PATTERN.test(name)) {
484
+ errors.push(`${where} entries must be lowercase names`);
485
+ continue;
486
+ }
487
+ if (seen.has(name))
488
+ errors.push(`${where} duplicates "${name}"`);
489
+ seen.add(name);
490
+ }
491
+ }
492
+ function validateRequires(requires, errors) {
493
+ if (requires === undefined)
494
+ return;
495
+ if (!Array.isArray(requires)) {
496
+ errors.push('Field "requires" must be an array');
497
+ return;
498
+ }
499
+ requires.forEach((raw, index) => {
500
+ const where = `requires[${index}]`;
501
+ if (!isPlainObject(raw)) {
502
+ errors.push(`${where} must be an object`);
503
+ return;
504
+ }
505
+ rejectUnknown(raw, ["service", "versionRange", "optional"], where, errors);
506
+ if (typeof raw.service !== "string" || !SERVICE_PATTERN.test(raw.service)) {
507
+ errors.push(`${where}.service must be a dotted lowercase name`);
508
+ }
509
+ if (typeof raw.versionRange !== "string" ||
510
+ !semver.validRange(raw.versionRange)) {
511
+ errors.push(`${where}.versionRange must be a valid semver range`);
512
+ }
513
+ if ("optional" in raw && typeof raw.optional !== "boolean") {
514
+ errors.push(`${where}.optional must be a boolean`);
515
+ }
516
+ });
517
+ }
518
+ function validateProvidesSecret(value, errors) {
519
+ if (value === undefined)
520
+ return;
521
+ if (!Array.isArray(value)) {
522
+ errors.push('Field "providesSecret" must be an array');
523
+ return;
524
+ }
525
+ const seen = new Set();
526
+ value.forEach((raw, index) => {
527
+ const where = `providesSecret[${index}]`;
528
+ if (!isPlainObject(raw)) {
529
+ errors.push(`${where} must be an object`);
530
+ return;
531
+ }
532
+ rejectUnknown(raw, ["key", "permission", "descriptionKey"], where, errors);
533
+ if (typeof raw.key !== "string" || !SECRET_KEY_PATTERN.test(raw.key)) {
534
+ errors.push(`${where}.key must be a lowercase slug`);
535
+ }
536
+ else {
537
+ if (seen.has(raw.key))
538
+ errors.push(`${where}.key duplicates "${raw.key}"`);
539
+ seen.add(raw.key);
540
+ }
541
+ requireString(raw.permission, `${where}.permission`, errors);
542
+ if ("descriptionKey" in raw) {
543
+ requireString(raw.descriptionKey, `${where}.descriptionKey`, errors);
544
+ }
545
+ });
546
+ }
547
+ function validateRequiresSecret(value, errors) {
548
+ if (value === undefined)
549
+ return;
550
+ if (!Array.isArray(value)) {
551
+ errors.push('Field "requiresSecret" must be an array');
552
+ return;
553
+ }
554
+ value.forEach((raw, index) => {
555
+ const where = `requiresSecret[${index}]`;
556
+ if (!isPlainObject(raw)) {
557
+ errors.push(`${where} must be an object`);
558
+ return;
559
+ }
560
+ rejectUnknown(raw, ["plugin", "key", "optional"], where, errors);
561
+ if (typeof raw.plugin !== "string" || !ID_PATTERN.test(raw.plugin)) {
562
+ errors.push(`${where}.plugin must be a valid plugin id`);
563
+ }
564
+ if (typeof raw.key !== "string" || !SECRET_KEY_PATTERN.test(raw.key)) {
565
+ errors.push(`${where}.key must be a lowercase slug`);
566
+ }
567
+ if ("optional" in raw && typeof raw.optional !== "boolean") {
568
+ errors.push(`${where}.optional must be a boolean`);
569
+ }
570
+ });
571
+ }
572
+ function validateContributes(contributes, pluginId, errors) {
573
+ if (contributes === undefined)
574
+ return;
575
+ if (!isPlainObject(contributes)) {
576
+ errors.push('Field "contributes" must be an object');
577
+ return;
578
+ }
579
+ rejectUnknown(contributes, ALLOWED_CONTRIBUTES, '"contributes"', errors);
580
+ validateTabs(contributes.tabs, errors);
581
+ validateViews(contributes.panels, "panels", errors);
582
+ validateViews(contributes.dashboardCards, "dashboardCards", errors);
583
+ if (contributes.guest !== undefined &&
584
+ typeof contributes.guest !== "boolean") {
585
+ errors.push('Field "contributes.guest" must be a boolean');
586
+ }
587
+ if (contributes.guestViews !== undefined) {
588
+ validateNameList(contributes.guestViews, "contributes.guestViews", errors);
589
+ if (contributes.guest !== true) {
590
+ errors.push('Field "contributes.guestViews" needs "contributes.guest"');
591
+ }
592
+ }
593
+ if (contributes.syncEntities !== undefined) {
594
+ validateSyncEntities(contributes.syncEntities, errors);
595
+ }
596
+ if (contributes.uses !== undefined) {
597
+ validateUses(contributes.uses, errors);
598
+ }
599
+ validatePermissions(contributes.permissions, pluginId, errors);
600
+ validateActions(contributes.actions, errors);
601
+ validateActionSlots(contributes.actionSlots, errors);
602
+ validateSettings(contributes.settings, errors);
603
+ validateHostCapability(contributes.hostCapability, errors);
604
+ validateAuthContribution(contributes.auth, errors);
605
+ validateHttpContribution(contributes.http, pluginId, errors);
606
+ validateUiPresets(contributes.uiPresets, errors);
607
+ validateKeybindingActions(contributes.keybindingActions, errors);
608
+ validateProtocols(contributes.protocols, errors);
609
+ }
610
+ export const PROTOCOL_ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
611
+ export const PROTOCOL_FIELD_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9]{0,31}$/;
612
+ function validateProtocols(value, errors) {
613
+ if (value === undefined)
614
+ return;
615
+ const where = "contributes.protocols";
616
+ if (!Array.isArray(value)) {
617
+ errors.push(`Field "${where}" must be an array`);
618
+ return;
619
+ }
620
+ const seen = new Set();
621
+ value.forEach((raw, index) => {
622
+ const at = `${where}[${index}]`;
623
+ if (!isPlainObject(raw)) {
624
+ errors.push(`${at} must be an object`);
625
+ return;
626
+ }
627
+ rejectUnknown(raw, ["id", "credentialFields", "defaultPort", "hostLoginFallback"], at, errors);
628
+ if (typeof raw.id !== "string" || !PROTOCOL_ID_PATTERN.test(raw.id)) {
629
+ errors.push(`${at}.id must be lowercase letters, digits and dashes, starting with a letter`);
630
+ }
631
+ else if (RESERVED_PROTOCOL_IDS.includes(raw.id)) {
632
+ errors.push(`${at}.id "${raw.id}" belongs to core`);
633
+ }
634
+ else {
635
+ if (seen.has(raw.id))
636
+ errors.push(`${at}.id duplicates "${raw.id}"`);
637
+ seen.add(raw.id);
638
+ }
639
+ if (raw.defaultPort !== undefined &&
640
+ (typeof raw.defaultPort !== "number" ||
641
+ !Number.isInteger(raw.defaultPort) ||
642
+ raw.defaultPort < 1 ||
643
+ raw.defaultPort > 65535)) {
644
+ errors.push(`${at}.defaultPort must be a port number`);
645
+ }
646
+ if (raw.hostLoginFallback !== undefined) {
647
+ if (!Array.isArray(raw.hostLoginFallback) ||
648
+ raw.hostLoginFallback.some((entry) => entry !== "username" && entry !== "password")) {
649
+ errors.push(`${at}.hostLoginFallback must list "username" and/or "password"`);
650
+ }
651
+ }
652
+ if (raw.credentialFields === undefined)
653
+ return;
654
+ if (!Array.isArray(raw.credentialFields)) {
655
+ errors.push(`${at}.credentialFields must be an array`);
656
+ return;
657
+ }
658
+ const keys = new Set();
659
+ raw.credentialFields.forEach((field, fieldIndex) => {
660
+ const fieldAt = `${at}.credentialFields[${fieldIndex}]`;
661
+ if (!isPlainObject(field)) {
662
+ errors.push(`${fieldAt} must be an object`);
663
+ return;
664
+ }
665
+ rejectUnknown(field, ["key", "secret"], fieldAt, errors);
666
+ if (typeof field.key !== "string" ||
667
+ !PROTOCOL_FIELD_KEY_PATTERN.test(field.key)) {
668
+ errors.push(`${fieldAt}.key must be letters and digits`);
669
+ }
670
+ else if (RESERVED_PROTOCOL_FIELD_KEYS.includes(field.key)) {
671
+ errors.push(`${fieldAt}.key "${field.key}" is part of every login`);
672
+ }
673
+ else {
674
+ if (keys.has(field.key)) {
675
+ errors.push(`${fieldAt}.key duplicates "${field.key}"`);
676
+ }
677
+ keys.add(field.key);
678
+ }
679
+ if (field.secret !== undefined && typeof field.secret !== "boolean") {
680
+ errors.push(`${fieldAt}.secret must be a boolean`);
681
+ }
682
+ });
683
+ });
684
+ }
685
+ const KEYBINDING_ACTION_PATTERN = /^[a-zA-Z][a-zA-Z0-9.-]{0,63}$/;
686
+ const KEYBINDING_PARAM_PATTERN = /^[a-zA-Z][a-zA-Z0-9]{0,31}$/;
687
+ function validateKeybindingParam(param, at, errors) {
688
+ if (!isPlainObject(param)) {
689
+ errors.push(`${at} must be an object`);
690
+ return;
691
+ }
692
+ rejectUnknown(param, ["type", "required", "pattern", "maxLength", "syncEntity"], at, errors);
693
+ if (param.type !== "string" && param.type !== "boolean") {
694
+ errors.push(`${at}.type must be "string" or "boolean"`);
695
+ }
696
+ if (param.required !== undefined && typeof param.required !== "boolean") {
697
+ errors.push(`${at}.required must be a boolean`);
698
+ }
699
+ if (param.pattern !== undefined) {
700
+ let valid = typeof param.pattern === "string";
701
+ if (valid) {
702
+ try {
703
+ new RegExp(param.pattern);
704
+ }
705
+ catch {
706
+ valid = false;
707
+ }
708
+ }
709
+ if (!valid)
710
+ errors.push(`${at}.pattern must be a regular expression`);
711
+ }
712
+ if (param.maxLength !== undefined &&
713
+ (typeof param.maxLength !== "number" ||
714
+ !Number.isInteger(param.maxLength) ||
715
+ param.maxLength < 1)) {
716
+ errors.push(`${at}.maxLength must be a positive integer`);
717
+ }
718
+ if (param.syncEntity !== undefined) {
719
+ if (typeof param.syncEntity !== "string" ||
720
+ !/^[a-zA-Z][a-zA-Z0-9]*$/.test(param.syncEntity)) {
721
+ errors.push(`${at}.syncEntity must be a sync entity wire name`);
722
+ }
723
+ else if (param.type !== "string") {
724
+ errors.push(`${at}.syncEntity needs a string parameter`);
725
+ }
726
+ }
727
+ }
728
+ function validateKeybindingActions(value, errors) {
729
+ if (value === undefined)
730
+ return;
731
+ const where = "contributes.keybindingActions";
732
+ if (!Array.isArray(value)) {
733
+ errors.push(`Field "${where}" must be an array`);
734
+ return;
735
+ }
736
+ const seen = new Set();
737
+ value.forEach((raw, index) => {
738
+ const at = `${where}[${index}]`;
739
+ if (!isPlainObject(raw)) {
740
+ errors.push(`${at} must be an object`);
741
+ return;
742
+ }
743
+ rejectUnknown(raw, ["id", "params"], at, errors);
744
+ if (typeof raw.id !== "string" || !KEYBINDING_ACTION_PATTERN.test(raw.id)) {
745
+ errors.push(`${at}.id must be letters, digits, dots and dashes`);
746
+ }
747
+ else if (CORE_KEYBINDING_ACTIONS.includes(raw.id)) {
748
+ errors.push(`${at}.id "${raw.id}" is one of the shell's own actions`);
749
+ }
750
+ else {
751
+ if (seen.has(raw.id))
752
+ errors.push(`${at}.id duplicates "${raw.id}"`);
753
+ seen.add(raw.id);
754
+ }
755
+ if (raw.params === undefined)
756
+ return;
757
+ if (!isPlainObject(raw.params)) {
758
+ errors.push(`${at}.params must be an object`);
759
+ return;
760
+ }
761
+ for (const [name, param] of Object.entries(raw.params)) {
762
+ if (!KEYBINDING_PARAM_PATTERN.test(name) || name === "type") {
763
+ errors.push(`${at}.params.${name} is not a valid parameter name`);
764
+ }
765
+ validateKeybindingParam(param, `${at}.params.${name}`, errors);
766
+ }
767
+ });
768
+ }
769
+ /**
770
+ * Checks one saved keybinding action against the parameters its declaration
771
+ * lists. A parameter the declaration does not list is refused, so a binding
772
+ * cannot carry arbitrary data under a declared type.
773
+ */
774
+ export function validateKeybindingActionParams(action, declaration) {
775
+ const params = declaration.params ?? {};
776
+ for (const key of Object.keys(action)) {
777
+ if (key !== "type" && !(key in params) && action[key] != null) {
778
+ return false;
779
+ }
780
+ }
781
+ for (const [name, param] of Object.entries(params)) {
782
+ const value = action[name];
783
+ if (value === undefined || value === null) {
784
+ if (param.required)
785
+ return false;
786
+ continue;
787
+ }
788
+ if (param.type === "boolean") {
789
+ if (typeof value !== "boolean")
790
+ return false;
791
+ continue;
792
+ }
793
+ if (typeof value !== "string")
794
+ return false;
795
+ if (param.required && value.length === 0)
796
+ return false;
797
+ if (value.length > (param.maxLength ?? 65_536))
798
+ return false;
799
+ if (param.pattern && !new RegExp(param.pattern).test(value))
800
+ return false;
801
+ }
802
+ return true;
803
+ }
804
+ function isPresetValue(value) {
805
+ return (typeof value === "boolean" ||
806
+ (typeof value === "number" && Number.isFinite(value)) ||
807
+ typeof value === "string" ||
808
+ (Array.isArray(value) && value.every((item) => typeof item === "string")));
809
+ }
810
+ function validateUiPresets(value, errors) {
811
+ if (value === undefined)
812
+ return;
813
+ const where = "contributes.uiPresets";
814
+ if (!isPlainObject(value)) {
815
+ errors.push(`Field "${where}" must be an object`);
816
+ return;
817
+ }
818
+ rejectUnknown(value, ["simple", "balanced", "advanced"], where, errors);
819
+ let keys;
820
+ for (const level of ["simple", "balanced", "advanced"]) {
821
+ const preset = value[level];
822
+ if (!isPlainObject(preset)) {
823
+ errors.push(`${where}.${level} must be an object`);
824
+ continue;
825
+ }
826
+ for (const [key, entry] of Object.entries(preset)) {
827
+ if (!PRESET_KEY_PATTERN.test(key)) {
828
+ errors.push(`${where}.${level} has an invalid key ${JSON.stringify(key)}`);
829
+ }
830
+ if (!isPresetValue(entry)) {
831
+ errors.push(`${where}.${level}.${key} must be a boolean, number, string or string array`);
832
+ }
833
+ }
834
+ const names = Object.keys(preset).sort().join(",");
835
+ if (keys === undefined)
836
+ keys = names;
837
+ else if (keys !== names) {
838
+ errors.push(`${where} presets must all name the same keys`);
839
+ }
840
+ }
841
+ }
842
+ const LEGACY_PATH_PATTERN = /^\/[a-z][a-z0-9-]*(\/[A-Za-z0-9_.-]+)+$/;
843
+ const REDIRECT_PATH_PATTERN = /^(\/[A-Za-z0-9_.-]+)+$/;
844
+ const RESERVED_PREFIXES = ["/plugin-api", "/plugin-ws", "/plugin-assets"];
845
+ function validateLegacyRedirects(value, errors) {
846
+ if (value === undefined)
847
+ return;
848
+ if (!Array.isArray(value)) {
849
+ errors.push('Field "contributes.http.legacyRedirects" must be an array');
850
+ return;
851
+ }
852
+ value.forEach((entry, index) => {
853
+ const at = `contributes.http.legacyRedirects[${index}]`;
854
+ if (!isPlainObject(entry)) {
855
+ errors.push(`${at} must be an object`);
856
+ return;
857
+ }
858
+ rejectUnknown(entry, ["from", "to", "status"], at, errors);
859
+ const { from, to, status } = entry;
860
+ if (typeof from !== "string" || !REDIRECT_PATH_PATTERN.test(from)) {
861
+ errors.push(`${at}.from must be an absolute path`);
862
+ }
863
+ else if (RESERVED_PREFIXES.some((prefix) => from === prefix || from.startsWith(`${prefix}/`))) {
864
+ errors.push(`${at}.from cannot be under ${from.split("/")[1]}`);
865
+ }
866
+ if (typeof to !== "string" || !REDIRECT_PATH_PATTERN.test(to)) {
867
+ errors.push(`${at}.to must be a path in the plugin's router`);
868
+ }
869
+ if (status !== undefined && status !== 307 && status !== 308) {
870
+ errors.push(`${at}.status must be 307 or 308`);
871
+ }
872
+ });
873
+ }
874
+ function validateHttpContribution(value, pluginId, errors) {
875
+ if (value === undefined)
876
+ return;
877
+ if (!isPlainObject(value)) {
878
+ errors.push('Field "contributes.http" must be an object');
879
+ return;
880
+ }
881
+ rejectUnknown(value, ["legacyPaths", "legacyRedirects", "adminImpersonation"], "contributes.http", errors);
882
+ if (value.adminImpersonation !== undefined &&
883
+ typeof value.adminImpersonation !== "boolean") {
884
+ errors.push('Field "contributes.http.adminImpersonation" must be a boolean');
885
+ }
886
+ validateLegacyRedirects(value.legacyRedirects, errors);
887
+ const paths = value.legacyPaths;
888
+ if (paths === undefined)
889
+ return;
890
+ if (!Array.isArray(paths)) {
891
+ errors.push('Field "contributes.http.legacyPaths" must be an array');
892
+ return;
893
+ }
894
+ paths.forEach((path, index) => {
895
+ const at = `contributes.http.legacyPaths[${index}]`;
896
+ if (typeof path !== "string" || !LEGACY_PATH_PATTERN.test(path)) {
897
+ errors.push(`${at} must be an absolute path like "/<plugin id>/name"`);
898
+ return;
899
+ }
900
+ if (pluginId && !path.startsWith(`/${pluginId}/`)) {
901
+ errors.push(`${at} must start with "/${pluginId}/"`);
902
+ }
903
+ });
904
+ }
905
+ const AUTH_ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
906
+ function validateAuthContribution(value, errors) {
907
+ if (value === undefined)
908
+ return;
909
+ if (!isPlainObject(value)) {
910
+ errors.push('Field "contributes.auth" must be an object');
911
+ return;
912
+ }
913
+ rejectUnknown(value, [
914
+ "sshAuthTypes",
915
+ "loginMethods",
916
+ "secondFactors",
917
+ "secretSchemes",
918
+ "keyboardInteractive",
919
+ ], "contributes.auth", errors);
920
+ for (const key of [
921
+ "sshAuthTypes",
922
+ "loginMethods",
923
+ "secondFactors",
924
+ "secretSchemes",
925
+ "keyboardInteractive",
926
+ ]) {
927
+ const list = value[key];
928
+ if (list === undefined)
929
+ continue;
930
+ if (!Array.isArray(list)) {
931
+ errors.push(`Field "contributes.auth.${key}" must be an array`);
932
+ continue;
933
+ }
934
+ const seen = new Set();
935
+ list.forEach((entry, index) => {
936
+ if (typeof entry !== "string" || !AUTH_ID_PATTERN.test(entry)) {
937
+ errors.push(`Field "contributes.auth.${key}[${index}]" must be a lowercase id (letters, digits and dashes)`);
938
+ return;
939
+ }
940
+ if (seen.has(entry)) {
941
+ errors.push(`Duplicate "${entry}" in contributes.auth.${key}`);
942
+ }
943
+ seen.add(entry);
944
+ });
945
+ }
946
+ }
947
+ function validateSettings(settings, errors) {
948
+ if (settings === undefined)
949
+ return;
950
+ const where = "contributes.settings";
951
+ if (!isPlainObject(settings)) {
952
+ errors.push(`${where} must be an object`);
953
+ return;
954
+ }
955
+ rejectUnknown(settings, ["admin", "user", "host"], `"${where}"`, errors);
956
+ for (const scope of ["admin", "user"]) {
957
+ const value = settings[scope];
958
+ if (value === undefined)
959
+ continue;
960
+ if (!Array.isArray(value)) {
961
+ errors.push(`${where}.${scope} must be an array of fields`);
962
+ continue;
963
+ }
964
+ validateSettingsFields(value, `${where}.${scope}`, errors);
965
+ }
966
+ if (settings.host !== undefined) {
967
+ const host = settings.host;
968
+ const at = `${where}.host`;
969
+ if (!isPlainObject(host)) {
970
+ errors.push(`${at} must be an object`);
971
+ return;
972
+ }
973
+ rejectUnknown(host, [
974
+ "enableKey",
975
+ "enableLabelKey",
976
+ "enableDescriptionKey",
977
+ "enableDefault",
978
+ "editorGroup",
979
+ "editorOrder",
980
+ "fields",
981
+ ], `"${at}"`, errors);
982
+ if ("enableKey" in host) {
983
+ if (typeof host.enableKey !== "string" ||
984
+ !SETTINGS_KEY_PATTERN.test(host.enableKey)) {
985
+ errors.push(`${at}.enableKey must be a short alphanumeric key`);
986
+ }
987
+ // An enable switch with no label is a blank row in the host editor.
988
+ requireString(host.enableLabelKey, `${at}.enableLabelKey`, errors);
989
+ }
990
+ if ("enableDescriptionKey" in host) {
991
+ requireString(host.enableDescriptionKey, `${at}.enableDescriptionKey`, errors);
992
+ }
993
+ if ("enableDefault" in host && typeof host.enableDefault !== "boolean") {
994
+ errors.push(`${at}.enableDefault must be a boolean`);
995
+ }
996
+ if ("editorGroup" in host &&
997
+ host.editorGroup !== "top" &&
998
+ host.editorGroup !== "ssh") {
999
+ errors.push(`${at}.editorGroup must be "top" or "ssh"`);
1000
+ }
1001
+ if ("editorOrder" in host &&
1002
+ (typeof host.editorOrder !== "number" ||
1003
+ !Number.isFinite(host.editorOrder))) {
1004
+ errors.push(`${at}.editorOrder must be a number`);
1005
+ }
1006
+ if (!Array.isArray(host.fields)) {
1007
+ errors.push(`${at}.fields must be an array`);
1008
+ return;
1009
+ }
1010
+ validateSettingsFields(host.fields, `${at}.fields`, errors, typeof host.enableKey === "string" ? host.enableKey : undefined);
1011
+ host.fields.forEach((raw, index) => {
1012
+ if (!isPlainObject(raw))
1013
+ return;
1014
+ const fieldAt = `${at}.fields[${index}]`;
1015
+ for (const flag of ["defaultable", "personal"]) {
1016
+ if (flag in raw && typeof raw[flag] !== "boolean") {
1017
+ errors.push(`${fieldAt}.${flag} must be a boolean`);
1018
+ }
1019
+ }
1020
+ if ("defaultLevels" in raw) {
1021
+ const levels = raw.defaultLevels;
1022
+ if (!Array.isArray(levels) ||
1023
+ levels.length === 0 ||
1024
+ levels.some((level) => !["admin", "user", "folder"].includes(level))) {
1025
+ errors.push(`${fieldAt}.defaultLevels must be a non-empty array of "admin", "user" or "folder"`);
1026
+ }
1027
+ }
1028
+ if (raw.type === "secret" && raw.defaultable === true) {
1029
+ errors.push(`${fieldAt} is a secret and cannot be defaultable`);
1030
+ }
1031
+ if ("shareRead" in raw &&
1032
+ !SHARE_LEVELS.includes(raw.shareRead)) {
1033
+ errors.push(`${fieldAt}.shareRead must be one of: ${SHARE_LEVELS.join(", ")}`);
1034
+ }
1035
+ if ("ownerOnly" in raw && typeof raw.ownerOnly !== "boolean") {
1036
+ errors.push(`${fieldAt}.ownerOnly must be a boolean`);
1037
+ }
1038
+ if ("secretKeys" in raw) {
1039
+ const keys = raw.secretKeys;
1040
+ if (raw.type !== "json") {
1041
+ errors.push(`${fieldAt}.secretKeys is only valid on json fields`);
1042
+ }
1043
+ else if (!Array.isArray(keys) ||
1044
+ keys.length === 0 ||
1045
+ keys.some((key) => typeof key !== "string" || key.length === 0)) {
1046
+ errors.push(`${fieldAt}.secretKeys must be a non-empty string array`);
1047
+ }
1048
+ }
1049
+ });
1050
+ }
1051
+ for (const scope of ["admin", "user"]) {
1052
+ const fields = settings[scope];
1053
+ if (!Array.isArray(fields))
1054
+ continue;
1055
+ fields.forEach((raw, index) => {
1056
+ if (!isPlainObject(raw))
1057
+ return;
1058
+ for (const key of [
1059
+ "defaultable",
1060
+ "defaultLevels",
1061
+ "personal",
1062
+ "shareRead",
1063
+ "ownerOnly",
1064
+ ]) {
1065
+ if (key in raw) {
1066
+ errors.push(`${where}.${scope}[${index}].${key} is only valid on host fields`);
1067
+ }
1068
+ }
1069
+ });
1070
+ }
1071
+ }
1072
+ /**
1073
+ * Validates one scope's fields.
1074
+ *
1075
+ * `requires` is checked against the keys declared in the same scope, plus the
1076
+ * host scope's enableKey, because a field pointing at a key that does not
1077
+ * exist would simply never render.
1078
+ */
1079
+ function validateSettingsFields(fields, where, errors, enableKey) {
1080
+ const seen = new Set();
1081
+ const booleanKeys = new Set();
1082
+ if (enableKey)
1083
+ booleanKeys.add(enableKey);
1084
+ fields.forEach((raw, index) => {
1085
+ const at = `${where}[${index}]`;
1086
+ if (!isPlainObject(raw)) {
1087
+ errors.push(`${at} must be an object`);
1088
+ return;
1089
+ }
1090
+ rejectUnknown(raw, ALLOWED_SETTINGS_FIELD, at, errors);
1091
+ if (typeof raw.key !== "string" || !SETTINGS_KEY_PATTERN.test(raw.key)) {
1092
+ errors.push(`${at}.key must be a short alphanumeric key`);
1093
+ }
1094
+ else {
1095
+ if (seen.has(raw.key)) {
1096
+ errors.push(`${at}.key duplicates "${raw.key}"`);
1097
+ }
1098
+ seen.add(raw.key);
1099
+ if (raw.type === "boolean")
1100
+ booleanKeys.add(raw.key);
1101
+ if (enableKey && raw.key === enableKey) {
1102
+ errors.push(`${at}.key "${raw.key}" is already the section's enableKey`);
1103
+ }
1104
+ }
1105
+ if (typeof raw.type !== "string" ||
1106
+ !SETTINGS_FIELD_TYPES.includes(raw.type)) {
1107
+ errors.push(`${at}.type must be one of: ${SETTINGS_FIELD_TYPES.join(", ")}`);
1108
+ return;
1109
+ }
1110
+ // A custom field draws its own label, so it needs a component instead.
1111
+ if (raw.type === "custom") {
1112
+ requireString(raw.component, `${at}.component`, errors);
1113
+ }
1114
+ else {
1115
+ requireString(raw.labelKey, `${at}.labelKey`, errors);
1116
+ if ("component" in raw) {
1117
+ errors.push(`${at}.component is only valid when type is "custom"`);
1118
+ }
1119
+ }
1120
+ for (const key of ["descriptionKey", "placeholderKey", "group"]) {
1121
+ if (key in raw)
1122
+ requireString(raw[key], `${at}.${key}`, errors);
1123
+ }
1124
+ if ("permission" in raw) {
1125
+ requireString(raw.permission, `${at}.permission`, errors);
1126
+ }
1127
+ if ("hidden" in raw && typeof raw.hidden !== "boolean") {
1128
+ errors.push(`${at}.hidden must be a boolean`);
1129
+ }
1130
+ if (raw.type === "select" || raw.type === "multiselect") {
1131
+ validateSettingsOptions(raw.options, at, errors);
1132
+ }
1133
+ else if ("options" in raw) {
1134
+ errors.push(`${at}.options is only valid for select and multiselect`);
1135
+ }
1136
+ if (raw.type === "number") {
1137
+ for (const bound of ["min", "max"]) {
1138
+ if (bound in raw && typeof raw[bound] !== "number") {
1139
+ errors.push(`${at}.${bound} must be a number`);
1140
+ }
1141
+ }
1142
+ if (typeof raw.min === "number" &&
1143
+ typeof raw.max === "number" &&
1144
+ raw.min > raw.max) {
1145
+ errors.push(`${at}.min must not be greater than ${at}.max`);
1146
+ }
1147
+ }
1148
+ else if ("min" in raw || "max" in raw) {
1149
+ errors.push(`${at}.min and ${at}.max are only valid for number fields`);
1150
+ }
1151
+ });
1152
+ // Second pass: every key is known by now, so forward references are fine.
1153
+ fields.forEach((raw, index) => {
1154
+ if (!isPlainObject(raw) || !("requires" in raw))
1155
+ return;
1156
+ const at = `${where}[${index}]`;
1157
+ if (typeof raw.requires !== "string") {
1158
+ errors.push(`${at}.requires must be a string`);
1159
+ return;
1160
+ }
1161
+ if (!booleanKeys.has(raw.requires)) {
1162
+ errors.push(`${at}.requires "${raw.requires}" must name a boolean field in the same scope`);
1163
+ }
1164
+ });
1165
+ }
1166
+ function validateSettingsOptions(options, where, errors) {
1167
+ if (!Array.isArray(options) || options.length === 0) {
1168
+ errors.push(`${where}.options must be a non-empty array`);
1169
+ return;
1170
+ }
1171
+ const seen = new Set();
1172
+ options.forEach((raw, index) => {
1173
+ const at = `${where}.options[${index}]`;
1174
+ if (!isPlainObject(raw)) {
1175
+ errors.push(`${at} must be an object`);
1176
+ return;
1177
+ }
1178
+ rejectUnknown(raw, ["value", "labelKey"], at, errors);
1179
+ if (requireString(raw.value, `${at}.value`, errors)) {
1180
+ if (seen.has(raw.value)) {
1181
+ errors.push(`${at}.value duplicates "${raw.value}"`);
1182
+ }
1183
+ seen.add(raw.value);
1184
+ }
1185
+ requireString(raw.labelKey, `${at}.labelKey`, errors);
1186
+ });
1187
+ }
1188
+ function validateTabs(tabs, errors) {
1189
+ if (tabs === undefined)
1190
+ return;
1191
+ if (!Array.isArray(tabs)) {
1192
+ errors.push('Field "contributes.tabs" must be an array');
1193
+ return;
1194
+ }
1195
+ const seen = new Set();
1196
+ tabs.forEach((raw, index) => {
1197
+ const where = `contributes.tabs[${index}]`;
1198
+ if (!isPlainObject(raw)) {
1199
+ errors.push(`${where} must be an object`);
1200
+ return;
1201
+ }
1202
+ rejectUnknown(raw, ["id", "titleKey", "icon", "openFrom"], where, errors);
1203
+ if (requireString(raw.id, `${where}.id`, errors)) {
1204
+ if (seen.has(raw.id))
1205
+ errors.push(`${where}.id duplicates "${raw.id}"`);
1206
+ seen.add(raw.id);
1207
+ }
1208
+ requireString(raw.titleKey, `${where}.titleKey`, errors);
1209
+ requireString(raw.icon, `${where}.icon`, errors);
1210
+ if (!Array.isArray(raw.openFrom) || raw.openFrom.length === 0) {
1211
+ errors.push(`${where}.openFrom must be a non-empty array`);
1212
+ return;
1213
+ }
1214
+ raw.openFrom.forEach((value, openIndex) => {
1215
+ if (!OPEN_FROM_VALUES.includes(value)) {
1216
+ errors.push(`${where}.openFrom[${openIndex}] must be one of: ${OPEN_FROM_VALUES.join(", ")}`);
1217
+ }
1218
+ });
1219
+ });
1220
+ }
1221
+ function validateViews(views, field, errors) {
1222
+ if (views === undefined)
1223
+ return;
1224
+ if (!Array.isArray(views)) {
1225
+ errors.push(`Field "contributes.${field}" must be an array`);
1226
+ return;
1227
+ }
1228
+ const seen = new Set();
1229
+ views.forEach((raw, index) => {
1230
+ const where = `contributes.${field}[${index}]`;
1231
+ if (!isPlainObject(raw)) {
1232
+ errors.push(`${where} must be an object`);
1233
+ return;
1234
+ }
1235
+ rejectUnknown(raw, ["id", "titleKey", "icon"], where, errors);
1236
+ if (requireString(raw.id, `${where}.id`, errors)) {
1237
+ if (seen.has(raw.id))
1238
+ errors.push(`${where}.id duplicates "${raw.id}"`);
1239
+ seen.add(raw.id);
1240
+ }
1241
+ requireString(raw.titleKey, `${where}.titleKey`, errors);
1242
+ if (raw.icon !== undefined)
1243
+ requireString(raw.icon, `${where}.icon`, errors);
1244
+ });
1245
+ }
1246
+ function validatePermissions(permissions, pluginId, errors) {
1247
+ if (permissions === undefined)
1248
+ return;
1249
+ const where = "contributes.permissions";
1250
+ if (!Array.isArray(permissions)) {
1251
+ errors.push(`${where} must be an array`);
1252
+ return;
1253
+ }
1254
+ const seen = new Set();
1255
+ permissions.forEach((entry, index) => {
1256
+ const at = `${where}[${index}]`;
1257
+ if (!isPlainObject(entry)) {
1258
+ errors.push(`${at} must be an object`);
1259
+ return;
1260
+ }
1261
+ rejectUnknown(entry, ["name", "titleKey", "descriptionKey", "defaultRoles"], at, errors);
1262
+ const name = entry.name;
1263
+ if (typeof name !== "string" || !PERMISSION_NAME_PATTERN.test(name)) {
1264
+ errors.push(`${at}.name must be a lowercase, dotted permission name`);
1265
+ }
1266
+ else {
1267
+ if (seen.has(name)) {
1268
+ errors.push(`${at}.name "${name}" is declared more than once`);
1269
+ }
1270
+ seen.add(name);
1271
+ // Core groups first: a plugin naming one could gate a route on core
1272
+ // authority it was never given.
1273
+ const head = name.split(".")[0];
1274
+ if (RESERVED_PERMISSION_PREFIXES.includes(head)) {
1275
+ errors.push(`${at}.name "${name}" starts with the reserved core group "${head}". Permissions are registered as <pluginId>.<name>, so drop the prefix.`);
1276
+ }
1277
+ // "ai" declaring "ai.use" would register as ai.ai.use, which is always
1278
+ // a mistake rather than an intent.
1279
+ if (pluginId && (name === pluginId || head === pluginId)) {
1280
+ errors.push(`${at}.name "${name}" already starts with this plugin's id. Permissions are registered as <pluginId>.<name>, so drop the prefix.`);
1281
+ }
1282
+ }
1283
+ requireString(entry.titleKey, `${at}.titleKey`, errors);
1284
+ requireString(entry.descriptionKey, `${at}.descriptionKey`, errors);
1285
+ if (entry.defaultRoles === undefined)
1286
+ return;
1287
+ if (!Array.isArray(entry.defaultRoles)) {
1288
+ errors.push(`${at}.defaultRoles must be an array`);
1289
+ return;
1290
+ }
1291
+ entry.defaultRoles.forEach((role, roleIndex) => {
1292
+ if (typeof role !== "string" ||
1293
+ !SYSTEM_ROLE_NAMES.includes(role)) {
1294
+ errors.push(`${at}.defaultRoles[${roleIndex}] must be one of: ${SYSTEM_ROLE_NAMES.join(", ")}`);
1295
+ }
1296
+ });
1297
+ });
1298
+ }
1299
+ function validateActions(actions, errors) {
1300
+ if (actions === undefined)
1301
+ return;
1302
+ if (!Array.isArray(actions)) {
1303
+ errors.push('Field "contributes.actions" must be an array');
1304
+ return;
1305
+ }
1306
+ const seen = new Set();
1307
+ actions.forEach((raw, index) => {
1308
+ const where = `contributes.actions[${index}]`;
1309
+ if (!isPlainObject(raw)) {
1310
+ errors.push(`${where} must be an object`);
1311
+ return;
1312
+ }
1313
+ rejectUnknown(raw, ["id", "titleKey", "handler", "icon", "permission", "slot", "kind"], where, errors);
1314
+ if (typeof raw.id !== "string" || !ACTION_ID_PATTERN.test(raw.id)) {
1315
+ errors.push(`${where}.id must be a dotted action id`);
1316
+ }
1317
+ else {
1318
+ if (seen.has(raw.id))
1319
+ errors.push(`${where}.id duplicates "${raw.id}"`);
1320
+ seen.add(raw.id);
1321
+ }
1322
+ requireString(raw.titleKey, `${where}.titleKey`, errors);
1323
+ if (typeof raw.handler !== "string" || !HANDLER_PATTERN.test(raw.handler)) {
1324
+ errors.push(`${where}.handler must be a JavaScript identifier`);
1325
+ }
1326
+ if ("icon" in raw)
1327
+ requireString(raw.icon, `${where}.icon`, errors);
1328
+ if ("permission" in raw) {
1329
+ requireString(raw.permission, `${where}.permission`, errors);
1330
+ }
1331
+ if ("slot" in raw)
1332
+ requireString(raw.slot, `${where}.slot`, errors);
1333
+ if ("kind" in raw &&
1334
+ !ACTION_CONTRIBUTION_KINDS.includes(raw.kind)) {
1335
+ errors.push(`${where}.kind must be one of: ${ACTION_CONTRIBUTION_KINDS.join(", ")}`);
1336
+ }
1337
+ });
1338
+ }
1339
+ function validateActionSlots(slots, errors) {
1340
+ if (slots === undefined)
1341
+ return;
1342
+ if (!Array.isArray(slots)) {
1343
+ errors.push('Field "contributes.actionSlots" must be an array');
1344
+ return;
1345
+ }
1346
+ const seen = new Set();
1347
+ slots.forEach((raw, index) => {
1348
+ const where = `contributes.actionSlots[${index}]`;
1349
+ if (!isPlainObject(raw)) {
1350
+ errors.push(`${where} must be an object`);
1351
+ return;
1352
+ }
1353
+ rejectUnknown(raw, ["id", "accepts", "descriptionKey"], where, errors);
1354
+ if (typeof raw.id !== "string" || !ACTION_ID_PATTERN.test(raw.id)) {
1355
+ errors.push(`${where}.id must be a dotted slot id`);
1356
+ }
1357
+ else {
1358
+ if (seen.has(raw.id))
1359
+ errors.push(`${where}.id duplicates "${raw.id}"`);
1360
+ seen.add(raw.id);
1361
+ }
1362
+ if (!Array.isArray(raw.accepts) || raw.accepts.length === 0) {
1363
+ errors.push(`${where}.accepts must be a non-empty array`);
1364
+ }
1365
+ else {
1366
+ raw.accepts.forEach((kind, kindIndex) => {
1367
+ if (!ACTION_CONTRIBUTION_KINDS.includes(kind)) {
1368
+ errors.push(`${where}.accepts[${kindIndex}] must be one of: ${ACTION_CONTRIBUTION_KINDS.join(", ")}`);
1369
+ }
1370
+ });
1371
+ }
1372
+ if ("descriptionKey" in raw) {
1373
+ requireString(raw.descriptionKey, `${where}.descriptionKey`, errors);
1374
+ }
1375
+ });
1376
+ }
1377
+ function validateHostCapability(value, errors) {
1378
+ if (value === undefined)
1379
+ return;
1380
+ const entries = Array.isArray(value) ? value : [value];
1381
+ if (Array.isArray(value) && value.length === 0) {
1382
+ errors.push('Field "contributes.hostCapability" must not be an empty array');
1383
+ return;
1384
+ }
1385
+ const seen = new Set();
1386
+ entries.forEach((raw, index) => {
1387
+ const where = Array.isArray(value)
1388
+ ? `contributes.hostCapability[${index}]`
1389
+ : "contributes.hostCapability";
1390
+ if (!isPlainObject(raw)) {
1391
+ errors.push(`${where} must be an object`);
1392
+ return;
1393
+ }
1394
+ rejectUnknown(raw, ["key", "labelKey", "editorTab"], where, errors);
1395
+ if (requireString(raw.key, `${where}.key`, errors)) {
1396
+ if (seen.has(raw.key))
1397
+ errors.push(`${where}.key duplicates "${raw.key}"`);
1398
+ seen.add(raw.key);
1399
+ }
1400
+ requireString(raw.labelKey, `${where}.labelKey`, errors);
1401
+ requireString(raw.editorTab, `${where}.editorTab`, errors);
1402
+ });
1403
+ }
1404
+ /**
1405
+ * Validates, then applies the cross-field rules a JSON schema cannot express
1406
+ * and fills in the entry-point defaults.
1407
+ */
1408
+ export function parseManifest(raw) {
1409
+ const errors = validateManifest(raw);
1410
+ if (errors.length > 0)
1411
+ return { errors };
1412
+ const manifest = raw;
1413
+ if (!isApiCompatible(manifest.engine.api)) {
1414
+ return {
1415
+ errors: [
1416
+ `Plugin needs plugin API ${manifest.engine.api}, but this build implements ${PLUGIN_API_VERSION}`,
1417
+ ],
1418
+ };
1419
+ }
1420
+ // Full ids: provides, providesSecret and actions all name permissions the
1421
+ // way an admin sees them, so they are compared against the qualified form.
1422
+ const declared = new Set((manifest.contributes?.permissions ?? []).map((permission) => qualifyPermission(manifest.id, permission.name)));
1423
+ // A permission the catalog never sees is one no admin can grant, so the
1424
+ // service or action would be invisible rather than denied.
1425
+ for (const provide of manifest.provides ?? []) {
1426
+ if (!declared.has(provide.permission)) {
1427
+ errors.push(`provides["${provide.service}"].permission "${provide.permission}" is not declared in contributes.permissions`);
1428
+ }
1429
+ }
1430
+ for (const secret of manifest.providesSecret ?? []) {
1431
+ if (!declared.has(secret.permission)) {
1432
+ errors.push(`providesSecret["${secret.key}"].permission "${secret.permission}" is not declared in contributes.permissions`);
1433
+ }
1434
+ }
1435
+ for (const action of manifest.contributes?.actions ?? []) {
1436
+ if (action.permission && !declared.has(action.permission)) {
1437
+ errors.push(`contributes.actions["${action.id}"].permission "${action.permission}" is not declared in contributes.permissions`);
1438
+ }
1439
+ }
1440
+ // A settings field may gate on one of this plugin's own permissions, or on
1441
+ // a core/other-plugin id used as given. Only the first form is checkable
1442
+ // here, and an undeclared one would hide the field from every admin.
1443
+ const settings = manifest.contributes?.settings;
1444
+ const settingsScopes = [
1445
+ ["admin", settings?.admin ?? []],
1446
+ ["user", settings?.user ?? []],
1447
+ ["host", settings?.host?.fields ?? []],
1448
+ ];
1449
+ for (const [scope, fields] of settingsScopes) {
1450
+ for (const field of fields) {
1451
+ if (!field.permission)
1452
+ continue;
1453
+ if (field.permission.includes("."))
1454
+ continue;
1455
+ if (!declared.has(qualifyPermission(manifest.id, field.permission))) {
1456
+ errors.push(`contributes.settings.${scope} field "${field.key}" requires permission "${field.permission}", which is not declared in contributes.permissions`);
1457
+ }
1458
+ }
1459
+ }
1460
+ const auth = manifest.contributes?.auth;
1461
+ const contributesAuth = (auth?.sshAuthTypes?.length ?? 0) +
1462
+ (auth?.loginMethods?.length ?? 0) +
1463
+ (auth?.secondFactors?.length ?? 0) +
1464
+ (auth?.secretSchemes?.length ?? 0) +
1465
+ (auth?.keyboardInteractive?.length ?? 0) >
1466
+ 0;
1467
+ if (contributesAuth && !manifest.capabilities.includes("auth:provide")) {
1468
+ errors.push('contributes.auth needs the "auth:provide" capability in the capabilities array');
1469
+ }
1470
+ for (const secret of manifest.requiresSecret ?? []) {
1471
+ if (secret.plugin === manifest.id) {
1472
+ errors.push(`requiresSecret["${secret.key}"] points at this plugin; use ctx.secrets.get instead`);
1473
+ }
1474
+ }
1475
+ for (const dependency of Object.keys(manifest.dependencies ?? {})) {
1476
+ if (dependency === manifest.id) {
1477
+ errors.push(`dependencies["${dependency}"] points at this plugin`);
1478
+ }
1479
+ if (manifest.optionalDependencies?.[dependency]) {
1480
+ errors.push(`"${dependency}" is listed in both dependencies and optionalDependencies`);
1481
+ }
1482
+ }
1483
+ for (const dependency of Object.keys(manifest.optionalDependencies ?? {})) {
1484
+ if (dependency === manifest.id) {
1485
+ errors.push(`optionalDependencies["${dependency}"] points at this plugin`);
1486
+ }
1487
+ }
1488
+ if (errors.length > 0)
1489
+ return { errors };
1490
+ return {
1491
+ manifest: {
1492
+ ...manifest,
1493
+ backend: manifest.backend ?? DEFAULT_BACKEND_ENTRY,
1494
+ frontend: manifest.frontend ?? DEFAULT_FRONTEND_ENTRY,
1495
+ locales: manifest.locales ?? DEFAULT_LOCALES_DIR,
1496
+ },
1497
+ errors: [],
1498
+ };
1499
+ }