@termix-ssh/plugin-sdk 1.0.11 → 1.0.13
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 +37 -0
- package/PLUGIN-GUIDE.md +178 -54
- 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 +404 -0
- package/dist/capabilities.js +7 -0
- package/dist/db.js +1 -0
- package/dist/desktop.d.ts +0 -243
- package/dist/frontend.d.ts +175 -2
- package/dist/frontend.js +7 -0
- package/dist/host/PluginsTab-OXQQ7M4I.js +3266 -0
- package/dist/host/chunk-54KCK6IZ.js +3167 -0
- package/dist/host/chunk-CALNJCHN.js +10435 -0
- package/dist/host/{chunk-7RRTYD6B.js → chunk-CN5UBTBP.js} +1412 -14
- package/dist/host/{chunk-RLQBKJRK.js → chunk-ERGN4YW6.js} +9204 -11492
- package/dist/host/{chunk-LSWB3ZZV.js → chunk-T2PZWXFJ.js} +6 -1
- package/dist/host/chunk-YGYBM7SQ.js +58 -0
- package/dist/host/core-classes.json +1 -1
- package/dist/host/{linked-server-UCW74T2V.js → linked-server-5VFO4XQB.js} +2 -2
- package/dist/host/{rbac-api-6UYMFIFV.js → rbac-api-5YMBJAO5.js} +2 -2
- package/dist/host/testing-host.js +123 -57
- package/dist/host/types/src/types/onboarding.d.ts +0 -2
- package/dist/host/types/src/types/ui-types.d.ts +2 -1
- package/dist/host/types/src/ui/api/audit-log-api.d.ts +36 -0
- package/dist/host/types/src/ui/api/credentials-api.d.ts +0 -22
- 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 +30 -0
- package/dist/host/types/src/ui/api/private-endpoints-api.d.ts +6 -0
- package/dist/host/types/src/ui/api/settings-api.d.ts +0 -6
- package/dist/host/types/src/ui/api/ssh-host-management-api.d.ts +0 -28
- 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/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/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 +4 -3
- 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/main-axios.d.ts +2 -2
- package/dist/host/types/src/ui/manage/manage-requests.d.ts +34 -0
- package/dist/host/types/src/ui/onboarding/onboarding-registry.d.ts +3 -1
- 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 +101 -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 +136 -0
- package/dist/host/types/src/ui/plugins/use-plugins-manager.d.ts +78 -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/build-host-tree.d.ts +25 -0
- package/dist/host/types/src/ui/sidebar/host-contributions.d.ts +10 -0
- package/dist/host/ui.js +6045 -4055
- package/dist/host/{use-permissions-GYCBY2AU.js → use-permissions-ISEDRY3J.js} +2 -2
- package/dist/manifest.d.ts +50 -1
- package/dist/manifest.js +104 -1
- package/dist/ssh-keys.d.ts +29 -0
- package/dist/ssh-keys.js +85 -0
- package/dist/testing/setup.js +1 -0
- package/dist/testing.d.ts +32 -15
- package/dist/testing.js +218 -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 +5 -1
- package/schema/manifest.schema.json +121 -8
- package/tsconfig.plugin.json +1 -0
- package/dist/host/chunk-YC2MAKSP.js +0 -14
package/dist/testing.js
CHANGED
|
@@ -124,6 +124,8 @@ export function createFakeContext(options = {}) {
|
|
|
124
124
|
const pluginId = options.pluginId ?? "test-plugin";
|
|
125
125
|
const disposals = [];
|
|
126
126
|
const emitted = [];
|
|
127
|
+
const hostStatusProviders = [];
|
|
128
|
+
const activityProviders = [];
|
|
127
129
|
const kv = new Map();
|
|
128
130
|
const tables = [];
|
|
129
131
|
const syncEntities = [];
|
|
@@ -158,6 +160,7 @@ export function createFakeContext(options = {}) {
|
|
|
158
160
|
};
|
|
159
161
|
const secretStore = new Map();
|
|
160
162
|
const audits = [];
|
|
163
|
+
const auditSubscribers = new Set();
|
|
161
164
|
const activities = [];
|
|
162
165
|
const trackedSessions = [];
|
|
163
166
|
const interactions = [];
|
|
@@ -261,7 +264,7 @@ export function createFakeContext(options = {}) {
|
|
|
261
264
|
files: {
|
|
262
265
|
// A fake path; a test that needs a real folder uses the OS temp dir
|
|
263
266
|
// itself, not this double.
|
|
264
|
-
dataDir: async () => `/tmp/${pluginId}`,
|
|
267
|
+
dataDir: async () => options.dataDir ?? `/tmp/${pluginId}`,
|
|
265
268
|
},
|
|
266
269
|
db: options.db
|
|
267
270
|
? {
|
|
@@ -461,6 +464,27 @@ export function createFakeContext(options = {}) {
|
|
|
461
464
|
},
|
|
462
465
|
disposables,
|
|
463
466
|
hosts: {
|
|
467
|
+
importMany: async (rows) => {
|
|
468
|
+
const created = rows.map(() => nextHostId++);
|
|
469
|
+
return {
|
|
470
|
+
success: created.length,
|
|
471
|
+
updated: 0,
|
|
472
|
+
skipped: 0,
|
|
473
|
+
failed: 0,
|
|
474
|
+
errors: [],
|
|
475
|
+
createdHostIds: created,
|
|
476
|
+
};
|
|
477
|
+
},
|
|
478
|
+
importSshConfig: async () => ({
|
|
479
|
+
success: 0,
|
|
480
|
+
updated: 0,
|
|
481
|
+
skipped: 0,
|
|
482
|
+
failed: 0,
|
|
483
|
+
errors: [],
|
|
484
|
+
createdHostIds: [],
|
|
485
|
+
}),
|
|
486
|
+
exportMany: async () => ({ hosts: [] }),
|
|
487
|
+
registerImportNormalizer: () => () => { },
|
|
464
488
|
list: async () => [...hostsById.values()],
|
|
465
489
|
get: async (hostId) => hostsById.get(hostId) ?? null,
|
|
466
490
|
checkAccess: async (hostId) => {
|
|
@@ -519,7 +543,27 @@ export function createFakeContext(options = {}) {
|
|
|
519
543
|
recordActivity: async (hostId, type, hostName) => {
|
|
520
544
|
activities.push({ hostId, type, hostName });
|
|
521
545
|
},
|
|
546
|
+
probeTargets: async (filter) => (options.probeTargets ?? []).filter((target) => !filter?.hostIds || filter.hostIds.includes(target.id)),
|
|
547
|
+
sessionActive: (hostId) => options.activeSessions?.includes(hostId) ?? false,
|
|
548
|
+
activity: {
|
|
549
|
+
provide: (provider) => {
|
|
550
|
+
activityProviders.push(provider);
|
|
551
|
+
return () => {
|
|
552
|
+
const at = activityProviders.indexOf(provider);
|
|
553
|
+
if (at >= 0)
|
|
554
|
+
activityProviders.splice(at, 1);
|
|
555
|
+
};
|
|
556
|
+
},
|
|
557
|
+
},
|
|
522
558
|
status: {
|
|
559
|
+
provide: (provider) => {
|
|
560
|
+
hostStatusProviders.push(provider);
|
|
561
|
+
return () => {
|
|
562
|
+
const at = hostStatusProviders.indexOf(provider);
|
|
563
|
+
if (at >= 0)
|
|
564
|
+
hostStatusProviders.splice(at, 1);
|
|
565
|
+
};
|
|
566
|
+
},
|
|
523
567
|
get: async (hostId) => hostStatuses.get(hostId) ?? null,
|
|
524
568
|
check: async (hostId) => hostStatuses.get(hostId) ?? null,
|
|
525
569
|
reportLogin: (hostId, outcome) => {
|
|
@@ -543,11 +587,36 @@ export function createFakeContext(options = {}) {
|
|
|
543
587
|
after: (delayMs, fn) => schedule("after", delayMs, fn),
|
|
544
588
|
},
|
|
545
589
|
audit: {
|
|
590
|
+
subscribe: (handler) => {
|
|
591
|
+
auditSubscribers.add(handler);
|
|
592
|
+
return () => {
|
|
593
|
+
auditSubscribers.delete(handler);
|
|
594
|
+
};
|
|
595
|
+
},
|
|
596
|
+
list: async (listOptions) => {
|
|
597
|
+
const matching = filterAuditEntries(options.auditEntries ?? [], listOptions?.filters).reverse();
|
|
598
|
+
const offset = listOptions?.offset ?? 0;
|
|
599
|
+
return {
|
|
600
|
+
entries: matching.slice(offset, offset + (listOptions?.limit ?? 50)),
|
|
601
|
+
total: matching.length,
|
|
602
|
+
};
|
|
603
|
+
},
|
|
604
|
+
exportBatches: async function* (filters, batchSize) {
|
|
605
|
+
const matching = filterAuditEntries(options.auditEntries ?? [], filters);
|
|
606
|
+
const size = batchSize ?? 500;
|
|
607
|
+
for (let at = 0; at < matching.length; at += size) {
|
|
608
|
+
yield matching.slice(at, at + size);
|
|
609
|
+
}
|
|
610
|
+
},
|
|
546
611
|
record: async (entry) => {
|
|
547
612
|
audits.push({ ...entry });
|
|
548
613
|
},
|
|
549
614
|
},
|
|
615
|
+
network: {
|
|
616
|
+
tcpProbe: async (host, port) => options.reachable?.(host, port) ?? false,
|
|
617
|
+
},
|
|
550
618
|
ssh: {
|
|
619
|
+
probe: async (hostId, probeOptions) => options.reachable?.(hostId, probeOptions?.port) ?? false,
|
|
551
620
|
connect: async (host, connectOptions) => {
|
|
552
621
|
sshConnections.push({ host, options: connectOptions });
|
|
553
622
|
return {
|
|
@@ -559,6 +628,16 @@ export function createFakeContext(options = {}) {
|
|
|
559
628
|
dispose: () => { },
|
|
560
629
|
};
|
|
561
630
|
},
|
|
631
|
+
exec: async (host) => {
|
|
632
|
+
sshConnections.push({ host });
|
|
633
|
+
return {
|
|
634
|
+
stdout: "",
|
|
635
|
+
stderr: "",
|
|
636
|
+
code: 0,
|
|
637
|
+
truncated: false,
|
|
638
|
+
timedOut: false,
|
|
639
|
+
};
|
|
640
|
+
},
|
|
562
641
|
withConnection: async (host, connectOptions, fn) => {
|
|
563
642
|
sshConnections.push({ host, pool: connectOptions.pool });
|
|
564
643
|
return fn(sshClient);
|
|
@@ -696,6 +775,7 @@ export function createFakeContext(options = {}) {
|
|
|
696
775
|
return { success: true };
|
|
697
776
|
},
|
|
698
777
|
available: () => options.desktopAvailable ?? false,
|
|
778
|
+
local: options.desktopLocal ?? unavailableDesktopLocal(),
|
|
699
779
|
},
|
|
700
780
|
credentials: {
|
|
701
781
|
listSshKeys: async () => [
|
|
@@ -716,6 +796,12 @@ export function createFakeContext(options = {}) {
|
|
|
716
796
|
credentialReads.push({ hostId, protocol });
|
|
717
797
|
return options.protocolTargets?.[`${hostId}:${protocol}`] ?? null;
|
|
718
798
|
},
|
|
799
|
+
publicKey: async (credentialId) => (options.sshKeyCredentials ?? []).find((k) => k.id === credentialId)
|
|
800
|
+
?.publicKey ?? null,
|
|
801
|
+
resolveKeyRefs: async (key, passphrase) => ({
|
|
802
|
+
key,
|
|
803
|
+
passphrase: passphrase ?? undefined,
|
|
804
|
+
}),
|
|
719
805
|
registerSecretResolver: (scheme, resolve) => {
|
|
720
806
|
auth.secretResolvers.set(scheme, resolve);
|
|
721
807
|
disposals.push(() => {
|
|
@@ -863,6 +949,8 @@ export function createFakeContext(options = {}) {
|
|
|
863
949
|
return errors;
|
|
864
950
|
},
|
|
865
951
|
disposals,
|
|
952
|
+
hostStatusProviders,
|
|
953
|
+
activityProviders,
|
|
866
954
|
emitted,
|
|
867
955
|
kv,
|
|
868
956
|
tables,
|
|
@@ -886,6 +974,10 @@ export function createFakeContext(options = {}) {
|
|
|
886
974
|
tls,
|
|
887
975
|
secretStore,
|
|
888
976
|
audits,
|
|
977
|
+
emitAudit: (entry) => {
|
|
978
|
+
for (const handler of auditSubscribers)
|
|
979
|
+
handler(entry);
|
|
980
|
+
},
|
|
889
981
|
activities,
|
|
890
982
|
trackedSessions,
|
|
891
983
|
interactions,
|
|
@@ -930,6 +1022,49 @@ export function createFakeContext(options = {}) {
|
|
|
930
1022
|
* (users:impersonate on issueUserToken), and users:read on ctx.users. Reading a plugin's own settings is deliberately
|
|
931
1023
|
* ungated. As the SDK grows a member, add its gate here in the same shape.
|
|
932
1024
|
*/
|
|
1025
|
+
function filterAuditEntries(entries, filters) {
|
|
1026
|
+
return entries.filter((entry) => (filters?.userId === undefined || entry.userId === filters.userId) &&
|
|
1027
|
+
(filters?.action === undefined || entry.action === filters.action) &&
|
|
1028
|
+
(filters?.resourceType === undefined ||
|
|
1029
|
+
entry.resourceType === filters.resourceType) &&
|
|
1030
|
+
(filters?.success === undefined || entry.success === filters.success) &&
|
|
1031
|
+
(filters?.startDate === undefined ||
|
|
1032
|
+
entry.timestamp >= filters.startDate) &&
|
|
1033
|
+
(filters?.endDate === undefined || entry.timestamp <= filters.endDate));
|
|
1034
|
+
}
|
|
1035
|
+
function gateDesktopLocal(target, check) {
|
|
1036
|
+
const wrap = (value) => {
|
|
1037
|
+
if (typeof value === "function") {
|
|
1038
|
+
return (...args) => {
|
|
1039
|
+
check();
|
|
1040
|
+
return value(...args);
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
if (value && typeof value === "object") {
|
|
1044
|
+
return new Proxy(value, {
|
|
1045
|
+
get: (obj, key) => wrap(obj[key]),
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
return value;
|
|
1049
|
+
};
|
|
1050
|
+
return new Proxy(target, {
|
|
1051
|
+
get: (obj, key) => key === "available"
|
|
1052
|
+
? obj.available
|
|
1053
|
+
: wrap(obj[key]),
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
function unavailableDesktopLocal() {
|
|
1057
|
+
const fail = () => {
|
|
1058
|
+
throw new Error("ctx.desktop.local needs the desktopLocal test option");
|
|
1059
|
+
};
|
|
1060
|
+
const deep = () => new Proxy(() => fail(), { get: () => deep(), apply: () => fail() });
|
|
1061
|
+
return {
|
|
1062
|
+
available: () => false,
|
|
1063
|
+
info: fail,
|
|
1064
|
+
fs: deep(),
|
|
1065
|
+
pty: deep(),
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
933
1068
|
export function createMockCtx(options = {}) {
|
|
934
1069
|
const granted = new Set(options.capabilities ?? []);
|
|
935
1070
|
const checked = [];
|
|
@@ -938,6 +1073,7 @@ export function createMockCtx(options = {}) {
|
|
|
938
1073
|
actor: options.actor,
|
|
939
1074
|
settings: options.settings,
|
|
940
1075
|
coreSettings: options.coreSettings,
|
|
1076
|
+
dataDir: options.dataDir,
|
|
941
1077
|
sshClient: options.sshClient,
|
|
942
1078
|
db: options.db,
|
|
943
1079
|
router: options.router,
|
|
@@ -950,7 +1086,12 @@ export function createMockCtx(options = {}) {
|
|
|
950
1086
|
protocolTargets: options.protocolTargets,
|
|
951
1087
|
sshKeyCredentials: options.sshKeyCredentials,
|
|
952
1088
|
desktopAvailable: options.desktopAvailable,
|
|
1089
|
+
desktopLocal: options.desktopLocal,
|
|
1090
|
+
auditEntries: options.auditEntries,
|
|
953
1091
|
hostStatuses: options.hostStatuses,
|
|
1092
|
+
probeTargets: options.probeTargets,
|
|
1093
|
+
activeSessions: options.activeSessions,
|
|
1094
|
+
reachable: options.reachable,
|
|
954
1095
|
notificationChannels: options.notificationChannels,
|
|
955
1096
|
fetch: options.fetch,
|
|
956
1097
|
process: options.process,
|
|
@@ -1077,6 +1218,24 @@ export function createMockCtx(options = {}) {
|
|
|
1077
1218
|
},
|
|
1078
1219
|
},
|
|
1079
1220
|
hosts: {
|
|
1221
|
+
importMany: async (rows, importOptions) => {
|
|
1222
|
+
require("hosts:write");
|
|
1223
|
+
if (importOptions?.credentials?.length)
|
|
1224
|
+
require("credentials:write");
|
|
1225
|
+
return ctx.hosts.importMany(rows, importOptions);
|
|
1226
|
+
},
|
|
1227
|
+
importSshConfig: async (content, importOptions) => {
|
|
1228
|
+
require("hosts:write");
|
|
1229
|
+
return ctx.hosts.importSshConfig(content, importOptions);
|
|
1230
|
+
},
|
|
1231
|
+
exportMany: async (exportOptions) => {
|
|
1232
|
+
require("hosts:export");
|
|
1233
|
+
return ctx.hosts.exportMany(exportOptions);
|
|
1234
|
+
},
|
|
1235
|
+
registerImportNormalizer: (normalizer) => {
|
|
1236
|
+
require("hosts:write");
|
|
1237
|
+
return ctx.hosts.registerImportNormalizer(normalizer);
|
|
1238
|
+
},
|
|
1080
1239
|
list: async () => {
|
|
1081
1240
|
require("hosts:read");
|
|
1082
1241
|
return ctx.hosts.list();
|
|
@@ -1125,7 +1284,26 @@ export function createMockCtx(options = {}) {
|
|
|
1125
1284
|
require("hosts:read");
|
|
1126
1285
|
return ctx.hosts.recordActivity(hostId, type, hostName);
|
|
1127
1286
|
},
|
|
1287
|
+
probeTargets: async (filter) => {
|
|
1288
|
+
require("hosts:read");
|
|
1289
|
+
return ctx.hosts.probeTargets(filter);
|
|
1290
|
+
},
|
|
1291
|
+
sessionActive: (hostId) => {
|
|
1292
|
+
require("hosts:read");
|
|
1293
|
+
return ctx.hosts.sessionActive(hostId);
|
|
1294
|
+
},
|
|
1295
|
+
activity: {
|
|
1296
|
+
provide: (provider) => {
|
|
1297
|
+
require("hosts:activity-provide");
|
|
1298
|
+
return ctx.hosts.activity.provide(provider);
|
|
1299
|
+
},
|
|
1300
|
+
},
|
|
1128
1301
|
status: {
|
|
1302
|
+
provide: (provider) => {
|
|
1303
|
+
require("hosts:read");
|
|
1304
|
+
require("hosts:status-provide");
|
|
1305
|
+
return ctx.hosts.status.provide(provider);
|
|
1306
|
+
},
|
|
1129
1307
|
get: async (hostId) => {
|
|
1130
1308
|
require("hosts:read");
|
|
1131
1309
|
return ctx.hosts.status.get(hostId);
|
|
@@ -1146,11 +1324,20 @@ export function createMockCtx(options = {}) {
|
|
|
1146
1324
|
},
|
|
1147
1325
|
ssh: {
|
|
1148
1326
|
...ctx.ssh,
|
|
1327
|
+
probe: async (hostId, probeOptions) => {
|
|
1328
|
+
require("ssh:connect");
|
|
1329
|
+
return ctx.ssh.probe(hostId, probeOptions);
|
|
1330
|
+
},
|
|
1149
1331
|
connect: async (host, connectOptions) => {
|
|
1150
1332
|
require("ssh:connect");
|
|
1151
1333
|
require("credentials:use");
|
|
1152
1334
|
return ctx.ssh.connect(host, connectOptions);
|
|
1153
1335
|
},
|
|
1336
|
+
exec: async (host, command, execOptions) => {
|
|
1337
|
+
require("ssh:connect");
|
|
1338
|
+
require("credentials:use");
|
|
1339
|
+
return ctx.ssh.exec(host, command, execOptions);
|
|
1340
|
+
},
|
|
1154
1341
|
withConnection: async (host, connectOptions, fn) => {
|
|
1155
1342
|
require("ssh:connect");
|
|
1156
1343
|
require("credentials:use");
|
|
@@ -1243,6 +1430,21 @@ export function createMockCtx(options = {}) {
|
|
|
1243
1430
|
return ctx.users.getLocale(userId);
|
|
1244
1431
|
},
|
|
1245
1432
|
},
|
|
1433
|
+
audit: {
|
|
1434
|
+
record: (entry) => ctx.audit.record(entry),
|
|
1435
|
+
subscribe: (handler) => {
|
|
1436
|
+
require("audit:read");
|
|
1437
|
+
return ctx.audit.subscribe(handler);
|
|
1438
|
+
},
|
|
1439
|
+
list: async (listOptions) => {
|
|
1440
|
+
require("audit:read");
|
|
1441
|
+
return ctx.audit.list(listOptions);
|
|
1442
|
+
},
|
|
1443
|
+
exportBatches: (filters, batchSize) => {
|
|
1444
|
+
require("audit:read");
|
|
1445
|
+
return ctx.audit.exportBatches(filters, batchSize);
|
|
1446
|
+
},
|
|
1447
|
+
},
|
|
1246
1448
|
desktop: {
|
|
1247
1449
|
openIsolatedWindow: async (request) => {
|
|
1248
1450
|
require("desktop:window");
|
|
@@ -1253,6 +1455,7 @@ export function createMockCtx(options = {}) {
|
|
|
1253
1455
|
return ctx.desktop.launchExternalClient(request);
|
|
1254
1456
|
},
|
|
1255
1457
|
available: () => ctx.desktop.available(),
|
|
1458
|
+
local: gateDesktopLocal(ctx.desktop.local, () => require("desktop:local")),
|
|
1256
1459
|
},
|
|
1257
1460
|
credentials: {
|
|
1258
1461
|
listSshKeys: async () => {
|
|
@@ -1271,6 +1474,14 @@ export function createMockCtx(options = {}) {
|
|
|
1271
1474
|
}
|
|
1272
1475
|
return ctx.credentials.resolveHostProtocol(hostId, protocol);
|
|
1273
1476
|
},
|
|
1477
|
+
publicKey: async (credentialId) => {
|
|
1478
|
+
require("credentials:use");
|
|
1479
|
+
return ctx.credentials.publicKey(credentialId);
|
|
1480
|
+
},
|
|
1481
|
+
resolveKeyRefs: async (key, passphrase) => {
|
|
1482
|
+
require("credentials:read");
|
|
1483
|
+
return ctx.credentials.resolveKeyRefs(key, passphrase);
|
|
1484
|
+
},
|
|
1274
1485
|
registerSecretResolver: (scheme, resolve) => {
|
|
1275
1486
|
require("auth:provide");
|
|
1276
1487
|
ctx.credentials.registerSecretResolver(scheme, resolve);
|
|
@@ -1294,6 +1505,12 @@ export function createMockCtx(options = {}) {
|
|
|
1294
1505
|
require("network:outbound");
|
|
1295
1506
|
return ctx.fetch(url, init);
|
|
1296
1507
|
},
|
|
1508
|
+
network: {
|
|
1509
|
+
tcpProbe: async (host, port, probeOptions) => {
|
|
1510
|
+
require("network:probe");
|
|
1511
|
+
return ctx.network.tcpProbe(host, port, probeOptions);
|
|
1512
|
+
},
|
|
1513
|
+
},
|
|
1297
1514
|
process: {
|
|
1298
1515
|
run: async (file, args, runOptions) => {
|
|
1299
1516
|
require("process:spawn");
|
|
@@ -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@termix-ssh/plugin-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "The contract between Termix core and a plugin: types, the capability catalog and the manifest schema.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -85,6 +85,10 @@
|
|
|
85
85
|
"types": "./dist/ssh-certs.d.ts",
|
|
86
86
|
"default": "./dist/ssh-certs.js"
|
|
87
87
|
},
|
|
88
|
+
"./ssh-keys": {
|
|
89
|
+
"types": "./dist/ssh-keys.d.ts",
|
|
90
|
+
"default": "./dist/ssh-keys.js"
|
|
91
|
+
},
|
|
88
92
|
"./tsconfig.plugin.json": "./tsconfig.plugin.json",
|
|
89
93
|
"./schema/manifest.schema.json": "./schema/manifest.schema.json",
|
|
90
94
|
"./ui": {
|