feat: move opkssh and warpgate into plugins (C3)

This commit is contained in:
LukeGus committed 2026-09-24 19:28:19 -05:00
1 parent 925da4a661
commit 98d367d7e2
336 files changed
+22380 -4746

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgate Gateway",
"description": "This host connects through a Warpgate SSH proxy. Termix will handle the browser-based approval flow automatically after authenticating."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgate-poort",
"description": "Hierdie gasheer koppel deur 'n Warpgate SSH-instaanbediener. Termix sal die blaaiergebaseerde goedkeuringsvloei outomaties hanteer na verifikasie."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "بوابة Warpgate",
"description": "يتصل هذا المضيف عبر وكيل SSH من Warpgate. سيتولى Termix عملية الموافقة عبر المتصفح تلقائيًا بعد المصادقة."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Порталът на Уорпгейт",
"description": "Този хост се свързва чрез Warpgate SSH прокси. Termix ще обработи процеса на одобрение, базиран на браузър, автоматично след удостоверяване."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "ওয়ার্পগেট গেটওয়ে",
"description": "এই হোস্টটি একটি ওয়ার্পগেট এসএসএইচ প্রক্সির মাধ্যমে সংযুক্ত হয়। প্রমাণীকরণের পর টার্মিক্স স্বয়ংক্রিয়ভাবে ব্রাউজার-ভিত্তিক অনুমোদন প্রক্রিয়াটি পরিচালনা করবে।"
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Porta de Warpgate",
"description": "Aquest amfitrió es connecta a través d'un proxy SSH Warpgate. Termix gestionarà automàticament el flux d'aprovació basat en navegador després de l'autenticació."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Brána Warpgate",
"description": "Tento hostitel se připojuje přes proxy Warpgate SSH. Termix po ověření automaticky zpracuje proces schvalování v prohlížeči."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgate Gateway",
"description": "Denne vært opretter forbindelse via en Warpgate SSH-proxy. Termix håndterer den browserbaserede godkendelsesproces automatisk efter godkendelse."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgate Gateway",
"description": "Dieser Host verbindet sich über einen Warpgate SSH-Proxy. Termix übernimmt nach der Authentifizierung automatisch den browserbasierten Genehmigungsablauf."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Πύλη Warpgate",
"description": "Αυτός ο κεντρικός υπολογιστής συνδέεται μέσω ενός διακομιστή μεσολάβησης Warpgate SSH. Το Termix θα χειριστεί αυτόματα τη ροή έγκρισης που βασίζεται σε πρόγραμμα περιήγησης μετά τον έλεγχο ταυτότητας."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgate Gateway",
"description": "Este host se conecta a través de un proxy SSH de Warpgate. Termix gestionará el flujo de aprobación basado en navegador automáticamente después de autenticarse."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgate-yhdyskäytävä",
"description": "Tämä isäntä muodostaa yhteyden Warpgate SSH -välityspalvelimen kautta. Termix käsittelee selainpohjaisen hyväksyntäprosessin automaattisesti todennuksen jälkeen."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgate Gateway",
"description": "Cet hôte se connecte via un proxy SSH Warpgate. Termix gérera automatiquement le flux d'approbation basé sur le navigateur après l'authentification."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "שער וורפגייט",
"description": "מארח זה מתחבר דרך פרוקסי Warpgate SSH. Termix יטפל בזרימת האישור מבוססת הדפדפן באופן אוטומטי לאחר האימות."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgate गेटवे",
"description": "यह होस्ट Warpgate SSH प्रॉक्सी के माध्यम से कनेक्ट होता है। Termix प्रमाणीकरण के बाद ब्राउज़र-आधारित अनुमोदन प्रवाह को स्वचालित रूप से संभालेगा।"
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgate átjáró",
"description": "Ez a gazdagép egy Warpgate SSH proxyn keresztül csatlakozik. A Termix a hitelesítés után automatikusan kezeli a böngészőalapú jóváhagyási folyamatot."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Gerbang Warpgate",
"description": "Host ini terhubung melalui proxy SSH Warpgate. Termix akan menangani alur persetujuan berbasis browser secara otomatis setelah otentikasi."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgate Gateway",
"description": "Questo host si connette tramite un proxy SSH Warpgate. Termix gestirà automaticamente il flusso di approvazione basato su browser dopo l'autenticazione."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgateゲートウェイ",
"description": "このホストはWarpgate SSHプロキシ経由で接続します。認証後、Termixがブラウザベースの承認フローを自動的に処理します。"
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgate 게이트웨이",
"description": "이 호스트는 Warpgate SSH 프록시를 통해 연결됩니다. Termix는 인증 후 브라우저 기반 승인 과정을 자동으로 처리합니다."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgate-poort",
"description": "Deze host maakt verbinding via een Warpgate SSH-proxy. Termix handelt de browsergebaseerde goedkeuringsprocedure automatisch af na authenticatie."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgate-porten",
"description": "Denne verten kobler seg til via en Warpgate SSH-proxy. Termix håndterer den nettleserbaserte godkjenningsflyten automatisk etter autentisering."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Brama Warpgate",
"description": "Ten host łączy się przez proxy SSH Warpgate. Termix automatycznie obsłuży proces zatwierdzania w przeglądarce po uwierzytelnieniu."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Portal de Dobra",
"description": "Este host se conecta através de um proxy SSH Warpgate. O Termix cuidará automaticamente do fluxo de aprovação baseado em navegador após a autenticação."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgate Gateway",
"description": "Este anfitrião liga-se através de um proxy SSH Warpgate. O Termix irá gerir automaticamente o fluxo de aprovação baseado no navegador após a autenticação."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Poarta Warpgate",
"description": "Această gazdă se conectează printr-un proxy SSH Warpgate. Termix va gestiona automat fluxul de aprobare bazat pe browser după autentificare."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Шлюз Warpgate",
"description": "Этот хост подключается через SSH-прокси Warpgate. Termix автоматически обработает процесс подтверждения в браузере после аутентификации."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Капија Варпгејта",
"description": "Овај хост се повезује преко Warpgate SSH проксија. Termix ће аутоматски обрадити ток одобрења заснован на прегледачу након аутентификације."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgate Gateway",
"description": "Denna värd ansluter via en Warpgate SSH-proxy. Termix hanterar det webbläsarbaserade godkännandeflödet automatiskt efter autentisering."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "ประตูวาร์ปเกต",
"description": "โฮสต์นี้เชื่อมต่อผ่านพร็อกซี SSH ของ Warpgate Termix จะจัดการขั้นตอนการอนุมัติผ่านเบราว์เซอร์โดยอัตโนมัติหลังจากยืนยันตัวตนแล้ว"
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgate Ağ Geçidi",
"description": "Bu sunucu bir Warpgate SSH proxy'si üzerinden bağlanır. Termix, kimlik doğrulamasından sonra tarayıcı tabanlı onay akışını otomatik olarak yönetir."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Брама Варпгейту",
"description": "Цей хост підключається через проксі-сервер Warpgate SSH. Termix автоматично обробить процес затвердження на основі браузера після автентифікації."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Cổng Warpgate",
"description": "Máy chủ này kết nối thông qua proxy SSH Warpgate. Termix sẽ tự động xử lý luồng phê duyệt trên trình duyệt sau khi xác thực."
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgate 网关",
"description": "此主机通过 Warpgate SSH 代理连接。Termix 将在认证后自动处理基于浏览器的审批流程。"
}
}
}
}
@@ -0,0 +1,10 @@
{
"settings": {
"host": {
"useWarpgate": {
"label": "Warpgate 閘道",
"description": "此主機透過 Warpgate SSH 代理連線。Termix 會在驗證後自動處理瀏覽器核准流程。"
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
{
"id": "warpgate",
"name": "Warpgate",
"version": "1.0.0",
"description": "Connect through a Warpgate SSH bastion: its browser approval shows as a sign-in dialog, and the stored password answers its password prompt.",
"author": {
"name": "Termix"
},
"license": "MIT",
"repository": "https://github.com/Termix-SSH/Termix",
"category": "Access & Security",
"icon": "DoorOpen",
"engine": {
"termix": ">=2.9.0",
"api": "1"
},
"capabilities": ["auth:provide"],
"contributes": {
"auth": {
"keyboardInteractive": ["warpgate"]
},
"settings": {
"host": {
"fields": [
{
"key": "useWarpgate",
"type": "boolean",
"labelKey": "settings.host.useWarpgate.label",
"descriptionKey": "settings.host.useWarpgate.description",
"default": false
}
]
}
}
},
"locales": "locales",
"platforms": ["linux", "win32", "darwin"]
}
+15
View File
@@ -0,0 +1,15 @@
{
"name": "@termix-plugin/warpgate",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "termix-plugin build",
"test": "termix-plugin test",
"typecheck": "tsc -p tsconfig.json",
"validate": "termix-plugin validate"
},
"devDependencies": {
"@termix/plugin-sdk": "*"
}
}
+37
View File
@@ -0,0 +1,37 @@
import type {
PluginKeyboardInteractiveDetection,
PluginKeyboardInteractivePrompt,
} from "@termix/plugin-sdk/backend";
const WARPGATE_PATTERN = /warpgate\s+authentication/i;
const URL_PATTERN = /https?:\/\/[^\s\n]+/i;
const SECURITY_KEY_PATTERN =
/security key[:\s]+([a-z0-9](?:\s+[a-z0-9]){3}|[a-z0-9]{4})/i;
/**
* Warpgate asks for its browser approval as a keyboard-interactive round
* that names itself and carries the sign-in URL and a security key.
*/
export function detectWarpgateRound(round: {
name: string;
instructions: string;
prompts: PluginKeyboardInteractivePrompt[];
}): PluginKeyboardInteractiveDetection | null {
const texts = round.prompts.map((prompt) => prompt.prompt);
const isWarpgate =
WARPGATE_PATTERN.test(round.name) ||
WARPGATE_PATTERN.test(round.instructions) ||
texts.some((text) => WARPGATE_PATTERN.test(text));
if (!isWarpgate) return null;
const fullText = `${round.name}\n${round.instructions}\n${texts.join("\n")}`;
const url = fullText.match(URL_PATTERN);
if (!url) return null;
const key = fullText.match(SECURITY_KEY_PATTERN);
return {
kind: "browser",
url: url[0],
code: key ? key[1] : "N/A",
instructions: round.instructions,
};
}
@@ -0,0 +1,14 @@
/**
* Registered as "warpgate.hostImportNormalizer" so core's host import keeps
* the flag. Reads a Termix export's pluginSettings.warpgate first, then the
* useWarpgate host field exports had before 2.9.0.
*/
export function hostImportNormalizer(
raw: Record<string, unknown>,
): Record<string, unknown> | null {
const pluginSettings = raw.pluginSettings as
Record<string, Record<string, unknown> | undefined> | undefined;
const value = pluginSettings?.warpgate?.useWarpgate ?? raw.useWarpgate;
if (value === undefined || value === null) return null;
return { useWarpgate: value === true || value === 1 || value === "true" };
}
+22
View File
@@ -0,0 +1,22 @@
import type { PluginContext } from "@termix/plugin-sdk/backend";
import { detectWarpgateRound } from "./detect.js";
import { hostImportNormalizer } from "./host-import.js";
export const HANDLER_ID = "warpgate";
export async function activate(ctx: PluginContext) {
ctx.auth.registerKeyboardInteractiveHandler({
id: HANDLER_ID,
label: "Warpgate",
detect: (round) => detectWarpgateRound(round),
// Warpgate asks for the password before its browser round; answer it
// silently so the only thing the user sees is the sign-in dialog.
autoAnswerPasswords: (_host, settings) => settings.useWarpgate === true,
});
ctx.registry.provide("warpgate.hostImportNormalizer", hostImportNormalizer);
}
export async function deactivate() {
// Registered through ctx, so core removes it.
}
+5
View File
@@ -0,0 +1,5 @@
import type { TermixApp } from "@termix/plugin-sdk/frontend";
// The host setting is drawn by core's schema form and the sign-in dialog is
// the transports' own, so there is nothing to register.
export function activate(_app: TermixApp): void {}
@@ -0,0 +1,113 @@
import { describe, expect, it } from "vitest";
import { PluginCapabilityError } from "@termix/plugin-sdk/backend";
import { createMockCtx } from "@termix/plugin-sdk/testing";
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
import manifestJson from "../../manifest.json";
import { activate } from "../../src/backend/index.js";
import { detectWarpgateRound } from "../../src/backend/detect.js";
import { hostImportNormalizer } from "../../src/backend/host-import.js";
const manifest = manifestJson as unknown as PluginManifest;
const HOST = { id: 1, ip: "10.0.0.1", port: 22, username: "root" };
async function register() {
const mock = createMockCtx({
pluginId: manifest.id,
manifest,
capabilities: manifest.capabilities,
});
await activate(mock.ctx);
return mock.auth.keyboardInteractiveHandlers[0];
}
describe("detectWarpgateRound", () => {
it("claims a Warpgate round with its URL and security key", () => {
expect(
detectWarpgateRound({
name: "Warpgate authentication",
instructions:
"Open https://gate.example/@warpgate#/login/abc to continue. Security key: a b c d",
prompts: [{ prompt: "Press Enter when done: " }],
}),
).toEqual({
kind: "browser",
url: "https://gate.example/@warpgate#/login/abc",
code: "a b c d",
instructions:
"Open https://gate.example/@warpgate#/login/abc to continue. Security key: a b c d",
});
});
it("finds the marker in a prompt too, and says N/A without a key", () => {
expect(
detectWarpgateRound({
name: "",
instructions: "",
prompts: [
{ prompt: "Warpgate authentication: visit https://gate.example/x" },
],
}),
).toMatchObject({ url: "https://gate.example/x", code: "N/A" });
});
it("leaves other rounds, and a Warpgate round without a URL, alone", () => {
expect(
detectWarpgateRound({
name: "",
instructions: "",
prompts: [{ prompt: "Verification code:" }],
}),
).toBeNull();
expect(
detectWarpgateRound({
name: "Warpgate authentication",
instructions: "",
prompts: [],
}),
).toBeNull();
});
});
describe("the warpgate handler", () => {
it("registers under its id and label", async () => {
const handler = await register();
expect(handler).toMatchObject({ id: "warpgate", label: "Warpgate" });
});
it("answers password prompts itself only on hosts with the setting on", async () => {
const handler = await register();
expect(handler.autoAnswerPasswords!(HOST, { useWarpgate: true })).toBe(
true,
);
expect(handler.autoAnswerPasswords!(HOST, { useWarpgate: false })).toBe(
false,
);
expect(handler.autoAnswerPasswords!(HOST, {})).toBe(false);
});
it("fails closed without auth:provide", async () => {
const mock = createMockCtx({
pluginId: manifest.id,
manifest,
capabilities: [],
});
await expect(activate(mock.ctx)).rejects.toThrow(PluginCapabilityError);
});
});
describe("hostImportNormalizer", () => {
it("reads the plugin's own settings first, then the 2.8 host field", () => {
expect(
hostImportNormalizer({
pluginSettings: { warpgate: { useWarpgate: true } },
}),
).toEqual({ useWarpgate: true });
expect(hostImportNormalizer({ useWarpgate: 1 })).toEqual({
useWarpgate: true,
});
expect(hostImportNormalizer({ useWarpgate: false })).toEqual({
useWarpgate: false,
});
expect(hostImportNormalizer({ name: "x" })).toBeNull();
});
});
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "@termix/plugin-sdk/tsconfig.plugin.json",
"include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"]
}
+3
View File
@@ -0,0 +1,3 @@
import { pluginVitestConfig } from "@termix/plugin-sdk/vitest-preset";
export default pluginVitestConfig(import.meta.url);