chore: lock the plugin boundary and remove 2.9.0 leftovers

This commit is contained in:
LukeGus committed 2026-09-25 07:24:46 -05:00
1 parent 6489b62ece
commit 2961297989
399 files changed
+3964 -5627

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@
"scripts": {
"build": "termix-plugin build",
"test": "termix-plugin test",
"typecheck": "tsc -b ../../tsconfig.plugins.json ../../tsconfig.plugins.frontend.json",
"typecheck": "tsc -b ../../tsconfig.plugins.backend.json ../../tsconfig.plugins.frontend.json",
"validate": "termix-plugin validate"
},
"devDependencies": {
+11 -10
View File
@@ -1,6 +1,4 @@
import { Router } from "express";
import { apiLogger } from "../../../../src/backend/utils/logger.js";
import { fetchWithProxy } from "../../../../src/backend/utils/proxy-agent.js";
import type { PluginContext } from "@termix/plugin-sdk/backend";
interface TailscaleDevice {
@@ -89,18 +87,20 @@ export function startTailscaleService(
);
const url = `${apiBase}/tailnet/-/devices?fields=all`;
const response = await fetchWithProxy(url, {
// The base is admin-set and may be a LAN Headscale; listing it also
// routes the call through the configured outbound proxy.
const response = await context.fetch(url, {
headers: {
Authorization: `Bearer ${apiKey}`,
"User-Agent": "Termix/1.0",
},
allowPrivateHosts: [new URL(apiBase).hostname],
});
if (!response.ok) {
apiLogger.warn("Tailscale API returned non-OK status", {
operation: "tailscale_devices",
status: response.status,
});
context.log.warn(
`Tailscale API returned non-OK status ${response.status}`,
);
if (response.status === 401 || response.status === 403) {
return res.status(401).json({
error: "Invalid Tailscale API key",
@@ -130,9 +130,10 @@ export function startTailscaleService(
res.json({ devices, hasApiKey: true });
} catch (err) {
apiLogger.error("Failed to fetch Tailscale devices", err, {
operation: "tailscale_devices",
});
context?.log.error(
"Failed to fetch Tailscale devices",
err instanceof Error ? err : undefined,
);
res.status(500).json({
error: "Failed to fetch Tailscale devices",
devices: [],
@@ -13,6 +13,8 @@ const AUTH_FAILED_PATTERN = /All configured authentication methods failed/i;
export const tailscaleSshAuthProvider: PluginSshAuthProvider = {
type: "tailscale",
labelKey: "hosts.filterAuthTailscale",
// Nothing host-specific to look up, so it works for an unsaved host.
quickConnect: true,
connectOptions: (_host, purpose) =>
purpose === "terminal"
? {
@@ -1,5 +1,5 @@
import React from "react";
import { Button } from "@/components/button.tsx";
import { Button } from "@termix/plugin-sdk/ui";
import { Shield, ExternalLink, Loader2 } from "lucide-react";
import { useTranslation } from "@termix/plugin-sdk/frontend";
@@ -1,11 +1,11 @@
import { useEffect, useState } from "react";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { Loader2, RefreshCw, Terminal } from "lucide-react";
import { Button } from "@/components/button";
import { Input } from "@/components/input";
import type { Host } from "@/types/ui-types";
import { Button } from "@termix/plugin-sdk/ui";
import { Input } from "@termix/plugin-sdk/ui";
import type { PluginHostRecord as Host } from "@termix/plugin-sdk/frontend";
import { getTailscaleDevices } from "./tailscale-api";
import { createQuickConnectHost } from "@/sidebar/quick-connect-host";
import { createQuickConnectHost } from "@termix/plugin-sdk/ui";
interface TailscaleDevice {
id: string;
@@ -61,7 +61,7 @@ export function TailscaleDevicesPanel({
port: 22,
username,
authType: "tailscale",
});
}) as unknown as Host;
onConnect(host, "terminal");
}
@@ -9,7 +9,7 @@
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { Loader2, RefreshCw } from "lucide-react";
import { Button } from "@/components/button";
import { Button } from "@termix/plugin-sdk/ui";
import type { SettingsComponentProps } from "@termix/plugin-sdk/frontend";
import { getTailscaleDevices } from "./tailscale-api";
+4 -1
View File
@@ -1,8 +1,9 @@
import type { ComponentType } from "react";
import { Radar } from "lucide-react";
import type { PanelProps, TermixApp } from "@termix/plugin-sdk/frontend";
import type { Host } from "@/types/ui-types";
import type { PluginHostRecord as Host } from "@termix/plugin-sdk/frontend";
import { TailscaleDevicesPanel } from "./TailscaleDevicesPanel";
import { setTailscaleApi } from "./tailscale-api";
import { TailscaleDevicesStatus } from "./TailscaleDevicesStatus";
import { TailscaleAuthEditor } from "./TailscaleAuthEditor";
import { TailscaleCheckOverlay } from "./TailscaleCheckOverlay";
@@ -22,6 +23,8 @@ function Panel({ shell }: PanelProps) {
}
export function activate(app: TermixApp): void {
setTailscaleApi(app.api);
app.onDispose(() => setTailscaleApi(null));
app.registerRailItem({
id: "tailscale",
icon: Radar,
+27 -24
View File
@@ -1,12 +1,11 @@
// The API key and base URL are this plugin's own settings now, read and
// written through the generic /plugins/tailscale/settings/admin routes that
// core renders from the manifest. Only the device list is left here, because
// it is this plugin's own route.
// The API key and base URL are this plugin's own settings, read and written
// through the generic /plugins/tailscale/settings/admin routes that core
// renders from the manifest. Only the device list is left here, because it is
// this plugin's own route.
import axios from "axios";
import { authApi, handleApiError } from "@/main-axios";
import type { PluginApiClient } from "@termix/plugin-sdk/frontend";
export async function getTailscaleDevices(): Promise<{
export interface TailscaleDevicesResponse {
devices: Array<{
id: string;
name: string;
@@ -17,27 +16,31 @@ export async function getTailscaleDevices(): Promise<{
}>;
hasApiKey: boolean;
error?: string;
}> {
}
let pluginApi: PluginApiClient | null = null;
/** Set from activate with app.api, cleared on deactivate. */
export function setTailscaleApi(api: PluginApiClient | null): void {
pluginApi = api;
}
export async function getTailscaleDevices(): Promise<TailscaleDevicesResponse> {
if (!pluginApi) throw new Error("The Tailscale plugin is not active");
try {
const response = await authApi.get("/plugin-api/tailscale/devices");
const response = await pluginApi.get<TailscaleDevicesResponse>("/devices");
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
const data = error.response?.data;
if (
data &&
typeof data === "object" &&
"hasApiKey" in data &&
typeof data.hasApiKey === "boolean"
) {
return data as {
devices: [];
hasApiKey: boolean;
error?: string;
};
}
// The route answers with the key state even when the tailnet call fails.
const data = (error as { response?: { data?: unknown } })?.response?.data;
if (
data &&
typeof data === "object" &&
"hasApiKey" in data &&
typeof (data as { hasApiKey: unknown }).hasApiKey === "boolean"
) {
return data as TailscaleDevicesResponse;
}
handleApiError(error, "fetch Tailscale devices");
throw error;
}
}
@@ -8,7 +8,8 @@ vi.mock("../../src/frontend/tailscale-api", () => ({
getTailscaleDevices: getTailscaleDevicesMock,
}));
vi.mock("react-i18next", () => ({
vi.mock("react-i18next", async (importOriginal) => ({
...(await importOriginal<object>()),
useTranslation: () => ({ t: (key: string) => key }),
}));
@@ -1,23 +1,29 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
getTailscaleDevices,
setTailscaleApi,
} from "../../src/frontend/tailscale-api";
const authApiMock = vi.hoisted(() => ({ get: vi.fn(), patch: vi.fn() }));
const handleApiErrorMock = vi.hoisted(() => vi.fn());
vi.mock("@/main-axios", () => ({
authApi: authApiMock,
statsApi: { get: vi.fn(), patch: vi.fn() },
handleApiError: handleApiErrorMock,
}));
import { getTailscaleDevices } from "../../src/frontend/tailscale-api";
const api = { get: vi.fn() };
beforeEach(() => {
vi.clearAllMocks();
setTailscaleApi(api as never);
});
describe("getTailscaleDevices", () => {
it("reads the plugin's own devices route", async () => {
api.get.mockResolvedValue({ data: { devices: [], hasApiKey: false } });
await expect(getTailscaleDevices()).resolves.toEqual({
devices: [],
hasApiKey: false,
});
expect(api.get).toHaveBeenCalledWith("/devices");
});
it("preserves configured-key state when discovery fails", async () => {
authApiMock.get.mockRejectedValue({
api.get.mockRejectedValue({
isAxiosError: true,
response: {
data: {
@@ -33,17 +39,17 @@ describe("getTailscaleDevices", () => {
hasApiKey: true,
error: "Failed to fetch Tailscale devices",
});
expect(handleApiErrorMock).not.toHaveBeenCalled();
});
it("uses normal API error handling when no structured state is available", async () => {
it("rethrows when no structured state is available", async () => {
const error = { isAxiosError: true, response: { data: {} } };
authApiMock.get.mockRejectedValue(error);
api.get.mockRejectedValue(error);
await expect(getTailscaleDevices()).rejects.toBe(error);
expect(handleApiErrorMock).toHaveBeenCalledWith(
error,
"fetch Tailscale devices",
);
});
it("refuses while the plugin is off", async () => {
setTailscaleApi(null);
await expect(getTailscaleDevices()).rejects.toThrow(/not active/);
});
});
-6
View File
@@ -1,10 +1,4 @@
{
"extends": "@termix/plugin-sdk/tsconfig.plugin.json",
"compilerOptions": {
"paths": {
"@/types/*": ["../../src/types/*"],
"@/*": ["../../src/ui/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"]
}