@termix-ssh/plugin-sdk 1.0.7 → 1.0.9

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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.9
4
+
5
+ ### Added
6
+
7
+ - Worker entries in `package.json` `termix.workers`, each bundled on its own into `dist/workers/<name>.js`
8
+ - A `.wasm`, `.ttf` or `.otf` import gives the file's bytes, in `termix-plugin build` and in plugin tests
9
+
10
+ ### Fixed
11
+
12
+ - The plugin guide listed jszip, guacamole-lite and @anthropic-ai/sdk as shared packages, but they are bundled
13
+
14
+ ## 1.0.8
15
+
16
+ ### Added
17
+
18
+ - `publicRateLimit` on `ctx.http.router()`, for a plugin whose public routes serve a proxied page
19
+
3
20
  ## 1.0.7
4
21
 
5
22
  ### Fixed
package/PLUGIN-GUIDE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Termix plugin guide
2
2
 
3
- SDK 1.0.7 | Plugin API 1.2.0
3
+ SDK 1.0.9 | Plugin API 1.2.0
4
4
 
5
5
  This guide is for anyone, person or AI coding tool, building a Termix plugin. It ships inside
6
6
  `@termix-ssh/plugin-sdk`, so the copy in your `node_modules` always matches the SDK you have installed.
@@ -283,7 +283,8 @@ router.post("/notes", async (req, res) => {
283
283
  - The `@openapi` path is the full path, starting with `/plugin-api/<id>/`. `build` turns these into
284
284
  `dist/openapi.json` and the docs site renders them.
285
285
  - `ctx.http.router({ public: ["/webhook/:token"], bodyLimit: "20mb", rawBody: false })`. Public routes
286
- have no actor: check their token yourself and rate limit them.
286
+ have no actor: check their token yourself. Core allows 120 public requests a minute per address;
287
+ `publicRateLimit` raises that for a proxied page that loads its own assets.
287
288
  - `ctx.http.baseUrl(req)` is the public address, for callback URLs.
288
289
  - `ctx.ws.route("/live", (socket, conn) => ...)`. Long lived sockets should check
289
290
  `conn.isDataUnlocked()` per message.
@@ -333,8 +334,27 @@ ctx.schedule.every(60_000, () => refresh(), { jitterMs: 5_000 });
333
334
  ### Shared packages
334
335
 
335
336
  These come from Termix and are never bundled: `@termix-ssh/plugin-sdk`, `express`, `ssh2`, `ws`, `multer`,
336
- `cookie-parser`, `axios`, `jszip`, `guacamole-lite`, `@anthropic-ai/sdk`, `drizzle-orm`, `sharp` and every
337
- Node builtin. List the ones you use in `peerDependencies`. Any other npm package is bundled.
337
+ `cookie-parser`, `axios`, `drizzle-orm`, `sharp` and every Node builtin. List the ones you use in
338
+ `peerDependencies`. Any other npm package is bundled.
339
+
340
+ ### Workers and WebAssembly
341
+
342
+ Heavy work (decoding, encoding, parsing) belongs in a worker thread so it never blocks the server. Name
343
+ each worker entry in `package.json`:
344
+
345
+ ```json
346
+ "termix": { "workers": { "engine": "src/backend/engine/worker.ts" } }
347
+ ```
348
+
349
+ `build` bundles it alone into `dist/workers/engine.js`; start it with
350
+ `new Worker(new URL("./workers/engine.js", import.meta.url))`. A worker bundles everything it imports and
351
+ can't use the shared packages above, so it must not import `@termix-ssh/plugin-sdk`. Talk to it with
352
+ `postMessage`.
353
+
354
+ Importing a `.wasm`, `.ttf` or `.otf` file gives its bytes as a `Uint8Array`, in `build` and in tests, so a module ships
355
+ inside the bundle with no file to find. Compile it once on the main thread with `new WebAssembly.Module`
356
+ and post that to each worker. Add `declare module "*.wasm" { const bytes: Uint8Array; export default bytes; }`
357
+ to a `.d.ts` in the plugin.
338
358
 
339
359
  ## Database
340
360
 
@@ -682,20 +702,20 @@ To run your own registry instead, see https://docs.termix.site/develop/registrie
682
702
 
683
703
  ## termix-plugin CLI
684
704
 
685
- | Command | What it does |
686
- | ------------------------------ | ---------------------------------------------------------------------------------------------- |
687
- | `build` | Bundle `dist/backend.js` and `dist/frontend.js`, CSS, locales, migrations, `dist/openapi.json` |
688
- | `validate` | Check the manifest, its files, migrations for every database, the changelog and `package.json` |
689
- | `test` | Apply patches, run Vitest |
690
- | `migrations` | Write a migration per database from `tables.ts`. `--check` fails when one is missing |
691
- | `openapi` | Write `dist/openapi.json` from `@openapi` blocks |
692
- | `pack` | Validate, then write `<id>-<version>.tmxplug`. `--out <dir>` |
693
- | `sign <file>` | Sign a `.tmxplug` with `TERMIX_PLUGIN_SIGNING_KEY` |
694
- | `verify <file> --key <base64>` | Check a `.tmxplug` against its `.sig` |
695
- | `keygen` | Write a new signing key pair. `--out <dir>` |
696
- | `changelog` | Print one version's notes. `--version x.y.z` |
697
- | `patch` | Apply the plugin's dependency patches (`build` and `test` do this too) |
698
- | `dev` | Build, pack and install on a server, again on every change. `--server`, `--key`, `--once` |
705
+ | Command | What it does |
706
+ | ------------------------------ | ----------------------------------------------------------------------------------------------- |
707
+ | `build` | Bundle `dist/backend.js`, `dist/frontend.js`, workers, CSS, locales, migrations, `openapi.json` |
708
+ | `validate` | Check the manifest, its files, migrations for every database, the changelog and `package.json` |
709
+ | `test` | Apply patches, run Vitest |
710
+ | `migrations` | Write a migration per database from `tables.ts`. `--check` fails when one is missing |
711
+ | `openapi` | Write `dist/openapi.json` from `@openapi` blocks |
712
+ | `pack` | Validate, then write `<id>-<version>.tmxplug`. `--out <dir>` |
713
+ | `sign <file>` | Sign a `.tmxplug` with `TERMIX_PLUGIN_SIGNING_KEY` |
714
+ | `verify <file> --key <base64>` | Check a `.tmxplug` against its `.sig` |
715
+ | `keygen` | Write a new signing key pair. `--out <dir>` |
716
+ | `changelog` | Print one version's notes. `--version x.y.z` |
717
+ | `patch` | Apply the plugin's dependency patches (`build` and `test` do this too) |
718
+ | `dev` | Build, pack and install on a server, again on every change. `--server`, `--key`, `--once` |
699
719
 
700
720
  ## SDK entry points
701
721
 
@@ -52,6 +52,43 @@ export function externalRequireInterop(externals) {
52
52
  };
53
53
  }
54
54
 
55
+ // A .wasm or font import becomes its bytes (a Uint8Array) inside the bundle,
56
+ // so it ships in backend.js or a worker with no separate file to find.
57
+ export const BINARY_EXTENSIONS = [".wasm", ".ttf", ".otf"];
58
+ const BINARY_LOADERS = Object.fromEntries(
59
+ BINARY_EXTENSIONS.map((ext) => [ext, "binary"]),
60
+ );
61
+
62
+ const REQUIRE_BANNER =
63
+ "import { createRequire as __termixCreateRequire } from 'node:module'; const require = __termixCreateRequire(import.meta.url);";
64
+
65
+ /**
66
+ * package.json "termix": { "workers": { "engine": "src/backend/engine/worker.ts" } }
67
+ * bundles each entry on its own into dist/workers/<name>.js, for
68
+ * new Worker(new URL("./workers/<name>.js", import.meta.url)). Nothing is left
69
+ * external: core's import fallback only runs on the main thread, so a worker
70
+ * can't rely on host packages and must not import the SDK.
71
+ */
72
+ export function readWorkerEntries(cwd, pluginId) {
73
+ const pkgPath = path.join(cwd, "package.json");
74
+ const pkg = fs.existsSync(pkgPath)
75
+ ? JSON.parse(fs.readFileSync(pkgPath, "utf8"))
76
+ : {};
77
+ const workers = pkg.termix?.workers ?? {};
78
+ return Object.entries(workers).map(([name, entry]) => {
79
+ if (!/^[a-z0-9-]+$/.test(name)) {
80
+ throw new Error(`${pluginId}: worker name "${name}" must be lowercase`);
81
+ }
82
+ const file = path.resolve(cwd, String(entry));
83
+ if (!fs.existsSync(file)) {
84
+ throw new Error(
85
+ `${pluginId}: worker "${name}" entry ${entry} is missing`,
86
+ );
87
+ }
88
+ return { name, file };
89
+ });
90
+ }
91
+
55
92
  const SDK_PATCHES = path.join(
56
93
  path.dirname(fileURLToPath(import.meta.url)),
57
94
  "..",
@@ -216,13 +253,27 @@ export async function build({ cwd }) {
216
253
  // node_modules import instead, resolved at runtime like a host-provided
217
254
  // package.
218
255
  external: [...BACKEND_EXTERNALS, ...(manifest.nativeDependencies ?? [])],
256
+ loader: BINARY_LOADERS,
219
257
  // Bundled CJS deps still call require() for Node builtins at runtime.
220
258
  // ESM has no ambient require, so give esbuild's require shim a real one.
221
- banner: {
222
- js: "import { createRequire as __termixCreateRequire } from 'node:module'; const require = __termixCreateRequire(import.meta.url);",
223
- },
259
+ banner: { js: REQUIRE_BANNER },
224
260
  });
225
261
 
262
+ for (const worker of readWorkerEntries(cwd, pluginId)) {
263
+ await esbuild.build({
264
+ entryPoints: [worker.file],
265
+ outfile: path.join(outDir, "workers", `${worker.name}.js`),
266
+ bundle: true,
267
+ format: "esm",
268
+ platform: "node",
269
+ target: "node22",
270
+ sourcemap: true,
271
+ logLevel: "warning",
272
+ loader: BINARY_LOADERS,
273
+ banner: { js: REQUIRE_BANNER },
274
+ });
275
+ }
276
+
226
277
  // Without this Node finds the host's typeless package.json, tries backend.js
227
278
  // as CommonJS first and warns before reparsing it as ESM.
228
279
  fs.writeFileSync(
package/dist/backend.d.ts CHANGED
@@ -303,6 +303,13 @@ export interface PluginRouterOptions {
303
303
  * (multer, a raw body for signature checking, a proxied stream).
304
304
  */
305
305
  rawBody?: boolean;
306
+ /**
307
+ * Requests per minute one address may make to this plugin's public routes.
308
+ * Defaults to 120, which a proxied page loading its own scripts and images
309
+ * can pass on its own. Capped at 10000, and an admin's
310
+ * TERMIX_PLUGIN_PUBLIC_RATE_LIMIT wins over it.
311
+ */
312
+ publicRateLimit?: number;
306
313
  }
307
314
  /**
308
315
  * HTTP routes, served at /plugin-api/<id>/ by the main backend server.
@@ -52016,6 +52016,8 @@ var en_default = {
52016
52016
  pinAppRailDesc: "Keep the left sidebar app rail always expanded instead of expanding on hover",
52017
52017
  expandAppRailOnHover: "Expand App Rail on Hover",
52018
52018
  expandAppRailOnHoverDesc: "Allow the left sidebar app rail to expand when the pointer moves over it",
52019
+ appRailHoverPushesContent: "Push content when it expands",
52020
+ appRailHoverPushesContentDesc: "Make room for the expanded rail instead of opening it over the page. Open sessions, like remote desktop, resize each time",
52019
52021
  showPinAppRailButton: "Show Pin App Rail Button",
52020
52022
  showPinAppRailButtonDesc: "Show a button in the app rail to pin it open. Off by default.",
52021
52023
  settingsNavigation: "Navigation",
@@ -53584,7 +53586,7 @@ async function pluginWsUrl(pluginId, path2, options2 = {}) {
53584
53586
  }
53585
53587
  const token = await fetchSocketTicket() ?? localStorage.getItem("jwt");
53586
53588
  const protocols2 = websocketAuthProtocols(token);
53587
- const devProxy = !define_import_meta_env_default.VITE_API_HOST && (window.location.port === "3000" || window.location.port === "5173");
53589
+ const devProxy = !define_import_meta_env_default.VITE_API_HOST;
53588
53590
  const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
53589
53591
  if (devProxy) {
53590
53592
  return {
@@ -40,7 +40,7 @@ import {
40
40
  setShellCallbacks,
41
41
  setShellHosts,
42
42
  setShellLayoutProvider
43
- } from "./chunk-LKN6OILD.js";
43
+ } from "./chunk-G346FUE4.js";
44
44
  import {
45
45
  setPermissionsForTesting
46
46
  } from "./chunk-DIOJ3KW2.js";
package/dist/host/ui.js CHANGED
@@ -139,7 +139,7 @@ import {
139
139
  useSurfaceKind,
140
140
  useTheme,
141
141
  verifySecondFactor
142
- } from "./chunk-LKN6OILD.js";
142
+ } from "./chunk-G346FUE4.js";
143
143
  import "./chunk-DIOJ3KW2.js";
144
144
  import {
145
145
  TabProvider,
@@ -8,11 +8,25 @@
8
8
  *
9
9
  * Two projects: tests/backend runs in node, tests/frontend in jsdom.
10
10
  */
11
+ /**
12
+ * termix-plugin build turns a .wasm or font import into its bytes. Tests get the same,
13
+ * so code that imports a module compiles it the same way under vitest.
14
+ */
15
+ export declare function wasmBinaryPlugin(): {
16
+ name: string;
17
+ enforce: "pre";
18
+ load(id: string): string;
19
+ };
11
20
  export interface PluginVitestOptions {
12
21
  /** Extra aliases merged over the defaults. */
13
22
  alias?: Record<string, string>;
14
23
  }
15
24
  export declare function pluginVitestConfig(configUrl: string, options?: PluginVitestOptions): {
25
+ plugins: {
26
+ name: string;
27
+ enforce: "pre";
28
+ load(id: string): string;
29
+ }[];
16
30
  resolve: {
17
31
  alias: Record<string, string>;
18
32
  };
@@ -21,6 +35,11 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
21
35
  setupFiles: string[];
22
36
  pool: "threads";
23
37
  projects: ({
38
+ plugins: {
39
+ name: string;
40
+ enforce: "pre";
41
+ load(id: string): string;
42
+ }[];
24
43
  resolve: {
25
44
  alias: Record<string, string>;
26
45
  };
@@ -35,6 +54,11 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
35
54
  server?: undefined;
36
55
  };
37
56
  } | {
57
+ plugins: {
58
+ name: string;
59
+ enforce: "pre";
60
+ load(id: string): string;
61
+ }[];
38
62
  resolve: {
39
63
  alias: Record<string, string>;
40
64
  };
@@ -29,6 +29,24 @@ function findRepoRoot(startDir) {
29
29
  dir = parent;
30
30
  }
31
31
  }
32
+ /**
33
+ * termix-plugin build turns a .wasm or font import into its bytes. Tests get the same,
34
+ * so code that imports a module compiles it the same way under vitest.
35
+ */
36
+ export function wasmBinaryPlugin() {
37
+ return {
38
+ name: "termix-wasm-binary",
39
+ enforce: "pre",
40
+ load(id) {
41
+ const file = id.split("?")[0];
42
+ if (![".wasm", ".ttf", ".otf"].some((ext) => file.endsWith(ext))) {
43
+ return null;
44
+ }
45
+ const base64 = fs.readFileSync(file).toString("base64");
46
+ return `export default new Uint8Array(Buffer.from(${JSON.stringify(base64)}, "base64"));`;
47
+ },
48
+ };
49
+ }
32
50
  export function pluginVitestConfig(configUrl, options = {}) {
33
51
  const pluginDir = path.dirname(fileURLToPath(configUrl));
34
52
  const manifestPath = path.join(pluginDir, "manifest.json");
@@ -61,7 +79,9 @@ export function pluginVitestConfig(configUrl, options = {}) {
61
79
  // Forks workers on Windows abort now and then with "Worker exited
62
80
  // unexpectedly". Threads don't.
63
81
  const pool = "threads";
82
+ const plugins = [wasmBinaryPlugin()];
64
83
  return {
84
+ plugins,
65
85
  resolve: { alias },
66
86
  test: {
67
87
  globals: true,
@@ -69,6 +89,7 @@ export function pluginVitestConfig(configUrl, options = {}) {
69
89
  pool,
70
90
  projects: [
71
91
  {
92
+ plugins,
72
93
  resolve: { alias },
73
94
  test: {
74
95
  name: `${pluginId}:backend`,
@@ -81,6 +102,7 @@ export function pluginVitestConfig(configUrl, options = {}) {
81
102
  },
82
103
  },
83
104
  {
105
+ plugins,
84
106
  resolve: { alias },
85
107
  test: {
86
108
  name: `${pluginId}:frontend`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termix-ssh/plugin-sdk",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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": {