feat: serve plugin http and websockets from one mount instead of ports

This commit is contained in:
LukeGus committed 2026-09-22 16:39:41 -05:00
1 parent c496a093ca
commit 8431570ff7
102 files changed
+2894 -2308

No files matched your search

+1 -1
View File
@@ -14,7 +14,7 @@
"termix": ">=2.9.0",
"api": "1"
},
"capabilities": ["db:own", "ui:surface"],
"capabilities": ["db:own", "network:serve", "ui:surface"],
"locales": "locales",
"platforms": ["linux", "win32", "darwin"]
}
+2 -2
View File
@@ -2,8 +2,8 @@ import type { PluginContext } from "@termix/plugin-sdk/backend";
import { startWorkspacesService, stopWorkspacesService } from "./routes.js";
export async function activate(ctx: PluginContext) {
startWorkspacesService();
ctx.log.info("Workspaces router registered at /workspaces");
startWorkspacesService(ctx.http.router());
ctx.log.info("Workspaces routes mounted at /plugin-api/workspaces");
}
export async function deactivate() {
+4 -8
View File
@@ -1,13 +1,9 @@
import type { AuthenticatedRequest } from "../../../../src/types/index.js";
import express, { type Request, type Response } from "express";
import express, { type Request, type Response, type Router } from "express";
import { databaseLogger } from "../../../../src/backend/utils/logger.js";
import { AuthManager } from "../../../../src/backend/utils/auth-manager.js";
import { createCurrentWorkspaceRepository } from "../../../../src/backend/database/repositories/factory.js";
import type { WorkspaceRecord } from "../../../../src/backend/database/repositories/workspace-repository.js";
import {
registerWorkspacesRouter,
unregisterWorkspacesRouter,
} from "../../../../src/backend/database/routes/workspace-dispatch.js";
export const router = express.Router();
@@ -639,10 +635,10 @@ router.delete(
},
);
export function startWorkspacesService(): void {
registerWorkspacesRouter(router);
export function startWorkspacesService(mountOn: Router): void {
mountOn.use(router);
}
export function stopWorkspacesService(): void {
unregisterWorkspacesRouter();
// Unmounted by the runtime when the plugin deactivates.
}
@@ -3,7 +3,7 @@ import type { Workspace, WorkspacePayload } from "@/types/ui-types";
export async function listWorkspaces(): Promise<Workspace[]> {
try {
const response = await authApi.get("/workspaces");
const response = await authApi.get("/plugin-api/workspaces");
return response.data;
} catch (error) {
throw handleApiError(error, "fetch workspaces");
@@ -17,7 +17,7 @@ export async function createWorkspace(data: {
payload: WorkspacePayload;
}): Promise<Workspace> {
try {
const response = await authApi.post("/workspaces", data);
const response = await authApi.post("/plugin-api/workspaces", data);
return response.data;
} catch (error) {
throw handleApiError(error, "create workspace");
@@ -29,7 +29,7 @@ export async function renameWorkspace(
data: { name?: string; color?: string | null; icon?: string | null },
): Promise<Workspace> {
try {
const response = await authApi.patch(`/workspaces/${id}`, data);
const response = await authApi.patch(`/plugin-api/workspaces/${id}`, data);
return response.data;
} catch (error) {
throw handleApiError(error, "update workspace");
@@ -41,7 +41,7 @@ export async function updateWorkspaceContent(
payload: WorkspacePayload,
): Promise<Workspace> {
try {
const response = await authApi.put(`/workspaces/${id}/content`, {
const response = await authApi.put(`/plugin-api/workspaces/${id}/content`, {
payload,
});
return response.data;
@@ -54,7 +54,7 @@ export async function deleteWorkspace(
id: number,
): Promise<{ success: boolean }> {
try {
const response = await authApi.delete(`/workspaces/${id}`);
const response = await authApi.delete(`/plugin-api/workspaces/${id}`);
return response.data;
} catch (error) {
throw handleApiError(error, "delete workspace");
@@ -66,9 +66,12 @@ export async function duplicateWorkspace(
name: string,
): Promise<Workspace> {
try {
const response = await authApi.post(`/workspaces/${id}/duplicate`, {
name,
});
const response = await authApi.post(
`/plugin-api/workspaces/${id}/duplicate`,
{
name,
},
);
return response.data;
} catch (error) {
throw handleApiError(error, "duplicate workspace");
@@ -77,7 +80,9 @@ export async function duplicateWorkspace(
export async function setDefaultWorkspace(id: number): Promise<Workspace> {
try {
const response = await authApi.post(`/workspaces/${id}/set-default`);
const response = await authApi.post(
`/plugin-api/workspaces/${id}/set-default`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "set default workspace");
@@ -86,7 +91,9 @@ export async function setDefaultWorkspace(id: number): Promise<Workspace> {
export async function unsetDefaultWorkspace(id: number): Promise<Workspace> {
try {
const response = await authApi.post(`/workspaces/${id}/unset-default`);
const response = await authApi.post(
`/plugin-api/workspaces/${id}/unset-default`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "unset default workspace");
@@ -95,7 +102,7 @@ export async function unsetDefaultWorkspace(id: number): Promise<Workspace> {
export async function applyWorkspaceServer(id: number): Promise<Workspace> {
try {
const response = await authApi.post(`/workspaces/${id}/apply`);
const response = await authApi.post(`/plugin-api/workspaces/${id}/apply`);
return response.data;
} catch (error) {
throw handleApiError(error, "apply workspace");
@@ -104,7 +111,7 @@ export async function applyWorkspaceServer(id: number): Promise<Workspace> {
export async function getLastSessionWorkspace(): Promise<Workspace | null> {
try {
const response = await authApi.get("/workspaces/last-session");
const response = await authApi.get("/plugin-api/workspaces/last-session");
return response.data;
} catch (error) {
throw handleApiError(error, "fetch last session workspace");
@@ -115,7 +122,7 @@ export async function saveLastSessionWorkspace(
payload: WorkspacePayload,
): Promise<Workspace> {
try {
const response = await authApi.put("/workspaces/last-session", {
const response = await authApi.put("/plugin-api/workspaces/last-session", {
payload,
});
return response.data;
@@ -46,73 +46,88 @@ const samplePayload: WorkspacePayload = {
};
describe("workspaces-api", () => {
it("listWorkspaces GETs /workspaces", async () => {
it("listWorkspaces GETs /plugin-api/workspaces", async () => {
authApiMock.get.mockResolvedValueOnce({ data: [{ id: 1 }] });
const result = await listWorkspaces();
expect(authApiMock.get).toHaveBeenCalledWith("/workspaces");
expect(authApiMock.get).toHaveBeenCalledWith("/plugin-api/workspaces");
expect(result).toEqual([{ id: 1 }]);
});
it("createWorkspace POSTs /workspaces with the payload", async () => {
it("createWorkspace POSTs /plugin-api/workspaces with the payload", async () => {
await createWorkspace({
name: "Test A",
color: "#fff",
payload: samplePayload,
});
expect(authApiMock.post).toHaveBeenCalledWith("/workspaces", {
expect(authApiMock.post).toHaveBeenCalledWith("/plugin-api/workspaces", {
name: "Test A",
color: "#fff",
payload: samplePayload,
});
});
it("renameWorkspace PATCHes /workspaces/:id", async () => {
it("renameWorkspace PATCHes /plugin-api/workspaces/:id", async () => {
await renameWorkspace(5, { name: "New Name" });
expect(authApiMock.patch).toHaveBeenCalledWith("/workspaces/5", {
expect(authApiMock.patch).toHaveBeenCalledWith("/plugin-api/workspaces/5", {
name: "New Name",
});
});
it("updateWorkspaceContent PUTs /workspaces/:id/content", async () => {
it("updateWorkspaceContent PUTs /plugin-api/workspaces/:id/content", async () => {
await updateWorkspaceContent(5, samplePayload);
expect(authApiMock.put).toHaveBeenCalledWith("/workspaces/5/content", {
payload: samplePayload,
});
expect(authApiMock.put).toHaveBeenCalledWith(
"/plugin-api/workspaces/5/content",
{
payload: samplePayload,
},
);
});
it("deleteWorkspace DELETEs /workspaces/:id", async () => {
it("deleteWorkspace DELETEs /plugin-api/workspaces/:id", async () => {
await deleteWorkspace(5);
expect(authApiMock.delete).toHaveBeenCalledWith("/workspaces/5");
expect(authApiMock.delete).toHaveBeenCalledWith("/plugin-api/workspaces/5");
});
it("duplicateWorkspace POSTs /workspaces/:id/duplicate with a name", async () => {
it("duplicateWorkspace POSTs /plugin-api/workspaces/:id/duplicate with a name", async () => {
await duplicateWorkspace(5, "Test A (copy)");
expect(authApiMock.post).toHaveBeenCalledWith("/workspaces/5/duplicate", {
name: "Test A (copy)",
});
expect(authApiMock.post).toHaveBeenCalledWith(
"/plugin-api/workspaces/5/duplicate",
{
name: "Test A (copy)",
},
);
});
it("setDefaultWorkspace POSTs /workspaces/:id/set-default", async () => {
it("setDefaultWorkspace POSTs /plugin-api/workspaces/:id/set-default", async () => {
await setDefaultWorkspace(5);
expect(authApiMock.post).toHaveBeenCalledWith("/workspaces/5/set-default");
expect(authApiMock.post).toHaveBeenCalledWith(
"/plugin-api/workspaces/5/set-default",
);
});
it("applyWorkspaceServer POSTs /workspaces/:id/apply", async () => {
it("applyWorkspaceServer POSTs /plugin-api/workspaces/:id/apply", async () => {
await applyWorkspaceServer(5);
expect(authApiMock.post).toHaveBeenCalledWith("/workspaces/5/apply");
expect(authApiMock.post).toHaveBeenCalledWith(
"/plugin-api/workspaces/5/apply",
);
});
it("getLastSessionWorkspace GETs /workspaces/last-session", async () => {
it("getLastSessionWorkspace GETs /plugin-api/workspaces/last-session", async () => {
authApiMock.get.mockResolvedValueOnce({ data: null });
const result = await getLastSessionWorkspace();
expect(authApiMock.get).toHaveBeenCalledWith("/workspaces/last-session");
expect(authApiMock.get).toHaveBeenCalledWith(
"/plugin-api/workspaces/last-session",
);
expect(result).toBeNull();
});
it("saveLastSessionWorkspace PUTs /workspaces/last-session", async () => {
it("saveLastSessionWorkspace PUTs /plugin-api/workspaces/last-session", async () => {
await saveLastSessionWorkspace(samplePayload);
expect(authApiMock.put).toHaveBeenCalledWith("/workspaces/last-session", {
payload: samplePayload,
});
expect(authApiMock.put).toHaveBeenCalledWith(
"/plugin-api/workspaces/last-session",
{
payload: samplePayload,
},
);
});
});