mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
chore: lock the plugin boundary and remove 2.9.0 leftovers
This commit is contained in:
1 parent
6489b62ece
commit
2961297989
399 files changed
+3964
-5627
No files matched your search
@@ -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": {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in new issue
Block a user