@termix-ssh/plugin-sdk 1.0.10 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/PLUGIN-GUIDE.md +151 -36
- package/cli/commands/build.mjs +36 -22
- package/cli/commands/pack.mjs +12 -2
- package/cli/commands/validate.mjs +63 -3
- package/cli/lib/plugin-dir.mjs +11 -0
- package/cli/lib/tmxplug.mjs +9 -0
- package/dist/backend.d.ts +37 -3
- package/dist/capabilities.js +2 -0
- package/dist/frontend.d.ts +152 -0
- package/dist/frontend.js +7 -0
- package/dist/host/PluginsTab-EV4MSGY6.js +2933 -0
- package/dist/host/{chunk-G346FUE4.js → chunk-AWR6FYDO.js} +8877 -11159
- package/dist/host/{chunk-NZG6HNQH.js → chunk-RUPLD6RF.js} +1504 -15
- package/dist/host/{chunk-DIOJ3KW2.js → chunk-VEWOYAMZ.js} +6 -1
- package/dist/host/chunk-X4QZKO5G.js +3120 -0
- package/dist/host/chunk-XKA3DBB2.js +10375 -0
- package/dist/host/chunk-YGYBM7SQ.js +58 -0
- package/dist/host/core-classes.json +1 -1
- package/dist/host/{linked-server-QAKKDBFM.js → linked-server-B4NEG4MA.js} +2 -2
- package/dist/host/{rbac-api-5267EMX7.js → rbac-api-OUO3LUOC.js} +2 -2
- package/dist/host/testing-host.js +123 -57
- package/dist/host/types/src/types/ui-types.d.ts +2 -1
- package/dist/host/types/src/ui/api/audit-log-api.d.ts +46 -0
- package/dist/host/types/src/ui/api/host-defaults-api.d.ts +42 -0
- package/dist/host/types/src/ui/api/host-tags-api.d.ts +2 -0
- package/dist/host/types/src/ui/api/look-api.d.ts +40 -0
- package/dist/host/types/src/ui/api/open-tabs-api.d.ts +4 -0
- package/dist/host/types/src/ui/api/plugins-api.d.ts +26 -0
- package/dist/host/types/src/ui/api/private-endpoints-api.d.ts +6 -0
- package/dist/host/types/src/ui/api/tls-api.d.ts +25 -0
- package/dist/host/types/src/ui/auth/ElectronLoginForm.d.ts +9 -0
- package/dist/host/types/src/ui/components/command.d.ts +78 -0
- package/dist/host/types/src/ui/components/folder-style.d.ts +16 -0
- package/dist/host/types/src/ui/components/theme-provider.d.ts +2 -1
- package/dist/host/types/src/ui/components/version-badge.d.ts +7 -0
- package/dist/host/types/src/ui/hooks/use-page-visible-interval.d.ts +10 -0
- package/dist/host/types/src/ui/lib/activity-types.d.ts +15 -0
- package/dist/host/types/src/ui/lib/app-version.d.ts +2 -0
- package/dist/host/types/src/ui/lib/apply-ui-preset.d.ts +19 -0
- package/dist/host/types/src/ui/lib/connection-origin.d.ts +4 -5
- package/dist/host/types/src/ui/lib/create-id.d.ts +2 -0
- package/dist/host/types/src/ui/lib/database-transfer-url.d.ts +8 -0
- package/dist/host/types/src/ui/lib/desktop-updates.d.ts +29 -0
- package/dist/host/types/src/ui/lib/electron.d.ts +5 -0
- package/dist/host/types/src/ui/lib/host-connection-tabs.d.ts +14 -0
- package/dist/host/types/src/ui/lib/host-to-ssh-host.d.ts +4 -0
- package/dist/host/types/src/ui/lib/issue-url.d.ts +21 -0
- package/dist/host/types/src/ui/lib/local-adaptive-preferences.d.ts +9 -0
- package/dist/host/types/src/ui/lib/pane-nav-hotkey.d.ts +7 -0
- package/dist/host/types/src/ui/lib/plugin-i18n.d.ts +16 -0
- package/dist/host/types/src/ui/lib/role-label.d.ts +3 -0
- package/dist/host/types/src/ui/look/LookSettings.d.ts +8 -0
- package/dist/host/types/src/ui/look/layout-registry.d.ts +34 -0
- package/dist/host/types/src/ui/look/look-store.d.ts +29 -0
- package/dist/host/types/src/ui/look/resolve-look.d.ts +18 -0
- package/dist/host/types/src/ui/look/theme-css.d.ts +7 -0
- package/dist/host/types/src/ui/look/theme-registry.d.ts +41 -0
- package/dist/host/types/src/ui/look/use-look-theme.d.ts +12 -0
- package/dist/host/types/src/ui/manage/manage-requests.d.ts +34 -0
- package/dist/host/types/src/ui/plugin-host/loader.d.ts +2 -0
- package/dist/host/types/src/ui/plugin-host/scope.d.ts +6 -0
- package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +102 -0
- package/dist/host/types/src/ui/plugin-host/view-ownership.d.ts +1 -1
- package/dist/host/types/src/ui/plugins/PluginConsentPrompt.d.ts +20 -0
- package/dist/host/types/src/ui/plugins/PluginDetail.d.ts +7 -0
- package/dist/host/types/src/ui/plugins/PluginReleaseNotes.d.ts +14 -0
- package/dist/host/types/src/ui/plugins/PluginsTab.d.ts +1 -0
- package/dist/host/types/src/ui/plugins/RegistriesSection.d.ts +4 -0
- package/dist/host/types/src/ui/plugins/plugin-bits.d.ts +23 -0
- package/dist/host/types/src/ui/plugins/plugin-model.d.ts +116 -0
- package/dist/host/types/src/ui/plugins/use-plugins-manager.d.ts +75 -0
- package/dist/host/types/src/ui/settings/appearance/appearance-controls.d.ts +53 -0
- package/dist/host/types/src/ui/settings/sync/LinkServerDialog.d.ts +12 -0
- package/dist/host/types/src/ui/settings/sync/ProxySettingsFields.d.ts +15 -0
- package/dist/host/types/src/ui/settings/sync/saved-servers.d.ts +3 -0
- package/dist/host/types/src/ui/shell/controller/ShellControllerProvider.d.ts +20 -0
- package/dist/host/types/src/ui/shell/controller/plugin-shell.d.ts +7 -0
- package/dist/host/types/src/ui/shell/controller/shell-store.d.ts +4 -0
- package/dist/host/types/src/ui/shell/controller/types.d.ts +156 -0
- package/dist/host/types/src/ui/shell/controller/use-hosts.d.ts +10 -0
- package/dist/host/types/src/ui/shell/controller/use-regions.d.ts +5 -0
- package/dist/host/types/src/ui/shell/controller/use-shell-keybindings.d.ts +12 -0
- package/dist/host/types/src/ui/shell/controller/use-shell-lifecycle.d.ts +17 -0
- package/dist/host/types/src/ui/shell/controller/use-split.d.ts +21 -0
- package/dist/host/types/src/ui/shell/controller/use-tabs.d.ts +70 -0
- package/dist/host/types/src/ui/shell/controller/use-workspace-layout.d.ts +4 -0
- package/dist/host/types/src/ui/shell/reconnect-tabs.d.ts +6 -0
- package/dist/host/types/src/ui/shell/shell-layout.d.ts +46 -0
- package/dist/host/types/src/ui/shell/split/EmptyPanePicker.d.ts +32 -0
- package/dist/host/types/src/ui/shell/split/SplitDropOverlay.d.ts +9 -0
- package/dist/host/types/src/ui/shell/split/SplitLayoutMenu.d.ts +16 -0
- package/dist/host/types/src/ui/shell/split/SplitView.d.ts +31 -0
- package/dist/host/types/src/ui/shell/split/split-drag.d.ts +58 -0
- package/dist/host/types/src/ui/shell/split/split-presets.d.ts +28 -0
- package/dist/host/types/src/ui/shell/split/split-tabs.d.ts +95 -0
- package/dist/host/types/src/ui/shell/split/split-targets.d.ts +30 -0
- package/dist/host/types/src/ui/shell/split/split-tree.d.ts +89 -0
- package/dist/host/types/src/ui/shell/stage/TabStage.d.ts +9 -0
- package/dist/host/types/src/ui/shell/stage/tab-stage-store.d.ts +24 -0
- package/dist/host/types/src/ui/shell/tab-close-guards.d.ts +9 -0
- package/dist/host/types/src/ui/shell/tab-handles.d.ts +8 -0
- package/dist/host/types/src/ui/shell/tabUtils.d.ts +15 -0
- package/dist/host/types/src/ui/sidebar/AuthEnrollmentSections.d.ts +5 -0
- package/dist/host/types/src/ui/sidebar/HostManagerData.d.ts +4 -0
- package/dist/host/types/src/ui/sidebar/HostManagerTabs.d.ts +10 -4
- package/dist/host/types/src/ui/sidebar/build-host-tree.d.ts +25 -0
- package/dist/host/types/src/ui/sidebar/host-export-payload.d.ts +30 -0
- package/dist/host/ui.js +6238 -4054
- package/dist/host/{use-permissions-2P2TP6GY.js → use-permissions-6VSHHUMB.js} +2 -2
- package/dist/manifest.d.ts +41 -1
- package/dist/manifest.js +89 -1
- package/dist/testing/setup.js +1 -0
- package/dist/testing.d.ts +12 -1
- package/dist/testing.js +25 -1
- package/dist/theme-css.d.ts +11 -0
- package/dist/theme-css.js +142 -0
- package/dist/vitest-preset.d.ts +18 -0
- package/dist/vitest-preset.js +14 -3
- package/package.json +1 -1
- package/schema/manifest.schema.json +111 -8
- package/tsconfig.plugin.json +1 -0
- package/dist/host/chunk-YC2MAKSP.js +0 -14
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
resetPermissionsCache,
|
|
8
8
|
setPermissionsForTesting,
|
|
9
9
|
usePermissions
|
|
10
|
-
} from "./chunk-
|
|
11
|
-
import "./chunk-
|
|
10
|
+
} from "./chunk-VEWOYAMZ.js";
|
|
11
|
+
import "./chunk-YGYBM7SQ.js";
|
|
12
12
|
export {
|
|
13
13
|
hasPermission,
|
|
14
14
|
loadPermissionChecker,
|
package/dist/manifest.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* made a manifest look like it declared something it did not.
|
|
11
11
|
*/
|
|
12
12
|
import { type PluginEnvVar } from "./docs.js";
|
|
13
|
+
export { validateThemeCss } from "./theme-css.js";
|
|
13
14
|
export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDocsUrl, pluginEnvVars, type PluginEnvVar, } from "./docs.js";
|
|
14
15
|
/**
|
|
15
16
|
* The plugin API this build implements. A minor bump adds to the API and
|
|
@@ -19,7 +20,7 @@ export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDoc
|
|
|
19
20
|
* "^1.2" needs 1.2 or later). engine.termix is checked against the core
|
|
20
21
|
* version by the server.
|
|
21
22
|
*/
|
|
22
|
-
export declare const PLUGIN_API_VERSION = "1.
|
|
23
|
+
export declare const PLUGIN_API_VERSION = "1.4.0";
|
|
23
24
|
/** The API major, the default engine.api for a new plugin. */
|
|
24
25
|
export declare const SUPPORTED_PLUGIN_API_VERSION: string;
|
|
25
26
|
/** Whether this build's plugin API satisfies a manifest's engine.api. */
|
|
@@ -265,6 +266,41 @@ export interface PluginAuthContribution {
|
|
|
265
266
|
/** Keyboard-interactive handler ids, e.g. "warpgate". */
|
|
266
267
|
keyboardInteractive?: string[];
|
|
267
268
|
}
|
|
269
|
+
/** A layout id or theme id: short, lowercase, used as a CSS class for themes. */
|
|
270
|
+
export declare const LOOK_ID_PATTERN: RegExp;
|
|
271
|
+
/**
|
|
272
|
+
* A whole-app layout the frontend registers with app.registerLayout. Needs
|
|
273
|
+
* the ui:layout capability. Declared here so core can list and choose it
|
|
274
|
+
* before any plugin code runs.
|
|
275
|
+
*/
|
|
276
|
+
export interface PluginLayoutContribution {
|
|
277
|
+
id: string;
|
|
278
|
+
/** i18n key in the plugin's namespace, or plain text. */
|
|
279
|
+
label: string;
|
|
280
|
+
description?: string;
|
|
281
|
+
/** Theme picked with this layout when nobody chose one. */
|
|
282
|
+
defaultTheme?: string;
|
|
283
|
+
/** The layout a default-look plugin starts with. */
|
|
284
|
+
default?: boolean;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* A theme: one CSS file of token rules scoped under `.<id>`, applied as a
|
|
288
|
+
* class on <html>. Needs the ui:theme capability. No code runs for a theme.
|
|
289
|
+
*/
|
|
290
|
+
export interface PluginThemeContribution {
|
|
291
|
+
id: string;
|
|
292
|
+
/** i18n key in the plugin's namespace, or plain text. */
|
|
293
|
+
label: string;
|
|
294
|
+
mode: "light" | "dark";
|
|
295
|
+
/** Path of the CSS file inside the plugin. */
|
|
296
|
+
css: string;
|
|
297
|
+
/** Colors the theme picker previews it with. */
|
|
298
|
+
swatch?: {
|
|
299
|
+
background: string;
|
|
300
|
+
panel: string;
|
|
301
|
+
text: string;
|
|
302
|
+
};
|
|
303
|
+
}
|
|
268
304
|
export interface PluginContributions {
|
|
269
305
|
tabs?: PluginTabContribution[];
|
|
270
306
|
panels?: PluginViewContribution[];
|
|
@@ -323,6 +359,10 @@ export interface PluginContributions {
|
|
|
323
359
|
* ctx.credentials.resolveHostProtocol.
|
|
324
360
|
*/
|
|
325
361
|
protocols?: PluginProtocolContribution[];
|
|
362
|
+
/** Whole-app layouts. Needs the ui:layout capability. */
|
|
363
|
+
layouts?: PluginLayoutContribution[];
|
|
364
|
+
/** Themes. Needs the ui:theme capability. */
|
|
365
|
+
themes?: PluginThemeContribution[];
|
|
326
366
|
}
|
|
327
367
|
/** A field a protocol login carries besides username and password. */
|
|
328
368
|
export interface PluginProtocolCredentialField {
|
package/dist/manifest.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import semver from "semver";
|
|
13
13
|
import { isKnownCapability } from "./capabilities.js";
|
|
14
14
|
import { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsUrl, } from "./docs.js";
|
|
15
|
+
export { validateThemeCss } from "./theme-css.js";
|
|
15
16
|
export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDocsUrl, pluginEnvVars, } from "./docs.js";
|
|
16
17
|
/**
|
|
17
18
|
* The plugin API this build implements. A minor bump adds to the API and
|
|
@@ -21,7 +22,7 @@ export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDoc
|
|
|
21
22
|
* "^1.2" needs 1.2 or later). engine.termix is checked against the core
|
|
22
23
|
* version by the server.
|
|
23
24
|
*/
|
|
24
|
-
export const PLUGIN_API_VERSION = "1.
|
|
25
|
+
export const PLUGIN_API_VERSION = "1.4.0";
|
|
25
26
|
/** The API major, the default engine.api for a new plugin. */
|
|
26
27
|
export const SUPPORTED_PLUGIN_API_VERSION = String(semver.major(PLUGIN_API_VERSION));
|
|
27
28
|
/** Whether this build's plugin API satisfies a manifest's engine.api. */
|
|
@@ -168,6 +169,8 @@ export const SETTINGS_FIELD_TYPES = [
|
|
|
168
169
|
"custom",
|
|
169
170
|
];
|
|
170
171
|
export const SETTINGS_SCOPES = ["admin", "user", "host"];
|
|
172
|
+
/** A layout id or theme id: short, lowercase, used as a CSS class for themes. */
|
|
173
|
+
export const LOOK_ID_PATTERN = /^[a-z][a-z0-9-]{1,39}$/;
|
|
171
174
|
/** Protocol ids core keeps for itself. */
|
|
172
175
|
export const RESERVED_PROTOCOL_IDS = ["ssh"];
|
|
173
176
|
/** Login keys a declared credential field may not reuse. */
|
|
@@ -243,6 +246,8 @@ const ALLOWED_CONTRIBUTES = new Set([
|
|
|
243
246
|
"uses",
|
|
244
247
|
"keybindingActions",
|
|
245
248
|
"protocols",
|
|
249
|
+
"layouts",
|
|
250
|
+
"themes",
|
|
246
251
|
]);
|
|
247
252
|
const ALLOWED_SETTINGS_FIELD = [
|
|
248
253
|
"key",
|
|
@@ -734,6 +739,81 @@ function validateContributes(contributes, pluginId, errors) {
|
|
|
734
739
|
validateUiPresets(contributes.uiPresets, errors);
|
|
735
740
|
validateKeybindingActions(contributes.keybindingActions, errors);
|
|
736
741
|
validateProtocols(contributes.protocols, errors);
|
|
742
|
+
validateLayouts(contributes.layouts, errors);
|
|
743
|
+
validateThemes(contributes.themes, errors);
|
|
744
|
+
}
|
|
745
|
+
/** An array of objects with unique look ids; calls `each` for every valid entry. */
|
|
746
|
+
function validateLookList(value, field, allowed, errors, each) {
|
|
747
|
+
if (value === undefined)
|
|
748
|
+
return;
|
|
749
|
+
if (!Array.isArray(value)) {
|
|
750
|
+
errors.push(`Field "contributes.${field}" must be an array`);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const seen = new Set();
|
|
754
|
+
value.forEach((raw, index) => {
|
|
755
|
+
const where = `contributes.${field}[${index}]`;
|
|
756
|
+
if (!isPlainObject(raw)) {
|
|
757
|
+
errors.push(`${where} must be an object`);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
rejectUnknown(raw, allowed, where, errors);
|
|
761
|
+
if (typeof raw.id !== "string" || !LOOK_ID_PATTERN.test(raw.id)) {
|
|
762
|
+
errors.push(`${where}.id must match ${LOOK_ID_PATTERN} (lowercase letters, digits, dashes)`);
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
if (seen.has(raw.id))
|
|
766
|
+
errors.push(`${where}.id duplicates "${raw.id}"`);
|
|
767
|
+
seen.add(raw.id);
|
|
768
|
+
}
|
|
769
|
+
requireString(raw.label, `${where}.label`, errors);
|
|
770
|
+
each(raw, where);
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
function validateLayouts(value, errors) {
|
|
774
|
+
validateLookList(value, "layouts", ["id", "label", "description", "defaultTheme", "default"], errors, (raw, where) => {
|
|
775
|
+
if (raw.description !== undefined)
|
|
776
|
+
requireString(raw.description, `${where}.description`, errors);
|
|
777
|
+
if (raw.defaultTheme !== undefined &&
|
|
778
|
+
(typeof raw.defaultTheme !== "string" ||
|
|
779
|
+
!LOOK_ID_PATTERN.test(raw.defaultTheme))) {
|
|
780
|
+
errors.push(`${where}.defaultTheme must be a theme id`);
|
|
781
|
+
}
|
|
782
|
+
if (raw.default !== undefined && typeof raw.default !== "boolean") {
|
|
783
|
+
errors.push(`${where}.default must be a boolean`);
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
if (Array.isArray(value) &&
|
|
787
|
+
value.filter((raw) => isPlainObject(raw) && raw.default === true).length > 1) {
|
|
788
|
+
errors.push('Only one entry in "contributes.layouts" may set default');
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const HEX_COLOR = /^#[0-9a-fA-F]{3,8}$/;
|
|
792
|
+
function validateThemes(value, errors) {
|
|
793
|
+
validateLookList(value, "themes", ["id", "label", "mode", "css", "swatch"], errors, (raw, where) => {
|
|
794
|
+
if (raw.mode !== "light" && raw.mode !== "dark") {
|
|
795
|
+
errors.push(`${where}.mode must be "light" or "dark"`);
|
|
796
|
+
}
|
|
797
|
+
if (requireString(raw.css, `${where}.css`, errors)) {
|
|
798
|
+
requireRelativePath(raw.css, `${where}.css`, errors);
|
|
799
|
+
if (!raw.css.endsWith(".css")) {
|
|
800
|
+
errors.push(`${where}.css must be a .css file`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (raw.swatch !== undefined) {
|
|
804
|
+
if (!isPlainObject(raw.swatch)) {
|
|
805
|
+
errors.push(`${where}.swatch must be an object`);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
rejectUnknown(raw.swatch, ["background", "panel", "text"], `${where}.swatch`, errors);
|
|
809
|
+
for (const key of ["background", "panel", "text"]) {
|
|
810
|
+
const color = raw.swatch[key];
|
|
811
|
+
if (typeof color !== "string" || !HEX_COLOR.test(color)) {
|
|
812
|
+
errors.push(`${where}.swatch.${key} must be a hex color`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
});
|
|
737
817
|
}
|
|
738
818
|
export const PROTOCOL_ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
|
|
739
819
|
export const PROTOCOL_FIELD_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9]{0,31}$/;
|
|
@@ -1595,6 +1675,14 @@ export function parseManifest(raw) {
|
|
|
1595
1675
|
if (contributesAuth && !manifest.capabilities.includes("auth:provide")) {
|
|
1596
1676
|
errors.push('contributes.auth needs the "auth:provide" capability in the capabilities array');
|
|
1597
1677
|
}
|
|
1678
|
+
if ((manifest.contributes?.layouts?.length ?? 0) > 0 &&
|
|
1679
|
+
!manifest.capabilities.includes("ui:layout")) {
|
|
1680
|
+
errors.push('contributes.layouts needs the "ui:layout" capability in the capabilities array');
|
|
1681
|
+
}
|
|
1682
|
+
if ((manifest.contributes?.themes?.length ?? 0) > 0 &&
|
|
1683
|
+
!manifest.capabilities.includes("ui:theme")) {
|
|
1684
|
+
errors.push('contributes.themes needs the "ui:theme" capability in the capabilities array');
|
|
1685
|
+
}
|
|
1598
1686
|
for (const secret of manifest.requiresSecret ?? []) {
|
|
1599
1687
|
if (secret.plugin === manifest.id) {
|
|
1600
1688
|
errors.push(`requiresSecret["${secret.key}"] points at this plugin; use ctx.secrets.get instead`);
|
package/dist/testing/setup.js
CHANGED
|
@@ -89,6 +89,7 @@ globalThis.__termixTestPluginHost = {
|
|
|
89
89
|
useHostStatus: () => null,
|
|
90
90
|
useCurrentUser: () => null,
|
|
91
91
|
useTheme: () => ({ theme: "dark" }),
|
|
92
|
+
useShell: () => null,
|
|
92
93
|
toast: { success: noop, error: noop, info: noop, warning: noop },
|
|
93
94
|
getApi: () => ({
|
|
94
95
|
get: offline,
|
package/dist/testing.d.ts
CHANGED
|
@@ -112,6 +112,8 @@ export interface FakeContextOptions {
|
|
|
112
112
|
linkedUsers?: Record<string, number>;
|
|
113
113
|
/** Failures before ctx.auth.loginRateLimit locks a key. Defaults to 5. */
|
|
114
114
|
loginAttemptLimit?: number;
|
|
115
|
+
/** What ctx.users.getLocale answers, by user id. A missing user answers null. */
|
|
116
|
+
locales?: Record<string, string>;
|
|
115
117
|
}
|
|
116
118
|
export interface FakeProcessOptions {
|
|
117
119
|
run?: (file: string, args: readonly string[], options?: PluginProcessOptions) => PluginProcessHandle | Promise<PluginProcessHandle>;
|
|
@@ -147,6 +149,12 @@ export interface FakeAuthRegistrations {
|
|
|
147
149
|
}>;
|
|
148
150
|
/** Failures recorded through ctx.auth.loginRateLimit, by "<ip>|<key>". */
|
|
149
151
|
loginFailures: Map<string, number>;
|
|
152
|
+
/** Every ctx.auth.issueUserToken call, in order. */
|
|
153
|
+
userTokens: Array<{
|
|
154
|
+
userId: string;
|
|
155
|
+
purpose: string;
|
|
156
|
+
ttlSeconds?: number;
|
|
157
|
+
}>;
|
|
150
158
|
}
|
|
151
159
|
export interface FakePluginContext {
|
|
152
160
|
ctx: PluginContext;
|
|
@@ -363,6 +371,8 @@ export interface MockContextOptions {
|
|
|
363
371
|
linkedUsers?: Record<string, number>;
|
|
364
372
|
/** See FakeContextOptions. */
|
|
365
373
|
loginAttemptLimit?: number;
|
|
374
|
+
/** See FakeContextOptions. */
|
|
375
|
+
locales?: Record<string, string>;
|
|
366
376
|
}
|
|
367
377
|
export interface MockPluginContext extends FakePluginContext {
|
|
368
378
|
/** Capability ids checked during the test, in order, denied ones included. */
|
|
@@ -377,7 +387,8 @@ export interface MockPluginContext extends FakePluginContext {
|
|
|
377
387
|
* topic outside the plugin's own namespace, settings:read-core on
|
|
378
388
|
* ctx.settings.readCore, ssh:connect plus credentials:use on ctx.ssh,
|
|
379
389
|
* notify:send on ctx.notify (notify:hub on serve), network:outbound on ctx.fetch, process:spawn on
|
|
380
|
-
* ctx.process, system:tls on ctx.system, plugins:read on ctx.plugins,
|
|
390
|
+
* ctx.process, system:tls on ctx.system, plugins:read on ctx.plugins, auth:provide on ctx.auth
|
|
391
|
+
* (users:impersonate on issueUserToken), and users:read on ctx.users. Reading a plugin's own settings is deliberately
|
|
381
392
|
* ungated. As the SDK grows a member, add its gate here in the same shape.
|
|
382
393
|
*/
|
|
383
394
|
export declare function createMockCtx(options?: MockContextOptions): MockPluginContext;
|
package/dist/testing.js
CHANGED
|
@@ -194,6 +194,7 @@ export function createFakeContext(options = {}) {
|
|
|
194
194
|
completedLogins: [],
|
|
195
195
|
revokedSessions: [],
|
|
196
196
|
loginFailures: new Map(),
|
|
197
|
+
userTokens: [],
|
|
197
198
|
};
|
|
198
199
|
const sshClient = options.sshClient ?? {};
|
|
199
200
|
let actor = options.actor;
|
|
@@ -673,6 +674,17 @@ export function createFakeContext(options = {}) {
|
|
|
673
674
|
},
|
|
674
675
|
},
|
|
675
676
|
countLinkedUsers: async (provider) => options.linkedUsers?.[provider] ?? 0,
|
|
677
|
+
issueUserToken: async (userId, tokenOptions) => {
|
|
678
|
+
auth.userTokens.push({ userId, ...tokenOptions });
|
|
679
|
+
const ttl = Math.min(tokenOptions.ttlSeconds ?? 300, 900);
|
|
680
|
+
return {
|
|
681
|
+
token: `fake-token-${auth.userTokens.length}`,
|
|
682
|
+
expiresAt: new Date(Date.now() + ttl * 1000).toISOString(),
|
|
683
|
+
};
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
users: {
|
|
687
|
+
getLocale: async (userId) => options.locales?.[userId] ?? null,
|
|
676
688
|
},
|
|
677
689
|
desktop: {
|
|
678
690
|
openIsolatedWindow: async (request) => {
|
|
@@ -914,7 +926,8 @@ export function createFakeContext(options = {}) {
|
|
|
914
926
|
* topic outside the plugin's own namespace, settings:read-core on
|
|
915
927
|
* ctx.settings.readCore, ssh:connect plus credentials:use on ctx.ssh,
|
|
916
928
|
* notify:send on ctx.notify (notify:hub on serve), network:outbound on ctx.fetch, process:spawn on
|
|
917
|
-
* ctx.process, system:tls on ctx.system, plugins:read on ctx.plugins,
|
|
929
|
+
* ctx.process, system:tls on ctx.system, plugins:read on ctx.plugins, auth:provide on ctx.auth
|
|
930
|
+
* (users:impersonate on issueUserToken), and users:read on ctx.users. Reading a plugin's own settings is deliberately
|
|
918
931
|
* ungated. As the SDK grows a member, add its gate here in the same shape.
|
|
919
932
|
*/
|
|
920
933
|
export function createMockCtx(options = {}) {
|
|
@@ -948,6 +961,7 @@ export function createMockCtx(options = {}) {
|
|
|
948
961
|
baseUrl: options.baseUrl,
|
|
949
962
|
linkedUsers: options.linkedUsers,
|
|
950
963
|
loginAttemptLimit: options.loginAttemptLimit,
|
|
964
|
+
locales: options.locales,
|
|
951
965
|
manifest: {
|
|
952
966
|
capabilities: options.capabilities ?? [],
|
|
953
967
|
...options.manifest,
|
|
@@ -1218,6 +1232,16 @@ export function createMockCtx(options = {}) {
|
|
|
1218
1232
|
require("auth:provide");
|
|
1219
1233
|
return ctx.auth.countLinkedUsers(provider);
|
|
1220
1234
|
},
|
|
1235
|
+
issueUserToken: async (userId, tokenOptions) => {
|
|
1236
|
+
require("users:impersonate");
|
|
1237
|
+
return ctx.auth.issueUserToken(userId, tokenOptions);
|
|
1238
|
+
},
|
|
1239
|
+
},
|
|
1240
|
+
users: {
|
|
1241
|
+
getLocale: async (userId) => {
|
|
1242
|
+
require("users:read");
|
|
1243
|
+
return ctx.users.getLocale(userId);
|
|
1244
|
+
},
|
|
1221
1245
|
},
|
|
1222
1246
|
desktop: {
|
|
1223
1247
|
openIsolatedWindow: async (request) => {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks a theme's CSS before core serves it.
|
|
3
|
+
*
|
|
4
|
+
* A theme is colors and styles only, so its file may hold nothing but rules
|
|
5
|
+
* whose every selector starts with the theme's own class (`.nord`,
|
|
6
|
+
* `:root.nord`, `html.nord ...`), optionally inside @media or @supports.
|
|
7
|
+
* Anything that could load from elsewhere or run code is refused: @import,
|
|
8
|
+
* url() to another host, other at-rules, expression() and javascript:.
|
|
9
|
+
*/
|
|
10
|
+
/** Every problem with a theme's CSS, empty when it is safe to serve. */
|
|
11
|
+
export declare function validateThemeCss(css: string, themeId: string): string[];
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks a theme's CSS before core serves it.
|
|
3
|
+
*
|
|
4
|
+
* A theme is colors and styles only, so its file may hold nothing but rules
|
|
5
|
+
* whose every selector starts with the theme's own class (`.nord`,
|
|
6
|
+
* `:root.nord`, `html.nord ...`), optionally inside @media or @supports.
|
|
7
|
+
* Anything that could load from elsewhere or run code is refused: @import,
|
|
8
|
+
* url() to another host, other at-rules, expression() and javascript:.
|
|
9
|
+
*/
|
|
10
|
+
const ALLOWED_BLOCK_AT_RULES = new Set(["media", "supports"]);
|
|
11
|
+
function stripComments(css) {
|
|
12
|
+
return css.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
13
|
+
}
|
|
14
|
+
/** Splits on commas that are not inside parentheses or brackets. */
|
|
15
|
+
function splitSelectors(prelude) {
|
|
16
|
+
const parts = [];
|
|
17
|
+
let depth = 0;
|
|
18
|
+
let current = "";
|
|
19
|
+
for (const ch of prelude) {
|
|
20
|
+
if (ch === "(" || ch === "[")
|
|
21
|
+
depth += 1;
|
|
22
|
+
if (ch === ")" || ch === "]")
|
|
23
|
+
depth -= 1;
|
|
24
|
+
if (ch === "," && depth === 0) {
|
|
25
|
+
parts.push(current);
|
|
26
|
+
current = "";
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
current += ch;
|
|
30
|
+
}
|
|
31
|
+
parts.push(current);
|
|
32
|
+
return parts.map((part) => part.trim());
|
|
33
|
+
}
|
|
34
|
+
function withoutStrings(css) {
|
|
35
|
+
return css.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '""');
|
|
36
|
+
}
|
|
37
|
+
function escapeRegExp(value) {
|
|
38
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
39
|
+
}
|
|
40
|
+
/** Index of the brace closing the one at `open`, skipping strings. */
|
|
41
|
+
function matchingBrace(css, open) {
|
|
42
|
+
let depth = 0;
|
|
43
|
+
let quote = null;
|
|
44
|
+
for (let i = open; i < css.length; i += 1) {
|
|
45
|
+
const ch = css[i];
|
|
46
|
+
if (quote) {
|
|
47
|
+
if (ch === "\\")
|
|
48
|
+
i += 1;
|
|
49
|
+
else if (ch === quote)
|
|
50
|
+
quote = null;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (ch === '"' || ch === "'")
|
|
54
|
+
quote = ch;
|
|
55
|
+
else if (ch === "{")
|
|
56
|
+
depth += 1;
|
|
57
|
+
else if (ch === "}") {
|
|
58
|
+
depth -= 1;
|
|
59
|
+
if (depth === 0)
|
|
60
|
+
return i;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return -1;
|
|
64
|
+
}
|
|
65
|
+
function checkUrls(css, errors) {
|
|
66
|
+
const urls = css.matchAll(/url\(\s*(['"]?)([^'")]*)\1\s*\)/gi);
|
|
67
|
+
for (const match of urls) {
|
|
68
|
+
const target = match[2].trim();
|
|
69
|
+
if (target.startsWith("data:"))
|
|
70
|
+
continue;
|
|
71
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("//")) {
|
|
72
|
+
errors.push(`url(${target}) points outside the plugin`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function checkRules(css, scope, themeId, errors) {
|
|
77
|
+
let index = 0;
|
|
78
|
+
while (index < css.length) {
|
|
79
|
+
const rest = css.slice(index);
|
|
80
|
+
if (rest.trim() === "")
|
|
81
|
+
return;
|
|
82
|
+
const open = css.indexOf("{", index);
|
|
83
|
+
const semicolon = css.indexOf(";", index);
|
|
84
|
+
if (semicolon !== -1 && (open === -1 || semicolon < open)) {
|
|
85
|
+
const statement = css.slice(index, semicolon).trim();
|
|
86
|
+
errors.push(statement.startsWith("@")
|
|
87
|
+
? `${statement.split(/\s/)[0]} is not allowed in a theme`
|
|
88
|
+
: `Unexpected "${statement}" outside a rule`);
|
|
89
|
+
index = semicolon + 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (open === -1) {
|
|
93
|
+
errors.push(`Unexpected "${rest.trim()}" at the end`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const close = matchingBrace(css, open);
|
|
97
|
+
if (close === -1) {
|
|
98
|
+
errors.push("A rule is missing its closing brace");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const prelude = css.slice(index, open).trim();
|
|
102
|
+
const body = css.slice(open + 1, close);
|
|
103
|
+
if (prelude.startsWith("@")) {
|
|
104
|
+
const name = prelude.slice(1).split(/[\s(]/)[0].toLowerCase();
|
|
105
|
+
if (ALLOWED_BLOCK_AT_RULES.has(name)) {
|
|
106
|
+
checkRules(body, scope, themeId, errors);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
errors.push(`@${name} is not allowed in a theme`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
for (const selector of splitSelectors(prelude)) {
|
|
114
|
+
if (!scope.test(selector)) {
|
|
115
|
+
errors.push(`Selector "${selector}" must start with .${themeId}, :root.${themeId} or html.${themeId}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (withoutStrings(body).includes("{")) {
|
|
119
|
+
errors.push(`Nested rules are not allowed in "${prelude}"`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
index = close + 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Every problem with a theme's CSS, empty when it is safe to serve. */
|
|
126
|
+
export function validateThemeCss(css, themeId) {
|
|
127
|
+
const errors = [];
|
|
128
|
+
const clean = stripComments(css);
|
|
129
|
+
if (/@import\b/i.test(clean))
|
|
130
|
+
errors.push("@import is not allowed in a theme");
|
|
131
|
+
if (/expression\s*\(/i.test(clean))
|
|
132
|
+
errors.push("expression() is not allowed in a theme");
|
|
133
|
+
if (/javascript:/i.test(clean))
|
|
134
|
+
errors.push("javascript: is not allowed in a theme");
|
|
135
|
+
if (/-moz-binding|behavior\s*:/i.test(clean))
|
|
136
|
+
errors.push("Script bindings are not allowed in a theme");
|
|
137
|
+
checkUrls(clean, errors);
|
|
138
|
+
const id = escapeRegExp(themeId);
|
|
139
|
+
const scope = new RegExp(`^(?::root|html)?\\.${id}(?![\\w-])`);
|
|
140
|
+
checkRules(clean, scope, themeId, errors);
|
|
141
|
+
return [...new Set(errors)];
|
|
142
|
+
}
|
package/dist/vitest-preset.d.ts
CHANGED
|
@@ -27,8 +27,14 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
|
|
|
27
27
|
enforce: "pre";
|
|
28
28
|
load(id: string): string;
|
|
29
29
|
}[];
|
|
30
|
+
server: {
|
|
31
|
+
fs: {
|
|
32
|
+
allow: string[];
|
|
33
|
+
};
|
|
34
|
+
};
|
|
30
35
|
resolve: {
|
|
31
36
|
alias: Record<string, string>;
|
|
37
|
+
dedupe: string[];
|
|
32
38
|
};
|
|
33
39
|
test: {
|
|
34
40
|
globals: boolean;
|
|
@@ -40,8 +46,14 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
|
|
|
40
46
|
enforce: "pre";
|
|
41
47
|
load(id: string): string;
|
|
42
48
|
}[];
|
|
49
|
+
server: {
|
|
50
|
+
fs: {
|
|
51
|
+
allow: string[];
|
|
52
|
+
};
|
|
53
|
+
};
|
|
43
54
|
resolve: {
|
|
44
55
|
alias: Record<string, string>;
|
|
56
|
+
dedupe: string[];
|
|
45
57
|
};
|
|
46
58
|
test: {
|
|
47
59
|
name: string;
|
|
@@ -59,8 +71,14 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
|
|
|
59
71
|
enforce: "pre";
|
|
60
72
|
load(id: string): string;
|
|
61
73
|
}[];
|
|
74
|
+
server: {
|
|
75
|
+
fs: {
|
|
76
|
+
allow: string[];
|
|
77
|
+
};
|
|
78
|
+
};
|
|
62
79
|
resolve: {
|
|
63
80
|
alias: Record<string, string>;
|
|
81
|
+
dedupe: string[];
|
|
64
82
|
};
|
|
65
83
|
test: {
|
|
66
84
|
name: string;
|
package/dist/vitest-preset.js
CHANGED
|
@@ -80,9 +80,18 @@ export function pluginVitestConfig(configUrl, options = {}) {
|
|
|
80
80
|
// unexpectedly". Threads don't.
|
|
81
81
|
const pool = "threads";
|
|
82
82
|
const plugins = [wasmBinaryPlugin()];
|
|
83
|
+
// A local SDK linked into node_modules sits outside the plugin folder, so
|
|
84
|
+
// Vite must be allowed to read it, and React must come from the plugin.
|
|
85
|
+
const sdkDir = fs.realpathSync(fileURLToPath(new URL("..", import.meta.url)));
|
|
86
|
+
const server = { fs: { allow: [pluginDir, sdkDir] } };
|
|
87
|
+
const resolve = {
|
|
88
|
+
alias,
|
|
89
|
+
dedupe: ["react", "react-dom", "react-i18next", "i18next"],
|
|
90
|
+
};
|
|
83
91
|
return {
|
|
84
92
|
plugins,
|
|
85
|
-
|
|
93
|
+
server,
|
|
94
|
+
resolve,
|
|
86
95
|
test: {
|
|
87
96
|
globals: true,
|
|
88
97
|
setupFiles: [setupFile],
|
|
@@ -90,7 +99,8 @@ export function pluginVitestConfig(configUrl, options = {}) {
|
|
|
90
99
|
projects: [
|
|
91
100
|
{
|
|
92
101
|
plugins,
|
|
93
|
-
|
|
102
|
+
server,
|
|
103
|
+
resolve,
|
|
94
104
|
test: {
|
|
95
105
|
name: `${pluginId}:backend`,
|
|
96
106
|
root: pluginDir,
|
|
@@ -103,7 +113,8 @@ export function pluginVitestConfig(configUrl, options = {}) {
|
|
|
103
113
|
},
|
|
104
114
|
{
|
|
105
115
|
plugins,
|
|
106
|
-
|
|
116
|
+
server,
|
|
117
|
+
resolve,
|
|
107
118
|
test: {
|
|
108
119
|
name: `${pluginId}:frontend`,
|
|
109
120
|
root: pluginDir,
|
package/package.json
CHANGED