- {p.owned && (
+ {profile.owned && (
@@ -190,10 +209,10 @@ export function VaultProfileManager({
type="button"
variant="outline"
size="sm"
- className="h-7 text-[10px] self-start"
+ className="h-7 text-[10px] self-start rounded-none"
onClick={() => setForm({ ...emptyForm })}
>
-
{t("hosts.vaultNewProfile")}
+
{t("profiles.new")}
>
)}
@@ -201,33 +220,25 @@ export function VaultProfileManager({
{form && (
- {field(t("hosts.friendlyNameLabel"), "name", "Production Vault")}
+ {field(t("profiles.name"), "name", "Production Vault")}
+ {field(t("profiles.vaultAddr"), "vaultAddr", "https://vault:8200")}
+ {field(t("profiles.namespace"), "vaultNamespace", "admin")}
+ {field(t("profiles.oidcMount"), "oidcMount", "oidc")}
+ {field(t("profiles.oidcRole"), "oidcRole", "default")}
+ {field(t("profiles.sshMount"), "sshMount", "ssh-client-signer")}
+ {field(t("profiles.sshRole"), "sshRole", "my-role")}
{field(
- t("hosts.vaultAddrLabel"),
- "vaultAddr",
- "https://vault:8200",
- )}
- {field(t("hosts.vaultNamespaceLabel"), "vaultNamespace", "admin")}
- {field(t("hosts.vaultOidcMountLabel"), "oidcMount", "oidc")}
- {field(t("hosts.vaultOidcRoleLabel"), "oidcRole", "default")}
- {field(
- t("hosts.vaultSshMountLabel"),
- "sshMount",
- "ssh-client-signer",
- )}
- {field(t("hosts.vaultSshRoleLabel"), "sshRole", "my-role")}
- {field(
- t("hosts.vaultValidPrincipalsLabel"),
+ t("profiles.validPrincipals"),
"validPrincipals",
"root,deploy",
)}
setField("keyType", e.target.value)}
className="flex h-8 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
>
@@ -236,35 +247,36 @@ export function VaultProfileManager({
-
+ {canShare && (
+
+ )}
diff --git a/plugins/vault/src/frontend/index.tsx b/plugins/vault/src/frontend/index.tsx
new file mode 100644
index 000000000..aec147cff
--- /dev/null
+++ b/plugins/vault/src/frontend/index.tsx
@@ -0,0 +1,29 @@
+import type { ComponentType } from "react";
+import type { TermixApp } from "@termix/plugin-sdk/frontend";
+import { VaultAuthEditor } from "./VaultAuthEditor";
+import { VaultOverlay } from "./VaultOverlay";
+import { RedirectUriSetting } from "./RedirectUriSetting";
+
+export function activate(app: TermixApp): void {
+ app.registerSshAuthEditor({
+ authType: "vault",
+ titleKey: "hosts.filterAuthVault",
+ component: VaultAuthEditor,
+ });
+
+ // The Vault sign-in while a terminal connects to a Vault host.
+ app.registerSlotContribution("terminal.overlay", {
+ actionId: "vault.signIn",
+ titleKey: "dialog.title",
+ kind: "component",
+ component: VaultOverlay as unknown as ComponentType<
+ Record
+ >,
+ });
+
+ app.registerSettingsComponent("redirectUri", RedirectUriSetting);
+}
+
+export function deactivate(): void {
+ // Registrations through app are disposed by core.
+}
diff --git a/plugins/vault/src/frontend/types.ts b/plugins/vault/src/frontend/types.ts
new file mode 100644
index 000000000..2cb0e61f1
--- /dev/null
+++ b/plugins/vault/src/frontend/types.ts
@@ -0,0 +1,34 @@
+/** A Vault signer profile as GET /profiles returns it. */
+export interface VaultProfile {
+ id: number;
+ name: string;
+ vaultAddr: string;
+ vaultNamespace: string | null;
+ oidcMount: string | null;
+ oidcRole: string | null;
+ sshMount: string | null;
+ sshRole: string;
+ validPrincipals: string | null;
+ keyType: string | null;
+ shared: boolean;
+ owned: boolean;
+}
+
+export interface VaultProfilePayload {
+ name: string;
+ vaultAddr: string;
+ vaultNamespace: string | null;
+ oidcMount: string | null;
+ oidcRole: string | null;
+ sshMount: string | null;
+ sshRole: string;
+ validPrincipals: string | null;
+ keyType: string | null;
+ shared: boolean;
+}
+
+export function errorMessage(error: unknown, fallback: string): string {
+ const data = (error as { response?: { data?: { error?: unknown } } })
+ ?.response?.data;
+ return typeof data?.error === "string" ? data.error : fallback;
+}
diff --git a/plugins/vault/tests/backend/helpers.ts b/plugins/vault/tests/backend/helpers.ts
new file mode 100644
index 000000000..9d3d8ad30
--- /dev/null
+++ b/plugins/vault/tests/backend/helpers.ts
@@ -0,0 +1,174 @@
+import http from "node:http";
+import type { AddressInfo } from "node:net";
+import { fileURLToPath } from "node:url";
+import express, { type Router } from "express";
+import {
+ createMockCtx,
+ createTestDb,
+ type MockPluginContext,
+ type TestDb,
+ type TestDbOptions,
+} from "@termix/plugin-sdk/testing";
+import type {
+ PluginFetchInit,
+ PluginSshAuthProvider,
+} from "@termix/plugin-sdk/backend";
+import type { PluginManifest } from "@termix/plugin-sdk/manifest";
+import manifestJson from "../../manifest.json";
+
+export const pluginDir = fileURLToPath(new URL("../..", import.meta.url));
+export const manifest = manifestJson as unknown as PluginManifest;
+
+export const VAULT_ADDR = "https://vault.internal:8200";
+export const SIGNED_CERT = "ssh-ed25519-cert-v01@openssh.com AAAAsigned";
+
+/** A fake Vault answering the three calls the plugin makes. */
+export function createFakeVault(
+ options: { signStatus?: number; state?: string } = {},
+) {
+ const calls: Array<{ url: string; init?: PluginFetchInit }> = [];
+ const state = options.state ?? "vault-state-1";
+ const json = (status: number, body: unknown) =>
+ new Response(JSON.stringify(body), { status });
+
+ const fetch = async (url: string, init?: PluginFetchInit) => {
+ calls.push({ url, init });
+ const { pathname } = new URL(url);
+ if (pathname === "/v1/auth/oidc/oidc/auth_url") {
+ return json(200, {
+ data: {
+ auth_url: `https://idp.test/authorize?client_id=termix&state=${state}`,
+ },
+ });
+ }
+ if (pathname === "/v1/auth/oidc/oidc/callback") {
+ return json(200, { auth: { client_token: "hvs.token" } });
+ }
+ if (pathname.startsWith("/v1/ssh-client-signer/sign/")) {
+ if (options.signStatus && options.signStatus !== 200) {
+ return json(options.signStatus, { errors: ["permission denied"] });
+ }
+ return json(200, { data: { signed_key: SIGNED_CERT } });
+ }
+ return json(404, { errors: ["not found"] });
+ };
+ return { fetch, calls, state };
+}
+
+/** A fake terminal socket that records what it was sent. */
+export function fakeSocket() {
+ const sent: Array> = [];
+ return {
+ sent,
+ socket: { send: (data: string) => sent.push(JSON.parse(data)) },
+ };
+}
+
+export interface TestServer {
+ db: TestDb;
+ mock: MockPluginContext;
+ provider: () => PluginSshAuthProvider;
+ request: (
+ method: string,
+ path: string,
+ options?: { user?: string; body?: unknown },
+ // Route bodies vary per test; asserting on them is the point.
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ) => Promise<{ status: number; body: any; text: string }>;
+ close: () => Promise;
+}
+
+/**
+ * The plugin activated against a real in-memory database, its router mounted
+ * the way core mounts it: JSON parsed and the acting user taken from the
+ * request.
+ */
+export async function startServer(
+ options: {
+ permissions?: string[];
+ settings?: Record;
+ fetch?: (url: string, init?: PluginFetchInit) => Promise;
+ before?: TestDbOptions["before"];
+ } = {},
+): Promise {
+ const db = await createTestDb(pluginDir, { before: options.before });
+ for (const user of ["user-1", "user-2"]) {
+ db.sqlite.prepare("INSERT OR IGNORE INTO users (id) VALUES (?)").run(user);
+ }
+ db.sqlite.prepare("INSERT OR IGNORE INTO ssh_data (id) VALUES (?)").run(7);
+
+ let router: Router | null = null;
+ const mock = createMockCtx({
+ pluginId: manifest.id,
+ manifest,
+ capabilities: manifest.capabilities,
+ db: db.database,
+ router: () => (router = express.Router()),
+ permissions: options.permissions,
+ settings: options.settings,
+ fetch: options.fetch,
+ actor: "user-1",
+ });
+
+ const { activate } = await import("../../src/backend/index.js");
+ await activate(mock.ctx);
+
+ const app = express();
+ app.use(express.json());
+ app.use((req, _res, next) => {
+ mock.setActor(req.header("x-test-user") ?? undefined);
+ next();
+ });
+ app.use((req, res, next) => router!(req, res, next));
+ const server = http.createServer(app);
+ await new Promise((resolve) => server.listen(0, resolve));
+ const { port } = server.address() as AddressInfo;
+
+ return {
+ db,
+ mock,
+ provider: () =>
+ mock.auth.sshAuthProviders.find((provider) => provider.type === "vault")!,
+ async request(method, path, { user = "user-1", body } = {}) {
+ const response = await fetch(`http://127.0.0.1:${port}${path}`, {
+ method,
+ redirect: "manual",
+ headers: {
+ "x-test-user": user,
+ ...(body !== undefined ? { "content-type": "application/json" } : {}),
+ },
+ body: body !== undefined ? JSON.stringify(body) : undefined,
+ });
+ const text = await response.text();
+ let parsed: unknown = null;
+ try {
+ parsed = text ? JSON.parse(text) : null;
+ } catch {
+ parsed = null;
+ }
+ return { status: response.status, body: parsed, text };
+ },
+ async close() {
+ await new Promise((resolve) => server.close(() => resolve()));
+ db.close();
+ },
+ };
+}
+
+export const env = (userId = "user-1", hostId = 7) => ({
+ client: {},
+ userId,
+ hostId,
+ purpose: "terminal",
+ interactive: true,
+ log: () => {},
+});
+
+export const HOST = { id: 7, ip: "10.0.0.7", port: 22, username: "root" };
+
+export const PROFILE = {
+ name: "Prod",
+ vaultAddr: VAULT_ADDR,
+ sshRole: "ops",
+ oidcRole: "dev",
+};
diff --git a/plugins/vault/tests/backend/migrations.test.ts b/plugins/vault/tests/backend/migrations.test.ts
new file mode 100644
index 000000000..02b0060d8
--- /dev/null
+++ b/plugins/vault/tests/backend/migrations.test.ts
@@ -0,0 +1,121 @@
+import { afterEach, describe, expect, it } from "vitest";
+import { createTestDb, type TestDb } from "@termix/plugin-sdk/testing";
+import { pluginDir } from "./helpers";
+
+// vault_profiles and vault_tokens as core's SQLite bootstrap created them
+// before 2.9.0, including the sync_id column added later.
+const LEGACY_DDL = `
+ CREATE TABLE vault_profiles (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id TEXT NOT NULL,
+ name TEXT NOT NULL,
+ description TEXT,
+ folder TEXT,
+ tags TEXT,
+ vault_addr TEXT NOT NULL,
+ vault_namespace TEXT,
+ oidc_mount TEXT,
+ oidc_role TEXT,
+ ssh_mount TEXT,
+ ssh_role TEXT NOT NULL,
+ valid_principals TEXT,
+ key_type TEXT,
+ shared INTEGER NOT NULL DEFAULT 0,
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ sync_id TEXT,
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
+ );
+ CREATE TABLE vault_tokens (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id TEXT NOT NULL,
+ profile_id INTEGER NOT NULL,
+ ssh_cert TEXT NOT NULL,
+ private_key TEXT NOT NULL,
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ expires_at TEXT NOT NULL,
+ last_used TEXT,
+ UNIQUE(user_id, profile_id),
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
+ FOREIGN KEY (profile_id) REFERENCES vault_profiles (id) ON DELETE CASCADE
+ );
+`;
+
+let db: TestDb | null = null;
+
+afterEach(() => {
+ db?.close();
+ db = null;
+});
+
+const tableExists = (name: string) =>
+ !!db!.sqlite
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
+ .get(name);
+
+describe("adopting vault_profiles and vault_tokens", () => {
+ it("keeps every existing row when the legacy tables are there", async () => {
+ db = await createTestDb(pluginDir, {
+ before: (sqlite) => {
+ sqlite.exec("INSERT INTO users (id) VALUES ('user-1'), ('user-2')");
+ sqlite.exec(LEGACY_DDL);
+ sqlite
+ .prepare(
+ "INSERT INTO vault_profiles (id, user_id, name, vault_addr, ssh_role, shared, sync_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
+ )
+ .run(4, "user-1", "Prod", "https://vault:8200", "ops", 1, "sync-a");
+ sqlite
+ .prepare(
+ "INSERT INTO vault_tokens (user_id, profile_id, ssh_cert, private_key, expires_at) VALUES (?, ?, ?, ?, ?)",
+ )
+ .run("user-2", 4, "cert", "key", "2999-01-01T00:00:00.000Z");
+ },
+ });
+
+ expect(tableExists("vault_profiles")).toBe(false);
+ expect(tableExists("vault_tokens")).toBe(false);
+ expect(db.sqlite.prepare("SELECT * FROM p_vault_profiles").all()).toEqual([
+ expect.objectContaining({
+ id: 4,
+ user_id: "user-1",
+ name: "Prod",
+ shared: 1,
+ sync_id: "sync-a",
+ }),
+ ]);
+ expect(db.sqlite.prepare("SELECT * FROM p_vault_tokens").all()).toEqual([
+ expect.objectContaining({ user_id: "user-2", profile_id: 4 }),
+ ]);
+
+ // The 2.8 foreign key follows the rename, so deleting a profile still
+ // takes its cached certificates with it.
+ db.sqlite.exec("PRAGMA foreign_keys = ON");
+ db.sqlite.prepare("DELETE FROM p_vault_profiles WHERE id = 4").run();
+ expect(
+ db.sqlite.prepare("SELECT COUNT(*) AS n FROM p_vault_tokens").get(),
+ ).toEqual({ n: 0 });
+ });
+
+ it("creates both tables on a fresh install", async () => {
+ db = await createTestDb(pluginDir);
+ expect(tableExists("p_vault_profiles")).toBe(true);
+ expect(tableExists("p_vault_tokens")).toBe(true);
+ expect(
+ db.sqlite
+ .prepare(
+ "SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'idx_vault_tokens_user_profile'",
+ )
+ .get(),
+ ).toBeTruthy();
+ });
+
+ it("keeps one certificate per user and profile", async () => {
+ db = await createTestDb(pluginDir);
+ db.sqlite.exec("INSERT INTO users (id) VALUES ('user-1')");
+ const insert = db.sqlite.prepare(
+ "INSERT INTO p_vault_tokens (user_id, profile_id, ssh_cert, private_key, expires_at) VALUES ('user-1', 1, 'c', 'k', 'x')",
+ );
+ insert.run();
+ expect(() => insert.run()).toThrow(/UNIQUE/);
+ });
+});
diff --git a/src/backend/tests/hosts/vault-signer-core.test.ts b/plugins/vault/tests/backend/vault-client.test.ts
similarity index 50%
rename from src/backend/tests/hosts/vault-signer-core.test.ts
rename to plugins/vault/tests/backend/vault-client.test.ts
index 398750dd2..574b733ff 100644
--- a/src/backend/tests/hosts/vault-signer-core.test.ts
+++ b/plugins/vault/tests/backend/vault-client.test.ts
@@ -1,17 +1,19 @@
-import { describe, it, expect, beforeAll, afterEach, vi } from "vitest";
-import { execFileSync } from "child_process";
-import { mkdtempSync, readFileSync, rmSync } from "fs";
-import os from "os";
-import path from "path";
+import { describe, it, expect, beforeAll } from "vitest";
+import { execFileSync } from "node:child_process";
+import { mkdtempSync, readFileSync, rmSync } from "node:fs";
+import os from "node:os";
+import path from "node:path";
import ssh2Pkg from "ssh2";
+import type { PluginFetchInit } from "@termix/plugin-sdk/backend";
import {
+ allowedHosts,
+ completeVaultOidc,
generateEphemeralKeyPair,
parseCertValidBefore,
- startVaultOidc,
- completeVaultOidc,
signWithVault,
+ startVaultOidc,
type VaultProfileConfig,
-} from "../../hosts/vault-signer-core.js";
+} from "../../src/backend/vault-client.js";
const { utils: ssh2Utils } = ssh2Pkg;
@@ -25,12 +27,8 @@ describe("generateEphemeralKeyPair", () => {
const pair = generateEphemeralKeyPair(keyType);
expect(pair.privateKey).toContain("BEGIN OPENSSH PRIVATE KEY");
expect(pair.publicKey.split(/\s+/)[0]).toBe(keyType);
-
- // Both halves must be parseable by the same library that signs/connects.
- const priv = ssh2Utils.parseKey(pair.privateKey);
- expect(priv instanceof Error).toBe(false);
- const pub = ssh2Utils.parseKey(pair.publicKey);
- expect(pub instanceof Error).toBe(false);
+ expect(ssh2Utils.parseKey(pair.privateKey) instanceof Error).toBe(false);
+ expect(ssh2Utils.parseKey(pair.publicKey) instanceof Error).toBe(false);
});
}
@@ -48,24 +46,17 @@ describe("parseCertValidBefore", () => {
beforeAll(() => {
const dir = mkdtempSync(path.join(os.tmpdir(), "vault-cert-test-"));
try {
- execFileSync("ssh-keygen", [
- "-t",
- "ed25519",
- "-f",
- `${dir}/ca`,
- "-N",
- "",
- "-q",
- ]);
- execFileSync("ssh-keygen", [
- "-t",
- "ed25519",
- "-f",
- `${dir}/user`,
- "-N",
- "",
- "-q",
- ]);
+ for (const name of ["ca", "user"]) {
+ execFileSync("ssh-keygen", [
+ "-t",
+ "ed25519",
+ "-f",
+ `${dir}/${name}`,
+ "-N",
+ "",
+ "-q",
+ ]);
+ }
signedAt = Math.floor(Date.now() / 1000);
execFileSync("ssh-keygen", [
"-s",
@@ -87,12 +78,8 @@ describe("parseCertValidBefore", () => {
});
it("reads valid_before from a real ssh-keygen certificate", () => {
- if (!haveSshKeygen) {
- console.warn("ssh-keygen unavailable; skipping real-cert parse test");
- return;
- }
+ if (!haveSshKeygen) return;
const validBefore = parseCertValidBefore(cert);
- // -V +60m => valid_before is roughly signedAt + 3600 (start rounds to minute)
expect(validBefore).toBeGreaterThan(signedAt + 3300);
expect(validBefore).toBeLessThan(signedAt + 3900);
});
@@ -104,7 +91,7 @@ describe("parseCertValidBefore", () => {
});
});
-describe("Vault HTTP flow (mocked fetch)", () => {
+describe("the Vault HTTP flow", () => {
const profile: VaultProfileConfig = {
id: 1,
vaultAddr: "https://vault.example.com:8200/",
@@ -117,34 +104,23 @@ describe("Vault HTTP flow (mocked fetch)", () => {
keyType: "ssh-ed25519",
};
- afterEach(() => {
- vi.unstubAllGlobals();
- });
-
- function mockFetch(
- impl: (
- url: string,
- init: RequestInit,
- ) => { status?: number; body: unknown },
- ) {
- const calls: Array<{ url: string; init: RequestInit }> = [];
- vi.stubGlobal(
- "fetch",
- vi.fn(async (url: string, init: RequestInit) => {
- calls.push({ url, init });
- const { status = 200, body } = impl(url, init);
- return {
- ok: status >= 200 && status < 300,
- status,
- text: async () => JSON.stringify(body),
- } as Response;
- }),
- );
- return calls;
+ function fakeFetch(respond: () => { status?: number; body: unknown }) {
+ const calls: Array<{ url: string; init?: PluginFetchInit }> = [];
+ const fetch = async (url: string, init?: PluginFetchInit) => {
+ calls.push({ url, init });
+ const { status = 200, body } = respond();
+ return new Response(JSON.stringify(body), { status });
+ };
+ return { fetch, calls };
}
- it("startVaultOidc posts auth_url and extracts state", async () => {
- const calls = mockFetch(() => ({
+ it("allows only the profile's own Vault host past the private address guard", () => {
+ expect(allowedHosts(profile)).toEqual(["vault.example.com"]);
+ expect(allowedHosts({ ...profile, vaultAddr: "not a url" })).toEqual([]);
+ });
+
+ it("startVaultOidc posts auth_url and reads the state", async () => {
+ const { fetch, calls } = fakeFetch(() => ({
body: {
data: {
auth_url:
@@ -154,33 +130,31 @@ describe("Vault HTTP flow (mocked fetch)", () => {
}));
const result = await startVaultOidc(
+ fetch,
profile,
- "https://termix/vault/oidc/callback",
+ "https://termix/plugin-api/vault/oidc/callback",
);
expect(result.state).toBe("ST-abc123");
expect(result.clientNonce).toMatch(/^[0-9a-f]{40}$/);
- expect(calls).toHaveLength(1);
expect(calls[0].url).toBe(
"https://vault.example.com:8200/v1/auth/oidc/oidc/auth_url",
);
- expect(calls[0].init.method).toBe("POST");
- const headers = calls[0].init.headers as Record;
- expect(headers["X-Vault-Namespace"]).toBe("team-a");
- const body = JSON.parse(calls[0].init.body as string);
- expect(body).toMatchObject({
+ expect(calls[0].init?.method).toBe("POST");
+ expect(calls[0].init?.headers?.["X-Vault-Namespace"]).toBe("team-a");
+ expect(JSON.parse(String(calls[0].init?.body))).toMatchObject({
role: "developer",
- redirect_uri: "https://termix/vault/oidc/callback",
+ redirect_uri: "https://termix/plugin-api/vault/oidc/callback",
+ client_nonce: result.clientNonce,
});
- expect(body.client_nonce).toBe(result.clientNonce);
});
it("completeVaultOidc returns the client token", async () => {
- const calls = mockFetch(() => ({
+ const { fetch, calls } = fakeFetch(() => ({
body: { auth: { client_token: "hvs.TESTTOKEN" } },
}));
- const token = await completeVaultOidc(profile, {
+ const token = await completeVaultOidc(fetch, profile, {
state: "ST-abc123",
code: "auth-code",
clientNonce: "nonce123",
@@ -192,17 +166,18 @@ describe("Vault HTTP flow (mocked fetch)", () => {
expect(url.searchParams.get("state")).toBe("ST-abc123");
expect(url.searchParams.get("code")).toBe("auth-code");
expect(url.searchParams.get("client_nonce")).toBe("nonce123");
- expect(calls[0].init.method).toBe("GET");
+ expect(calls[0].init?.method).toBe("GET");
});
it("signWithVault posts the public key and returns signed_key", async () => {
- const calls = mockFetch(() => ({
+ const { fetch, calls } = fakeFetch(() => ({
body: {
data: { signed_key: "ssh-ed25519-cert-v01@openssh.com AAAAcert" },
},
}));
const cert = await signWithVault(
+ fetch,
profile,
"hvs.TESTTOKEN",
"ssh-ed25519 AAAApub comment",
@@ -212,11 +187,8 @@ describe("Vault HTTP flow (mocked fetch)", () => {
expect(calls[0].url).toBe(
"https://vault.example.com:8200/v1/ssh-client-signer/sign/my-role",
);
- const headers = calls[0].init.headers as Record;
- expect(headers["X-Vault-Token"]).toBe("hvs.TESTTOKEN");
- expect(headers["X-Vault-Namespace"]).toBe("team-a");
- const body = JSON.parse(calls[0].init.body as string);
- expect(body).toMatchObject({
+ expect(calls[0].init?.headers?.["X-Vault-Token"]).toBe("hvs.TESTTOKEN");
+ expect(JSON.parse(String(calls[0].init?.body))).toMatchObject({
public_key: "ssh-ed25519 AAAApub comment",
cert_type: "user",
valid_principals: "root,deploy",
@@ -224,45 +196,21 @@ describe("Vault HTTP flow (mocked fetch)", () => {
});
it("surfaces Vault error messages", async () => {
- mockFetch(() => ({
+ const { fetch } = fakeFetch(() => ({
status: 400,
body: { errors: ["role not found", "permission denied"] },
}));
-
await expect(
- signWithVault(profile, "tok", "ssh-ed25519 AAAA"),
+ signWithVault(fetch, profile, "tok", "ssh-ed25519 AAAA"),
).rejects.toThrow(/role not found; permission denied/);
});
-});
-// Live integration against a real Vault (e.g. `vault server -dev` in Docker).
-// Runs only when VAULT_ADDR + VAULT_TOKEN are set and the SSH signer mount
-// (VAULT_SSH_MOUNT/VAULT_SSH_ROLE) has been configured by the test harness.
-describe("Vault live signing", () => {
- const addr = process.env.VAULT_ADDR;
- const token = process.env.VAULT_TOKEN;
- const run = !!addr && !!token;
-
- it.skipIf(!run)("signs an ephemeral key against a real Vault", async () => {
- const profile: VaultProfileConfig = {
- id: 99,
- vaultAddr: addr!,
- sshMount: process.env.VAULT_SSH_MOUNT || "ssh-client-signer",
- sshRole: process.env.VAULT_SSH_ROLE || "my-role",
- validPrincipals: "root",
- keyType: "ssh-ed25519",
+ it("reports an unreachable Vault", async () => {
+ const fetch = async () => {
+ throw new Error("refused");
};
-
- const pair = generateEphemeralKeyPair(profile.keyType);
- const before = Math.floor(Date.now() / 1000);
- const cert = await signWithVault(profile, token!, pair.publicKey);
-
- expect(cert).toMatch(/-cert-v01@openssh\.com /);
- // The signed cert must parse with the same library used to connect.
- const parsed = ssh2Utils.parseKey(cert);
- expect(parsed instanceof Error).toBe(false);
-
- const validBefore = parseCertValidBefore(cert);
- expect(validBefore).toBeGreaterThan(before);
+ await expect(
+ startVaultOidc(fetch, profile, "https://termix/cb"),
+ ).rejects.toThrow(/Failed to reach Vault/);
});
});
diff --git a/plugins/vault/tests/backend/vault.test.ts b/plugins/vault/tests/backend/vault.test.ts
new file mode 100644
index 000000000..8d76cb873
--- /dev/null
+++ b/plugins/vault/tests/backend/vault.test.ts
@@ -0,0 +1,471 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { PluginCapabilityError } from "@termix/plugin-sdk/backend";
+import { createMockCtx, createTestDb } from "@termix/plugin-sdk/testing";
+import {
+ HOST,
+ PROFILE,
+ SIGNED_CERT,
+ VAULT_ADDR,
+ createFakeVault,
+ env,
+ fakeSocket,
+ manifest,
+ pluginDir,
+ startServer,
+ type TestServer,
+} from "./helpers.js";
+
+const certs = vi.hoisted(() => ({ applyCertificateAuth: vi.fn() }));
+vi.mock("@termix/plugin-sdk/ssh-certs", () => certs);
+
+let server: TestServer | null = null;
+
+beforeEach(() => {
+ certs.applyCertificateAuth.mockReset();
+});
+
+afterEach(async () => {
+ await server?.close();
+ server = null;
+});
+
+/** Creates a profile as user-1 and points host 7 at it. */
+async function profileOnHost(options: { shared?: boolean } = {}) {
+ const created = await server!.request("POST", "/profiles", {
+ body: { ...PROFILE, shared: options.shared ?? false },
+ });
+ expect(created.status).toBe(201);
+ await server!.mock.ctx.settings.setHost(7, "profileId", created.body.id);
+ return created.body as { id: number };
+}
+
+async function startSignIn(userId = "user-1") {
+ const socket = fakeSocket();
+ await server!.provider().startInteraction!({
+ userId,
+ hostId: 7,
+ host: { name: "box", ip: HOST.ip, username: HOST.username },
+ socket: socket.socket,
+ requestOrigin: "https://termix.test",
+ payload: {},
+ });
+ return socket;
+}
+
+describe("activate", () => {
+ it("registers the vault auth type, the sync entity and a public callback", async () => {
+ server = await startServer();
+ const provider = server.provider();
+ expect(provider).toMatchObject({
+ type: "vault",
+ interaction: "vault",
+ needsUserInteraction: true,
+ supportsBackground: false,
+ });
+ expect(server.mock.syncEntities).toEqual([
+ expect.objectContaining({ type: "vaultProfiles", order: 20 }),
+ ]);
+ expect(server.mock.httpRouters[0]?.public).toEqual(["/oidc/callback"]);
+ });
+
+ // secrets:own and network:outbound are checked when used, ui:surface by the
+ // frontend.
+ it.each(["db:own", "network:serve", "auth:provide"])(
+ "fails closed without %s",
+ async (capability) => {
+ const db = await createTestDb(pluginDir);
+ const mock = createMockCtx({
+ pluginId: manifest.id,
+ manifest,
+ capabilities: manifest.capabilities.filter((c) => c !== capability),
+ db: db.database,
+ router: () => ({ get: () => {}, use: () => {} }),
+ });
+ const { activate } = await import("../../src/backend/index.js");
+ await expect(activate(mock.ctx)).rejects.toThrow(PluginCapabilityError);
+ db.close();
+ },
+ );
+});
+
+describe("profiles", () => {
+ it("creates, lists, updates and deletes a profile", async () => {
+ server = await startServer();
+ const created = await server.request("POST", "/profiles", {
+ body: { ...PROFILE, vaultNamespace: " team ", tags: ["a", "b"] },
+ });
+ expect(created.status).toBe(201);
+ expect(created.body).toMatchObject({
+ name: "Prod",
+ vaultAddr: VAULT_ADDR,
+ vaultNamespace: "team",
+ sshRole: "ops",
+ tags: ["a", "b"],
+ shared: false,
+ owned: true,
+ });
+
+ const listed = await server.request("GET", "/profiles");
+ expect(listed.body).toHaveLength(1);
+
+ const updated = await server.request(
+ "PUT",
+ `/profiles/${created.body.id}`,
+ { body: { name: "Staging", keyType: "ssh-rsa" } },
+ );
+ expect(updated.status).toBe(200);
+ expect(updated.body).toMatchObject({ name: "Staging", keyType: "ssh-rsa" });
+
+ const deleted = await server.request(
+ "DELETE",
+ `/profiles/${created.body.id}`,
+ );
+ expect(deleted.status).toBe(200);
+ expect((await server.request("GET", "/profiles")).body).toEqual([]);
+ });
+
+ it("records a tombstone and drops cached certificates on delete", async () => {
+ server = await startServer();
+ const { id } = await profileOnHost();
+ server.db.sqlite
+ .prepare(
+ "INSERT INTO p_vault_tokens (user_id, profile_id, ssh_cert, private_key, expires_at) VALUES (?, ?, ?, ?, ?)",
+ )
+ .run("user-1", id, "c", "k", "2999-01-01T00:00:00.000Z");
+ const syncId = (
+ server.db.sqlite
+ .prepare("SELECT sync_id FROM p_vault_profiles WHERE id = ?")
+ .get(id) as { sync_id: string }
+ ).sync_id;
+
+ await server.request("DELETE", `/profiles/${id}`);
+
+ expect(server.mock.tombstones).toEqual([
+ { userId: "user-1", entityType: "vaultProfiles", syncId },
+ ]);
+ expect(
+ server.db.sqlite
+ .prepare("SELECT COUNT(*) AS n FROM p_vault_tokens")
+ .get(),
+ ).toEqual({ n: 0 });
+ });
+
+ it("requires name, address and signer role", async () => {
+ server = await startServer();
+ const response = await server.request("POST", "/profiles", {
+ body: { name: "x" },
+ });
+ expect(response.status).toBe(400);
+ });
+
+ it("shows other users only shared profiles, and only the owner may change one", async () => {
+ server = await startServer();
+ const own = await server.request("POST", "/profiles", { body: PROFILE });
+ const shared = await server.request("POST", "/profiles", {
+ body: { ...PROFILE, name: "Shared", shared: true },
+ });
+
+ const seen = await server.request("GET", "/profiles", { user: "user-2" });
+ expect(seen.body.map((p: { name: string }) => p.name)).toEqual(["Shared"]);
+ expect(seen.body[0].owned).toBe(false);
+
+ for (const id of [own.body.id, shared.body.id]) {
+ const put = await server.request("PUT", `/profiles/${id}`, {
+ user: "user-2",
+ body: { name: "mine" },
+ });
+ expect(put.status).toBe(403);
+ const del = await server.request("DELETE", `/profiles/${id}`, {
+ user: "user-2",
+ });
+ expect(del.status).toBe(403);
+ }
+ });
+
+ it("needs the share permission to share a profile", async () => {
+ server = await startServer({ permissions: ["vault.use"] });
+ const create = await server.request("POST", "/profiles", {
+ body: { ...PROFILE, shared: true },
+ });
+ expect(create.status).toBe(403);
+
+ const own = await server.request("POST", "/profiles", { body: PROFILE });
+ expect(own.status).toBe(201);
+ const share = await server.request("PUT", `/profiles/${own.body.id}`, {
+ body: { shared: true },
+ });
+ expect(share.status).toBe(403);
+ const unshare = await server.request("PUT", `/profiles/${own.body.id}`, {
+ body: { shared: false },
+ });
+ expect(unshare.status).toBe(200);
+ });
+
+ it("refuses every profile route without the use permission", async () => {
+ server = await startServer({ permissions: [] });
+ for (const [method, path] of [
+ ["GET", "/profiles"],
+ ["POST", "/profiles"],
+ ["PUT", "/profiles/1"],
+ ["DELETE", "/profiles/1"],
+ ]) {
+ const response = await server.request(method, path, {
+ body: method === "GET" ? undefined : PROFILE,
+ });
+ expect(response.status, `${method} ${path}`).toBe(403);
+ }
+ });
+
+ it("answers 404 for a profile that does not exist", async () => {
+ server = await startServer();
+ expect(
+ (await server.request("PUT", "/profiles/99", { body: {} })).status,
+ ).toBe(404);
+ expect((await server.request("DELETE", "/profiles/99")).status).toBe(404);
+ expect((await server.request("DELETE", "/profiles/abc")).status).toBe(400);
+ });
+});
+
+describe("signing a certificate", () => {
+ it("runs the OIDC flow against Vault and stores the certificate sealed", async () => {
+ const vault = createFakeVault();
+ server = await startServer({ fetch: vault.fetch });
+ const { id } = await profileOnHost();
+
+ const socket = await startSignIn();
+ expect(socket.sent).toEqual([
+ expect.objectContaining({
+ type: "vault_auth_url",
+ hostId: 7,
+ requestId: vault.state,
+ url: expect.stringContaining("https://idp.test/authorize"),
+ }),
+ ]);
+ const authUrlCall = vault.calls[0];
+ expect(JSON.parse(String(authUrlCall.init?.body))).toMatchObject({
+ role: "dev",
+ redirect_uri: "https://termix.test/plugin-api/vault/oidc/callback",
+ });
+ expect(authUrlCall.init?.allowPrivateHosts).toEqual(["vault.internal"]);
+
+ const callback = await server.request(
+ "GET",
+ `/oidc/callback?state=${vault.state}&code=abc`,
+ { user: "" },
+ );
+ expect(callback.status).toBe(200);
+ expect(callback.text).toContain("Vault sign-in complete");
+ expect(socket.sent.at(-1)).toMatchObject({
+ type: "vault_completed",
+ hostId: 7,
+ });
+
+ const signCall = vault.calls.find((call) => call.url.includes("/sign/"));
+ expect(signCall?.url).toBe(`${VAULT_ADDR}/v1/ssh-client-signer/sign/ops`);
+ expect(signCall?.init?.headers?.["X-Vault-Token"]).toBe("hvs.token");
+
+ const row = server.db.sqlite
+ .prepare("SELECT * FROM p_vault_tokens WHERE profile_id = ?")
+ .get(id) as { ssh_cert: string; private_key: string };
+ expect(row.ssh_cert).not.toBe(SIGNED_CERT);
+ expect(await server.mock.ctx.secrets.unseal(row.ssh_cert)).toBe(
+ SIGNED_CERT,
+ );
+ expect(await server.mock.ctx.secrets.unseal(row.private_key)).toContain(
+ "OPENSSH PRIVATE KEY",
+ );
+
+ const outcome = await server.provider().prepare({}, HOST, env());
+ expect(outcome).toEqual({ status: "ready" });
+ expect(certs.applyCertificateAuth).toHaveBeenCalledWith(
+ {},
+ {},
+ expect.objectContaining({ certificate: SIGNED_CERT }),
+ "root",
+ );
+ });
+
+ it("answers a second use of the same callback with an error", async () => {
+ const vault = createFakeVault();
+ server = await startServer({ fetch: vault.fetch });
+ await profileOnHost();
+ await startSignIn();
+ const path = `/oidc/callback?state=${vault.state}&code=abc`;
+ expect((await server.request("GET", path)).status).toBe(200);
+ expect((await server.request("GET", path)).status).toBe(400);
+ });
+
+ it("reports Vault refusing to sign", async () => {
+ const vault = createFakeVault({ signStatus: 403 });
+ server = await startServer({ fetch: vault.fetch });
+ await profileOnHost();
+ const socket = await startSignIn();
+ const callback = await server.request(
+ "GET",
+ `/oidc/callback?state=${vault.state}&code=abc`,
+ );
+ expect(callback.status).toBe(400);
+ expect(socket.sent.at(-1)).toMatchObject({
+ type: "vault_error",
+ error: expect.stringContaining("permission denied"),
+ });
+ });
+
+ it("passes an identity provider error through", async () => {
+ server = await startServer();
+ const callback = await server.request(
+ "GET",
+ "/oidc/callback?error=access_denied",
+ );
+ expect(callback.status).toBe(400);
+ expect(callback.text).toContain("access_denied");
+ });
+
+ it("sends the 2.8 redirect URI while legacyCallback is on", async () => {
+ const vault = createFakeVault();
+ server = await startServer({
+ fetch: vault.fetch,
+ settings: { legacyCallback: true },
+ });
+ await profileOnHost();
+ await startSignIn();
+ expect(JSON.parse(String(vault.calls[0].init?.body)).redirect_uri).toBe(
+ "https://termix.test/vault/oidc/callback",
+ );
+ });
+
+ it("lets a pending sign-in be cancelled", async () => {
+ const vault = createFakeVault();
+ server = await startServer({ fetch: vault.fetch });
+ await profileOnHost();
+ await startSignIn();
+ await server.provider().cancelInteraction!({ userId: "user-1", hostId: 7 });
+ const callback = await server.request(
+ "GET",
+ `/oidc/callback?state=${vault.state}&code=abc`,
+ );
+ expect(callback.status).toBe(400);
+ });
+
+ it("refuses to start without a profile on the host", async () => {
+ server = await startServer();
+ await expect(startSignIn()).rejects.toThrow(/No Vault signer profile/);
+ });
+});
+
+describe("the vault provider", () => {
+ it("fails without a profile on the host", async () => {
+ server = await startServer();
+ expect(await server.provider().prepare({}, HOST, env())).toMatchObject({
+ status: "error",
+ message: "Host has no Vault signer profile configured",
+ });
+ });
+
+ it("asks for a sign-in without a certificate", async () => {
+ server = await startServer();
+ await profileOnHost();
+ expect(await server.provider().prepare({}, HOST, env())).toEqual({
+ status: "interaction-required",
+ interaction: "vault",
+ message: expect.any(String),
+ flag: "requiresVaultAuth",
+ });
+ });
+
+ it("uses a profile another user shared with the host's owner", async () => {
+ server = await startServer();
+ const shared = await server.request("POST", "/profiles", {
+ user: "user-2",
+ body: { ...PROFILE, shared: true },
+ });
+ await server.mock.ctx.settings.setHost(7, "profileId", shared.body.id);
+ expect((await server.provider().prepare({}, HOST, env())).status).toBe(
+ "interaction-required",
+ );
+ });
+
+ it("drops a certificate 2.8 cached with the user's data key", async () => {
+ server = await startServer();
+ const { id } = await profileOnHost();
+ server.db.sqlite
+ .prepare(
+ "INSERT INTO p_vault_tokens (user_id, profile_id, ssh_cert, private_key, expires_at) VALUES (?, ?, ?, ?, ?)",
+ )
+ .run(
+ "user-1",
+ id,
+ "v1:dek-cipher",
+ "v1:dek-cipher",
+ "2999-01-01T00:00:00.000Z",
+ );
+
+ const outcome = await server.provider().prepare({}, HOST, env());
+ expect(outcome.status).toBe("interaction-required");
+ expect(
+ server.db.sqlite
+ .prepare("SELECT COUNT(*) AS n FROM p_vault_tokens")
+ .get(),
+ ).toEqual({ n: 0 });
+ });
+
+ it("forgets an expired certificate", async () => {
+ server = await startServer();
+ const { id } = await profileOnHost();
+ const seal = server.mock.ctx.secrets.seal;
+ server.db.sqlite
+ .prepare(
+ "INSERT INTO p_vault_tokens (user_id, profile_id, ssh_cert, private_key, expires_at) VALUES (?, ?, ?, ?, ?)",
+ )
+ .run(
+ "user-1",
+ id,
+ await seal("c"),
+ await seal("k"),
+ "2000-01-01T00:00:00.000Z",
+ );
+ expect((await server.provider().prepare({}, HOST, env())).status).toBe(
+ "interaction-required",
+ );
+ expect(
+ server.db.sqlite
+ .prepare("SELECT COUNT(*) AS n FROM p_vault_tokens")
+ .get(),
+ ).toEqual({ n: 0 });
+ });
+
+ it("drops the certificate when the host rejects it", async () => {
+ const vault = createFakeVault();
+ server = await startServer({ fetch: vault.fetch });
+ await profileOnHost();
+ await startSignIn();
+ await server.request("GET", `/oidc/callback?state=${vault.state}&code=abc`);
+
+ const outcome = server.provider().onAuthFailed!(HOST, env(), {
+ error: new Error("All configured authentication methods failed"),
+ retries: 0,
+ canRetry: false,
+ methodNotAvailable: false,
+ });
+ expect(outcome).toMatchObject({ status: "interaction-required" });
+ await vi.waitFor(() =>
+ expect(
+ server!.db.sqlite
+ .prepare("SELECT COUNT(*) AS n FROM p_vault_tokens")
+ .get(),
+ ).toEqual({ n: 0 }),
+ );
+ });
+
+ it("ignores other auth failures", async () => {
+ server = await startServer();
+ expect(
+ server.provider().onAuthFailed!(HOST, env(), {
+ error: new Error("connection reset"),
+ retries: 0,
+ canRetry: false,
+ methodNotAvailable: false,
+ }),
+ ).toBeUndefined();
+ });
+});
diff --git a/plugins/vault/tests/frontend/activate.test.tsx b/plugins/vault/tests/frontend/activate.test.tsx
new file mode 100644
index 000000000..3b1642d9f
--- /dev/null
+++ b/plugins/vault/tests/frontend/activate.test.tsx
@@ -0,0 +1,268 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import {
+ act,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from "@testing-library/react";
+import {
+ renderWithApp,
+ type RenderedPluginApp,
+} from "@termix/plugin-sdk/testing";
+import type { PluginManifest } from "@termix/plugin-sdk/manifest";
+import * as plugin from "../../src/frontend/index";
+import { VaultOverlay } from "../../src/frontend/VaultOverlay";
+import { VaultAuthEditor } from "../../src/frontend/VaultAuthEditor";
+import { redirectUri } from "../../src/frontend/RedirectUriSetting";
+import manifestJson from "../../manifest.json";
+import locales from "../../locales/en.json";
+
+const stubs = vi.hoisted(() => ({
+ permissions: new Set(["use", "share"]),
+ api: {
+ get: vi.fn(),
+ post: vi.fn(),
+ put: vi.fn(),
+ delete: vi.fn(),
+ patch: vi.fn(),
+ },
+ toast: { success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() },
+}));
+
+vi.mock("@termix/plugin-sdk/frontend", async (importOriginal) => ({
+ ...(await importOriginal()),
+ useTranslation: () => ({
+ t: (key: string, options?: Record) =>
+ options?.name ? `${key}:${options.name}` : key,
+ }),
+ usePluginApi: () => stubs.api,
+ usePermission: (permission: string) => stubs.permissions.has(permission),
+ useToast: () => stubs.toast,
+}));
+
+const manifest = manifestJson as unknown as PluginManifest;
+
+let rendered: RenderedPluginApp | null = null;
+
+beforeEach(() => {
+ stubs.permissions = new Set(["use", "share"]);
+ for (const fn of Object.values(stubs.api)) fn.mockReset();
+ stubs.api.get.mockResolvedValue({ data: [] });
+});
+
+afterEach(async () => {
+ await rendered?.deactivate();
+ rendered = null;
+});
+
+describe("vault activate", () => {
+ it("registers the terminal overlay and the settings field, and removes them on deactivate", async () => {
+ rendered = await renderWithApp(plugin, { manifest, locales });
+ expect(rendered.registered.settingsComponents()).toEqual(["redirectUri"]);
+ expect(rendered.registered.slot("terminal.overlay")).toEqual([
+ "vault.signIn",
+ ]);
+ await rendered.deactivate();
+ expect(rendered.registered.settingsComponents()).toEqual([]);
+ expect(rendered.registered.slot("terminal.overlay")).toEqual([]);
+ rendered = null;
+ });
+});
+
+describe("redirectUri", () => {
+ it("names the plugin callback, or the 2.8 one", () => {
+ expect(redirectUri(false, "https://t.example/app/")).toBe(
+ "https://t.example/app/plugin-api/vault/oidc/callback",
+ );
+ expect(redirectUri(true, "https://t.example/")).toBe(
+ "https://t.example/vault/oidc/callback",
+ );
+ });
+});
+
+describe("VaultAuthEditor", () => {
+ const profiles = [
+ {
+ id: 3,
+ name: "Prod",
+ vaultAddr: "https://v",
+ sshRole: "r",
+ shared: false,
+ owned: true,
+ },
+ {
+ id: 4,
+ name: "Team",
+ vaultAddr: "https://v",
+ sshRole: "r",
+ shared: true,
+ owned: false,
+ },
+ ];
+
+ it("lists the profiles and stores the choice as the profileId host setting", async () => {
+ stubs.api.get.mockResolvedValue({ data: profiles });
+ const setField = vi.fn();
+ render(
+ ,
+ );
+ await waitFor(() => expect(screen.getByText("Prod")).toBeTruthy());
+ expect(stubs.api.get).toHaveBeenCalledWith("/profiles");
+ expect(screen.getByText("editor.sharedProfile:Team")).toBeTruthy();
+
+ fireEvent.change(screen.getByRole("combobox"), { target: { value: "4" } });
+ expect(setField).toHaveBeenCalledWith("pluginSettings", {
+ other: { x: 1 },
+ vault: { profileId: 4 },
+ });
+ });
+
+ it("shows the saved profile", async () => {
+ stubs.api.get.mockResolvedValue({ data: profiles });
+ render(
+ ,
+ );
+ await waitFor(() =>
+ expect((screen.getByRole("combobox") as HTMLSelectElement).value).toBe(
+ "3",
+ ),
+ );
+ });
+
+ it("creates a profile from the manager, shared only with the share permission", async () => {
+ stubs.api.post.mockResolvedValue({ data: {} });
+ render();
+ fireEvent.click(screen.getByText("profiles.manage"));
+ fireEvent.click(screen.getByText("profiles.new"));
+ const inputs = screen.getAllByRole("textbox");
+ fireEvent.change(inputs[0], { target: { value: "Prod" } });
+ fireEvent.change(inputs[1], { target: { value: "https://vault:8200" } });
+ fireEvent.change(inputs[6], { target: { value: "ops" } });
+ fireEvent.click(screen.getByRole("checkbox"));
+ await act(async () => {
+ fireEvent.click(screen.getByText("profiles.create"));
+ });
+ expect(stubs.api.post).toHaveBeenCalledWith(
+ "/profiles",
+ expect.objectContaining({
+ name: "Prod",
+ vaultAddr: "https://vault:8200",
+ sshRole: "ops",
+ shared: true,
+ }),
+ );
+ });
+
+ it("hides sharing without the share permission and managing without use", async () => {
+ stubs.permissions = new Set(["use"]);
+ const { unmount } = render(
+ ,
+ );
+ fireEvent.click(screen.getByText("profiles.manage"));
+ fireEvent.click(screen.getByText("profiles.new"));
+ expect(screen.queryByRole("checkbox")).toBeNull();
+ unmount();
+
+ stubs.permissions = new Set();
+ stubs.api.get.mockClear();
+ render();
+ expect(screen.queryByText("profiles.manage")).toBeNull();
+ expect(stubs.api.get).not.toHaveBeenCalled();
+ });
+});
+
+describe("VaultOverlay", () => {
+ function setup() {
+ let listener: (message: {
+ type: string;
+ [key: string]: unknown;
+ }) => void = () => {};
+ const props = {
+ host: { id: 7 },
+ subscribe: vi.fn((next: typeof listener) => {
+ listener = next;
+ return () => {};
+ }),
+ holdConnectTimeout: vi.fn(),
+ fail: vi.fn(),
+ disconnect: vi.fn(),
+ send: vi.fn(),
+ connectPayload: vi.fn(() => ({ hostId: 7, cols: 80, rows: 24 })),
+ };
+ render();
+ const emit = (message: { type: string; [key: string]: unknown }) =>
+ act(() => listener(message));
+ return { props, emit };
+ }
+
+ it("starts the sign-in once, then reports a second request as a failure", () => {
+ const { props, emit } = setup();
+ emit({ type: "vault_auth_required", hostId: 7 });
+ expect(props.send).toHaveBeenCalledWith("vault_start_auth", { hostId: 7 });
+ expect(props.holdConnectTimeout).toHaveBeenCalledWith(true);
+
+ emit({ type: "vault_auth_required", hostId: 7 });
+ expect(props.fail).toHaveBeenCalledWith("dialog.failed");
+ });
+
+ it("opens Vault's sign-in, waits, and reconnects once signed", () => {
+ const close = vi.fn();
+ const open = vi
+ .spyOn(window, "open")
+ .mockReturnValue({ close, focus: vi.fn() } as unknown as Window);
+ const { props, emit } = setup();
+ emit({ type: "vault_auth_required", hostId: 7 });
+ emit({
+ type: "vault_auth_url",
+ hostId: 7,
+ requestId: "st-1",
+ url: "https://idp.test/authorize",
+ });
+ expect(open).toHaveBeenCalledWith(
+ "https://idp.test/authorize",
+ "termix-vault-oidc",
+ "width=540,height=720",
+ );
+ expect(screen.getByText("dialog.description")).toBeTruthy();
+
+ emit({ type: "vault_completed", hostId: 7 });
+ expect(close).toHaveBeenCalled();
+ expect(props.send).toHaveBeenCalledWith("vault_auth_completed", {
+ hostId: 7,
+ cols: 80,
+ rows: 24,
+ });
+ expect(screen.queryByText("dialog.title")).toBeNull();
+ open.mockRestore();
+ });
+
+ it("cancels a pending sign-in", () => {
+ const open = vi.spyOn(window, "open").mockReturnValue(null);
+ const { props, emit } = setup();
+ emit({
+ type: "vault_auth_url",
+ hostId: 7,
+ requestId: "st-2",
+ url: "https://idp.test/authorize",
+ });
+ fireEvent.click(screen.getByText("common.cancel"));
+ expect(props.send).toHaveBeenCalledWith("vault_cancel", {
+ hostId: 7,
+ requestId: "st-2",
+ });
+ open.mockRestore();
+ });
+
+ it("shows Vault's error", () => {
+ const { emit } = setup();
+ emit({ type: "vault_error", hostId: 7, error: "permission denied" });
+ expect(screen.getByText("permission denied")).toBeTruthy();
+ });
+});
diff --git a/plugins/vault/tsconfig.json b/plugins/vault/tsconfig.json
new file mode 100644
index 000000000..7b35314c3
--- /dev/null
+++ b/plugins/vault/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "@termix/plugin-sdk/tsconfig.plugin.json",
+ "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"]
+}
diff --git a/plugins/vault/vitest.config.ts b/plugins/vault/vitest.config.ts
new file mode 100644
index 000000000..31ba004fc
--- /dev/null
+++ b/plugins/vault/vitest.config.ts
@@ -0,0 +1,3 @@
+import { pluginVitestConfig } from "@termix/plugin-sdk/vitest-preset";
+
+export default pluginVitestConfig(import.meta.url);
diff --git a/src/backend/auth/legacy-providers.ts b/src/backend/auth/legacy-providers.ts
deleted file mode 100644
index cc68fee11..000000000
--- a/src/backend/auth/legacy-providers.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-/**
- * Auth methods that still live in core for 2.9.0, registered through the same
- * interfaces a plugin uses. Phase C moves each one into its own plugin by
- * relocating its block from here; D1 deletes this file once it is empty.
- *
- * Nothing outside this file should branch on these type names.
- */
-
-import { getErrorMessage } from "../utils/error-message.js";
-import { sshLogger } from "../utils/logger.js";
-import { applyCertificateAuth } from "@termix/plugin-sdk/ssh-certs";
-import { registerSshAuthProvider } from "../hosts/connect/auth-provider-registry.js";
-import type {
- SshAuthProvider,
- SshConnectHost,
-} from "../hosts/connect/types.js";
-
-const VAULT_REQUIRED_MESSAGE =
- "Vault SSH signer authentication required. Please open a Terminal connection first.";
-
-const AUTH_FAILED_PATTERN = /All configured authentication methods failed/i;
-
-function vaultProfileId(host: SshConnectHost): number | undefined {
- return host.vaultProfile?.id ?? undefined;
-}
-
-const vaultProvider: SshAuthProvider = {
- type: "vault",
- pluginId: "core",
- labelKey: "hosts.filterAuthVault",
- needsUserInteraction: true,
- interaction: "vault",
- prepare: async (config, host, env) => {
- const profileId = vaultProfileId(host);
- if (!profileId) {
- return {
- status: "error",
- code: "failed",
- message: "Host has no Vault signer profile configured",
- };
- }
- const { getVaultCert } = await import("../hosts/vault-signer-auth.js");
- const cert = await getVaultCert(env.userId, profileId);
- if (!cert) {
- env.log(
- "info",
- "No valid Vault certificate found, requesting authentication",
- );
- return {
- status: "interaction-required",
- interaction: "vault",
- message: VAULT_REQUIRED_MESSAGE,
- flag: "requiresVaultAuth",
- };
- }
- try {
- await applyCertificateAuth(
- config,
- env.client,
- { privateKey: cert.privateKey, certificate: cert.sshCert },
- host.username,
- );
- } catch (error) {
- return {
- status: "error",
- code: "failed",
- message:
- "Vault SSH signer authentication failed: " + getErrorMessage(error),
- };
- }
- env.log("info", "Using cached Vault-signed certificate");
- return { status: "ready" };
- },
- onAuthFailed: (host, env, context) => {
- if (!AUTH_FAILED_PATTERN.test(context.error.message)) return;
- sshLogger.warn("Vault certificate authentication failed", {
- operation: "vault_auth_failed",
- hostId: env.hostId,
- userId: env.userId,
- });
- const profileId = vaultProfileId(host);
- if (profileId) {
- void import("../hosts/vault-signer-auth.js")
- .then(({ deleteVaultCert }) => deleteVaultCert(env.userId, profileId))
- .catch(() => {});
- }
- return {
- status: "interaction-required",
- interaction: "vault",
- message:
- "Vault authentication failed or expired. Please authenticate again.",
- };
- },
- startInteraction: async (request) => {
- const { loadVaultProfileForHost, startVaultAuth } =
- await import("../hosts/vault-oidc-auth.js");
- const profile = await loadVaultProfileForHost(
- request.hostId,
- request.userId,
- );
- if (!profile) {
- throw new Error("No Vault signer profile configured for this host");
- }
- await startVaultAuth(
- request.userId,
- request.hostId,
- profile,
- request.socket,
- request.requestOrigin,
- );
- },
- cancelInteraction: async (request) => {
- if (request.hostId === undefined) return;
- const { cancelVaultAuthByHost } =
- await import("../hosts/vault-oidc-auth.js");
- cancelVaultAuthByHost(request.userId, request.hostId);
- },
-};
-
-let sshRegistered = false;
-
-export function registerLegacySshAuthProviders(): void {
- if (sshRegistered) return;
- sshRegistered = true;
-
- registerSshAuthProvider(vaultProvider);
-}
-
-/** Test helper. */
-export function resetLegacyProvidersForTests(): void {
- sshRegistered = false;
-}
diff --git a/src/backend/auth/registry.ts b/src/backend/auth/registry.ts
index 1a9fe371e..984dc4b4b 100644
--- a/src/backend/auth/registry.ts
+++ b/src/backend/auth/registry.ts
@@ -2,8 +2,8 @@
* Login methods and second factors, from core and from plugins.
*
* Password login is registered by core and always present. Everything else
- * (OIDC, LDAP, passkeys, TOTP, ...) registers here, from a plugin through
- * ctx.auth or, in 2.9.0, from legacy-providers.ts.
+ * (OIDC, LDAP, passkeys, TOTP, ...) registers here from a plugin through
+ * ctx.auth.
*/
import type {
diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts
index 20ceeb578..bac004ed9 100644
--- a/src/backend/database/database.ts
+++ b/src/backend/database/database.ts
@@ -7,6 +7,7 @@ import multer from "multer";
import cookieParser from "cookie-parser";
import userRoutes from "./routes/users.js";
import hostRoutes from "./routes/host.js";
+import { registerVaultCompatRoutes } from "./routes/host-compat-routes.js";
import alertRoutes from "./routes/alerts.js";
import credentialsRoutes from "./routes/credentials.js";
import sshAuthRoutes from "./routes/ssh-auth-routes.js";
@@ -18,7 +19,6 @@ import credentialSidebarPreferencesRoutes from "./routes/credential-sidebar-pref
import uiPreferencesRoutes from "./routes/ui-preferences.js";
import termixIdRoutes from "./routes/termix-id.js";
import { registerAuditLogRoutes } from "./routes/audit-log-routes.js";
-import vaultRoutes from "./routes/vault.js";
import notificationChannelsRoutes from "./routes/notification-channels-routes.js";
import syncRoutes from "./routes/sync.js";
import dashboardRoutes from "./routes/dashboard-routes.js";
@@ -1795,7 +1795,9 @@ app.use("/credential-sidebar/preferences", credentialSidebarPreferencesRoutes);
app.use("/ui-preferences", uiPreferencesRoutes);
app.use("/termix-id", termixIdRoutes);
registerAuditLogRoutes(app, authenticateJWT);
-app.use("/vault", vaultRoutes);
+const vaultCompatRoutes = express.Router();
+registerVaultCompatRoutes(vaultCompatRoutes);
+app.use("/vault", vaultCompatRoutes);
app.use("/", notificationChannelsRoutes);
app.use("/sync", syncRoutes);
app.use("/dashboard", dashboardRoutes);
diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts
index ead5cf75e..45c81a065 100644
--- a/src/backend/database/db/index.ts
+++ b/src/backend/database/db/index.ts
@@ -978,11 +978,6 @@ const migrateSchema = () => {
"override_credential_username",
"INTEGER",
);
- addColumnIfNotExists(
- "ssh_data",
- "vault_profile_id",
- "INTEGER REFERENCES vault_profiles(id) ON DELETE SET NULL",
- );
addColumnIfNotExists("ssh_data", "autostart_password", "TEXT");
addColumnIfNotExists("ssh_data", "autostart_key", "TEXT");
@@ -1579,67 +1574,6 @@ const migrateSchema = () => {
});
}
- try {
- sqlite.prepare("SELECT id FROM vault_profiles LIMIT 1").get();
- } catch {
- try {
- sqlite.exec(`
- CREATE TABLE IF NOT EXISTS vault_profiles (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id TEXT NOT NULL,
- name TEXT NOT NULL,
- description TEXT,
- folder TEXT,
- tags TEXT,
- vault_addr TEXT NOT NULL,
- vault_namespace TEXT,
- oidc_mount TEXT,
- oidc_role TEXT,
- ssh_mount TEXT,
- ssh_role TEXT NOT NULL,
- valid_principals TEXT,
- key_type TEXT,
- shared INTEGER NOT NULL DEFAULT 0,
- created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
- );
- `);
- } catch (createError) {
- databaseLogger.warn("Failed to create vault_profiles table", {
- operation: "schema_migration",
- error: createError,
- });
- }
- }
-
- try {
- sqlite.prepare("SELECT id FROM vault_tokens LIMIT 1").get();
- } catch {
- try {
- sqlite.exec(`
- CREATE TABLE IF NOT EXISTS vault_tokens (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id TEXT NOT NULL,
- profile_id INTEGER NOT NULL,
- ssh_cert TEXT NOT NULL,
- private_key TEXT NOT NULL,
- created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
- expires_at TEXT NOT NULL,
- last_used TEXT,
- UNIQUE(user_id, profile_id),
- FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
- FOREIGN KEY (profile_id) REFERENCES vault_profiles (id) ON DELETE CASCADE
- );
- `);
- } catch (createError) {
- databaseLogger.warn("Failed to create vault_tokens table", {
- operation: "schema_migration",
- error: createError,
- });
- }
- }
-
try {
const existingRoles = sqlite.prepare("SELECT name, is_system FROM roles").all() as Array<{ name: string; is_system: number }>;
@@ -1878,7 +1812,6 @@ const migrateSchema = () => {
addColumnIfNotExists("ssh_folders", "sync_id", "TEXT");
addColumnIfNotExists("snippets", "sync_id", "TEXT");
addColumnIfNotExists("snippet_folders", "sync_id", "TEXT");
- addColumnIfNotExists("vault_profiles", "sync_id", "TEXT");
// Plugin runtime: lastError reports why a plugin is blocked or failed, and
// grants record whether a capability came from an admin or from bundling.
@@ -1896,7 +1829,6 @@ const migrateSchema = () => {
"ssh_folders",
"snippets",
"snippet_folders",
- "vault_profiles",
];
for (const table of syncIdTables) {
diff --git a/src/backend/database/db/schema.mysql.ts b/src/backend/database/db/schema.mysql.ts
index 605e2c68f..a44f321c2 100644
--- a/src/backend/database/db/schema.mysql.ts
+++ b/src/backend/database/db/schema.mysql.ts
@@ -194,13 +194,6 @@ export const hosts = mysqlTable(
credentialId: int("credential_id").references(() => sshCredentials.id, { onDelete: "set null" }),
overrideCredentialUsername: boolean("override_credential_username"),
- // When authType is "vault", the host authenticates via a Vault SSH signer
- // profile (shared settings, no secrets). The signing certificate is obtained
- // per-user at connect time via an interactive Vault OIDC flow.
- vaultProfileId: int("vault_profile_id").references(
- () => vaultProfiles.id,
- { onDelete: "set null" },
- ),
enableTerminal: boolean("enable_terminal")
.notNull()
.default(true),
@@ -660,71 +653,6 @@ export const auditLogs = mysqlTable(
],
);
-// Vault SSH signer profiles. These hold ONLY non-secret connection settings and
-// are intended to be shared across users (shared === true makes a profile
-// visible to every user on the server). Each user authenticates to Vault via an
-// interactive OIDC flow at connect time; no tokens or keys are stored here.
-export const vaultProfiles = mysqlTable("vault_profiles", {
- id: int("id").autoincrement().primaryKey(),
- userId: varchar("user_id", { length: 255 })
- .notNull()
- .references(() => users.id, { onDelete: "cascade" }),
- name: varchar("name", { length: 255 }).notNull(),
- description: text("description"),
- folder: varchar("folder", { length: 255 }),
- tags: text("tags"),
- // Vault server connection (non-secret)
- vaultAddr: text("vault_addr").notNull(),
- vaultNamespace: text("vault_namespace"),
- // OIDC auth method mount + role used to obtain a Vault token interactively
- oidcMount: text("oidc_mount"),
- oidcRole: text("oidc_role"),
- // SSH secrets engine mount + signer role used to sign the ephemeral key
- sshMount: text("ssh_mount"),
- sshRole: text("ssh_role").notNull(),
- validPrincipals: text("valid_principals"),
- // Ephemeral keypair algorithm to generate per connection
- keyType: text("key_type"),
- // When true the profile is visible/usable by all users on the server
- shared: boolean("shared").notNull().default(false),
- syncId: varchar("sync_id", { length: 255 }).unique(),
- createdAt: text("created_at")
- .notNull()
- .default(sql`(CURRENT_TIMESTAMP)`),
- updatedAt: text("updated_at")
- .notNull()
- .default(sql`(CURRENT_TIMESTAMP)`),
-});
-
-// Per-user cache of the ephemeral SSH private key + Vault-signed certificate.
-// Transient: rows live only until the certificate expires. Secret fields are
-// encrypted under the user's data-encryption key (see field-crypto.ts).
-export const vaultTokens = mysqlTable(
- "vault_tokens",
- {
- id: int("id").autoincrement().primaryKey(),
- userId: varchar("user_id", { length: 255 })
- .notNull()
- .references(() => users.id, { onDelete: "cascade" }),
- profileId: int("profile_id")
- .notNull()
- .references(() => vaultProfiles.id, { onDelete: "cascade" }),
-
- sshCert: text("ssh_cert").notNull(),
- privateKey: text("private_key").notNull(),
-
- createdAt: text("created_at")
- .notNull()
- .default(sql`(CURRENT_TIMESTAMP)`),
- expiresAt: varchar("expires_at", { length: 255 }).notNull(),
- lastUsed: text("last_used"),
- },
- // Declared inline in the production DDL as UNIQUE(...), but never here,
- // so the generated Postgres and MySQL schemas allowed duplicates the
- // SQLite deployment forbids — and the upsert had nothing to conflict on.
- (table) => [uniqueIndex("idx_vault_tokens_user_profile").on(table.userId, table.profileId)],
-);
-
export const apiKeys = mysqlTable(
"api_keys",
{
diff --git a/src/backend/database/db/schema.pg.ts b/src/backend/database/db/schema.pg.ts
index f37ad417c..f943c1b0c 100644
--- a/src/backend/database/db/schema.pg.ts
+++ b/src/backend/database/db/schema.pg.ts
@@ -195,13 +195,6 @@ export const hosts = pgTable(
credentialId: integer("credential_id").references(() => sshCredentials.id, { onDelete: "set null" }),
overrideCredentialUsername: boolean("override_credential_username"),
- // When authType is "vault", the host authenticates via a Vault SSH signer
- // profile (shared settings, no secrets). The signing certificate is obtained
- // per-user at connect time via an interactive Vault OIDC flow.
- vaultProfileId: integer("vault_profile_id").references(
- () => vaultProfiles.id,
- { onDelete: "set null" },
- ),
enableTerminal: boolean("enable_terminal")
.notNull()
.default(true),
@@ -661,71 +654,6 @@ export const auditLogs = pgTable(
],
);
-// Vault SSH signer profiles. These hold ONLY non-secret connection settings and
-// are intended to be shared across users (shared === true makes a profile
-// visible to every user on the server). Each user authenticates to Vault via an
-// interactive OIDC flow at connect time; no tokens or keys are stored here.
-export const vaultProfiles = pgTable("vault_profiles", {
- id: serial("id").primaryKey(),
- userId: varchar("user_id", { length: 255 })
- .notNull()
- .references(() => users.id, { onDelete: "cascade" }),
- name: varchar("name", { length: 255 }).notNull(),
- description: text("description"),
- folder: varchar("folder", { length: 255 }),
- tags: text("tags"),
- // Vault server connection (non-secret)
- vaultAddr: text("vault_addr").notNull(),
- vaultNamespace: text("vault_namespace"),
- // OIDC auth method mount + role used to obtain a Vault token interactively
- oidcMount: text("oidc_mount"),
- oidcRole: text("oidc_role"),
- // SSH secrets engine mount + signer role used to sign the ephemeral key
- sshMount: text("ssh_mount"),
- sshRole: text("ssh_role").notNull(),
- validPrincipals: text("valid_principals"),
- // Ephemeral keypair algorithm to generate per connection
- keyType: text("key_type"),
- // When true the profile is visible/usable by all users on the server
- shared: boolean("shared").notNull().default(false),
- syncId: varchar("sync_id", { length: 255 }).unique(),
- createdAt: text("created_at")
- .notNull()
- .default(sql`CURRENT_TIMESTAMP`),
- updatedAt: text("updated_at")
- .notNull()
- .default(sql`CURRENT_TIMESTAMP`),
-});
-
-// Per-user cache of the ephemeral SSH private key + Vault-signed certificate.
-// Transient: rows live only until the certificate expires. Secret fields are
-// encrypted under the user's data-encryption key (see field-crypto.ts).
-export const vaultTokens = pgTable(
- "vault_tokens",
- {
- id: serial("id").primaryKey(),
- userId: varchar("user_id", { length: 255 })
- .notNull()
- .references(() => users.id, { onDelete: "cascade" }),
- profileId: integer("profile_id")
- .notNull()
- .references(() => vaultProfiles.id, { onDelete: "cascade" }),
-
- sshCert: text("ssh_cert").notNull(),
- privateKey: text("private_key").notNull(),
-
- createdAt: text("created_at")
- .notNull()
- .default(sql`CURRENT_TIMESTAMP`),
- expiresAt: varchar("expires_at", { length: 255 }).notNull(),
- lastUsed: text("last_used"),
- },
- // Declared inline in the production DDL as UNIQUE(...), but never here,
- // so the generated Postgres and MySQL schemas allowed duplicates the
- // SQLite deployment forbids — and the upsert had nothing to conflict on.
- (table) => [uniqueIndex("idx_vault_tokens_user_profile").on(table.userId, table.profileId)],
-);
-
export const apiKeys = pgTable(
"api_keys",
{
diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts
index dc283451d..518321254 100644
--- a/src/backend/database/db/schema.ts
+++ b/src/backend/database/db/schema.ts
@@ -187,13 +187,6 @@ export const hosts = sqliteTable(
overrideCredentialUsername: integer("override_credential_username", {
mode: "boolean",
}),
- // When authType is "vault", the host authenticates via a Vault SSH signer
- // profile (shared settings, no secrets). The signing certificate is obtained
- // per-user at connect time via an interactive Vault OIDC flow.
- vaultProfileId: integer("vault_profile_id").references(
- () => vaultProfiles.id,
- { onDelete: "set null" },
- ),
enableTerminal: integer("enable_terminal", { mode: "boolean" })
.notNull()
.default(true),
@@ -653,71 +646,6 @@ export const auditLogs = sqliteTable(
],
);
-// Vault SSH signer profiles. These hold ONLY non-secret connection settings and
-// are intended to be shared across users (shared === true makes a profile
-// visible to every user on the server). Each user authenticates to Vault via an
-// interactive OIDC flow at connect time; no tokens or keys are stored here.
-export const vaultProfiles = sqliteTable("vault_profiles", {
- id: integer("id").primaryKey({ autoIncrement: true }),
- userId: text("user_id")
- .notNull()
- .references(() => users.id, { onDelete: "cascade" }),
- name: text("name").notNull(),
- description: text("description"),
- folder: text("folder"),
- tags: text("tags"),
- // Vault server connection (non-secret)
- vaultAddr: text("vault_addr").notNull(),
- vaultNamespace: text("vault_namespace"),
- // OIDC auth method mount + role used to obtain a Vault token interactively
- oidcMount: text("oidc_mount"),
- oidcRole: text("oidc_role"),
- // SSH secrets engine mount + signer role used to sign the ephemeral key
- sshMount: text("ssh_mount"),
- sshRole: text("ssh_role").notNull(),
- validPrincipals: text("valid_principals"),
- // Ephemeral keypair algorithm to generate per connection
- keyType: text("key_type"),
- // When true the profile is visible/usable by all users on the server
- shared: integer("shared", { mode: "boolean" }).notNull().default(false),
- syncId: text("sync_id").unique(),
- createdAt: text("created_at")
- .notNull()
- .default(sql`CURRENT_TIMESTAMP`),
- updatedAt: text("updated_at")
- .notNull()
- .default(sql`CURRENT_TIMESTAMP`),
-});
-
-// Per-user cache of the ephemeral SSH private key + Vault-signed certificate.
-// Transient: rows live only until the certificate expires. Secret fields are
-// encrypted under the user's data-encryption key (see field-crypto.ts).
-export const vaultTokens = sqliteTable(
- "vault_tokens",
- {
- id: integer("id").primaryKey({ autoIncrement: true }),
- userId: text("user_id")
- .notNull()
- .references(() => users.id, { onDelete: "cascade" }),
- profileId: integer("profile_id")
- .notNull()
- .references(() => vaultProfiles.id, { onDelete: "cascade" }),
-
- sshCert: text("ssh_cert", { length: 8192 }).notNull(),
- privateKey: text("private_key", { length: 8192 }).notNull(),
-
- createdAt: text("created_at")
- .notNull()
- .default(sql`CURRENT_TIMESTAMP`),
- expiresAt: text("expires_at").notNull(),
- lastUsed: text("last_used"),
- },
- // Declared inline in the production DDL as UNIQUE(...), but never here,
- // so the generated Postgres and MySQL schemas allowed duplicates the
- // SQLite deployment forbids — and the upsert had nothing to conflict on.
- (table) => [uniqueIndex("idx_vault_tokens_user_profile").on(table.userId, table.profileId)],
-);
-
export const apiKeys = sqliteTable(
"api_keys",
{
diff --git a/src/backend/database/repositories/factory.ts b/src/backend/database/repositories/factory.ts
index ae3132ed5..ab69eff72 100644
--- a/src/backend/database/repositories/factory.ts
+++ b/src/backend/database/repositories/factory.ts
@@ -42,8 +42,6 @@ import { TrustedDeviceRepository } from "./trusted-device-repository.js";
import { UserDataExportRepository } from "./user-data-export-repository.js";
import { UserPreferenceRepository } from "./user-preference-repository.js";
import { UserRepository } from "./user-repository.js";
-import { VaultProfileRepository } from "./vault-profile-repository.js";
-import { VaultTokenRepository } from "./vault-token-repository.js";
/**
* The context every repository runs against.
@@ -416,20 +414,6 @@ export function createCurrentSharedCredentialSecretsRepository(): SharedCredenti
);
}
-export function createCurrentVaultProfileRepository(): VaultProfileRepository {
- return new VaultProfileRepository(
- createCurrentRepositoryContext(),
- createCurrentRepositoryWriteHook("vault_profile_repository_write"),
- );
-}
-
-export function createCurrentVaultTokenRepository(): VaultTokenRepository {
- return new VaultTokenRepository(
- createCurrentRepositoryContext(),
- createCurrentRepositoryWriteHook("vault_token_repository_write"),
- );
-}
-
/**
* Loads the settings cache. Must run during startup on engines without a
* synchronous read, before anything calls getCurrentSettingValue.
diff --git a/src/backend/database/repositories/host-resolution-repository.ts b/src/backend/database/repositories/host-resolution-repository.ts
index a93afafa4..4e120a4ab 100644
--- a/src/backend/database/repositories/host-resolution-repository.ts
+++ b/src/backend/database/repositories/host-resolution-repository.ts
@@ -18,7 +18,6 @@ export interface HostUpdateStateRecord {
rdpCredentialId: number | null;
vncCredentialId: number | null;
telnetCredentialId: number | null;
- vaultProfileId: number | null;
authType: string;
parentHostId: number | null;
folder: string | null;
@@ -146,7 +145,6 @@ export class HostResolutionRepository {
rdpCredentialId: hosts.rdpCredentialId,
vncCredentialId: hosts.vncCredentialId,
telnetCredentialId: hosts.telnetCredentialId,
- vaultProfileId: hosts.vaultProfileId,
authType: hosts.authType,
parentHostId: hosts.parentHostId,
folder: hosts.folder,
diff --git a/src/backend/database/repositories/vault-profile-repository.ts b/src/backend/database/repositories/vault-profile-repository.ts
deleted file mode 100644
index bcf8cec56..000000000
--- a/src/backend/database/repositories/vault-profile-repository.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import { desc, eq, or } from "drizzle-orm";
-import { randomUUID } from "crypto";
-import { vaultProfiles } from "../db/schema.js";
-import type { DatabaseContext } from "./database-context.js";
-import { rowsAffected } from "./mutation-result.js";
-import {
- deleteReturning,
- insertReturning,
- updateReturning,
-} from "./returning.js";
-
-export type VaultProfileRecord = typeof vaultProfiles.$inferSelect;
-
-export interface VaultProfileCreateInput {
- userId: string;
- name: string;
- description?: string | null;
- folder?: string | null;
- tags?: string | null;
- vaultAddr: string;
- vaultNamespace?: string | null;
- oidcMount?: string | null;
- oidcRole?: string | null;
- sshMount?: string | null;
- sshRole: string;
- validPrincipals?: string | null;
- keyType?: string | null;
- shared?: boolean;
-}
-
-export type VaultProfileUpdateInput = Partial<
- Omit
-> & {
- updatedAt?: string;
-};
-
-export class VaultProfileRepository {
- constructor(
- private readonly context: DatabaseContext,
- private readonly onWrite?: () => void | Promise,
- ) {}
-
- async listVisibleToUser(userId: string): Promise {
- return this.context.drizzle
- .select()
- .from(vaultProfiles)
- .where(
- or(eq(vaultProfiles.userId, userId), eq(vaultProfiles.shared, true)),
- )
- .orderBy(desc(vaultProfiles.updatedAt));
- }
-
- async create(input: VaultProfileCreateInput): Promise {
- const [created] = await insertReturning(this.context, vaultProfiles, {
- syncId: randomUUID(),
- userId: input.userId,
- name: input.name,
- description: input.description,
- folder: input.folder,
- tags: input.tags,
- vaultAddr: input.vaultAddr,
- vaultNamespace: input.vaultNamespace,
- oidcMount: input.oidcMount,
- oidcRole: input.oidcRole,
- sshMount: input.sshMount,
- sshRole: input.sshRole,
- validPrincipals: input.validPrincipals,
- keyType: input.keyType,
- shared: input.shared ?? false,
- });
-
- await this.afterWrite();
- return created;
- }
-
- async findById(id: number): Promise {
- const rows = await this.context.drizzle
- .select()
- .from(vaultProfiles)
- .where(eq(vaultProfiles.id, id))
- .limit(1);
-
- return rows[0] ?? null;
- }
-
- async updateById(
- id: number,
- input: VaultProfileUpdateInput,
- ): Promise {
- const [updated] = await updateReturning(
- this.context,
- vaultProfiles,
- {
- ...input,
- updatedAt: input.updatedAt ?? new Date().toISOString(),
- },
- eq(vaultProfiles.id, id),
- );
-
- if (updated) {
- await this.afterWrite();
- }
-
- return updated ?? null;
- }
-
- async deleteById(id: number): Promise<{ syncId: string | null } | null> {
- const rows = await deleteReturning(
- this.context,
- vaultProfiles,
- eq(vaultProfiles.id, id),
- );
-
- if (rows.length === 0) return null;
- await this.afterWrite();
- return { syncId: rows[0].syncId };
- }
-
- async deleteByUserId(userId: string): Promise {
- const result = await this.context.drizzle
- .delete(vaultProfiles)
- .where(eq(vaultProfiles.userId, userId));
-
- if (rowsAffected(result) > 0) {
- await this.afterWrite();
- }
-
- return rowsAffected(result);
- }
-
- private async afterWrite(): Promise {
- await this.onWrite?.();
- }
-}
diff --git a/src/backend/database/repositories/vault-token-repository.ts b/src/backend/database/repositories/vault-token-repository.ts
deleted file mode 100644
index a14ea70b7..000000000
--- a/src/backend/database/repositories/vault-token-repository.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import { and, eq } from "drizzle-orm";
-import { vaultTokens } from "../db/schema.js";
-import type { DatabaseContext } from "./database-context.js";
-import { rowsAffected } from "./mutation-result.js";
-import { upsert } from "./returning.js";
-
-export type VaultTokenRecord = typeof vaultTokens.$inferSelect;
-
-export interface VaultTokenUpsertInput {
- userId: string;
- profileId: number;
- sshCert: string;
- privateKey: string;
- expiresAt: string;
- createdAt?: string;
-}
-
-export class VaultTokenRepository {
- constructor(
- private readonly context: DatabaseContext,
- private readonly onWrite?: () => void | Promise,
- ) {}
-
- async upsert(input: VaultTokenUpsertInput): Promise {
- const createdAt = input.createdAt ?? new Date().toISOString();
-
- await upsert(
- this.context,
- vaultTokens,
- {
- userId: input.userId,
- profileId: input.profileId,
- sshCert: input.sshCert,
- privateKey: input.privateKey,
- expiresAt: input.expiresAt,
- },
- {
- target: [vaultTokens.userId, vaultTokens.profileId],
- set: {
- sshCert: input.sshCert,
- privateKey: input.privateKey,
- expiresAt: input.expiresAt,
- createdAt,
- },
- },
- );
-
- await this.afterWrite();
- }
-
- async findByUserAndProfile(
- userId: string,
- profileId: number,
- ): Promise {
- const rows = await this.context.drizzle
- .select()
- .from(vaultTokens)
- .where(
- and(
- eq(vaultTokens.userId, userId),
- eq(vaultTokens.profileId, profileId),
- ),
- )
- .limit(1);
-
- return rows[0] ?? null;
- }
-
- async updateLastUsed(
- userId: string,
- profileId: number,
- lastUsed = new Date().toISOString(),
- ): Promise {
- const result = await this.context.drizzle
- .update(vaultTokens)
- .set({ lastUsed })
- .where(
- and(
- eq(vaultTokens.userId, userId),
- eq(vaultTokens.profileId, profileId),
- ),
- );
-
- if (rowsAffected(result) > 0) {
- await this.afterWrite();
- }
-
- return rowsAffected(result) > 0;
- }
-
- async deleteByUserAndProfile(
- userId: string,
- profileId: number,
- ): Promise {
- const result = await this.context.drizzle
- .delete(vaultTokens)
- .where(
- and(
- eq(vaultTokens.userId, userId),
- eq(vaultTokens.profileId, profileId),
- ),
- );
-
- if (rowsAffected(result) > 0) {
- await this.afterWrite();
- }
-
- return rowsAffected(result) > 0;
- }
-
- async deleteByUserId(userId: string): Promise {
- const result = await this.context.drizzle
- .delete(vaultTokens)
- .where(eq(vaultTokens.userId, userId));
-
- if (rowsAffected(result) > 0) {
- await this.afterWrite();
- }
-
- return rowsAffected(result);
- }
-
- private async afterWrite(): Promise {
- await this.onWrite?.();
- }
-}
diff --git a/src/backend/database/routes/delete-user-data.ts b/src/backend/database/routes/delete-user-data.ts
index 5327c2d27..298cf48c4 100644
--- a/src/backend/database/routes/delete-user-data.ts
+++ b/src/backend/database/routes/delete-user-data.ts
@@ -23,10 +23,8 @@ import {
createCurrentTrustedDeviceRepository,
createCurrentUserPreferenceRepository,
createCurrentUserRepository,
- createCurrentVaultProfileRepository,
createCurrentSharedCredentialSecretsRepository,
createCurrentCredentialAccessRepository,
- createCurrentVaultTokenRepository,
} from "../repositories/factory.js";
export async function deleteUserAndRelatedData(
@@ -96,11 +94,9 @@ export async function deleteUserAndRelatedData(
// secret_sources cascade on the user's refUser() foreign key, as the
// homepage and secret-sources plugins' adopted tables. The secret
// source's token in ctx.secrets is cleaned up generically below, with
- // every other plugin_settings row for this user. The opkssh plugin's
- // cached certificates cascade the same way.
+ // every other plugin_settings row for this user. The opkssh and vault
+ // plugins' tables cascade the same way.
- await createCurrentVaultTokenRepository().deleteByUserId(userId);
- await createCurrentVaultProfileRepository().deleteByUserId(userId);
await createCurrentTermixIdentityCaRepository().deleteByUserId(userId);
await createCurrentTermixIdentityRepository().deleteByUserId(userId);
await createCurrentOpenTabRepository().deleteByUserId(userId);
diff --git a/src/backend/database/routes/host-compat-routes.ts b/src/backend/database/routes/host-compat-routes.ts
index dfc808f04..e660c20fd 100644
--- a/src/backend/database/routes/host-compat-routes.ts
+++ b/src/backend/database/routes/host-compat-routes.ts
@@ -1,6 +1,7 @@
/**
- * The 2.8 URLs under /host that something outside Termix still depends on:
- * the OPKSSH and Step CA redirect URIs registered with identity providers.
+ * The 2.8 auth callback URLs something outside Termix still depends on: the
+ * OPKSSH and Step CA redirect URIs registered with identity providers, and
+ * the Vault one listed in Vault OIDC roles.
* Each forwards to the plugin route that replaced it, so with the plugin off
* it signs nobody in.
*/
@@ -50,3 +51,21 @@ export function registerHostAuthCompatRoutes(router: Router): void {
res.redirect(307, localUrl(req, "/plugin-api/step-ca/callback"));
});
}
+
+export function registerVaultCompatRoutes(router: Router): void {
+ /**
+ * @openapi
+ * /vault/oidc/callback:
+ * get:
+ * summary: Vault OIDC callback (2.8 URL)
+ * description: The redirect URI Vault OIDC roles allowed before 2.9. Redirects to /plugin-api/vault/oidc/callback with the same query, so existing Vault roles keep working.
+ * tags:
+ * - Vault
+ * responses:
+ * 307:
+ * description: Redirect to the vault plugin's callback.
+ */
+ router.get("/oidc/callback", (req, res) => {
+ res.redirect(307, localUrl(req, "/plugin-api/vault/oidc/callback"));
+ });
+}
diff --git a/src/backend/database/routes/host-normalizers.ts b/src/backend/database/routes/host-normalizers.ts
index b5168a9a8..2a064de61 100644
--- a/src/backend/database/routes/host-normalizers.ts
+++ b/src/backend/database/routes/host-normalizers.ts
@@ -42,7 +42,6 @@ export const OWNER_PRIVATE_AUTH_FIELDS = {
"authType",
"authMethod",
"credentialId",
- "vaultProfileId",
"overrideCredentialUsername",
"shareSshAuth",
"password",
diff --git a/src/backend/database/routes/host.ts b/src/backend/database/routes/host.ts
index a8c32ab4f..df10e3eb7 100644
--- a/src/backend/database/routes/host.ts
+++ b/src/backend/database/routes/host.ts
@@ -183,7 +183,6 @@ router.post(
authType,
shareSshAuth,
credentialId,
- vaultProfileId,
key,
keyPassword,
keyType,
@@ -305,8 +304,6 @@ router.post(
authType: effectiveAuthType,
shareSshAuth: shareSshAuth === true ? 1 : 0,
credentialId: credentialId || null,
- vaultProfileId:
- effectiveAuthType === "vault" ? vaultProfileId || null : null,
overrideCredentialUsername: overrideCredentialUsername ? 1 : 0,
pin: pin ? 1 : 0,
enableTerminal: enableTerminal ? 1 : 0,
@@ -858,7 +855,6 @@ router.put(
authType,
shareSshAuth,
credentialId,
- vaultProfileId,
key,
keyPassword,
keyType,
@@ -981,8 +977,6 @@ router.put(
authType: effectiveAuthType,
shareSshAuth: shareSshAuth === true ? 1 : 0,
credentialId: credentialId || null,
- vaultProfileId:
- effectiveAuthType === "vault" ? vaultProfileId || null : null,
overrideCredentialUsername: overrideCredentialUsername ? 1 : 0,
pin: pin ? 1 : 0,
enableTerminal: enableTerminal ? 1 : 0,
diff --git a/src/backend/database/routes/sync-entities.ts b/src/backend/database/routes/sync-entities.ts
index f452f6fd8..5a5f9d947 100644
--- a/src/backend/database/routes/sync-entities.ts
+++ b/src/backend/database/routes/sync-entities.ts
@@ -15,7 +15,6 @@ import {
hosts,
sshCredentials,
sshFolders,
- vaultProfiles,
userPreferences,
} from "../db/schema.js";
import {
@@ -47,11 +46,6 @@ const HOST_REFERENCES = [
syncField: "telnetCredentialSyncId",
entityType: "sshCredentials",
},
- {
- field: "vaultProfileId",
- syncField: "vaultProfileSyncId",
- entityType: "vaultProfiles",
- },
{
field: "parentHostId",
syncField: "parentHostSyncId",
@@ -78,11 +72,8 @@ export function registerCoreSyncEntities(): void {
order: 10,
});
- registerEntity(CORE_OWNER, {
- type: "vaultProfiles",
- table: vaultProfiles,
- order: 20,
- });
+ // vaultProfiles (order 20) moved to the vault plugin. Hosts no longer
+ // reference it: a host's profile is the plugin's profileId host setting.
registerEntity(CORE_OWNER, {
type: "sshFolders",
diff --git a/src/backend/database/routes/sync-references.ts b/src/backend/database/routes/sync-references.ts
index 843298c1e..b09f51a38 100644
--- a/src/backend/database/routes/sync-references.ts
+++ b/src/backend/database/routes/sync-references.ts
@@ -7,8 +7,7 @@ import { getEntity } from "../../plugins/sync-registry.js";
* A plain string rather than a union: the set is whatever is registered, and a
* plugin can both register an entity and reference one. sync.ts resolves the
* name through the registry, so an unknown one is an error there rather than a
- * silent fallthrough - which is what the old three-way if/else did, routing
- * anything that was not hosts or sshCredentials to vaultProfiles.
+ * silent fallthrough.
*/
export type SyncReferenceEntity = string;
diff --git a/src/backend/database/routes/vault.ts b/src/backend/database/routes/vault.ts
deleted file mode 100644
index 55e915c51..000000000
--- a/src/backend/database/routes/vault.ts
+++ /dev/null
@@ -1,448 +0,0 @@
-import express, { type Request, type Response } from "express";
-import {
- createCurrentVaultProfileRepository,
- createCurrentUserRepository,
- createCurrentSyncTombstoneRepository,
-} from "../repositories/factory.js";
-import type { VaultProfileUpdateInput } from "../repositories/vault-profile-repository.js";
-import type { AuthenticatedRequest } from "../../../types/index.js";
-import { authLogger } from "../../utils/logger.js";
-import { AuthManager } from "../../utils/auth-manager.js";
-import { completeVaultAuth } from "../../hosts/vault-oidc-auth.js";
-
-const router = express.Router();
-
-const authManager = AuthManager.getInstance();
-const authenticateJWT = authManager.createAuthMiddleware();
-
-function isNonEmptyString(val: unknown): val is string {
- return typeof val === "string" && val.trim().length > 0;
-}
-
-async function userIsAdmin(userId: string): Promise {
- try {
- const user = await createCurrentUserRepository().findById(userId);
- return !!user?.isAdmin;
- } catch {
- return false;
- }
-}
-
-function formatProfile(
- row: Record,
- currentUserId: string,
-): Record {
- return {
- id: row.id,
- name: row.name,
- description: row.description,
- folder: row.folder,
- tags:
- typeof row.tags === "string"
- ? row.tags
- ? (row.tags as string).split(",").filter(Boolean)
- : []
- : [],
- vaultAddr: row.vaultAddr,
- vaultNamespace: row.vaultNamespace,
- oidcMount: row.oidcMount,
- oidcRole: row.oidcRole,
- sshMount: row.sshMount,
- sshRole: row.sshRole,
- validPrincipals: row.validPrincipals,
- keyType: row.keyType,
- shared: !!row.shared,
- owned: row.userId === currentUserId,
- createdAt: row.createdAt,
- updatedAt: row.updatedAt,
- };
-}
-
-/**
- * @openapi
- * /vault/oidc/callback:
- * get:
- * summary: Vault OIDC callback
- * description: Unauthenticated endpoint the IdP redirects to after login. Correlates the authorization code to a pending session via the Vault-issued state parameter.
- * tags:
- * - Vault
- * parameters:
- * - in: query
- * name: state
- * required: true
- * schema:
- * type: string
- * - in: query
- * name: code
- * required: true
- * schema:
- * type: string
- * - in: query
- * name: error
- * schema:
- * type: string
- * responses:
- * 200:
- * description: HTML page confirming sign-in success or failure.
- * 400:
- * description: Missing parameters or authentication failure.
- */
-router.get("/oidc/callback", async (req: Request, res: Response) => {
- const state = String(req.query.state || "");
- const code = String(req.query.code || "");
- const oidcError = req.query.error ? String(req.query.error) : "";
-
- const esc = (value: string): string =>
- value
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
-
- const html = (title: string, message: string) =>
- `${esc(title)}
-
-${esc(title)}
${esc(message)}
-
`;
-
- if (oidcError) {
- return res
- .status(400)
- .send(html("Vault sign-in failed", `Vault returned: ${oidcError}`));
- }
- if (!state || !code) {
- return res
- .status(400)
- .send(html("Vault sign-in failed", "Missing state or code."));
- }
-
- const result = await completeVaultAuth(state, code);
- if (result.ok) {
- return res.send(
- html(
- "Vault sign-in complete",
- "You can close this window and return to Termix.",
- ),
- );
- }
- return res
- .status(400)
- .send(
- html("Vault sign-in failed", result.error || "Authentication failed."),
- );
-});
-
-/**
- * @openapi
- * /vault/profiles:
- * get:
- * summary: List Vault profiles
- * description: Returns all Vault signer profiles owned by the authenticated user or marked as shared.
- * tags:
- * - Vault
- * responses:
- * 200:
- * description: Array of Vault profile objects.
- * 500:
- * description: Failed to list vault profiles.
- */
-router.get(
- "/profiles",
- authenticateJWT,
- async (req: Request, res: Response) => {
- const userId = (req as AuthenticatedRequest).userId;
- try {
- const rows =
- await createCurrentVaultProfileRepository().listVisibleToUser(userId);
- res.json(
- rows.map((r) => formatProfile(r as Record, userId)),
- );
- } catch (err) {
- authLogger.error("Failed to list vault profiles", err);
- res.status(500).json({ error: "Failed to list vault profiles" });
- }
- },
-);
-
-/**
- * @openapi
- * /vault/profiles:
- * post:
- * summary: Create a Vault profile
- * description: Creates a new Vault signer profile owned by the authenticated user. The shared flag requires admin privileges.
- * tags:
- * - Vault
- * requestBody:
- * required: true
- * content:
- * application/json:
- * schema:
- * type: object
- * required:
- * - name
- * - vaultAddr
- * - sshRole
- * properties:
- * name:
- * type: string
- * vaultAddr:
- * type: string
- * vaultNamespace:
- * type: string
- * oidcMount:
- * type: string
- * oidcRole:
- * type: string
- * sshMount:
- * type: string
- * sshRole:
- * type: string
- * validPrincipals:
- * type: string
- * keyType:
- * type: string
- * shared:
- * type: boolean
- * responses:
- * 201:
- * description: Created Vault profile object.
- * 400:
- * description: Missing required fields.
- * 403:
- * description: Non-admin attempted to create a shared profile.
- * 500:
- * description: Failed to create vault profile.
- */
-router.post(
- "/profiles",
- authenticateJWT,
- async (req: Request, res: Response) => {
- const userId = (req as AuthenticatedRequest).userId;
- const {
- name,
- description,
- folder,
- tags,
- vaultAddr,
- vaultNamespace,
- oidcMount,
- oidcRole,
- sshMount,
- sshRole,
- validPrincipals,
- keyType,
- shared,
- } = req.body;
-
- if (
- !isNonEmptyString(name) ||
- !isNonEmptyString(vaultAddr) ||
- !isNonEmptyString(sshRole)
- ) {
- return res
- .status(400)
- .json({ error: "name, vaultAddr and sshRole are required" });
- }
-
- const wantShared = !!shared;
- if (wantShared && !(await userIsAdmin(userId))) {
- return res.status(403).json({
- error: "Only administrators can create shared Vault profiles",
- });
- }
-
- try {
- const inserted = await createCurrentVaultProfileRepository().create({
- userId,
- name: name.trim(),
- description: description?.trim() || null,
- folder: folder?.trim() || null,
- tags: Array.isArray(tags) ? tags.join(",") : tags || "",
- vaultAddr: vaultAddr.trim(),
- vaultNamespace: vaultNamespace?.trim() || null,
- oidcMount: oidcMount?.trim() || null,
- oidcRole: oidcRole?.trim() || null,
- sshMount: sshMount?.trim() || null,
- sshRole: sshRole.trim(),
- validPrincipals: validPrincipals?.trim() || null,
- keyType: keyType?.trim() || null,
- shared: wantShared,
- });
- res
- .status(201)
- .json(formatProfile(inserted as Record, userId));
- } catch (err) {
- authLogger.error("Failed to create vault profile", err);
- res.status(500).json({ error: "Failed to create vault profile" });
- }
- },
-);
-
-/**
- * @openapi
- * /vault/profiles/{id}:
- * put:
- * summary: Update a Vault profile
- * description: Updates a Vault signer profile. Only the owner may edit; toggling shared to true requires admin privileges.
- * tags:
- * - Vault
- * parameters:
- * - in: path
- * name: id
- * required: true
- * schema:
- * type: integer
- * requestBody:
- * content:
- * application/json:
- * schema:
- * type: object
- * responses:
- * 200:
- * description: Updated Vault profile object.
- * 400:
- * description: Invalid profile id.
- * 403:
- * description: Non-owner attempted to edit, or non-admin attempted to share.
- * 404:
- * description: Profile not found.
- * 500:
- * description: Failed to update vault profile.
- */
-router.put(
- "/profiles/:id",
- authenticateJWT,
- async (req: Request, res: Response) => {
- const userId = (req as AuthenticatedRequest).userId;
- const id = parseInt(String(req.params.id), 10);
- if (!Number.isFinite(id)) {
- return res.status(400).json({ error: "Invalid profile id" });
- }
-
- try {
- const repository = createCurrentVaultProfileRepository();
- const existing = await repository.findById(id);
- if (!existing) {
- return res.status(404).json({ error: "Profile not found" });
- }
- if (existing.userId !== userId) {
- return res
- .status(403)
- .json({ error: "Only the owner can edit this profile" });
- }
-
- const body = req.body;
- const fields: VaultProfileUpdateInput = {
- updatedAt: new Date().toISOString(),
- };
- if (body.name !== undefined) fields.name = body.name?.trim();
- if (body.description !== undefined)
- fields.description = body.description?.trim() || null;
- if (body.folder !== undefined)
- fields.folder = body.folder?.trim() || null;
- if (body.tags !== undefined)
- fields.tags = Array.isArray(body.tags)
- ? body.tags.join(",")
- : body.tags || "";
- if (body.vaultAddr !== undefined && isNonEmptyString(body.vaultAddr))
- fields.vaultAddr = body.vaultAddr.trim();
- if (body.vaultNamespace !== undefined)
- fields.vaultNamespace = body.vaultNamespace?.trim() || null;
- if (body.oidcMount !== undefined)
- fields.oidcMount = body.oidcMount?.trim() || null;
- if (body.oidcRole !== undefined)
- fields.oidcRole = body.oidcRole?.trim() || null;
- if (body.sshMount !== undefined)
- fields.sshMount = body.sshMount?.trim() || null;
- if (body.sshRole !== undefined && isNonEmptyString(body.sshRole))
- fields.sshRole = body.sshRole.trim();
- if (body.validPrincipals !== undefined)
- fields.validPrincipals = body.validPrincipals?.trim() || null;
- if (body.keyType !== undefined)
- fields.keyType = body.keyType?.trim() || null;
- if (body.shared !== undefined) {
- if (!!body.shared && !(await userIsAdmin(userId))) {
- return res.status(403).json({
- error: "Only administrators can share Vault profiles",
- });
- }
- fields.shared = !!body.shared;
- }
-
- const updated = await repository.updateById(id, fields);
- if (!updated) {
- return res.status(404).json({ error: "Profile not found" });
- }
- res.json(formatProfile(updated as Record, userId));
- } catch (err) {
- authLogger.error("Failed to update vault profile", err);
- res.status(500).json({ error: "Failed to update vault profile" });
- }
- },
-);
-
-/**
- * @openapi
- * /vault/profiles/{id}:
- * delete:
- * summary: Delete a Vault profile
- * description: Permanently deletes a Vault signer profile. Only the owner may delete it.
- * tags:
- * - Vault
- * parameters:
- * - in: path
- * name: id
- * required: true
- * schema:
- * type: integer
- * responses:
- * 200:
- * description: Deletion confirmed.
- * 400:
- * description: Invalid profile id.
- * 403:
- * description: Non-owner attempted to delete.
- * 404:
- * description: Profile not found.
- * 500:
- * description: Failed to delete vault profile.
- */
-router.delete(
- "/profiles/:id",
- authenticateJWT,
- async (req: Request, res: Response) => {
- const userId = (req as AuthenticatedRequest).userId;
- const id = parseInt(String(req.params.id), 10);
- if (!Number.isFinite(id)) {
- return res.status(400).json({ error: "Invalid profile id" });
- }
- try {
- const repository = createCurrentVaultProfileRepository();
- const existing = await repository.findById(id);
- if (!existing) {
- return res.status(404).json({ error: "Profile not found" });
- }
- if (existing.userId !== userId) {
- return res
- .status(403)
- .json({ error: "Only the owner can delete this profile" });
- }
- const deleted = await repository.deleteById(id);
- if (deleted?.syncId) {
- await createCurrentSyncTombstoneRepository().record(
- userId,
- "vaultProfiles",
- deleted.syncId,
- );
- }
- res.json({ success: true });
- } catch (err) {
- authLogger.error("Failed to delete vault profile", err);
- res.status(500).json({ error: "Failed to delete vault profile" });
- }
- },
-);
-
-export default router;
diff --git a/src/backend/hosts/connect/auth-provider-registry.ts b/src/backend/hosts/connect/auth-provider-registry.ts
index 168bc5d2e..c720e0797 100644
--- a/src/backend/hosts/connect/auth-provider-registry.ts
+++ b/src/backend/hosts/connect/auth-provider-registry.ts
@@ -2,8 +2,7 @@
* Every SSH auth type the server knows how to connect with.
*
* Core registers password, key, credential, agent and none. The rest come from
- * plugins through ctx.auth.registerSshAuthProvider, or for 2.9.0 from
- * src/backend/auth/legacy-providers.ts. A type with no provider fails the
+ * plugins through ctx.auth.registerSshAuthProvider. A type with no provider fails the
* connect with a message naming the plugin that would provide it.
*/
diff --git a/src/backend/hosts/connect/core-providers.ts b/src/backend/hosts/connect/core-providers.ts
index 42db2cafc..16e3807f6 100644
--- a/src/backend/hosts/connect/core-providers.ts
+++ b/src/backend/hosts/connect/core-providers.ts
@@ -1,4 +1,3 @@
-import { registerLegacySshAuthProviders } from "../../auth/legacy-providers.js";
import { registerBuiltinSshAuthProviders } from "./builtin-providers.js";
let registered = false;
@@ -11,7 +10,6 @@ export function ensureCoreSshAuthProviders(): void {
if (registered) return;
registered = true;
registerBuiltinSshAuthProviders();
- registerLegacySshAuthProviders();
}
/** Test helper, pairs with resetSshAuthRegistryForTests. */
diff --git a/src/backend/hosts/connect/types.ts b/src/backend/hosts/connect/types.ts
index 2601c7b22..50bc6bf47 100644
--- a/src/backend/hosts/connect/types.ts
+++ b/src/backend/hosts/connect/types.ts
@@ -41,7 +41,6 @@ export interface SshConnectHost {
*/
pluginSettings?: Record> | null;
forceKeyboardInteractive?: boolean | null;
- vaultProfile?: { id?: number | null } | null;
terminalConfig?: Record | null;
jumpHosts?: Array<{ hostId: number }> | null;
useSocks5?: boolean | null;
diff --git a/src/backend/hosts/file-manager/index.ts b/src/backend/hosts/file-manager/index.ts
index d9d03998a..45dacdc7d 100644
--- a/src/backend/hosts/file-manager/index.ts
+++ b/src/backend/hosts/file-manager/index.ts
@@ -600,7 +600,6 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
let resolvedSocks5Username = socks5Username;
let resolvedSocks5Password = socks5Password;
let resolvedSocks5ProxyChain = socks5ProxyChain;
- let resolvedVaultProfileId: number | null = null;
if (hostId && userId && !password && !sshKey) {
try {
const { resolveHostById, resolveHostBySyncId } =
@@ -625,9 +624,6 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
resolvedTerminalConfig = resolvedHost.terminalConfig as unknown as
Record | undefined;
resolvedScpLegacy = resolvedHost.scpLegacy ?? false;
- resolvedVaultProfileId =
- (resolvedHost.vaultProfile as { id?: number } | undefined)?.id ??
- null;
if (resolvedHost.useSocks5) {
resolvedUseSocks5 = resolvedHost.useSocks5;
resolvedSocks5Host = resolvedHost.socks5Host;
@@ -695,9 +691,6 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
resolvedTerminalConfig = resolvedHost.terminalConfig as unknown as
Record | undefined;
resolvedScpLegacy = resolvedHost.scpLegacy ?? false;
- resolvedVaultProfileId =
- (resolvedHost.vaultProfile as { id?: number } | undefined)?.id ??
- null;
if (resolvedHost.useSocks5) {
resolvedUseSocks5 = resolvedHost.useSocks5;
resolvedSocks5Host = resolvedHost.socks5Host;
@@ -787,9 +780,6 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
keyPassword: resolvedCredentials.keyPassword,
certPublicKey: resolvedCredentials.certPublicKey,
terminalConfig: resolvedTerminalConfig ?? null,
- vaultProfile: resolvedVaultProfileId
- ? { id: resolvedVaultProfileId }
- : null,
jumpHosts: resolvedJumpHosts,
useSocks5: resolvedUseSocks5,
socks5Host: resolvedSocks5Host,
diff --git a/src/backend/hosts/host-resolver.ts b/src/backend/hosts/host-resolver.ts
index b927eb210..92eff06af 100644
--- a/src/backend/hosts/host-resolver.ts
+++ b/src/backend/hosts/host-resolver.ts
@@ -3,7 +3,6 @@ import { findUsableCredential } from "./usable-credential.js";
import { resolveExternalSecretRefs } from "./external-secrets.js";
import {
createCurrentHostResolutionRepository,
- createCurrentVaultProfileRepository,
createCurrentUserRepository,
} from "../database/repositories/factory.js";
import type { HostResolutionHostRecord } from "../database/repositories/host-resolution-repository.js";
@@ -232,26 +231,6 @@ export async function resolveHostById(
sharedAuthResolution === "recipient-override" ? userId : ownerId,
);
- // Resolve a Vault SSH signer profile (shared settings, no secrets). The
- // certificate itself is obtained per-user at connect time via Vault OIDC.
- if (host.vaultProfileId && sharedAuthResolution !== "recipient-override") {
- try {
- const profile = await createCurrentVaultProfileRepository().findById(
- host.vaultProfileId as number,
- );
- if (profile) {
- (host as Record).vaultProfile = profile;
- host.authType = "vault";
- }
- } catch (e) {
- sshLogger.warn("Failed to resolve vault profile for host", {
- operation: "host_resolver_vault_profile",
- hostId,
- error: getErrorMessage(e, "Unknown"),
- });
- }
- }
-
// Keyboard-interactive handlers run synchronously mid-handshake and can
// only read their host settings from here.
try {
diff --git a/src/backend/hosts/vault-oidc-auth.ts b/src/backend/hosts/vault-oidc-auth.ts
deleted file mode 100644
index ce10c385b..000000000
--- a/src/backend/hosts/vault-oidc-auth.ts
+++ /dev/null
@@ -1,214 +0,0 @@
-// Orchestrates the interactive Vault OIDC login that yields a signed,
-// short-lived SSH certificate. Mirrors the OPKSSH session manager but is
-// driven entirely over Vault's HTTP API (no external binary).
-//
-// Lifecycle:
-// - startVaultAuth(): generate ephemeral keypair, ask Vault for an OIDC
-// auth_url, stash a pending session keyed by the Vault-issued `state`,
-// and send the auth_url to the browser over the terminal WebSocket.
-// - The browser completes OIDC; the IdP redirects to VAULT_OIDC_CALLBACK_PATH.
-// - completeVaultAuth(): exchange the code for a Vault token, sign the
-// ephemeral key, cache the cert, and notify the browser to reconnect.
-
-import { WebSocket } from "ws";
-import {
- createCurrentHostResolutionRepository,
- createCurrentVaultProfileRepository,
-} from "../database/repositories/factory.js";
-import { sshLogger } from "../utils/logger.js";
-import {
- type VaultProfileConfig,
- generateEphemeralKeyPair,
- startVaultOidc,
- completeVaultOidc,
- signWithVault,
- storeVaultCert,
-} from "./vault-signer-auth.js";
-
-export const VAULT_OIDC_CALLBACK_PATH = "/vault/oidc/callback";
-
-const AUTH_TIMEOUT = 5 * 60 * 1000;
-
-interface VaultAuthSession {
- state: string;
- userId: string;
- hostId: number;
- profile: VaultProfileConfig;
- clientNonce: string;
- ephemeralPrivateKey: string;
- ephemeralPublicKey: string;
- ws: WebSocket;
- timeout: NodeJS.Timeout;
-}
-
-// Keyed by the Vault-issued OIDC `state` so the unauthenticated browser callback
-// can correlate back to the originating session.
-const sessions = new Map();
-
-function rowToProfileConfig(row: Record): VaultProfileConfig {
- return {
- id: row.id as number,
- vaultAddr: row.vaultAddr as string,
- vaultNamespace: (row.vaultNamespace as string | null) ?? null,
- oidcMount: (row.oidcMount as string | null) ?? null,
- oidcRole: (row.oidcRole as string | null) ?? null,
- sshMount: (row.sshMount as string | null) ?? null,
- sshRole: row.sshRole as string,
- validPrincipals: (row.validPrincipals as string | null) ?? null,
- keyType: (row.keyType as string | null) ?? null,
- };
-}
-
-/** Load the Vault profile referenced by a host, or null if not configured. */
-export async function loadVaultProfileForHost(
- hostId: number,
- userId: string,
-): Promise {
- const host = await createCurrentHostResolutionRepository().findHostById(
- hostId,
- userId,
- );
- if (!host || host.vaultProfileId == null) return null;
-
- const profile = await createCurrentVaultProfileRepository().findById(
- host.vaultProfileId as number,
- );
- if (!profile) return null;
-
- return rowToProfileConfig(profile as Record);
-}
-
-/**
- * Begin an interactive Vault OIDC login and send the auth URL to the browser.
- */
-export async function startVaultAuth(
- userId: string,
- hostId: number,
- profile: VaultProfileConfig,
- ws: WebSocket,
- requestOrigin: string,
-): Promise {
- const ephemeral = generateEphemeralKeyPair(profile.keyType);
- const redirectUri = `${requestOrigin}${VAULT_OIDC_CALLBACK_PATH}`;
-
- const { authUrl, state, clientNonce } = await startVaultOidc(
- profile,
- redirectUri,
- );
-
- const existing = sessions.get(state);
- if (existing) clearTimeout(existing.timeout);
-
- const timeout = setTimeout(() => {
- sessions.delete(state);
- }, AUTH_TIMEOUT);
-
- sessions.set(state, {
- state,
- userId,
- hostId,
- profile,
- clientNonce,
- ephemeralPrivateKey: ephemeral.privateKey,
- ephemeralPublicKey: ephemeral.publicKey,
- ws,
- timeout,
- });
-
- sshLogger.info("Started Vault OIDC auth session", {
- operation: "vault_oidc_start",
- userId,
- hostId,
- profileId: profile.id,
- });
-
- ws.send(
- JSON.stringify({
- type: "vault_auth_url",
- hostId,
- url: authUrl,
- }),
- );
-}
-
-/**
- * Complete a Vault OIDC login from the browser callback: exchange the code for a
- * Vault token, sign the ephemeral key, cache the certificate, and notify the
- * browser to resume the SSH connection.
- */
-export async function completeVaultAuth(
- state: string,
- code: string,
-): Promise<{ ok: boolean; error?: string }> {
- const session = sessions.get(state);
- if (!session) {
- return { ok: false, error: "No matching Vault authentication session" };
- }
-
- try {
- const token = await completeVaultOidc(session.profile, {
- state,
- code,
- clientNonce: session.clientNonce,
- });
- const signedCert = await signWithVault(
- session.profile,
- token,
- session.ephemeralPublicKey,
- );
- const expiresAt = await storeVaultCert(
- session.userId,
- session.profile.id,
- session.ephemeralPrivateKey,
- signedCert,
- );
-
- sshLogger.success("Completed Vault OIDC auth and cached certificate", {
- operation: "vault_oidc_complete",
- userId: session.userId,
- hostId: session.hostId,
- profileId: session.profile.id,
- });
-
- session.ws.send(
- JSON.stringify({
- type: "vault_completed",
- hostId: session.hostId,
- expiresAt,
- }),
- );
- return { ok: true };
- } catch (e) {
- const msg = e instanceof Error ? e.message : String(e);
- sshLogger.error("Vault OIDC completion failed", e, {
- operation: "vault_oidc_complete_error",
- userId: session.userId,
- hostId: session.hostId,
- });
- try {
- session.ws.send(
- JSON.stringify({
- type: "vault_error",
- hostId: session.hostId,
- error: msg,
- }),
- );
- } catch {
- // ws may be closed
- }
- return { ok: false, error: msg };
- } finally {
- clearTimeout(session.timeout);
- sessions.delete(state);
- }
-}
-
-/** Cancel a pending Vault auth session (e.g. user dismissed the dialog). */
-export function cancelVaultAuthByHost(userId: string, hostId: number): void {
- for (const [state, s] of sessions) {
- if (s.userId === userId && s.hostId === hostId) {
- clearTimeout(s.timeout);
- sessions.delete(state);
- }
- }
-}
diff --git a/src/backend/hosts/vault-signer-auth.ts b/src/backend/hosts/vault-signer-auth.ts
deleted file mode 100644
index 05a092914..000000000
--- a/src/backend/hosts/vault-signer-auth.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-// HashiCorp Vault SSH signer authentication.
-//
-// Flow (Vault-driven over HTTP):
-// 1. Generate an ephemeral SSH keypair (never persisted long-term).
-// 2. The user authenticates to Vault via an interactive OIDC flow
-// (auth//oidc/auth_url -> browser -> auth//oidc/callback),
-// yielding a short-lived Vault token.
-// 3. Vault's SSH secrets engine signs the ephemeral public key
-// (/sign/) -> short-lived OpenSSH certificate.
-// 4. The ephemeral private key + certificate are cached per-user (encrypted)
-// until the certificate expires, then used to connect via applyCertificateAuth.
-//
-// No Vault tokens, AppRole secrets, or long-lived private keys are ever stored.
-// Vault connection SETTINGS live in shareable vault_profiles rows.
-//
-// The pure (DB-free) signing/OIDC/cert logic lives in vault-signer-core.ts and
-// is re-exported here so callers have a single import surface.
-
-import { createCurrentVaultTokenRepository } from "../database/repositories/factory.js";
-import { DataCrypto } from "../utils/data-crypto.js";
-import { FieldCrypto } from "../utils/field-crypto.js";
-import { parseCertValidBefore } from "./vault-signer-core.js";
-
-export type {
- VaultProfileConfig,
- EphemeralKeyPair,
-} from "./vault-signer-core.js";
-export {
- generateEphemeralKeyPair,
- startVaultOidc,
- completeVaultOidc,
- signWithVault,
- parseCertValidBefore,
-} from "./vault-signer-core.js";
-
-// Re-sign once we're within this many seconds of expiry.
-const EXPIRY_SKEW_SECONDS = 60;
-
-function cacheRecordId(userId: string, profileId: number): string {
- return `vault-${userId}-${profileId}`;
-}
-
-/**
- * Store an ephemeral private key + signed certificate for a user/profile.
- * Returns the expiry as an ISO string.
- */
-export async function storeVaultCert(
- userId: string,
- profileId: number,
- privateKey: string,
- signedCert: string,
-): Promise {
- const userDataKey = DataCrypto.getUserDataKey(userId);
- if (!userDataKey) {
- throw new Error("User data key not found");
- }
-
- const validBefore = parseCertValidBefore(signedCert);
- const expiresMs =
- validBefore > 0 ? validBefore * 1000 : Date.now() + 5 * 60 * 1000;
- const expiresAt = new Date(expiresMs).toISOString();
-
- const recordId = cacheRecordId(userId, profileId);
- const encryptedCert = FieldCrypto.encryptField(
- signedCert,
- userDataKey,
- recordId,
- "ssh_cert",
- );
- const encryptedKey = FieldCrypto.encryptField(
- privateKey,
- userDataKey,
- recordId,
- "private_key",
- );
-
- await createCurrentVaultTokenRepository().upsert({
- userId,
- profileId,
- sshCert: encryptedCert,
- privateKey: encryptedKey,
- expiresAt,
- });
-
- return expiresAt;
-}
-
-/**
- * Return a cached, still-valid ephemeral key + certificate for a user/profile,
- * or null if none exists or it has (nearly) expired.
- */
-export async function getVaultCert(
- userId: string,
- profileId: number,
-): Promise<{ privateKey: string; sshCert: string } | null> {
- const repository = createCurrentVaultTokenRepository();
- const row = await repository.findByUserAndProfile(userId, profileId);
-
- if (!row) return null;
-
- const expiresMs = new Date(row.expiresAt).getTime();
- if (expiresMs - EXPIRY_SKEW_SECONDS * 1000 < Date.now()) {
- await deleteVaultCert(userId, profileId);
- return null;
- }
-
- const userDataKey = DataCrypto.getUserDataKey(userId);
- if (!userDataKey) {
- throw new Error("User data key not found");
- }
-
- const recordId = cacheRecordId(userId, profileId);
- const sshCert = FieldCrypto.decryptField(
- row.sshCert,
- userDataKey,
- recordId,
- "ssh_cert",
- );
- const privateKey = FieldCrypto.decryptField(
- row.privateKey,
- userDataKey,
- recordId,
- "private_key",
- );
-
- await repository.updateLastUsed(userId, profileId);
-
- return { privateKey, sshCert };
-}
-
-export async function deleteVaultCert(
- userId: string,
- profileId: number,
-): Promise {
- await createCurrentVaultTokenRepository().deleteByUserAndProfile(
- userId,
- profileId,
- );
-}
diff --git a/src/backend/starter.ts b/src/backend/starter.ts
index bef231f20..d2453b6c8 100644
--- a/src/backend/starter.ts
+++ b/src/backend/starter.ts
@@ -381,6 +381,10 @@ async function provisionLocalDesktopUserIfNeeded(): Promise {
await import("./utils/crypto-migration/step-ca-settings-migration.js");
await runStepCaSettingsMigration();
+ const { runVaultSettingsMigration } =
+ await import("./utils/crypto-migration/vault-settings-migration.js");
+ await runVaultSettingsMigration();
+
const { runSecretSourcesTokenMigration } =
await import("./utils/crypto-migration/secret-sources-token-migration.js");
await runSecretSourcesTokenMigration();
diff --git a/src/backend/tests/database/repositories/host-resolution-repository.test.ts b/src/backend/tests/database/repositories/host-resolution-repository.test.ts
index 4ef296d8f..653237504 100644
--- a/src/backend/tests/database/repositories/host-resolution-repository.test.ts
+++ b/src/backend/tests/database/repositories/host-resolution-repository.test.ts
@@ -195,7 +195,6 @@ describe("HostResolutionRepository", () => {
rdpCredentialId: null,
vncCredentialId: null,
telnetCredentialId: null,
- vaultProfileId: null,
authType: "password",
parentHostId: null,
folder: null,
diff --git a/src/backend/tests/database/repositories/vault-profile-repository.test.ts b/src/backend/tests/database/repositories/vault-profile-repository.test.ts
deleted file mode 100644
index b5e9c4e2c..000000000
--- a/src/backend/tests/database/repositories/vault-profile-repository.test.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-import { afterEach, describe, expect, it } from "vitest";
-import { TestSqliteDatabase } from "./test-support.js";
-import { VaultProfileRepository } from "../../../database/repositories/vault-profile-repository.js";
-
-describe("VaultProfileRepository", () => {
- let adapter: TestSqliteDatabase | null = null;
-
- afterEach(async () => {
- if (adapter) {
- await adapter.close();
- adapter = null;
- }
- });
-
- async function createRepository(
- onWrite?: () => void | Promise,
- ): Promise {
- adapter = new TestSqliteDatabase();
- const context = await adapter.connect();
- await adapter.exec(`
- INSERT INTO users (id, username, password_hash)
- VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash');
- INSERT INTO vault_profiles (
- id, user_id, name, vault_addr, ssh_role, shared, updated_at
- )
- VALUES (1, 'user-1', 'owned', 'https://vault.one', 'role-one', 0, '2026-01-01T00:00:00.000Z'),
- (2, 'user-2', 'shared', 'https://vault.two', 'role-two', 1, '2026-01-02T00:00:00.000Z'),
- (3, 'user-2', 'hidden', 'https://vault.three', 'role-three', 0, '2026-01-03T00:00:00.000Z');
- `);
-
- return new VaultProfileRepository(context, onWrite);
- }
-
- it("lists profiles owned by or shared with the user", async () => {
- const repo = await createRepository();
-
- const rows = await repo.listVisibleToUser("user-1");
-
- expect(rows.map((row) => row.id)).toEqual([2, 1]);
- });
-
- it("creates and reads a profile", async () => {
- let writeCount = 0;
- const repo = await createRepository(() => {
- writeCount += 1;
- });
-
- const created = await repo.create({
- userId: "user-1",
- name: "new",
- description: "desc",
- folder: "folder",
- tags: "prod,ssh",
- vaultAddr: "https://vault.new",
- vaultNamespace: "ns",
- oidcMount: "oidc",
- oidcRole: "oidc-role",
- sshMount: "ssh",
- sshRole: "ssh-role",
- validPrincipals: "root",
- keyType: "ed25519",
- shared: true,
- });
-
- const found = await repo.findById(created.id);
- expect(found).toMatchObject({
- userId: "user-1",
- name: "new",
- vaultAddr: "https://vault.new",
- sshRole: "ssh-role",
- shared: true,
- });
- expect(writeCount).toBe(1);
- });
-
- it("updates and deletes profiles", async () => {
- let writeCount = 0;
- const repo = await createRepository(() => {
- writeCount += 1;
- });
-
- const updated = await repo.updateById(1, {
- name: "renamed",
- shared: true,
- updatedAt: "2026-02-01T00:00:00.000Z",
- });
- expect(updated).toMatchObject({
- id: 1,
- name: "renamed",
- shared: true,
- updatedAt: "2026-02-01T00:00:00.000Z",
- });
-
- expect(await repo.updateById(999, { name: "missing" })).toBeNull();
- expect(await repo.deleteById(1)).toEqual({ syncId: null });
- expect(await repo.deleteById(1)).toBeNull();
- expect(await repo.findById(1)).toBeNull();
- expect(writeCount).toBe(2);
- });
-
- it("deletes all profiles for a user", async () => {
- let writeCount = 0;
- const repo = await createRepository(() => {
- writeCount += 1;
- });
-
- await expect(repo.deleteByUserId("user-2")).resolves.toBe(2);
- await expect(repo.deleteByUserId("missing")).resolves.toBe(0);
-
- expect(await repo.findById(2)).toBeNull();
- expect(await repo.findById(3)).toBeNull();
- expect((await repo.findById(1))?.userId).toBe("user-1");
- expect(writeCount).toBe(1);
- });
-});
diff --git a/src/backend/tests/database/repositories/vault-token-repository.test.ts b/src/backend/tests/database/repositories/vault-token-repository.test.ts
deleted file mode 100644
index 01484f5be..000000000
--- a/src/backend/tests/database/repositories/vault-token-repository.test.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import { afterEach, describe, expect, it } from "vitest";
-import { TestSqliteDatabase } from "./test-support.js";
-import { VaultTokenRepository } from "../../../database/repositories/vault-token-repository.js";
-
-describe("VaultTokenRepository", () => {
- let adapter: TestSqliteDatabase | null = null;
-
- afterEach(async () => {
- if (adapter) {
- await adapter.close();
- adapter = null;
- }
- });
-
- async function createRepository(
- onWrite?: () => void | Promise,
- ): Promise {
- adapter = new TestSqliteDatabase();
- const context = await adapter.connect();
- await adapter.exec(`
- INSERT INTO users (id, username, password_hash)
- VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash');
- INSERT INTO vault_profiles (id, user_id, name, vault_addr, ssh_role)
- VALUES (1, 'user-1', 'one', 'http://vault', 'r'), (2, 'user-2', 'two', 'http://vault', 'r');
- INSERT INTO vault_tokens (
- user_id, profile_id, ssh_cert, private_key, expires_at
- )
- VALUES
- ('user-1', 1, 'cert-1', 'key-1', '2099-01-01T00:00:00.000Z'),
- ('user-2', 2, 'cert-2', 'key-2', '2099-01-01T00:00:00.000Z');
- `);
-
- return new VaultTokenRepository(context, onWrite);
- }
-
- it("finds and upserts a token by user and profile", async () => {
- let writeCount = 0;
- const repo = await createRepository(() => {
- writeCount += 1;
- });
-
- const existing = await repo.findByUserAndProfile("user-1", 1);
- expect(existing?.sshCert).toBe("cert-1");
-
- await repo.upsert({
- userId: "user-1",
- profileId: 1,
- sshCert: "new-cert",
- privateKey: "new-key",
- expiresAt: "2099-02-01T00:00:00.000Z",
- createdAt: "2026-01-01T00:00:00.000Z",
- });
-
- const updated = await repo.findByUserAndProfile("user-1", 1);
- expect(updated).toMatchObject({
- sshCert: "new-cert",
- privateKey: "new-key",
- expiresAt: "2099-02-01T00:00:00.000Z",
- createdAt: "2026-01-01T00:00:00.000Z",
- });
- expect(writeCount).toBe(1);
- });
-
- it("updates last-used and deletes one token", async () => {
- let writeCount = 0;
- const repo = await createRepository(() => {
- writeCount += 1;
- });
-
- expect(
- await repo.updateLastUsed("user-1", 1, "2026-01-02T00:00:00.000Z"),
- ).toBe(true);
- expect(await repo.updateLastUsed("missing", 1)).toBe(false);
- expect((await repo.findByUserAndProfile("user-1", 1))?.lastUsed).toBe(
- "2026-01-02T00:00:00.000Z",
- );
-
- expect(await repo.deleteByUserAndProfile("user-1", 1)).toBe(true);
- expect(await repo.deleteByUserAndProfile("user-1", 1)).toBe(false);
- expect(await repo.findByUserAndProfile("user-1", 1)).toBeNull();
- expect(await repo.findByUserAndProfile("user-2", 2)).not.toBeNull();
- expect(writeCount).toBe(2);
- });
-
- it("deletes all tokens for a user", async () => {
- let writeCount = 0;
- const repo = await createRepository(() => {
- writeCount += 1;
- });
-
- await repo.upsert({
- userId: "user-1",
- profileId: 2,
- sshCert: "cert-extra",
- privateKey: "key-extra",
- expiresAt: "2099-03-01T00:00:00.000Z",
- });
-
- await expect(repo.deleteByUserId("user-1")).resolves.toBe(2);
- await expect(repo.deleteByUserId("missing")).resolves.toBe(0);
-
- expect(await repo.findByUserAndProfile("user-1", 1)).toBeNull();
- expect(await repo.findByUserAndProfile("user-1", 2)).toBeNull();
- expect(await repo.findByUserAndProfile("user-2", 2)).not.toBeNull();
- expect(writeCount).toBe(2);
- });
-});
diff --git a/src/backend/tests/database/routes/host-compat-routes.test.ts b/src/backend/tests/database/routes/host-compat-routes.test.ts
index 6094be251..b86231792 100644
--- a/src/backend/tests/database/routes/host-compat-routes.test.ts
+++ b/src/backend/tests/database/routes/host-compat-routes.test.ts
@@ -1,5 +1,5 @@
/**
- * The OPKSSH and Step CA redirect URIs identity providers were registered with before
+ * The OPKSSH, Step CA and Vault redirect URIs identity providers were registered with before
* 2.9 must keep reaching the plugin's callback, query intact.
*/
@@ -62,3 +62,29 @@ describe("the old Step CA callback", () => {
);
});
});
+
+describe("the old Vault callback", () => {
+ it("forwards to the plugin's callback with the query", async () => {
+ const { registerVaultCompatRoutes } =
+ await import("../../../database/routes/host-compat-routes.js");
+ const router = express.Router();
+ registerVaultCompatRoutes(router);
+ const app = express();
+ app.use("/vault", router);
+ const vaultServer = http.createServer(app);
+ await new Promise((resolve) => vaultServer.listen(0, resolve));
+ const port = (vaultServer.address() as AddressInfo).port;
+ try {
+ const response = await fetch(
+ `http://127.0.0.1:${port}/vault/oidc/callback?code=abc&state=xyz`,
+ { redirect: "manual" },
+ );
+ expect(response.status).toBe(307);
+ expect(response.headers.get("location")).toBe(
+ "/plugin-api/vault/oidc/callback?code=abc&state=xyz",
+ );
+ } finally {
+ await new Promise((resolve) => vaultServer.close(() => resolve()));
+ }
+ });
+});
diff --git a/src/backend/tests/database/routes/sync.test.ts b/src/backend/tests/database/routes/sync.test.ts
index 7a843e704..15ebbca9b 100644
--- a/src/backend/tests/database/routes/sync.test.ts
+++ b/src/backend/tests/database/routes/sync.test.ts
@@ -33,7 +33,6 @@ describe("isValidEntityType", () => {
"hosts",
"sshCredentials",
"sshFolders",
- "vaultProfiles",
"userPreferences",
]) {
expect(isValidEntityType(type)).toBe(true);
diff --git a/src/backend/tests/database/sync-references.test.ts b/src/backend/tests/database/sync-references.test.ts
index 1450f4345..4a8f268da 100644
--- a/src/backend/tests/database/sync-references.test.ts
+++ b/src/backend/tests/database/sync-references.test.ts
@@ -23,7 +23,6 @@ describe("sync references", () => {
rdpCredentialId: 13,
vncCredentialId: null,
telnetCredentialId: null,
- vaultProfileId: 4,
parentHostId: 3,
},
async (entityType, id) => `${entityType}-${id}`,
@@ -34,17 +33,14 @@ describe("sync references", () => {
rdpCredentialSyncId: "sshCredentials-13",
vncCredentialSyncId: null,
telnetCredentialSyncId: null,
- vaultProfileSyncId: "vaultProfiles-4",
parentHostSyncId: "hosts-3",
});
expect(row).not.toHaveProperty("credentialId");
- expect(row).not.toHaveProperty("vaultProfileId");
});
it("resolves stable sync IDs to IDs from the receiving database", async () => {
const ids = new Map([
["sshCredentials:credential-sync", 91],
- ["vaultProfiles:vault-sync", 37],
["hosts:parent-sync", 52],
]);
const row = await deserializeSyncReferences(
@@ -55,7 +51,6 @@ describe("sync references", () => {
rdpCredentialSyncId: null,
vncCredentialSyncId: null,
telnetCredentialSyncId: null,
- vaultProfileSyncId: "vault-sync",
parentHostSyncId: "parent-sync",
},
async (entityType, syncId) => ids.get(`${entityType}:${syncId}`) ?? null,
@@ -66,7 +61,6 @@ describe("sync references", () => {
rdpCredentialId: null,
vncCredentialId: null,
telnetCredentialId: null,
- vaultProfileId: 37,
parentHostId: 52,
});
expect(row).not.toHaveProperty("credentialSyncId");
diff --git a/src/backend/tests/hosts/connect/pipeline.test.ts b/src/backend/tests/hosts/connect/pipeline.test.ts
index 002b6291f..839236af9 100644
--- a/src/backend/tests/hosts/connect/pipeline.test.ts
+++ b/src/backend/tests/hosts/connect/pipeline.test.ts
@@ -19,8 +19,7 @@ const mocks = vi.hoisted(() => ({
verifier: () => {},
createHostVerifier: vi.fn(),
applyCertificateAuth: vi.fn(),
- getVaultCert: vi.fn(),
- deleteVaultCert: vi.fn(),
+ forgetCert: vi.fn(),
applyAgentAuth: vi.fn(),
performPortKnocking: vi.fn(),
createJumpHostChain: vi.fn(),
@@ -41,10 +40,6 @@ vi.mock("../../../hosts/host-key-verifier.js", () => ({
vi.mock("@termix/plugin-sdk/ssh-certs", () => ({
applyCertificateAuth: mocks.applyCertificateAuth,
}));
-vi.mock("../../../hosts/vault-signer-auth.js", () => ({
- getVaultCert: mocks.getVaultCert,
- deleteVaultCert: mocks.deleteVaultCert,
-}));
vi.mock("../../../hosts/terminal-auth-helpers.js", () => ({
applyAgentAuth: mocks.applyAgentAuth,
performPortKnocking: mocks.performPortKnocking,
@@ -107,6 +102,39 @@ function host(overrides: Partial = {}): SshConnectHost {
};
}
+/**
+ * A plugin-style provider that needs a browser sign-in until it has a
+ * certificate, and forgets it when the host rejects it.
+ */
+function registerSignInProvider(hasCert: boolean) {
+ return registerSshAuthProvider({
+ type: "signin",
+ pluginId: "fixture-plugin",
+ labelKey: "fixture.label",
+ needsUserInteraction: true,
+ interaction: "signin",
+ prepare: async (config) => {
+ if (!hasCert) {
+ return {
+ status: "interaction-required",
+ interaction: "signin",
+ message: "Sign in first",
+ };
+ }
+ config.password = "cert";
+ return { status: "ready" };
+ },
+ onAuthFailed: (_host, env) => {
+ mocks.forgetCert(env.userId, env.hostId);
+ return {
+ status: "interaction-required",
+ interaction: "signin",
+ message: "Sign in again",
+ };
+ },
+ });
+}
+
async function build(
target: SshConnectHost,
purpose: Parameters[1]["purpose"] = "terminal",
@@ -286,30 +314,20 @@ describe("buildConnectConfig per auth type", () => {
setSshAuthTypeOwnerSource(() => []);
});
- it("vault needs a profile, then a cached certificate", async () => {
- const noProfile = await build(host({ authType: "vault" }));
- expect(noProfile.outcome).toMatchObject({ status: "error" });
+ // Vault is the vault plugin's provider: see
+ // plugins/vault/tests/backend/vault.test.ts.
- mocks.getVaultCert.mockResolvedValueOnce(null);
- const noCert = await build(
- host({ authType: "vault", vaultProfile: { id: 3 } }),
- );
- // The file manager turns the flag into requiresVaultAuth in its 401.
- expect(noCert.outcome).toMatchObject({
- status: "interaction-required",
- interaction: "vault",
- flag: "requiresVaultAuth",
+ it("a vault host without the vault plugin names it", async () => {
+ setSshAuthTypeOwnerSource(() => [
+ { type: "vault", pluginId: "vault", pluginName: "HashiCorp Vault" },
+ ]);
+ const { outcome } = await build(host({ authType: "vault" }));
+ expect(outcome).toEqual({
+ status: "error",
+ code: "provider-missing",
+ message: "This host uses vault, which needs the HashiCorp Vault plugin",
});
-
- mocks.getVaultCert.mockResolvedValueOnce({
- privateKey: "k",
- sshCert: "c",
- });
- const ready = await build(
- host({ authType: "vault", vaultProfile: { id: 3 } }),
- );
- expect(ready.outcome.status).toBe("ready");
- expect(mocks.getVaultCert).toHaveBeenLastCalledWith("user-1", 3);
+ setSshAuthTypeOwnerSource(() => []);
});
// Tailscale is a plugin-registered SSH auth provider now: see
@@ -454,20 +472,19 @@ describe("connectHost", () => {
});
it("throws the auth outcome instead of connecting", async () => {
- mocks.getVaultCert.mockResolvedValue(null);
- const vaultHost = () =>
- host({ authType: "vault", vaultProfile: { id: 3 } });
- const attempt = connectHost(vaultHost(), {
+ const dispose = registerSignInProvider(false);
+ const attempt = connectHost(host({ authType: "signin" }), {
userId: "user-1",
purpose: "tmux",
});
await expect(attempt).rejects.toBeInstanceOf(SshConnectError);
await expect(
- connectHost(vaultHost(), {
+ connectHost(host({ authType: "signin" }), {
userId: "user-1",
purpose: "tmux",
}).catch((error) => error.code),
- ).resolves.toBe("vault-required");
+ ).resolves.toBe("signin-required");
+ dispose();
});
it("goes through the jump chain and forwards to the target", async () => {
@@ -619,19 +636,17 @@ describe("connectHost", () => {
it("lets the provider react to an auth failure", async () => {
FakeSshClient.nextBehaviour = ["auth-fail"];
- mocks.getVaultCert.mockResolvedValueOnce({
- privateKey: "k",
- sshCert: "c",
- });
+ const dispose = registerSignInProvider(true);
await expect(
- connectHost(host({ authType: "vault", vaultProfile: { id: 3 } }), {
+ connectHost(host({ authType: "signin" }), {
userId: "user-1",
purpose: "fleet",
}),
).rejects.toThrow(/All configured authentication methods failed/);
await vi.waitFor(() =>
- expect(mocks.deleteVaultCert).toHaveBeenCalledWith("user-1", 3),
+ expect(mocks.forgetCert).toHaveBeenCalledWith("user-1", 7),
);
+ dispose();
});
it("pools under the old key shape", async () => {
diff --git a/src/backend/tests/hosts/host-resolver-sync-id.test.ts b/src/backend/tests/hosts/host-resolver-sync-id.test.ts
index 9f0eeeedc..4d7d43aa2 100644
--- a/src/backend/tests/hosts/host-resolver-sync-id.test.ts
+++ b/src/backend/tests/hosts/host-resolver-sync-id.test.ts
@@ -11,7 +11,6 @@ vi.mock("../../database/repositories/factory.js", () => ({
findHostOwnerId,
findHostById,
}),
- createCurrentVaultProfileRepository: () => ({}),
createCurrentUserRepository: () => ({ findById: vi.fn() }),
}));
diff --git a/src/backend/tests/hosts/host-resolver.test.ts b/src/backend/tests/hosts/host-resolver.test.ts
index 44ca995c1..8d49cd85f 100644
--- a/src/backend/tests/hosts/host-resolver.test.ts
+++ b/src/backend/tests/hosts/host-resolver.test.ts
@@ -6,7 +6,6 @@ const state = vi.hoisted(() => ({
isAdminBypass: false,
overrideCredentialId: null as number | null,
credentials: new Map>(),
- vaultProfile: null as Record | null,
auditCalls: [] as Record[],
folderCredentialId: null as number | null,
sharedSecret: null as Record | null,
@@ -26,9 +25,6 @@ vi.mock("../../database/repositories/factory.js", () => ({
createCurrentSharedHostAuthOverrideRepository: () => ({
findCredentialId: async () => state.overrideCredentialId,
}),
- createCurrentVaultProfileRepository: () => ({
- findById: async () => state.vaultProfile,
- }),
createCurrentUserRepository: () => ({
findById: async (userId: string) => ({ id: userId, username: userId }),
}),
@@ -115,7 +111,6 @@ function baseHost(overrides: Record = {}) {
keyType: null,
credentialId: null,
shareSshAuth: false,
- vaultProfileId: null,
sudoPassword: "owner-sudo",
autostartPassword: "auto-pass",
autostartKey: null,
@@ -136,7 +131,6 @@ beforeEach(() => {
state.isAdminBypass = false;
state.overrideCredentialId = null;
state.credentials.clear();
- state.vaultProfile = null;
state.auditCalls = [];
state.folderCredentialId = null;
state.sharedSecret = null;
@@ -372,13 +366,11 @@ describe("resolveHostById", () => {
expect(host.key).toBe("RECIPIENT-KEY");
});
- it("fully replaces Vault authentication with the recipient override", async () => {
+ it("fully replaces a plugin auth type with the recipient override", async () => {
state.host = baseHost({
authType: "vault",
password: null,
- vaultProfileId: 7,
});
- state.vaultProfile = { id: 7 };
state.overrideCredentialId = 5;
state.credentials.set("5:recipient", {
id: 5,
@@ -399,7 +391,6 @@ describe("resolveHostById", () => {
expect(host.authType).toBe("key");
expect(host.key).toBe("RECIPIENT-KEY");
expect(host.certPublicKey).toBe("ssh-ed25519-cert-v01@example certificate");
- expect(host.vaultProfile).toBeUndefined();
});
it("falls back to the host username when the override credential has none", async () => {
diff --git a/src/backend/tests/plugins/sync-registry.test.ts b/src/backend/tests/plugins/sync-registry.test.ts
index f67104395..7ef5dc396 100644
--- a/src/backend/tests/plugins/sync-registry.test.ts
+++ b/src/backend/tests/plugins/sync-registry.test.ts
@@ -96,15 +96,16 @@ describe("core sync entities", () => {
registerCoreSyncEntities();
});
- // The frozen array also lists networkTopology, snippets/snippetFolders and
- // dashboardServiceLinks/homepageItems, now registered by the
- // network-topology, snippets and homepage plugins rather than core.
+ // The frozen array also lists networkTopology, snippets/snippetFolders,
+ // dashboardServiceLinks/homepageItems and vaultProfiles, now registered by
+ // the network-topology, snippets, homepage and vault plugins rather than core.
const PLUGIN_OWNED_TYPES = new Set([
"networkTopology",
"snippets",
"snippetFolders",
"dashboardServiceLinks",
"homepageItems",
+ "vaultProfiles",
]);
it("registers every core-owned entity the Electron client knows about", () => {
diff --git a/src/backend/tests/utils/vault-settings-migration.test.ts b/src/backend/tests/utils/vault-settings-migration.test.ts
new file mode 100644
index 000000000..2632585f3
--- /dev/null
+++ b/src/backend/tests/utils/vault-settings-migration.test.ts
@@ -0,0 +1,152 @@
+/**
+ * The Vault host settings migration.
+ *
+ * Every host's ssh_data.vault_profile_id must come through into the vault
+ * plugin's profileId host setting, and an install that used Vault keeps the
+ * 2.8 redirect URI. Running it twice must change nothing.
+ */
+
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const state = vi.hoisted(() => ({
+ pluginRows: [] as Array<{
+ scope: string;
+ scopeId: string | null;
+ key: string;
+ value: string | null;
+ }>,
+ hostRows: [] as Array>,
+ legacyProfiles: 0,
+ adoptedProfiles: 0,
+ pluginInstalled: true,
+ columnGone: false,
+}));
+
+vi.mock("../../database/repositories/factory.js", () => ({
+ createCurrentPluginRepository: () => ({
+ findById: async (id: string) =>
+ state.pluginInstalled && id === "vault" ? { id } : null,
+ }),
+ createCurrentPluginSettingsRepository: () => ({
+ get: async (
+ _pluginId: string,
+ scope: string,
+ scopeId: string | null,
+ key: string,
+ ) =>
+ state.pluginRows.find(
+ (row) =>
+ row.scope === scope && row.scopeId === scopeId && row.key === key,
+ ) ?? null,
+ set: async (
+ _pluginId: string,
+ scope: string,
+ scopeId: string | null,
+ key: string,
+ value: string | null,
+ ) => {
+ state.pluginRows.push({ scope, scopeId, key, value });
+ },
+ }),
+}));
+
+vi.mock("../../utils/crypto-migration/raw-rows.js", () => ({
+ selectRows: async (query: unknown) => {
+ const text = JSON.stringify(query);
+ if (text.includes("vault_profile_id")) {
+ if (state.columnGone) throw new Error("no such column");
+ return state.hostRows.filter((row) => row.vault_profile_id != null);
+ }
+ const count = text.includes("p_vault_profiles")
+ ? state.adoptedProfiles
+ : state.legacyProfiles;
+ return Array.from({ length: Math.min(count, 1) }, () => ({ "1": 1 }));
+ },
+ runStatement: async () => {},
+}));
+
+vi.mock("../../utils/logger.js", () => ({
+ databaseLogger: { info: vi.fn(), warn: vi.fn() },
+}));
+
+import { runVaultSettingsMigration } from "../../utils/crypto-migration/vault-settings-migration.js";
+
+const setting = (scopeId: string | null, key: string) =>
+ state.pluginRows.find((row) => row.scopeId === scopeId && row.key === key)
+ ?.value;
+
+beforeEach(() => {
+ state.pluginRows = [];
+ state.hostRows = [];
+ state.legacyProfiles = 0;
+ state.adoptedProfiles = 0;
+ state.pluginInstalled = true;
+ state.columnGone = false;
+});
+
+describe("runVaultSettingsMigration", () => {
+ it("moves every host's profile, keeps the old redirect URI, and a second run changes nothing", async () => {
+ state.hostRows = [
+ { id: 1, vault_profile_id: 7 },
+ { id: 2, vault_profile_id: null },
+ { id: 3, vault_profile_id: "9" },
+ ];
+ state.adoptedProfiles = 2;
+
+ const first = await runVaultSettingsMigration();
+ expect(first).toEqual({
+ hostsMoved: 2,
+ hostsSkipped: 0,
+ legacyCallback: true,
+ });
+ expect(setting("1", "profileId")).toBe("7");
+ expect(setting("2", "profileId")).toBeUndefined();
+ expect(setting("3", "profileId")).toBe("9");
+ expect(setting(null, "legacyCallback")).toBe("true");
+
+ const rows = state.pluginRows.length;
+ const second = await runVaultSettingsMigration();
+ expect(second).toEqual({
+ hostsMoved: 0,
+ hostsSkipped: 2,
+ legacyCallback: false,
+ });
+ expect(state.pluginRows).toHaveLength(rows);
+ });
+
+ it("keeps a host's value already set in the plugin", async () => {
+ state.pluginRows.push({
+ scope: "host",
+ scopeId: "1",
+ key: "profileId",
+ value: "4",
+ });
+ state.hostRows = [{ id: 1, vault_profile_id: 7 }];
+ await runVaultSettingsMigration();
+ expect(setting("1", "profileId")).toBe("4");
+ });
+
+ it("keeps the old redirect URI for profiles the plugin has not adopted yet", async () => {
+ state.legacyProfiles = 1;
+ const result = await runVaultSettingsMigration();
+ expect(result.legacyCallback).toBe(true);
+ });
+
+ it("uses the new redirect URI on a fresh install, and profiles made later do not change that", async () => {
+ state.columnGone = true;
+ const first = await runVaultSettingsMigration();
+ expect(first.legacyCallback).toBe(false);
+ expect(setting(null, "legacyCallback")).toBe("false");
+
+ state.adoptedProfiles = 3;
+ await runVaultSettingsMigration();
+ expect(setting(null, "legacyCallback")).toBe("false");
+ });
+
+ it("waits for the plugin row", async () => {
+ state.pluginInstalled = false;
+ state.hostRows = [{ id: 1, vault_profile_id: 7 }];
+ await runVaultSettingsMigration();
+ expect(state.pluginRows).toHaveLength(0);
+ });
+});
diff --git a/src/backend/utils/crypto-migration/vault-settings-migration.ts b/src/backend/utils/crypto-migration/vault-settings-migration.ts
new file mode 100644
index 000000000..762e7d699
--- /dev/null
+++ b/src/backend/utils/crypto-migration/vault-settings-migration.ts
@@ -0,0 +1,149 @@
+/**
+ * Moves each host's Vault signer profile out of ssh_data and into the vault
+ * plugin's profileId host setting.
+ *
+ * An install that used Vault keeps sending the pre-2.9 redirect URI
+ * (/vault/oidc/callback), because that is the one its Vault OIDC roles
+ * allow: the plugin's legacyCallback setting is turned on.
+ *
+ * The column stays in the database, unused: core's drizzle migrations run
+ * before this does, so dropping it there would lose the data first.
+ *
+ * Idempotent: a host or setting that already has a value is skipped, so this
+ * is safe on every boot.
+ */
+
+import { sql } from "drizzle-orm";
+import { databaseLogger } from "../logger.js";
+import {
+ createCurrentPluginRepository,
+ createCurrentPluginSettingsRepository,
+} from "../../database/repositories/factory.js";
+import { selectRows } from "./raw-rows.js";
+
+const PLUGIN_ID = "vault";
+const SETTING_KEY = "profileId";
+
+interface LegacyHostRow {
+ id: number;
+ vault_profile_id: number | string | null;
+}
+
+export interface VaultSettingsMigrationResult {
+ hostsMoved: number;
+ hostsSkipped: number;
+ legacyCallback: boolean;
+}
+
+async function legacyRows(): Promise {
+ try {
+ // Raw SQL: schema.ts no longer declares vault_profile_id.
+ return await selectRows(
+ sql`SELECT id, vault_profile_id FROM ssh_data WHERE vault_profile_id IS NOT NULL`,
+ );
+ } catch {
+ // A fresh install never had the column.
+ return [];
+ }
+}
+
+/** Whether 2.8 had any Vault profile, before or after the plugin adopted them. */
+async function hadProfiles(): Promise {
+ for (const table of ["vault_profiles", "p_vault_profiles"]) {
+ try {
+ const rows = await selectRows(
+ sql`SELECT 1 FROM ${sql.identifier(table)} LIMIT 1`,
+ );
+ if (rows.length > 0) return true;
+ } catch {
+ // table not there
+ }
+ }
+ return false;
+}
+
+export async function runVaultSettingsMigration(): Promise {
+ const result: VaultSettingsMigrationResult = {
+ hostsMoved: 0,
+ hostsSkipped: 0,
+ legacyCallback: false,
+ };
+
+ // plugin_settings has a foreign key to plugins: nothing to migrate into
+ // until the plugin row exists.
+ try {
+ const plugin = await createCurrentPluginRepository().findById(PLUGIN_ID);
+ if (!plugin) return result;
+ } catch {
+ return result;
+ }
+
+ const pluginSettings = createCurrentPluginSettingsRepository();
+ const rows = await legacyRows();
+
+ for (const row of rows) {
+ const profileId = Number(row.vault_profile_id);
+ if (!Number.isInteger(profileId) || profileId <= 0) continue;
+ const scopeId = String(row.id);
+ try {
+ const existing = await pluginSettings.get(
+ PLUGIN_ID,
+ "host",
+ scopeId,
+ SETTING_KEY,
+ );
+ if (existing) {
+ result.hostsSkipped++;
+ continue;
+ }
+ await pluginSettings.set(
+ PLUGIN_ID,
+ "host",
+ scopeId,
+ SETTING_KEY,
+ JSON.stringify(profileId),
+ );
+ result.hostsMoved++;
+ } catch (error) {
+ databaseLogger.warn("Vault host settings migration failed for a host", {
+ operation: "vault_settings_migration",
+ hostId: row.id,
+ error: error instanceof Error ? error.message : String(error),
+ });
+ }
+ }
+
+ // Decided once, on the first boot with the plugin: written either way, so
+ // profiles created on a fresh install later never turn it on.
+ try {
+ const existing = await pluginSettings.get(
+ PLUGIN_ID,
+ "admin",
+ null,
+ "legacyCallback",
+ );
+ if (!existing || existing.value === null) {
+ result.legacyCallback = rows.length > 0 || (await hadProfiles());
+ await pluginSettings.set(
+ PLUGIN_ID,
+ "admin",
+ null,
+ "legacyCallback",
+ JSON.stringify(result.legacyCallback),
+ );
+ }
+ } catch (error) {
+ databaseLogger.warn("Vault legacy callback migration failed", {
+ operation: "vault_settings_migration",
+ error: error instanceof Error ? error.message : String(error),
+ });
+ }
+
+ if (result.hostsMoved > 0) {
+ databaseLogger.info(
+ `Moved the Vault profile for ${result.hostsMoved} host(s) into plugin settings`,
+ { operation: "vault_settings_migration" },
+ );
+ }
+ return result;
+}
diff --git a/src/backend/utils/field-crypto.ts b/src/backend/utils/field-crypto.ts
index dc2ea890e..94eb6bff9 100644
--- a/src/backend/utils/field-crypto.ts
+++ b/src/backend/utils/field-crypto.ts
@@ -40,7 +40,6 @@ class FieldCrypto {
// webhook URLs, which are credentials like any other.
notification_channels: new Set(["config"]),
termix_identity_ca: new Set(["privateKey"]),
- vault_tokens: new Set(["sshCert", "privateKey"]),
};
static encryptField(
diff --git a/src/types/index.ts b/src/types/index.ts
index a1478d329..42c71ec53 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -152,8 +152,6 @@ export type Host = {
autostartKeyPassword?: string;
credentialId?: number;
- vaultProfileId?: number | null;
- vaultProfile?: { id?: number | null };
overrideCredentialUsername?: boolean;
userId?: string;
enableTerminal: boolean;
@@ -299,7 +297,6 @@ export interface HostData {
keyType?: string;
sudoPassword?: string;
credentialId?: number | null;
- vaultProfileId?: number | null;
connectionOrigin?: "local" | "remote" | null;
overrideCredentialUsername?: boolean;
enableTerminal?: boolean;
diff --git a/src/types/ui-types.ts b/src/types/ui-types.ts
index 057ad41be..2904fdf5c 100644
--- a/src/types/ui-types.ts
+++ b/src/types/ui-types.ts
@@ -35,7 +35,6 @@ export type Host = {
| "agent";
shareSshAuth?: boolean;
credentialId?: string;
- vaultProfileId?: string;
overrideCredentialUsername?: boolean;
password?: string;
hasPassword?: boolean;
@@ -200,26 +199,6 @@ export type Credential = {
permissionLevel?: "use" | "manage";
};
-// HashiCorp Vault SSH signer profile — shareable connection settings only
-// (no secrets). Users authenticate to Vault via OIDC at connect time.
-export type VaultProfile = {
- id: string;
- name: string;
- description?: string;
- folder?: string;
- tags?: string[];
- vaultAddr: string;
- vaultNamespace?: string;
- oidcMount?: string;
- oidcRole?: string;
- sshMount?: string;
- sshRole: string;
- validPrincipals?: string;
- keyType?: string;
- shared: boolean;
- owned: boolean;
-};
-
export type HostFolder = {
name: string;
children: (Host | HostFolder)[];
diff --git a/src/ui/api/vault-profiles-api.ts b/src/ui/api/vault-profiles-api.ts
deleted file mode 100644
index fbecea0e1..000000000
--- a/src/ui/api/vault-profiles-api.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { authApi, handleApiError } from "@/main-axios";
-
-export interface VaultProfilePayload {
- name: string;
- description?: string | null;
- folder?: string | null;
- tags?: string[];
- vaultAddr: string;
- vaultNamespace?: string | null;
- oidcMount?: string | null;
- oidcRole?: string | null;
- sshMount?: string | null;
- sshRole: string;
- validPrincipals?: string | null;
- keyType?: string | null;
- shared?: boolean;
-}
-
-export async function getVaultProfiles(): Promise[]> {
- try {
- const response = await authApi.get("/vault/profiles");
- return response.data;
- } catch (error) {
- throw handleApiError(error, "fetch vault profiles");
- }
-}
-
-export async function createVaultProfile(
- payload: VaultProfilePayload,
-): Promise> {
- try {
- const response = await authApi.post("/vault/profiles", payload);
- return response.data;
- } catch (error) {
- throw handleApiError(error, "create vault profile");
- }
-}
-
-export async function updateVaultProfile(
- id: number,
- payload: Partial,
-): Promise> {
- try {
- const response = await authApi.put(`/vault/profiles/${id}`, payload);
- return response.data;
- } catch (error) {
- throw handleApiError(error, "update vault profile");
- }
-}
-
-export async function deleteVaultProfile(
- id: number,
-): Promise> {
- try {
- const response = await authApi.delete(`/vault/profiles/${id}`);
- return response.data;
- } catch (error) {
- throw handleApiError(error, "delete vault profile");
- }
-}
diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json
index a3412f169..8b66c2f9a 100644
--- a/src/ui/locales/en.json
+++ b/src/ui/locales/en.json
@@ -607,30 +607,6 @@
"authenticationMethodDesc": "Select the one method Termix will use to authenticate this SSH connection. Other methods are not tried automatically.",
"storedCredential": "Stored Credential",
"selectACredential": "Select a credential...",
- "vaultProfile": "Vault Signer Profile",
- "selectAVaultProfile": "Select a Vault profile...",
- "vaultProfileHint": "Settings come from the shared profile; you'll sign in to Vault via OIDC when you connect. No secrets are stored.",
- "vaultNewProfile": "New profile",
- "vaultManageProfiles": "Manage Vault profiles",
- "vaultAddrLabel": "Vault Address",
- "vaultNamespaceLabel": "Namespace",
- "vaultOidcMountLabel": "OIDC Auth Mount",
- "vaultOidcRoleLabel": "OIDC Role",
- "vaultSshMountLabel": "SSH Secrets Mount",
- "vaultSshRoleLabel": "SSH Signer Role",
- "vaultValidPrincipalsLabel": "Valid Principals",
- "vaultKeyTypeLabel": "Ephemeral Key Type",
- "vaultSharedLabel": "Share with all users",
- "vaultCreateProfile": "Create profile",
- "vaultProfileCreated": "Vault profile created",
- "vaultProfileSaved": "Vault profile saved",
- "vaultProfileDeleted": "Vault profile deleted",
- "vaultProfileSaveFailed": "Failed to save Vault profile",
- "vaultProfileDeleteFailed": "Failed to delete Vault profile",
- "vaultSaveProfile": "Save profile",
- "vaultProfileValidationError": "Name, Vault address and SSH signer role are required",
- "vaultNoProfiles": "No Vault profiles yet.",
- "vaultSharedBadge": "shared",
"keyTypeLabel": "Key Type",
"keyTypeAuto": "Auto Detect",
"keyPasteTab": "Paste",
diff --git a/src/ui/locales/translated/af_ZA.json b/src/ui/locales/translated/af_ZA.json
index 0819a7c9f..ae138d2fd 100644
--- a/src/ui/locales/translated/af_ZA.json
+++ b/src/ui/locales/translated/af_ZA.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Kies die een metode wat Termix sal gebruik om hierdie SSH-verbinding te verifieer. Ander metodes word nie outomaties probeer nie.",
"storedCredential": "Gestoorde geloofsbriewe",
"selectACredential": "Kies 'n geloofsbrief...",
- "vaultProfile": "Kluis Ondertekenaar Profiel",
- "selectAVaultProfile": "Kies 'n kluisprofiel...",
- "vaultProfileHint": "Instellings kom van die gedeelde profiel; jy sal by Vault aanmeld via OIDC wanneer jy koppel. Geen geheime word gestoor nie.",
- "vaultNewProfile": "Nuwe profiel",
"secretRefHint": "Jy kan 'n 1Password-verwysing (op://vault/item/field) in plaas van die geheim plak; dit word van jou geheime bron afgehaal wanneer jy koppel.",
"secretSourcesManage": "Bestuur geheime bronne",
"secretSourcesTitle": "Geheime bronne",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Naam, bediener-URL en token word vereis",
"secretSourceSaved": "Geheime bron gestoor",
"secretSourceDeleted": "Geheime bron verwyder",
- "vaultManageProfiles": "Bestuur Vault-profiele",
- "vaultAddrLabel": "Kluisadres",
- "vaultNamespaceLabel": "Naamruimte",
- "vaultOidcMountLabel": "OIDC-magtigingsmontering",
- "vaultOidcRoleLabel": "OIDC-rol",
- "vaultSshMountLabel": "SSH-geheime berg",
- "vaultSshRoleLabel": "SSH-ondertekenaarrol",
- "vaultValidPrincipalsLabel": "Geldige Prinsipale",
- "vaultKeyTypeLabel": "Efemere Sleuteltipe",
- "vaultSharedLabel": "Deel met alle gebruikers",
- "vaultCreateProfile": "Skep profiel",
- "vaultProfileCreated": "Kluisprofiel geskep",
- "vaultProfileSaved": "Kluisprofiel gestoor",
- "vaultProfileDeleted": "Kluisprofiel uitgevee",
- "vaultProfileSaveFailed": "Kon nie kluisprofiel stoor nie",
- "vaultProfileDeleteFailed": "Kon nie kluisprofiel uitvee nie",
- "vaultSaveProfile": "Stoor profiel",
- "vaultProfileValidationError": "Naam, kluisadres en SSH-ondertekenrol word vereis",
- "vaultNoProfiles": "Geen Vault-profiele nog nie.",
- "vaultSharedBadge": "gedeel",
"keyTypeLabel": "Sleuteltipe",
"keyTypeAuto": "Outomatiese opsporing",
"keyPasteTab": "Plak",
diff --git a/src/ui/locales/translated/ar_SA.json b/src/ui/locales/translated/ar_SA.json
index 5a0c0ce59..80d62d3a6 100644
--- a/src/ui/locales/translated/ar_SA.json
+++ b/src/ui/locales/translated/ar_SA.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "اختر الطريقة التي سيستخدمها Termix للتحقق من صحة اتصال SSH هذا. لن يتم تجربة الطرق الأخرى تلقائيًا.",
"storedCredential": "بيانات اعتماد مخزنة",
"selectACredential": "اختيار بيانات اعتماد...",
- "vaultProfile": "ملف توقيع Vault",
- "selectAVaultProfile": "اختيار ملف توقيع Vault...",
- "vaultProfileHint": "تأتي الإعدادات من الملف المشترك؛ ستقوم بتسجيل الدخول إلى Vault عبر OIDC عند الاتصال. لا يتم تخزين أي أسرار.",
- "vaultNewProfile": "ملف جديد",
"secretRefHint": "يمكنك لصق مرجع 1Password (op://vault/item/field) بدلاً من السر؛ يتم جلبه من مصدر السر الخاص بك عند الاتصال.",
"secretSourcesManage": "إدارة المصادر السرية",
"secretSourcesTitle": "مصادر سرية",
@@ -739,26 +735,6 @@
"secretSourceRequired": "الاسم وعنوان URL للخادم والرمز المميز مطلوبة",
"secretSourceSaved": "تم حفظ المصدر السري",
"secretSourceDeleted": "تم حذف المصدر السري",
- "vaultManageProfiles": "إدارة ملفات Vault",
- "vaultAddrLabel": "عنوان Vault",
- "vaultNamespaceLabel": "مساحة الاسم",
- "vaultOidcMountLabel": "تثبيت مصادقة OIDC",
- "vaultOidcRoleLabel": "دور OIDC",
- "vaultSshMountLabel": "تثبيت أسرار SSH",
- "vaultSshRoleLabel": "دور موقع SSH",
- "vaultValidPrincipalsLabel": "المستخدمون المسموح بهم",
- "vaultKeyTypeLabel": "نوع المفتاح المؤقت",
- "vaultSharedLabel": "مشاركة مع جميع المستخدمين",
- "vaultCreateProfile": "إنشاء ملف تعريف",
- "vaultProfileCreated": "تم إنشاء ملف تعريف Vault",
- "vaultProfileSaved": "تم حفظ ملف تعريف Vault",
- "vaultProfileDeleted": "تم حذف ملف تعريف Vault",
- "vaultProfileSaveFailed": "فشل حفظ ملف تعريف Vault",
- "vaultProfileDeleteFailed": "فشل حذف ملف تعريف Vault",
- "vaultSaveProfile": "حفظ ملف التعريف",
- "vaultProfileValidationError": "الاسم وعنوان Vault ودور الموقّع SSH مطلوب",
- "vaultNoProfiles": "لا توجد ملفات تعريف Vault بعد.",
- "vaultSharedBadge": "مشترك",
"keyTypeLabel": "نوع المفتاح",
"keyTypeAuto": "اكتشاف تلقائي",
"keyPasteTab": "لصق",
diff --git a/src/ui/locales/translated/bg_BG.json b/src/ui/locales/translated/bg_BG.json
index a0e64506c..63b9aa33c 100644
--- a/src/ui/locales/translated/bg_BG.json
+++ b/src/ui/locales/translated/bg_BG.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Изберете единствения метод, който Termix ще използва за удостоверяване на тази SSH връзка. Други методи не се опитват автоматично.",
"storedCredential": "Съхранени идентификационни данни",
"selectACredential": "Изберете идентификационен номер...",
- "vaultProfile": "Профил на подписващ в трезора",
- "selectAVaultProfile": "Изберете профил в трезора...",
- "vaultProfileHint": "Настройките идват от споделения профил; ще влезете в Vault чрез OIDC, когато се свържете. Не се съхраняват тайни.",
- "vaultNewProfile": "Нов профил",
"secretRefHint": "Можете да поставите препратка към 1Password (op://vault/item/field) вместо тайната; тя се извлича от вашия източник на тайна при свързване.",
"secretSourcesManage": "Управлявайте тайните източници",
"secretSourcesTitle": "Тайни източници",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Име, URL адрес на сървъра и токен са задължителни",
"secretSourceSaved": "Секретният източник е запазен",
"secretSourceDeleted": "Секретният източник е изтрит",
- "vaultManageProfiles": "Управление на профили в хранилището",
- "vaultAddrLabel": "Адрес на трезора",
- "vaultNamespaceLabel": "Именно пространство",
- "vaultOidcMountLabel": "OIDC Authentication Mount",
- "vaultOidcRoleLabel": "Роля на OIDC",
- "vaultSshMountLabel": "SSH Secrets Mount",
- "vaultSshRoleLabel": "Роля на SSH подписващ",
- "vaultValidPrincipalsLabel": "Валидни принципали",
- "vaultKeyTypeLabel": "Тип ефимерен ключ",
- "vaultSharedLabel": "Споделяне с всички потребители",
- "vaultCreateProfile": "Създаване на профил",
- "vaultProfileCreated": "Профилът на трезора е създаден",
- "vaultProfileSaved": "Профилът на трезора е запазен",
- "vaultProfileDeleted": "Профилът в трезора е изтрит",
- "vaultProfileSaveFailed": "Запазването на профила в Трезора не бе успешно",
- "vaultProfileDeleteFailed": "Изтриването на профила в Vault не бе успешно",
- "vaultSaveProfile": "Запазване на профил",
- "vaultProfileValidationError": "Име, адрес на трезора и роля на SSH подписващ са задължителни",
- "vaultNoProfiles": "Все още няма профили в Трезора.",
- "vaultSharedBadge": "споделено",
"keyTypeLabel": "Тип ключ",
"keyTypeAuto": "Автоматично откриване",
"keyPasteTab": "Паста",
diff --git a/src/ui/locales/translated/bn_BD.json b/src/ui/locales/translated/bn_BD.json
index f8ef2b06f..287edcc2a 100644
--- a/src/ui/locales/translated/bn_BD.json
+++ b/src/ui/locales/translated/bn_BD.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "এই SSH সংযোগটি প্রমাণীকরণের জন্য Termix যে পদ্ধতিটি ব্যবহার করবে, সেটি নির্বাচন করুন। অন্য পদ্ধতিগুলো স্বয়ংক্রিয়ভাবে চেষ্টা করা হয় না।",
"storedCredential": "সংরক্ষিত পরিচয়পত্র",
"selectACredential": "একটি শংসাপত্র নির্বাচন করুন...",
- "vaultProfile": "ভল্ট স্বাক্ষরকারীর প্রোফাইল",
- "selectAVaultProfile": "একটি ভল্ট প্রোফাইল নির্বাচন করুন...",
- "vaultProfileHint": "সেটিংস শেয়ার করা প্রোফাইল থেকে আসে; সংযোগ করার সময় আপনি OIDC-এর মাধ্যমে ভল্টে সাইন ইন করবেন। কোনো গোপনীয় তথ্য সংরক্ষিত থাকে না।",
- "vaultNewProfile": "নতুন প্রোফাইল",
"secretRefHint": "আপনি সিক্রেটের পরিবর্তে একটি 1Password রেফারেন্স (op://vault/item/field) পেস্ট করতে পারেন; সংযোগ করার সময় এটি আপনার সিক্রেট সোর্স থেকে নিয়ে আসা হয়।",
"secretSourcesManage": "গোপন উৎস পরিচালনা করুন",
"secretSourcesTitle": "গোপন সূত্র",
@@ -739,26 +735,6 @@
"secretSourceRequired": "নাম, সার্ভার ইউআরএল এবং টোকেন প্রয়োজন।",
"secretSourceSaved": "গোপন উৎস সংরক্ষিত",
"secretSourceDeleted": "গোপন সূত্র মুছে ফেলা হয়েছে",
- "vaultManageProfiles": "ভল্ট প্রোফাইলগুলি পরিচালনা করুন",
- "vaultAddrLabel": "ভল্ট ঠিকানা",
- "vaultNamespaceLabel": "নেমস্পেস",
- "vaultOidcMountLabel": "OIDC অথোরাইজেশন মাউন্ট",
- "vaultOidcRoleLabel": "OIDC ভূমিকা",
- "vaultSshMountLabel": "SSH সিক্রেটস মাউন্ট",
- "vaultSshRoleLabel": "SSH স্বাক্ষরকারীর ভূমিকা",
- "vaultValidPrincipalsLabel": "বৈধ অধ্যক্ষরা",
- "vaultKeyTypeLabel": "ক্ষণস্থায়ী কী টাইপ",
- "vaultSharedLabel": "সকল ব্যবহারকারীর সাথে শেয়ার করুন",
- "vaultCreateProfile": "প্রোফাইল তৈরি করুন",
- "vaultProfileCreated": "ভল্ট প্রোফাইল তৈরি করা হয়েছে",
- "vaultProfileSaved": "ভল্ট প্রোফাইল সংরক্ষিত হয়েছে",
- "vaultProfileDeleted": "ভল্ট প্রোফাইল মুছে ফেলা হয়েছে",
- "vaultProfileSaveFailed": "ভল্ট প্রোফাইল সংরক্ষণ করতে ব্যর্থ হয়েছে",
- "vaultProfileDeleteFailed": "ভল্ট প্রোফাইল মুছে ফেলা ব্যর্থ হয়েছে",
- "vaultSaveProfile": "প্রোফাইল সংরক্ষণ করুন",
- "vaultProfileValidationError": "নাম, ভল্ট অ্যাড্রেস এবং SSH সাইনার রোল প্রয়োজন।",
- "vaultNoProfiles": "এখনো কোনো ভল্ট প্রোফাইল তৈরি হয়নি।",
- "vaultSharedBadge": "শেয়ার করা",
"keyTypeLabel": "চাবির ধরন",
"keyTypeAuto": "স্বয়ংক্রিয়ভাবে সনাক্ত করুন",
"keyPasteTab": "পেস্ট",
diff --git a/src/ui/locales/translated/ca_ES.json b/src/ui/locales/translated/ca_ES.json
index a7a8dfa10..8a95e0b0e 100644
--- a/src/ui/locales/translated/ca_ES.json
+++ b/src/ui/locales/translated/ca_ES.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Seleccioneu el mètode que Termix utilitzarà per autenticar aquesta connexió SSH. Els altres mètodes no es proven automàticament.",
"storedCredential": "Credencial emmagatzemada",
"selectACredential": "Selecciona una credencial...",
- "vaultProfile": "Perfil del signant de la caixa forta",
- "selectAVaultProfile": "Seleccioneu un perfil de Vault...",
- "vaultProfileHint": "La configuració prové del perfil compartit; iniciaràs la sessió a Vault mitjançant OIDC quan et connectis. No s'emmagatzemen secrets.",
- "vaultNewProfile": "Nou perfil",
"secretRefHint": "Podeu enganxar una referència 1Password (op://vault/item/field) en comptes del secret; s'obté de la font del secret quan us connecteu.",
"secretSourcesManage": "Gestiona fonts secretes",
"secretSourcesTitle": "Fonts secretes",
@@ -739,26 +735,6 @@
"secretSourceRequired": "El nom, l'URL del servidor i el testimoni són obligatoris",
"secretSourceSaved": "Font secreta desada",
"secretSourceDeleted": "Font secreta suprimida",
- "vaultManageProfiles": "Gestiona els perfils de Vault",
- "vaultAddrLabel": "Adreça de la caixa forta",
- "vaultNamespaceLabel": "Espai de noms",
- "vaultOidcMountLabel": "Muntatge d'autenticació OIDC",
- "vaultOidcRoleLabel": "Rol de l'OIDC",
- "vaultSshMountLabel": "Muntanya de Secrets SSH",
- "vaultSshRoleLabel": "Rol de signant SSH",
- "vaultValidPrincipalsLabel": "Principals vàlids",
- "vaultKeyTypeLabel": "Tipus de clau efímera",
- "vaultSharedLabel": "Comparteix amb tots els usuaris",
- "vaultCreateProfile": "Crea un perfil",
- "vaultProfileCreated": "Perfil de la caixa forta creat",
- "vaultProfileSaved": "Perfil de la caixa forta desat",
- "vaultProfileDeleted": "Perfil de la caixa forta suprimit",
- "vaultProfileSaveFailed": "No s'ha pogut desar el perfil de Vault",
- "vaultProfileDeleteFailed": "No s'ha pogut suprimir el perfil de Vault",
- "vaultSaveProfile": "Desa el perfil",
- "vaultProfileValidationError": "El nom, l'adreça de la caixa forta i el rol de signant SSH són obligatoris.",
- "vaultNoProfiles": "Encara no hi ha perfils de Vault.",
- "vaultSharedBadge": "compartit",
"keyTypeLabel": "Tipus de clau",
"keyTypeAuto": "Detecció automàtica",
"keyPasteTab": "Enganxa",
diff --git a/src/ui/locales/translated/cs_CZ.json b/src/ui/locales/translated/cs_CZ.json
index d281c86f9..94c7833fa 100644
--- a/src/ui/locales/translated/cs_CZ.json
+++ b/src/ui/locales/translated/cs_CZ.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Vyberte jednu metodu, kterou Termix použije k ověření tohoto SSH připojení. Ostatní metody se nebudou automaticky zkoušet.",
"storedCredential": "Uložené přihlašovací údaje",
"selectACredential": "Vyberte pověření...",
- "vaultProfile": "Profil podepisujícího trezoru",
- "selectAVaultProfile": "Vyberte profil úložiště...",
- "vaultProfileHint": "Nastavení pocházejí ze sdíleného profilu; po připojení se do úložiště přihlásíte přes OIDC. Žádné tajné kódy se neukládají.",
- "vaultNewProfile": "Nový profil",
"secretRefHint": "Místo tajného hesla můžete vložit odkaz na 1Password (op://vault/item/field); při připojování se načte ze zdroje vašeho tajného hesla.",
"secretSourcesManage": "Správa tajných zdrojů",
"secretSourcesTitle": "Tajné zdroje",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Jméno, URL adresa serveru a token jsou povinné",
"secretSourceSaved": "Tajný zdroj uložen",
"secretSourceDeleted": "Tajný zdroj smazán",
- "vaultManageProfiles": "Správa profilů úložiště",
- "vaultAddrLabel": "Adresa trezoru",
- "vaultNamespaceLabel": "Jmenný prostor",
- "vaultOidcMountLabel": "Připojení autorizace OIDC",
- "vaultOidcRoleLabel": "Role OIDC",
- "vaultSshMountLabel": "SSH tajemství Mount",
- "vaultSshRoleLabel": "Role SSH podepisujícího",
- "vaultValidPrincipalsLabel": "Platné principy",
- "vaultKeyTypeLabel": "Typ dočasného klíče",
- "vaultSharedLabel": "Sdílet se všemi uživateli",
- "vaultCreateProfile": "Vytvořit profil",
- "vaultProfileCreated": "Profil úložiště vytvořen",
- "vaultProfileSaved": "Profil trezoru uložen",
- "vaultProfileDeleted": "Profil trezoru smazán",
- "vaultProfileSaveFailed": "Uložení profilu úložiště se nezdařilo.",
- "vaultProfileDeleteFailed": "Profil v úložišti se nepodařilo smazat.",
- "vaultSaveProfile": "Uložit profil",
- "vaultProfileValidationError": "Jméno, adresa trezoru a role SSH signatáře jsou povinné.",
- "vaultNoProfiles": "Zatím žádné profily v úložišti Vault.",
- "vaultSharedBadge": "sdílené",
"keyTypeLabel": "Typ klíče",
"keyTypeAuto": "Automatická detekce",
"keyPasteTab": "Pasta",
diff --git a/src/ui/locales/translated/da_DK.json b/src/ui/locales/translated/da_DK.json
index 3864d9f41..65eb97dc3 100644
--- a/src/ui/locales/translated/da_DK.json
+++ b/src/ui/locales/translated/da_DK.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Vælg den metode, som Termix vil bruge til at godkende denne SSH-forbindelse. Andre metoder afprøves ikke automatisk.",
"storedCredential": "Gemte legitimationsoplysninger",
"selectACredential": "Vælg en legitimationsoplysninger...",
- "vaultProfile": "Profil for underskriver af arkiv",
- "selectAVaultProfile": "Vælg en Vault-profil...",
- "vaultProfileHint": "Indstillingerne kommer fra den delte profil; du logger ind på Vault via OIDC, når du opretter forbindelse. Der gemmes ingen hemmeligheder.",
- "vaultNewProfile": "Ny profil",
"secretRefHint": "Du kan indsætte en 1Password-reference (op://vault/item/field) i stedet for hemmeligheden; den hentes fra din hemmelige kilde, når du opretter forbindelse.",
"secretSourcesManage": "Administrer hemmelige kilder",
"secretSourcesTitle": "Hemmelige kilder",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Navn, server-URL og token er påkrævet",
"secretSourceSaved": "Hemmelig kilde gemt",
"secretSourceDeleted": "Hemmelig kilde slettet",
- "vaultManageProfiles": "Administrer Vault-profiler",
- "vaultAddrLabel": "Adresse i boksen",
- "vaultNamespaceLabel": "Navnerum",
- "vaultOidcMountLabel": "OIDC-godkendelsesmontering",
- "vaultOidcRoleLabel": "OIDC-rolle",
- "vaultSshMountLabel": "SSH-hemmeligheder monteres",
- "vaultSshRoleLabel": "SSH-underskriverrolle",
- "vaultValidPrincipalsLabel": "Gyldige principper",
- "vaultKeyTypeLabel": "Kortvarig nøgletype",
- "vaultSharedLabel": "Del med alle brugere",
- "vaultCreateProfile": "Opret profil",
- "vaultProfileCreated": "Vault-profil oprettet",
- "vaultProfileSaved": "Vault-profil gemt",
- "vaultProfileDeleted": "Vault-profil slettet",
- "vaultProfileSaveFailed": "Kunne ikke gemme Vault-profilen",
- "vaultProfileDeleteFailed": "Kunne ikke slette Vault-profilen",
- "vaultSaveProfile": "Gem profil",
- "vaultProfileValidationError": "Navn, Vault-adresse og SSH-underskriverrolle er påkrævet",
- "vaultNoProfiles": "Ingen Vault-profiler endnu.",
- "vaultSharedBadge": "delt",
"keyTypeLabel": "Nøgletype",
"keyTypeAuto": "Automatisk registrering",
"keyPasteTab": "Indsæt",
diff --git a/src/ui/locales/translated/de_DE.json b/src/ui/locales/translated/de_DE.json
index c32988adf..ea825114d 100644
--- a/src/ui/locales/translated/de_DE.json
+++ b/src/ui/locales/translated/de_DE.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Wählen Sie die Methode aus, die Termix zur Authentifizierung dieser SSH-Verbindung verwenden soll. Andere Methoden werden nicht automatisch ausprobiert.",
"storedCredential": "Gespeicherte Anmeldedaten",
"selectACredential": "Anmeldedaten auswählen...",
- "vaultProfile": "Vault-Signer-Profil",
- "selectAVaultProfile": "Vault-Profil auswählen...",
- "vaultProfileHint": "Die Einstellungen stammen aus dem freigegebenen Profil; Sie melden sich beim Verbinden per OIDC bei Vault an. Es werden keine Geheimnisse gespeichert.",
- "vaultNewProfile": "Neues Profil",
"secretRefHint": "Sie können anstelle des Geheimnisses eine 1Password-Referenz (op://vault/item/field) einfügen; diese wird beim Verbindungsaufbau aus Ihrer Geheimnisquelle abgerufen.",
"secretSourcesManage": "Geheime Quellen verwalten",
"secretSourcesTitle": "Geheime Quellen",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Name, Server-URL und Token werden benötigt.",
"secretSourceSaved": "Geheime Quelle gerettet",
"secretSourceDeleted": "Geheime Quelle gelöscht",
- "vaultManageProfiles": "Vault-Profile verwalten",
- "vaultAddrLabel": "Vault-Adresse",
- "vaultNamespaceLabel": "Namespace",
- "vaultOidcMountLabel": "OIDC-Auth-Mount",
- "vaultOidcRoleLabel": "OIDC-Rolle",
- "vaultSshMountLabel": "SSH-Secrets-Mount",
- "vaultSshRoleLabel": "SSH-Signer-Rolle",
- "vaultValidPrincipalsLabel": "Gültige Principals",
- "vaultKeyTypeLabel": "Ephemerer Schlüsseltyp",
- "vaultSharedLabel": "Mit allen Benutzern teilen",
- "vaultCreateProfile": "Profil erstellen",
- "vaultProfileCreated": "Vault-Profil erstellt",
- "vaultProfileSaved": "Vault-Profil gespeichert",
- "vaultProfileDeleted": "Vault-Profil gelöscht",
- "vaultProfileSaveFailed": "Fehler beim Speichern des Vault-Profils",
- "vaultProfileDeleteFailed": "Fehler beim Löschen des Vault-Profils",
- "vaultSaveProfile": "Profil speichern",
- "vaultProfileValidationError": "Name, Vault-Adresse und SSH-Signer-Rolle sind erforderlich",
- "vaultNoProfiles": "Noch keine Vault-Profile.",
- "vaultSharedBadge": "geteilt",
"keyTypeLabel": "Schlüsseltyp",
"keyTypeAuto": "Automatisch erkennen",
"keyPasteTab": "Einfügen",
diff --git a/src/ui/locales/translated/el_GR.json b/src/ui/locales/translated/el_GR.json
index 7e5672961..34962f00d 100644
--- a/src/ui/locales/translated/el_GR.json
+++ b/src/ui/locales/translated/el_GR.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Επιλέξτε τη μέθοδο που θα χρησιμοποιήσει το Termix για τον έλεγχο ταυτότητας αυτής της σύνδεσης SSH. Άλλες μέθοδοι δεν δοκιμάζονται αυτόματα.",
"storedCredential": "Αποθηκευμένα διαπιστευτήρια",
"selectACredential": "Επιλέξτε ένα πιστοποιητικό...",
- "vaultProfile": "Προφίλ Υπογράφοντος Vault",
- "selectAVaultProfile": "Επιλέξτε ένα προφίλ Vault...",
- "vaultProfileHint": "Οι ρυθμίσεις προέρχονται από το κοινόχρηστο προφίλ. Θα συνδεθείτε στο Vault μέσω OIDC όταν συνδεθείτε. Δεν αποθηκεύονται μυστικά.",
- "vaultNewProfile": "Νέο προφίλ",
"secretRefHint": "Μπορείτε να επικολλήσετε μια αναφορά 1Password (op://vault/item/field) αντί για το μυστικό. Αυτό ανακτάται από την μυστική σας πηγή κατά τη σύνδεση.",
"secretSourcesManage": "Διαχείριση μυστικών πηγών",
"secretSourcesTitle": "Μυστικές πηγές",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Απαιτούνται όνομα, διεύθυνση URL διακομιστή και διακριτικό",
"secretSourceSaved": "Η μυστική πηγή αποθηκεύτηκε",
"secretSourceDeleted": "Η μυστική πηγή διαγράφηκε",
- "vaultManageProfiles": "Διαχείριση προφίλ Vault",
- "vaultAddrLabel": "Διεύθυνση θησαυροφυλακίου",
- "vaultNamespaceLabel": "Χώρος ονομάτων",
- "vaultOidcMountLabel": "Μοντάζ ελέγχου ταυτότητας OIDC",
- "vaultOidcRoleLabel": "Ρόλος OIDC",
- "vaultSshMountLabel": "SSH Secrets Mount",
- "vaultSshRoleLabel": "Ρόλος Υπογράφοντος SSH",
- "vaultValidPrincipalsLabel": "Έγκυρες Αρχές",
- "vaultKeyTypeLabel": "Εφήμερος Τύπος Κλειδιού",
- "vaultSharedLabel": "Κοινοποίηση με όλους τους χρήστες",
- "vaultCreateProfile": "Δημιουργία προφίλ",
- "vaultProfileCreated": "Δημιουργήθηκε προφίλ Vault",
- "vaultProfileSaved": "Το προφίλ του Vault αποθηκεύτηκε",
- "vaultProfileDeleted": "Το προφίλ Vault διαγράφηκε",
- "vaultProfileSaveFailed": "Αποτυχία αποθήκευσης προφίλ Vault",
- "vaultProfileDeleteFailed": "Αποτυχία διαγραφής προφίλ Vault",
- "vaultSaveProfile": "Αποθήκευση προφίλ",
- "vaultProfileValidationError": "Απαιτούνται όνομα, διεύθυνση vault και ρόλος υπογράφοντος SSH",
- "vaultNoProfiles": "Δεν υπάρχουν ακόμη προφίλ Vault.",
- "vaultSharedBadge": "κοινόχρηστο",
"keyTypeLabel": "Τύπος κλειδιού",
"keyTypeAuto": "Αυτόματη ανίχνευση",
"keyPasteTab": "Πάστα",
diff --git a/src/ui/locales/translated/es_ES.json b/src/ui/locales/translated/es_ES.json
index ab8f19ee8..aed162ff9 100644
--- a/src/ui/locales/translated/es_ES.json
+++ b/src/ui/locales/translated/es_ES.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Seleccione el método que Termix utilizará para autenticar esta conexión SSH. Los demás métodos no se intentarán automáticamente.",
"storedCredential": "Credencial almacenada",
"selectACredential": "Seleccionar una credencial...",
- "vaultProfile": "Perfil de firmante de Vault",
- "selectAVaultProfile": "Seleccionar un perfil de Vault...",
- "vaultProfileHint": "La configuración proviene del perfil compartido; se iniciará sesión en Vault a través de OIDC al conectarse. No se almacenan secretos.",
- "vaultNewProfile": "Nuevo perfil",
"secretRefHint": "Puedes pegar una referencia de 1Password (op://vault/item/field) en lugar de la clave secreta; esta se obtiene de tu fuente de claves secretas al conectarte.",
"secretSourcesManage": "Gestionar fuentes secretas",
"secretSourcesTitle": "Fuentes secretas",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Se requieren nombre, URL del servidor y token.",
"secretSourceSaved": "Fuente secreta guardada",
"secretSourceDeleted": "Fuente secreta eliminada",
- "vaultManageProfiles": "Administrar perfiles de Vault",
- "vaultAddrLabel": "Dirección de Vault",
- "vaultNamespaceLabel": "Espacio de nombres",
- "vaultOidcMountLabel": "Montaje de autenticación OIDC",
- "vaultOidcRoleLabel": "Rol OIDC",
- "vaultSshMountLabel": "Montaje de secretos SSH",
- "vaultSshRoleLabel": "Rol de firmante SSH",
- "vaultValidPrincipalsLabel": "Principals válidos",
- "vaultKeyTypeLabel": "Tipo de clave efímera",
- "vaultSharedLabel": "Compartir con todos los usuarios",
- "vaultCreateProfile": "Crear perfil",
- "vaultProfileCreated": "Perfil de Vault creado",
- "vaultProfileSaved": "Perfil de Vault guardado",
- "vaultProfileDeleted": "Perfil de Vault eliminado",
- "vaultProfileSaveFailed": "Error al guardar el perfil de Vault",
- "vaultProfileDeleteFailed": "Error al eliminar el perfil de Vault",
- "vaultSaveProfile": "Guardar perfil",
- "vaultProfileValidationError": "Se requieren el nombre, la dirección de Vault y el rol de firmante SSH",
- "vaultNoProfiles": "Aún no hay perfiles de Vault.",
- "vaultSharedBadge": "compartido",
"keyTypeLabel": "Tipo de clave",
"keyTypeAuto": "Detección automática",
"keyPasteTab": "Pegar",
diff --git a/src/ui/locales/translated/fi_FI.json b/src/ui/locales/translated/fi_FI.json
index a05bcb8cb..8615edf3a 100644
--- a/src/ui/locales/translated/fi_FI.json
+++ b/src/ui/locales/translated/fi_FI.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Valitse yksi menetelmä, jota Termix käyttää tämän SSH-yhteyden todentamiseen. Muita menetelmiä ei kokeilla automaattisesti.",
"storedCredential": "Tallennetut tunnistetiedot",
"selectACredential": "Valitse tunnistetiedot...",
- "vaultProfile": "Varaston allekirjoittajan profiili",
- "selectAVaultProfile": "Valitse holviprofiili...",
- "vaultProfileHint": "Asetukset tulevat jaetusta profiilista; kirjaudut Holviin OIDC:n kautta, kun muodostat yhteyden. Salaisuuksia ei tallenneta.",
- "vaultNewProfile": "Uusi profiili",
"secretRefHint": "Voit liittää 1Password-viittauksen (op://vault/item/field) salaisuuden sijaan; se noudetaan salaisuuden lähteestäsi yhteyden muodostamisen yhteydessä.",
"secretSourcesManage": "Hallitse salaisia lähteitä",
"secretSourcesTitle": "Salaiset lähteet",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Nimi, palvelimen URL-osoite ja tunnus vaaditaan",
"secretSourceSaved": "Salainen lähde tallennettu",
"secretSourceDeleted": "Salainen lähde poistettu",
- "vaultManageProfiles": "Hallinnoi holviprofiileja",
- "vaultAddrLabel": "Holvin osoite",
- "vaultNamespaceLabel": "Nimiavaruus",
- "vaultOidcMountLabel": "OIDC-todennusliitos",
- "vaultOidcRoleLabel": "OIDC-rooli",
- "vaultSshMountLabel": "SSH-salaisuuksien kiinnitys",
- "vaultSshRoleLabel": "SSH-allekirjoittajan rooli",
- "vaultValidPrincipalsLabel": "Voimassa olevat pääasialliset",
- "vaultKeyTypeLabel": "Lyhytaikainen avaintyyppi",
- "vaultSharedLabel": "Jaa kaikkien käyttäjien kanssa",
- "vaultCreateProfile": "Luo profiili",
- "vaultProfileCreated": "Holvin profiili luotu",
- "vaultProfileSaved": "Holvin profiili tallennettu",
- "vaultProfileDeleted": "Holvin profiili poistettu",
- "vaultProfileSaveFailed": "Holvin profiilin tallentaminen epäonnistui",
- "vaultProfileDeleteFailed": "Holviprofiilin poistaminen epäonnistui",
- "vaultSaveProfile": "Tallenna profiili",
- "vaultProfileValidationError": "Nimi, holvin osoite ja SSH-allekirjoittajan rooli vaaditaan",
- "vaultNoProfiles": "Ei vielä holviprofiileja.",
- "vaultSharedBadge": "jaettu",
"keyTypeLabel": "Avaimen tyyppi",
"keyTypeAuto": "Automaattinen tunnistus",
"keyPasteTab": "Liitä",
diff --git a/src/ui/locales/translated/fr_FR.json b/src/ui/locales/translated/fr_FR.json
index 5d489d97f..71b54b7cb 100644
--- a/src/ui/locales/translated/fr_FR.json
+++ b/src/ui/locales/translated/fr_FR.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Sélectionnez la méthode que Termix utilisera pour authentifier cette connexion SSH. Les autres méthodes ne seront pas essayées automatiquement.",
"storedCredential": "Identifiant enregistré",
"selectACredential": "Sélectionner un identifiant...",
- "vaultProfile": "Profil de signature Vault",
- "selectAVaultProfile": "Sélectionner un profil Vault...",
- "vaultProfileHint": "Les paramètres proviennent du profil partagé ; vous vous connecterez à Vault via OIDC lors de la connexion. Aucun secret n'est stocké.",
- "vaultNewProfile": "Nouveau profil",
"secretRefHint": "Vous pouvez coller une référence 1Password (op://vault/item/field) à la place du secret ; elle sera récupérée depuis votre source de secret lors de la connexion.",
"secretSourcesManage": "Gérer les sources secrètes",
"secretSourcesTitle": "Sources secrètes",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Le nom, l'URL du serveur et le jeton sont requis.",
"secretSourceSaved": "Source secrète sauvegardée",
"secretSourceDeleted": "Source secrète supprimée",
- "vaultManageProfiles": "Gérer les profils Vault",
- "vaultAddrLabel": "Adresse Vault",
- "vaultNamespaceLabel": "Espace de noms",
- "vaultOidcMountLabel": "Point de montage d'authentification OIDC",
- "vaultOidcRoleLabel": "Rôle OIDC",
- "vaultSshMountLabel": "Point de montage des secrets SSH",
- "vaultSshRoleLabel": "Rôle de signature SSH",
- "vaultValidPrincipalsLabel": "Principaux valides",
- "vaultKeyTypeLabel": "Type de clé éphémère",
- "vaultSharedLabel": "Partager avec tous les utilisateurs",
- "vaultCreateProfile": "Créer un profil",
- "vaultProfileCreated": "Profil Vault créé",
- "vaultProfileSaved": "Profil Vault enregistré",
- "vaultProfileDeleted": "Profil Vault supprimé",
- "vaultProfileSaveFailed": "Échec de l'enregistrement du profil Vault",
- "vaultProfileDeleteFailed": "Échec de la suppression du profil Vault",
- "vaultSaveProfile": "Enregistrer le profil",
- "vaultProfileValidationError": "Le nom, l'adresse Vault et le rôle de signataire SSH sont requis",
- "vaultNoProfiles": "Aucun profil Vault pour le moment",
- "vaultSharedBadge": "partagé",
"keyTypeLabel": "Type de clé",
"keyTypeAuto": "Détection automatique",
"keyPasteTab": "Coller",
diff --git a/src/ui/locales/translated/he_IL.json b/src/ui/locales/translated/he_IL.json
index 6d9a49bef..44396ea89 100644
--- a/src/ui/locales/translated/he_IL.json
+++ b/src/ui/locales/translated/he_IL.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "בחר את השיטה שבה Termix ישתמש כדי לאמת חיבור SSH זה. שיטות אחרות לא ינוסו אוטומטית.",
"storedCredential": "אישורים מאוחסנים",
"selectACredential": "בחר אישור...",
- "vaultProfile": "פרופיל חותם בכספת",
- "selectAVaultProfile": "בחר פרופיל כספת...",
- "vaultProfileHint": "ההגדרות מגיעות מהפרופיל המשותף; תיכנס לכספת דרך OIDC כשתתחבר. לא נשמרים סודות.",
- "vaultNewProfile": "פרופיל חדש",
"secretRefHint": "ניתן להדביק הפניה ל-1Password (op://vault/item/field) במקום הסוד; היא נלקחת ממקור הסוד שלך בעת ההתחברות.",
"secretSourcesManage": "ניהול מקורות סודיים",
"secretSourcesTitle": "מקורות סודיים",
@@ -739,26 +735,6 @@
"secretSourceRequired": "נדרשים שם, כתובת URL של השרת וטופס",
"secretSourceSaved": "מקור סודי נשמר",
"secretSourceDeleted": "מקור סודי נמחק",
- "vaultManageProfiles": "ניהול פרופילי כספת",
- "vaultAddrLabel": "כתובת הכספת",
- "vaultNamespaceLabel": "מרחב השמות",
- "vaultOidcMountLabel": "אישור OIDC",
- "vaultOidcRoleLabel": "תפקיד OIDC",
- "vaultSshMountLabel": "סודות SSH מאוורר",
- "vaultSshRoleLabel": "תפקיד חותם SSH",
- "vaultValidPrincipalsLabel": "עקרונות תקפים",
- "vaultKeyTypeLabel": "סוג מפתח זמני",
- "vaultSharedLabel": "שתף עם כל המשתמשים",
- "vaultCreateProfile": "צור פרופיל",
- "vaultProfileCreated": "פרופיל הכספת נוצר",
- "vaultProfileSaved": "פרופיל הכספת נשמר",
- "vaultProfileDeleted": "פרופיל הכספת נמחק",
- "vaultProfileSaveFailed": "שמירת פרופיל הכספת נכשלה",
- "vaultProfileDeleteFailed": "נכשלה מחיקת פרופיל הכספת",
- "vaultSaveProfile": "שמור פרופיל",
- "vaultProfileValidationError": "שם, כתובת כספת ותפקיד חתימה ב-SSH נדרשים",
- "vaultNoProfiles": "אין עדיין פרופילים בכספת.",
- "vaultSharedBadge": "מְשׁוּתָף",
"keyTypeLabel": "סוג מפתח",
"keyTypeAuto": "זיהוי אוטומטי",
"keyPasteTab": "לְהַדבִּיק",
diff --git a/src/ui/locales/translated/hi_IN.json b/src/ui/locales/translated/hi_IN.json
index 35c8f1673..be330b94d 100644
--- a/src/ui/locales/translated/hi_IN.json
+++ b/src/ui/locales/translated/hi_IN.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "वह तरीका चुनें जिसका उपयोग टर्मिक्स इस एसएसएच कनेक्शन को प्रमाणित करने के लिए करेगा। अन्य तरीकों को स्वचालित रूप से नहीं आजमाया जाएगा।",
"storedCredential": "संग्रहीत क्रेडेंशियल",
"selectACredential": "एक क्रेडेंशियल चुनें...",
- "vaultProfile": "Vault साइनर प्रोफ़ाइल",
- "selectAVaultProfile": "एक Vault प्रोफ़ाइल चुनें...",
- "vaultProfileHint": "सेटिंग साझा प्रोफ़ाइल से आती हैं; कनेक्ट करते समय आप OIDC के माध्यम से Vault में साइन इन करेंगे। कोई सीक्रेट संग्रहीत नहीं किए जाते।",
- "vaultNewProfile": "नई प्रोफ़ाइल",
"secretRefHint": "आप गुप्त पासवर्ड के स्थान पर 1Password संदर्भ (op://vault/item/field) पेस्ट कर सकते हैं; कनेक्ट करते समय इसे आपके गुप्त पासवर्ड स्रोत से प्राप्त किया जाता है।",
"secretSourcesManage": "गुप्त स्रोतों का प्रबंधन करें",
"secretSourcesTitle": "गुप्त सूत्रों",
@@ -739,26 +735,6 @@
"secretSourceRequired": "नाम, सर्वर यूआरएल और टोकन आवश्यक हैं।",
"secretSourceSaved": "गुप्त स्रोत सुरक्षित",
"secretSourceDeleted": "गुप्त सूत्र हटा दिया गया",
- "vaultManageProfiles": "Vault प्रोफ़ाइल प्रबंधित करें",
- "vaultAddrLabel": "Vault पता",
- "vaultNamespaceLabel": "नेमस्पेस",
- "vaultOidcMountLabel": "OIDC ऑथ माउंट",
- "vaultOidcRoleLabel": "OIDC रोल",
- "vaultSshMountLabel": "SSH सीक्रेट माउंट",
- "vaultSshRoleLabel": "SSH साइनर रोल",
- "vaultValidPrincipalsLabel": "मान्य प्रिंसिपल",
- "vaultKeyTypeLabel": "एफेमरल कुंजी प्रकार",
- "vaultSharedLabel": "सभी उपयोगकर्ताओं के साथ साझा करें",
- "vaultCreateProfile": "प्रोफ़ाइल बनाएँ",
- "vaultProfileCreated": "Vault प्रोफ़ाइल बनाई गई",
- "vaultProfileSaved": "Vault प्रोफ़ाइल सहेजी गई",
- "vaultProfileDeleted": "Vault प्रोफ़ाइल हटाई गई",
- "vaultProfileSaveFailed": "Vault प्रोफ़ाइल सहेजने में विफल",
- "vaultProfileDeleteFailed": "Vault प्रोफ़ाइल हटाने में विफल",
- "vaultSaveProfile": "प्रोफ़ाइल सहेजें",
- "vaultProfileValidationError": "नाम, Vault पता और SSH हस्ताक्षरकर्ता भूमिका आवश्यक हैं",
- "vaultNoProfiles": "अभी तक कोई Vault प्रोफ़ाइल नहीं है।",
- "vaultSharedBadge": "साझा",
"keyTypeLabel": "कुंजी प्रकार",
"keyTypeAuto": "स्वतः पहचानें",
"keyPasteTab": "चिपकाएँ",
diff --git a/src/ui/locales/translated/hu_HU.json b/src/ui/locales/translated/hu_HU.json
index d84d07051..44dec1a04 100644
--- a/src/ui/locales/translated/hu_HU.json
+++ b/src/ui/locales/translated/hu_HU.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Válassza ki azt az egy módszert, amelyet a Termix az SSH-kapcsolat hitelesítéséhez használ. A többi módszert a rendszer nem próbálja ki automatikusan.",
"storedCredential": "Tárolt hitelesítő adatok",
"selectACredential": "Válasszon hitelesítő adatot...",
- "vaultProfile": "Vault aláírói profil",
- "selectAVaultProfile": "Válasszon ki egy Vault profilt...",
- "vaultProfileHint": "A beállítások a megosztott profilból származnak; a csatlakozáskor OIDC-n keresztül fog bejelentkezni a Vaultba. A rendszer nem tárol titkos adatokat.",
- "vaultNewProfile": "Új profil",
"secretRefHint": "A titkos kód helyett beilleszthet egy 1Password hivatkozást (op://vault/item/field); ezt a titkos kód forrásából kéri le a rendszer a csatlakozáskor.",
"secretSourcesManage": "Titkos források kezelése",
"secretSourcesTitle": "Titkos források",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Név, szerver URL és token megadása kötelező",
"secretSourceSaved": "Titkos forrás mentve",
"secretSourceDeleted": "Titkos forrás törölve",
- "vaultManageProfiles": "Vault profilok kezelése",
- "vaultAddrLabel": "Trezor címe",
- "vaultNamespaceLabel": "Névtér",
- "vaultOidcMountLabel": "OIDC hitelesítési csatolás",
- "vaultOidcRoleLabel": "OIDC szerepkör",
- "vaultSshMountLabel": "SSH titkok felhúzása",
- "vaultSshRoleLabel": "SSH aláíró szerepkör",
- "vaultValidPrincipalsLabel": "Érvényes megbízók",
- "vaultKeyTypeLabel": "Efemer kulcstípus",
- "vaultSharedLabel": "Megosztás az összes felhasználóval",
- "vaultCreateProfile": "Profil létrehozása",
- "vaultProfileCreated": "Létrehozva a trezor profilja",
- "vaultProfileSaved": "A trezor profilja mentve",
- "vaultProfileDeleted": "Tárolóprofil törölve",
- "vaultProfileSaveFailed": "Nem sikerült menteni a Vault profilját",
- "vaultProfileDeleteFailed": "Nem sikerült törölni a Vault profilját",
- "vaultSaveProfile": "Profil mentése",
- "vaultProfileValidationError": "Név, trezor címe és SSH aláírói szerepkör megadása kötelező.",
- "vaultNoProfiles": "Még nincsenek Vault-profilok.",
- "vaultSharedBadge": "megosztott",
"keyTypeLabel": "Kulcstípus",
"keyTypeAuto": "Automatikus felismerés",
"keyPasteTab": "Paszta",
diff --git a/src/ui/locales/translated/id_ID.json b/src/ui/locales/translated/id_ID.json
index 0206606eb..459a3152f 100644
--- a/src/ui/locales/translated/id_ID.json
+++ b/src/ui/locales/translated/id_ID.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Pilih salah satu metode yang akan digunakan Termix untuk mengautentikasi koneksi SSH ini. Metode lain tidak dicoba secara otomatis.",
"storedCredential": "Kredensial Tersimpan",
"selectACredential": "Pilih kredensial...",
- "vaultProfile": "Profil Penandatangan Vault",
- "selectAVaultProfile": "Pilih profil Vault...",
- "vaultProfileHint": "Pengaturan berasal dari profil bersama; Anda akan masuk ke Vault melalui OIDC saat terhubung. Tidak ada rahasia yang disimpan.",
- "vaultNewProfile": "Profil baru",
"secretRefHint": "Anda dapat menempelkan referensi 1Password (op://vault/item/field) sebagai pengganti rahasia; referensi tersebut akan diambil dari sumber rahasia Anda saat terhubung.",
"secretSourcesManage": "Mengelola sumber rahasia",
"secretSourcesTitle": "Sumber rahasia",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Nama, URL server, dan token wajib diisi.",
"secretSourceSaved": "Sumber rahasia tersimpan",
"secretSourceDeleted": "Sumber rahasia telah dihapus.",
- "vaultManageProfiles": "Kelola profil Vault",
- "vaultAddrLabel": "Alamat Brankas",
- "vaultNamespaceLabel": "Ruang nama",
- "vaultOidcMountLabel": "OIDC Otorisasi Pemasangan",
- "vaultOidcRoleLabel": "Peran OIDC",
- "vaultSshMountLabel": "Pemasangan Rahasia SSH",
- "vaultSshRoleLabel": "Peran Penandatangan SSH",
- "vaultValidPrincipalsLabel": "Prinsip-prinsip yang Sah",
- "vaultKeyTypeLabel": "Jenis Kunci Sementara",
- "vaultSharedLabel": "Bagikan dengan semua pengguna",
- "vaultCreateProfile": "Buat profil",
- "vaultProfileCreated": "Profil Vault telah dibuat.",
- "vaultProfileSaved": "Profil Vault tersimpan.",
- "vaultProfileDeleted": "Profil Vault dihapus",
- "vaultProfileSaveFailed": "Gagal menyimpan profil Vault",
- "vaultProfileDeleteFailed": "Gagal menghapus profil Vault",
- "vaultSaveProfile": "Simpan profil",
- "vaultProfileValidationError": "Nama, alamat Vault, dan peran penandatangan SSH wajib diisi.",
- "vaultNoProfiles": "Belum ada profil Vault.",
- "vaultSharedBadge": "dibagikan",
"keyTypeLabel": "Jenis Kunci",
"keyTypeAuto": "Deteksi Otomatis",
"keyPasteTab": "Pasta",
diff --git a/src/ui/locales/translated/it_IT.json b/src/ui/locales/translated/it_IT.json
index 462d65593..9642a6b5f 100644
--- a/src/ui/locales/translated/it_IT.json
+++ b/src/ui/locales/translated/it_IT.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Seleziona il metodo che Termix utilizzerà per autenticare questa connessione SSH. Gli altri metodi non verranno tentati automaticamente.",
"storedCredential": "Credenziale memorizzata",
"selectACredential": "Seleziona una credenziale...",
- "vaultProfile": "Profilo firmatario Vault",
- "selectAVaultProfile": "Seleziona un profilo Vault...",
- "vaultProfileHint": "Le impostazioni provengono dal profilo condiviso; effettuerai l'accesso a Vault tramite OIDC quando ti connetti. Nessun segreto è memorizzato.",
- "vaultNewProfile": "Nuovo profilo",
"secretRefHint": "Al posto del segreto, puoi incollare un riferimento a 1Password (op://vault/item/field); questo verrà recuperato dalla tua fonte di segreti al momento della connessione.",
"secretSourcesManage": "Gestire le fonti segrete",
"secretSourcesTitle": "Fonti segrete",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Sono richiesti nome, URL del server e token.",
"secretSourceSaved": "fonte segreta salvata",
"secretSourceDeleted": "Fonte segreta eliminata",
- "vaultManageProfiles": "Gestisci profili Vault",
- "vaultAddrLabel": "Indirizzo Vault",
- "vaultNamespaceLabel": "Namespace",
- "vaultOidcMountLabel": "Mount di autenticazione OIDC",
- "vaultOidcRoleLabel": "Ruolo OIDC",
- "vaultSshMountLabel": "Mount dei segreti SSH",
- "vaultSshRoleLabel": "Ruolo firmatario SSH",
- "vaultValidPrincipalsLabel": "Principal validi",
- "vaultKeyTypeLabel": "Tipo di chiave effimera",
- "vaultSharedLabel": "Condividi con tutti gli utenti",
- "vaultCreateProfile": "Crea profilo",
- "vaultProfileCreated": "Profilo Vault creato",
- "vaultProfileSaved": "Profilo Vault salvato",
- "vaultProfileDeleted": "Profilo Vault eliminato",
- "vaultProfileSaveFailed": "Salvataggio profilo Vault non riuscito",
- "vaultProfileDeleteFailed": "Eliminazione profilo Vault non riuscita",
- "vaultSaveProfile": "Salva profilo",
- "vaultProfileValidationError": "Nome, indirizzo Vault e ruolo di firma SSH sono obbligatori",
- "vaultNoProfiles": "Nessun profilo Vault ancora.",
- "vaultSharedBadge": "condiviso",
"keyTypeLabel": "Tipo chiave",
"keyTypeAuto": "Rilevamento automatico",
"keyPasteTab": "Incolla",
diff --git a/src/ui/locales/translated/ja_JP.json b/src/ui/locales/translated/ja_JP.json
index d3d43eb7a..b86f32ad0 100644
--- a/src/ui/locales/translated/ja_JP.json
+++ b/src/ui/locales/translated/ja_JP.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "TermixがこのSSH接続を認証するために使用する方法を1つ選択してください。他の方法は自動的には試行されません。",
"storedCredential": "保存済み認証情報",
"selectACredential": "認証情報を選択...",
- "vaultProfile": "Vault 署名者プロファイル",
- "selectAVaultProfile": "Vault プロファイルを選択...",
- "vaultProfileHint": "設定は共有プロファイルから取得され、接続時に OIDC で Vault にサインインします。シークレットは保存されません。",
- "vaultNewProfile": "新規プロファイル",
"secretRefHint": "シークレットの代わりに1Passwordの参照(op://vault/item/field)を貼り付けることもできます。これは接続時にシークレットソースから取得されます。",
"secretSourcesManage": "秘密情報源の管理",
"secretSourcesTitle": "秘密の情報源",
@@ -739,26 +735,6 @@
"secretSourceRequired": "名前、サーバーURL、トークンは必須です。",
"secretSourceSaved": "秘密の情報源が保存されました",
"secretSourceDeleted": "秘密情報源を削除",
- "vaultManageProfiles": "Vault プロファイルの管理",
- "vaultAddrLabel": "Vault アドレス",
- "vaultNamespaceLabel": "名前空間",
- "vaultOidcMountLabel": "OIDC 認証マウント",
- "vaultOidcRoleLabel": "OIDC ロール",
- "vaultSshMountLabel": "SSH シークレットマウント",
- "vaultSshRoleLabel": "SSH 署名者ロール",
- "vaultValidPrincipalsLabel": "有効なプリンシパル",
- "vaultKeyTypeLabel": "エフェメラルキータイプ",
- "vaultSharedLabel": "すべてのユーザーと共有",
- "vaultCreateProfile": "プロファイルを作成",
- "vaultProfileCreated": "Vaultプロファイルが作成されました",
- "vaultProfileSaved": "Vaultプロファイルが保存されました",
- "vaultProfileDeleted": "Vaultプロファイルが削除されました",
- "vaultProfileSaveFailed": "Vaultプロファイルの保存に失敗しました",
- "vaultProfileDeleteFailed": "Vaultプロファイルの削除に失敗しました",
- "vaultSaveProfile": "プロファイルを保存",
- "vaultProfileValidationError": "名前、Vaultアドレス、SSH署名ロールは必須です",
- "vaultNoProfiles": "まだVaultプロファイルがありません。",
- "vaultSharedBadge": "共有",
"keyTypeLabel": "鍵の種類",
"keyTypeAuto": "自動検出",
"keyPasteTab": "貼り付け",
diff --git a/src/ui/locales/translated/ko_KR.json b/src/ui/locales/translated/ko_KR.json
index 3b2e4db01..0184526d7 100644
--- a/src/ui/locales/translated/ko_KR.json
+++ b/src/ui/locales/translated/ko_KR.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Termix에서 이 SSH 연결을 인증하는 데 사용할 방법을 하나 선택하십시오. 다른 방법은 자동으로 시도되지 않습니다.",
"storedCredential": "저장된 자격 증명",
"selectACredential": "자격 증명 선택...",
- "vaultProfile": "Vault 서명자 프로필",
- "selectAVaultProfile": "Vault 프로필 선택...",
- "vaultProfileHint": "설정은 공유 프로필에서 가져옵니다. 연결 시 OIDC를 통해 Vault에 로그인하게 됩니다. 비밀은 저장되지 않습니다.",
- "vaultNewProfile": "새 프로필",
"secretRefHint": "비밀 키 대신 1Password 참조(op://vault/item/field)를 붙여넣을 수 있습니다. 연결 시 비밀 키 소스에서 참조가 자동으로 가져와집니다.",
"secretSourcesManage": "비밀 소스를 관리하세요",
"secretSourcesTitle": "비밀 정보원",
@@ -739,26 +735,6 @@
"secretSourceRequired": "이름, 서버 URL 및 토큰은 필수 입력 사항입니다.",
"secretSourceSaved": "비밀 소스 저장됨",
"secretSourceDeleted": "비밀 소스 삭제됨",
- "vaultManageProfiles": "Vault 프로필 관리",
- "vaultAddrLabel": "Vault 주소",
- "vaultNamespaceLabel": "네임스페이스",
- "vaultOidcMountLabel": "OIDC 인증 마운트",
- "vaultOidcRoleLabel": "OIDC 역할",
- "vaultSshMountLabel": "SSH 시크릿 마운트",
- "vaultSshRoleLabel": "SSH 서명자 역할",
- "vaultValidPrincipalsLabel": "유효한 프린시펄",
- "vaultKeyTypeLabel": "임시 키 유형",
- "vaultSharedLabel": "모든 사용자와 공유",
- "vaultCreateProfile": "프로필 생성",
- "vaultProfileCreated": "Vault 프로필 생성됨",
- "vaultProfileSaved": "Vault 프로필 저장됨",
- "vaultProfileDeleted": "Vault 프로필 삭제됨",
- "vaultProfileSaveFailed": "Vault 프로필 저장 실패",
- "vaultProfileDeleteFailed": "Vault 프로필 삭제 실패",
- "vaultSaveProfile": "프로필 저장",
- "vaultProfileValidationError": "이름, Vault 주소 및 SSH 서명자 역할이 필요합니다.",
- "vaultNoProfiles": "아직 Vault 프로필이 없습니다.",
- "vaultSharedBadge": "공유",
"keyTypeLabel": "키 유형",
"keyTypeAuto": "자동 감지",
"keyPasteTab": "붙여넣기",
diff --git a/src/ui/locales/translated/nl_NL.json b/src/ui/locales/translated/nl_NL.json
index 2997a793d..5ddfd405d 100644
--- a/src/ui/locales/translated/nl_NL.json
+++ b/src/ui/locales/translated/nl_NL.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Selecteer de methode die Termix zal gebruiken om deze SSH-verbinding te authenticeren. Andere methoden worden niet automatisch geprobeerd.",
"storedCredential": "Opgeslagen inloggegevens",
"selectACredential": "Selecteer een certificaat...",
- "vaultProfile": "Vault-ondertekenaarprofiel",
- "selectAVaultProfile": "Selecteer een Vault-profiel...",
- "vaultProfileHint": "De instellingen zijn afkomstig van het gedeelde profiel; u meldt zich bij Vault aan via OIDC wanneer u verbinding maakt. Er worden geen geheimen opgeslagen.",
- "vaultNewProfile": "Nieuw profiel",
"secretRefHint": "Je kunt in plaats van het geheim een 1Password-referentie (op://vault/item/field) plakken; deze wordt bij het verbinden opgehaald uit je geheime bron.",
"secretSourcesManage": "Beheer geheime bronnen",
"secretSourcesTitle": "Geheime bronnen",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Naam, server-URL en token zijn vereist.",
"secretSourceSaved": "Geheime bron opgeslagen",
"secretSourceDeleted": "Geheime bron verwijderd",
- "vaultManageProfiles": "Beheer Vault-profielen",
- "vaultAddrLabel": "Kluisadres",
- "vaultNamespaceLabel": "Naamruimte",
- "vaultOidcMountLabel": "OIDC-authenticatiekoppeling",
- "vaultOidcRoleLabel": "OIDC-rol",
- "vaultSshMountLabel": "SSH-geheimen Mount",
- "vaultSshRoleLabel": "SSH-ondertekenaarrol",
- "vaultValidPrincipalsLabel": "Geldige principes",
- "vaultKeyTypeLabel": "Tijdelijk sleuteltype",
- "vaultSharedLabel": "Delen met alle gebruikers",
- "vaultCreateProfile": "Profiel aanmaken",
- "vaultProfileCreated": "Vault-profiel aangemaakt",
- "vaultProfileSaved": "Vault-profiel opgeslagen",
- "vaultProfileDeleted": "Kluisprofiel verwijderd",
- "vaultProfileSaveFailed": "Het opslaan van het Vault-profiel is mislukt.",
- "vaultProfileDeleteFailed": "Het verwijderen van het Vault-profiel is mislukt.",
- "vaultSaveProfile": "Profiel opslaan",
- "vaultProfileValidationError": "Naam, kluisadres en SSH-ondertekeningsrol zijn vereist.",
- "vaultNoProfiles": "Nog geen Vault-profielen beschikbaar.",
- "vaultSharedBadge": "gedeeld",
"keyTypeLabel": "Sleuteltype",
"keyTypeAuto": "Automatische detectie",
"keyPasteTab": "Plakken",
diff --git a/src/ui/locales/translated/no_NO.json b/src/ui/locales/translated/no_NO.json
index c7b909a67..e3db8f564 100644
--- a/src/ui/locales/translated/no_NO.json
+++ b/src/ui/locales/translated/no_NO.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Velg den metoden Termix vil bruke for å autentisere denne SSH-tilkoblingen. Andre metoder blir ikke prøvd automatisk.",
"storedCredential": "Lagret legitimasjon",
"selectACredential": "Velg en legitimasjon...",
- "vaultProfile": "Profil for hvelvsigner",
- "selectAVaultProfile": "Velg en Vault-profil...",
- "vaultProfileHint": "Innstillingene kommer fra den delte profilen. Du logger deg på Vault via OIDC når du kobler til. Ingen hemmeligheter lagres.",
- "vaultNewProfile": "Ny profil",
"secretRefHint": "Du kan lime inn en 1Password-referanse (op://vault/item/field) i stedet for hemmeligheten; den hentes fra hemmelighetskilden din når du kobler til.",
"secretSourcesManage": "Administrer hemmelige kilder",
"secretSourcesTitle": "Hemmelige kilder",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Navn, server-URL og token er obligatorisk",
"secretSourceSaved": "Hemmelig kilde lagret",
"secretSourceDeleted": "Hemmelig kilde slettet",
- "vaultManageProfiles": "Administrer Vault-profiler",
- "vaultAddrLabel": "Hvelvadresse",
- "vaultNamespaceLabel": "Navnerom",
- "vaultOidcMountLabel": "OIDC-autentiseringsmontering",
- "vaultOidcRoleLabel": "OIDC-rolle",
- "vaultSshMountLabel": "SSH-hemmeligheter monteres",
- "vaultSshRoleLabel": "SSH-signatørrolle",
- "vaultValidPrincipalsLabel": "Gyldige prinsipaler",
- "vaultKeyTypeLabel": "Kortvarig nøkkeltype",
- "vaultSharedLabel": "Del med alle brukere",
- "vaultCreateProfile": "Opprett profil",
- "vaultProfileCreated": "Vault-profil opprettet",
- "vaultProfileSaved": "Vault-profilen er lagret",
- "vaultProfileDeleted": "Vault-profilen er slettet",
- "vaultProfileSaveFailed": "Kunne ikke lagre Vault-profilen",
- "vaultProfileDeleteFailed": "Kunne ikke slette Vault-profilen",
- "vaultSaveProfile": "Lagre profil",
- "vaultProfileValidationError": "Navn, hvelvadresse og SSH-signerrolle er obligatorisk",
- "vaultNoProfiles": "Ingen Vault-profiler ennå.",
- "vaultSharedBadge": "delt",
"keyTypeLabel": "Nøkkeltype",
"keyTypeAuto": "Automatisk deteksjon",
"keyPasteTab": "Lim inn",
diff --git a/src/ui/locales/translated/pl_PL.json b/src/ui/locales/translated/pl_PL.json
index 8b1542670..d795079b6 100644
--- a/src/ui/locales/translated/pl_PL.json
+++ b/src/ui/locales/translated/pl_PL.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Wybierz jedną metodę, której Termix użyje do uwierzytelnienia tego połączenia SSH. Inne metody nie są próbowane automatycznie.",
"storedCredential": "Zapisane poświadczenie",
"selectACredential": "Wybierz poświadczenie...",
- "vaultProfile": "Profil podpisujący Vault",
- "selectAVaultProfile": "Wybierz profil Vault...",
- "vaultProfileHint": "Ustawienia pochodzą z profilu współdzielonego. Po połączeniu zalogujesz się do Vault przez OIDC. Nie są przechowywane żadne sekrety.",
- "vaultNewProfile": "Nowy profil",
"secretRefHint": "Zamiast klucza tajnego możesz wkleić odnośnik 1Password (op://vault/item/field). Zostanie on pobrany ze źródła klucza tajnego podczas nawiązywania połączenia.",
"secretSourcesManage": "Zarządzaj tajnymi źródłami",
"secretSourcesTitle": "Tajne źródła",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Wymagane są nazwa, adres URL serwera i token",
"secretSourceSaved": "Tajne źródło uratowane",
"secretSourceDeleted": "Usunięto tajne źródło",
- "vaultManageProfiles": "Zarządzaj profilami Vault",
- "vaultAddrLabel": "Adres Vault",
- "vaultNamespaceLabel": "Przestrzeń nazw",
- "vaultOidcMountLabel": "Mount uwierzytelniania OIDC",
- "vaultOidcRoleLabel": "Rola OIDC",
- "vaultSshMountLabel": "Mount sekretów SSH",
- "vaultSshRoleLabel": "Rola podpisująca SSH",
- "vaultValidPrincipalsLabel": "Dozwoleni użytkownicy (principals)",
- "vaultKeyTypeLabel": "Typ klucza efemerycznego",
- "vaultSharedLabel": "Udostępnij wszystkim użytkownikom",
- "vaultCreateProfile": "Utwórz profil",
- "vaultProfileCreated": "Utworzono profil Vault",
- "vaultProfileSaved": "Zapisano profil Vault",
- "vaultProfileDeleted": "Usunięto profil Vault",
- "vaultProfileSaveFailed": "Nie udało się zapisać profilu Vault",
- "vaultProfileDeleteFailed": "Nie udało się usunąć profilu Vault",
- "vaultSaveProfile": "Zapisz profil",
- "vaultProfileValidationError": "Wymagane są nazwa, adres Vault i rola podpisująca SSH",
- "vaultNoProfiles": "Brak profili Vault.",
- "vaultSharedBadge": "współdzielony",
"keyTypeLabel": "Typ klucza",
"keyTypeAuto": "Automatyczne wykrywanie",
"keyPasteTab": "Wklej",
diff --git a/src/ui/locales/translated/pt_BR.json b/src/ui/locales/translated/pt_BR.json
index 2691c87e1..5394cd720 100644
--- a/src/ui/locales/translated/pt_BR.json
+++ b/src/ui/locales/translated/pt_BR.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Selecione o método que o Termix usará para autenticar esta conexão SSH. Outros métodos não serão tentados automaticamente.",
"storedCredential": "Credencial armazenada",
"selectACredential": "Selecione uma credencial...",
- "vaultProfile": "Perfil do assinante do Vault",
- "selectAVaultProfile": "Selecione um perfil do Vault...",
- "vaultProfileHint": "As configurações são provenientes do perfil compartilhado; você fará login no Vault via OIDC ao se conectar. Nenhum segredo é armazenado.",
- "vaultNewProfile": "Novo perfil",
"secretRefHint": "Você pode colar uma referência do 1Password (op://vault/item/field) em vez do segredo; ela é obtida da sua fonte de segredos ao conectar-se.",
"secretSourcesManage": "Gerenciar fontes secretas",
"secretSourcesTitle": "Fontes secretas",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Nome, URL do servidor e token são obrigatórios.",
"secretSourceSaved": "Fonte secreta salva",
"secretSourceDeleted": "Fonte secreta excluída",
- "vaultManageProfiles": "Gerenciar perfis do Vault",
- "vaultAddrLabel": "Endereço do cofre",
- "vaultNamespaceLabel": "Espaço de nomes",
- "vaultOidcMountLabel": "Montagem de autenticação OIDC",
- "vaultOidcRoleLabel": "Função OIDC",
- "vaultSshMountLabel": "Montagem de segredos SSH",
- "vaultSshRoleLabel": "Função de assinatura SSH",
- "vaultValidPrincipalsLabel": "Principais válidos",
- "vaultKeyTypeLabel": "Tipo de chave efêmera",
- "vaultSharedLabel": "Compartilhe com todos os usuários",
- "vaultCreateProfile": "Criar perfil",
- "vaultProfileCreated": "Perfil do Vault criado",
- "vaultProfileSaved": "Perfil do Vault salvo",
- "vaultProfileDeleted": "Perfil do Vault excluído",
- "vaultProfileSaveFailed": "Falha ao salvar o perfil do Vault",
- "vaultProfileDeleteFailed": "Falha ao excluir o perfil do Vault",
- "vaultSaveProfile": "Salvar perfil",
- "vaultProfileValidationError": "Nome, endereço do Vault e função de assinatura SSH são obrigatórios.",
- "vaultNoProfiles": "Ainda não há perfis no Vault.",
- "vaultSharedBadge": "compartilhado",
"keyTypeLabel": "Tipo de chave",
"keyTypeAuto": "Detecção automática",
"keyPasteTab": "Colar",
diff --git a/src/ui/locales/translated/pt_PT.json b/src/ui/locales/translated/pt_PT.json
index 04d61addd..c52026dc4 100644
--- a/src/ui/locales/translated/pt_PT.json
+++ b/src/ui/locales/translated/pt_PT.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Selecione o método que o Termix irá utilizar para autenticar esta ligação SSH. Outros métodos não serão tentados automaticamente.",
"storedCredential": "Credencial guardada",
"selectACredential": "Selecionar uma credencial...",
- "vaultProfile": "Perfil de assinatura Vault",
- "selectAVaultProfile": "Selecionar um perfil Vault...",
- "vaultProfileHint": "As definições provêm do perfil partilhado; ao ligar, iniciará sessão no Vault via OIDC. Nenhum segredo é armazenado.",
- "vaultNewProfile": "Novo perfil",
"secretRefHint": "Pode colar uma referência do 1Password (op://vault/item/field) em vez do segredo; é obtida a partir da sua fonte de segredos ao conectar-se.",
"secretSourcesManage": "Gerir fontes secretas",
"secretSourcesTitle": "Fontes secretas",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Nome, URL do servidor e token são obrigatórios.",
"secretSourceSaved": "Fonte secreta guardada",
"secretSourceDeleted": "Fonte secreta excluída",
- "vaultManageProfiles": "Gerir perfis Vault",
- "vaultAddrLabel": "Endereço Vault",
- "vaultNamespaceLabel": "Namespace",
- "vaultOidcMountLabel": "Mount de autenticação OIDC",
- "vaultOidcRoleLabel": "Função OIDC",
- "vaultSshMountLabel": "Mount de segredos SSH",
- "vaultSshRoleLabel": "Função de assinatura SSH",
- "vaultValidPrincipalsLabel": "Principais válidos",
- "vaultKeyTypeLabel": "Tipo de chave efémera",
- "vaultSharedLabel": "Partilhar com todos os utilizadores",
- "vaultCreateProfile": "Criar perfil",
- "vaultProfileCreated": "Perfil Vault criado",
- "vaultProfileSaved": "Perfil Vault guardado",
- "vaultProfileDeleted": "Perfil Vault eliminado",
- "vaultProfileSaveFailed": "Falha ao guardar perfil Vault",
- "vaultProfileDeleteFailed": "Falha ao eliminar perfil Vault",
- "vaultSaveProfile": "Guardar perfil",
- "vaultProfileValidationError": "Nome, endereço Vault e função de signatário SSH são obrigatórios",
- "vaultNoProfiles": "Ainda sem perfis Vault.",
- "vaultSharedBadge": "partilhado",
"keyTypeLabel": "Tipo de chave",
"keyTypeAuto": "Detetar automaticamente",
"keyPasteTab": "Colar",
diff --git a/src/ui/locales/translated/ro_RO.json b/src/ui/locales/translated/ro_RO.json
index 84d575c8a..0a0373ec9 100644
--- a/src/ui/locales/translated/ro_RO.json
+++ b/src/ui/locales/translated/ro_RO.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Selectați metoda pe care Termix o va folosi pentru autentificarea acestei conexiuni SSH. Celelalte metode nu sunt încercate automat.",
"storedCredential": "Acreditare stocată",
"selectACredential": "Selectați o acreditare...",
- "vaultProfile": "Profilul semnatarului din seif",
- "selectAVaultProfile": "Selectați un profil Vault...",
- "vaultProfileHint": "Setările provin din profilul partajat; te vei conecta la Vault prin OIDC când te conectezi. Nu sunt stocate secrete.",
- "vaultNewProfile": "Profil nou",
"secretRefHint": "Puteți lipi o referință 1Password (op://vault/item/field) în loc de secret; acesta este preluat din sursa secretului la conectare.",
"secretSourcesManage": "Gestionați sursele secrete",
"secretSourcesTitle": "Surse secrete",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Numele, adresa URL a serverului și tokenul sunt obligatorii",
"secretSourceSaved": "Sursă secretă salvată",
"secretSourceDeleted": "Sursa secretă a fost ștearsă",
- "vaultManageProfiles": "Gestionați profilurile din seif",
- "vaultAddrLabel": "Adresă seif",
- "vaultNamespaceLabel": "Spațiu de nume",
- "vaultOidcMountLabel": "Montare autentificare OIDC",
- "vaultOidcRoleLabel": "Rolul OIDC",
- "vaultSshMountLabel": "Secretele SSH Mount",
- "vaultSshRoleLabel": "Rolul semnatarului SSH",
- "vaultValidPrincipalsLabel": "Principali validi",
- "vaultKeyTypeLabel": "Tip de cheie efemeră",
- "vaultSharedLabel": "Partajați cu toți utilizatorii",
- "vaultCreateProfile": "Creați un profil",
- "vaultProfileCreated": "Profilul seifului a fost creat",
- "vaultProfileSaved": "Profilul seifului a fost salvat",
- "vaultProfileDeleted": "Profilul seifului a fost șters",
- "vaultProfileSaveFailed": "Nu s-a putut salva profilul din Seif",
- "vaultProfileDeleteFailed": "Nu s-a putut șterge profilul Vault",
- "vaultSaveProfile": "Salvați profilul",
- "vaultProfileValidationError": "Numele, adresa seifului și rolul semnatarului SSH sunt obligatorii.",
- "vaultNoProfiles": "Încă nu există profiluri Vault.",
- "vaultSharedBadge": "partajat",
"keyTypeLabel": "Tipul cheii",
"keyTypeAuto": "Detectare automată",
"keyPasteTab": "Pastă",
diff --git a/src/ui/locales/translated/ru_RU.json b/src/ui/locales/translated/ru_RU.json
index 0bec6dc37..523aae370 100644
--- a/src/ui/locales/translated/ru_RU.json
+++ b/src/ui/locales/translated/ru_RU.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Выберите один из методов, который Termix будет использовать для аутентификации этого SSH-соединения. Другие методы автоматически не используются.",
"storedCredential": "Сохранённые учётные данные",
"selectACredential": "Выберите учётные данные...",
- "vaultProfile": "Профиль подписчика Vault",
- "selectAVaultProfile": "Выберите профиль Vault...",
- "vaultProfileHint": "Настройки берутся из общего профиля; при подключении вы войдёте в Vault через OIDC. Секреты не хранятся.",
- "vaultNewProfile": "Новый профиль",
"secretRefHint": "Вместо секретного ключа можно вставить ссылку на 1Password (op://vault/item/field); она будет получена из вашего источника секретных ключей при подключении.",
"secretSourcesManage": "Управление секретными источниками",
"secretSourcesTitle": "Секретные источники",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Необходимо указать имя, URL-адрес сервера и токен.",
"secretSourceSaved": "Секретный источник сохранен",
"secretSourceDeleted": "Секретный источник удален",
- "vaultManageProfiles": "Управление профилями Vault",
- "vaultAddrLabel": "Адрес Vault",
- "vaultNamespaceLabel": "Пространство имён",
- "vaultOidcMountLabel": "Монтирование аутентификации OIDC",
- "vaultOidcRoleLabel": "Роль OIDC",
- "vaultSshMountLabel": "Монтирование секретов SSH",
- "vaultSshRoleLabel": "Роль подписчика SSH",
- "vaultValidPrincipalsLabel": "Допустимые принципалы",
- "vaultKeyTypeLabel": "Тип эфемерного ключа",
- "vaultSharedLabel": "Общий доступ для всех пользователей",
- "vaultCreateProfile": "Создать профиль",
- "vaultProfileCreated": "Профиль Vault создан",
- "vaultProfileSaved": "Профиль Vault сохранён",
- "vaultProfileDeleted": "Профиль Vault удалён",
- "vaultProfileSaveFailed": "Не удалось сохранить профиль Vault",
- "vaultProfileDeleteFailed": "Не удалось удалить профиль Vault",
- "vaultSaveProfile": "Сохранить профиль",
- "vaultProfileValidationError": "Требуются имя, адрес Vault и роль SSH подписанта",
- "vaultNoProfiles": "Пока нет профилей Vault.",
- "vaultSharedBadge": "общий",
"keyTypeLabel": "Тип ключа",
"keyTypeAuto": "Автоопределение",
"keyPasteTab": "Вставить",
diff --git a/src/ui/locales/translated/sr_SP.json b/src/ui/locales/translated/sr_SP.json
index 0b52a4f6c..a54b3b25d 100644
--- a/src/ui/locales/translated/sr_SP.json
+++ b/src/ui/locales/translated/sr_SP.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Изаберите једну методу коју ће Termix користити за аутентификацију ове SSH везе. Остале методе се не испробавају аутоматски.",
"storedCredential": "Сачувани акредитив",
"selectACredential": "Изаберите акредитив...",
- "vaultProfile": "Профил потписника трезора",
- "selectAVaultProfile": "Изаберите профил трезора...",
- "vaultProfileHint": "Подешавања долазе из дељеног профила; пријавићете се у Vault преко OIDC-а када се повежете. Ниједне тајне се не чувају.",
- "vaultNewProfile": "Нови профил",
"secretRefHint": "Можете да налепите референцу 1Password (op://vault/item/field) уместо тајне лозинке; она се преузима из вашег тајног извора приликом повезивања.",
"secretSourcesManage": "Управљајте тајним изворима",
"secretSourcesTitle": "Тајни извори",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Потребно је име, URL сервера и токен",
"secretSourceSaved": "Тајни извор сачуван",
"secretSourceDeleted": "Тајни извор је обрисан",
- "vaultManageProfiles": "Управљање профилима трезора",
- "vaultAddrLabel": "Адреса трезора",
- "vaultNamespaceLabel": "Именски простор",
- "vaultOidcMountLabel": "OIDC аутентификација монтирања",
- "vaultOidcRoleLabel": "Улога OIDC-а",
- "vaultSshMountLabel": "SSH тајне носач",
- "vaultSshRoleLabel": "Улога SSH потписника",
- "vaultValidPrincipalsLabel": "Важећи принципали",
- "vaultKeyTypeLabel": "Тип ефемерног кључа",
- "vaultSharedLabel": "Подели са свим корисницима",
- "vaultCreateProfile": "Направи профил",
- "vaultProfileCreated": "Профил трезора је креиран",
- "vaultProfileSaved": "Профил трезора је сачуван",
- "vaultProfileDeleted": "Профил трезора је обрисан",
- "vaultProfileSaveFailed": "Чување профила у трезору није успело",
- "vaultProfileDeleteFailed": "Брисање профила у Трезору није успело",
- "vaultSaveProfile": "Сачувај профил",
- "vaultProfileValidationError": "Име, адреса трезора и улога SSH потписника су обавезни",
- "vaultNoProfiles": "Још нема профила у Трезору.",
- "vaultSharedBadge": "дељено",
"keyTypeLabel": "Тип кључа",
"keyTypeAuto": "Аутоматско откривање",
"keyPasteTab": "Залепи",
diff --git a/src/ui/locales/translated/sv_SE.json b/src/ui/locales/translated/sv_SE.json
index d37a13bcd..426d5f98a 100644
--- a/src/ui/locales/translated/sv_SE.json
+++ b/src/ui/locales/translated/sv_SE.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Välj den metod som Termix kommer att använda för att autentisera denna SSH-anslutning. Andra metoder testas inte automatiskt.",
"storedCredential": "Lagrade autentiseringsuppgifter",
"selectACredential": "Välj en behörighetsinloggning...",
- "vaultProfile": "Profil för valvsignerare",
- "selectAVaultProfile": "Välj en Vault-profil...",
- "vaultProfileHint": "Inställningarna kommer från den delade profilen; du loggar in på Vault via OIDC när du ansluter. Inga hemligheter lagras.",
- "vaultNewProfile": "Ny profil",
"secretRefHint": "Du kan klistra in en 1Password-referens (op://vault/item/field) istället för hemligheten; den hämtas från din hemliga källa när du ansluter.",
"secretSourcesManage": "Hantera hemliga källor",
"secretSourcesTitle": "Hemliga källor",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Namn, server-URL och token krävs",
"secretSourceSaved": "Hemlig källa sparad",
"secretSourceDeleted": "Hemlig källa raderad",
- "vaultManageProfiles": "Hantera Vault-profiler",
- "vaultAddrLabel": "Valvadress",
- "vaultNamespaceLabel": "Namnrymd",
- "vaultOidcMountLabel": "OIDC-autentiseringsmontering",
- "vaultOidcRoleLabel": "OIDC-roll",
- "vaultSshMountLabel": "SSH-hemligheter monteras",
- "vaultSshRoleLabel": "SSH-signerarroll",
- "vaultValidPrincipalsLabel": "Giltiga principer",
- "vaultKeyTypeLabel": "Tillfällig nyckeltyp",
- "vaultSharedLabel": "Dela med alla användare",
- "vaultCreateProfile": "Skapa profil",
- "vaultProfileCreated": "Arkivprofil skapad",
- "vaultProfileSaved": "Arkivprofilen har sparats",
- "vaultProfileDeleted": "Arkivprofil raderad",
- "vaultProfileSaveFailed": "Kunde inte spara arkivprofilen",
- "vaultProfileDeleteFailed": "Misslyckades med att ta bort Arkiv-profilen",
- "vaultSaveProfile": "Spara profil",
- "vaultProfileValidationError": "Namn, valvadress och SSH-signerarroll krävs",
- "vaultNoProfiles": "Inga Vault-profiler ännu.",
- "vaultSharedBadge": "delad",
"keyTypeLabel": "Nyckeltyp",
"keyTypeAuto": "Automatisk identifiering",
"keyPasteTab": "Klistra",
diff --git a/src/ui/locales/translated/th_TH.json b/src/ui/locales/translated/th_TH.json
index 1838d2ee2..e19a06bf2 100644
--- a/src/ui/locales/translated/th_TH.json
+++ b/src/ui/locales/translated/th_TH.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "เลือกวิธีการตรวจสอบสิทธิ์การเชื่อมต่อ SSH ที่ Termix จะใช้เพียงวิธีเดียวเท่านั้น ระบบจะไม่ลองใช้วิธีการอื่นโดยอัตโนมัติ",
"storedCredential": "ข้อมูลประจำตัวที่บันทึกไว้",
"selectACredential": "เลือกข้อมูลประจำตัว...",
- "vaultProfile": "โปรไฟล์ผู้ลงนาม Vault",
- "selectAVaultProfile": "เลือกโปรไฟล์ Vault...",
- "vaultProfileHint": "การตั้งค่าต่างๆ มาจากโปรไฟล์ที่ใช้ร่วมกัน คุณจะเข้าสู่ระบบ Vault ผ่าน OIDC เมื่อเชื่อมต่อ ไม่มีข้อมูลลับใดๆ ถูกจัดเก็บไว้",
- "vaultNewProfile": "โปรไฟล์ใหม่",
"secretRefHint": "คุณสามารถวางข้อมูลอ้างอิง 1Password (op://vault/item/field) แทนรหัสลับได้ โดยระบบจะดึงข้อมูลนี้จากแหล่งข้อมูลลับของคุณเมื่อทำการเชื่อมต่อ",
"secretSourcesManage": "จัดการแหล่งข้อมูลลับ",
"secretSourcesTitle": "แหล่งข่าวลับ",
@@ -739,26 +735,6 @@
"secretSourceRequired": "จำเป็นต้องระบุชื่อ ที่อยู่ URL ของเซิร์ฟเวอร์ และโทเค็น",
"secretSourceSaved": "แหล่งข่าวลับถูกบันทึกไว้แล้ว",
"secretSourceDeleted": "แหล่งข่าวลับถูกลบแล้ว",
- "vaultManageProfiles": "จัดการโปรไฟล์ Vault",
- "vaultAddrLabel": "ที่อยู่ห้องนิรภัย",
- "vaultNamespaceLabel": "เนมสเปซ",
- "vaultOidcMountLabel": "OIDC Auth Mount",
- "vaultOidcRoleLabel": "บทบาทของ OIDC",
- "vaultSshMountLabel": "SSH Secrets Mount",
- "vaultSshRoleLabel": "บทบาทผู้ลงนาม SSH",
- "vaultValidPrincipalsLabel": "หลักการที่ถูกต้อง",
- "vaultKeyTypeLabel": "ประเภทคีย์ชั่วคราว",
- "vaultSharedLabel": "แชร์กับผู้ใช้ทั้งหมด",
- "vaultCreateProfile": "สร้างโปรไฟล์",
- "vaultProfileCreated": "สร้างโปรไฟล์ Vault แล้ว",
- "vaultProfileSaved": "บันทึกโปรไฟล์ Vault แล้ว",
- "vaultProfileDeleted": "โปรไฟล์ Vault ถูกลบแล้ว",
- "vaultProfileSaveFailed": "ไม่สามารถบันทึกโปรไฟล์ Vault ได้",
- "vaultProfileDeleteFailed": "ไม่สามารถลบโปรไฟล์ Vault ได้",
- "vaultSaveProfile": "บันทึกโปรไฟล์",
- "vaultProfileValidationError": "จำเป็นต้องระบุชื่อ ที่อยู่ Vault และบทบาทผู้ลงนาม SSH",
- "vaultNoProfiles": "ยังไม่มีโปรไฟล์ Vault ใดๆ",
- "vaultSharedBadge": "แชร์",
"keyTypeLabel": "ประเภทกุญแจ",
"keyTypeAuto": "ตรวจจับอัตโนมัติ",
"keyPasteTab": "แปะ",
diff --git a/src/ui/locales/translated/tr_TR.json b/src/ui/locales/translated/tr_TR.json
index a8683eb43..343a93353 100644
--- a/src/ui/locales/translated/tr_TR.json
+++ b/src/ui/locales/translated/tr_TR.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Termix'in bu SSH bağlantısını doğrulamak için kullanacağı yöntemi seçin. Diğer yöntemler otomatik olarak denenmez.",
"storedCredential": "Kayıtlı Kimlik",
"selectACredential": "Bir kimlik seçin...",
- "vaultProfile": "Vault İmzalayıcı Profili",
- "selectAVaultProfile": "Bir Vault profili seçin...",
- "vaultProfileHint": "Ayarlar paylaşılan profilden gelir; bağlandığınızda OIDC ile Vault'ta oturum açarsınız. Hiçbir gizli bilgi saklanmaz.",
- "vaultNewProfile": "Yeni profil",
"secretRefHint": "Gizli anahtar yerine 1Password referansını (op://vault/item/field) yapıştırabilirsiniz; bağlantı kurulurken bu referans gizli anahtar kaynağınızdan alınır.",
"secretSourcesManage": "Gizli kaynakları yönetin",
"secretSourcesTitle": "Gizli kaynaklar",
@@ -739,26 +735,6 @@
"secretSourceRequired": "İsim, sunucu URL'si ve belirteç (token) gereklidir.",
"secretSourceSaved": "Gizli kaynak kaydedildi.",
"secretSourceDeleted": "Gizli kaynak silindi",
- "vaultManageProfiles": "Vault Profillerini Yönet",
- "vaultAddrLabel": "Vault Adresi",
- "vaultNamespaceLabel": "Ad Alanı",
- "vaultOidcMountLabel": "OIDC Kimlik Doğrulama Bağlama",
- "vaultOidcRoleLabel": "OIDC Rolü",
- "vaultSshMountLabel": "SSH Gizli Dizin Bağlama",
- "vaultSshRoleLabel": "SSH İmzalayıcı Rolü",
- "vaultValidPrincipalsLabel": "Geçerli Asıllar",
- "vaultKeyTypeLabel": "Geçici Anahtar Türü",
- "vaultSharedLabel": "Tüm Kullanıcılarla Paylaş",
- "vaultCreateProfile": "Profil oluştur",
- "vaultProfileCreated": "Vault profili oluşturuldu",
- "vaultProfileSaved": "Vault profili kaydedildi",
- "vaultProfileDeleted": "Vault profili silindi",
- "vaultProfileSaveFailed": "Vault profili kaydedilemedi",
- "vaultProfileDeleteFailed": "Vault profili silinemedi",
- "vaultSaveProfile": "Profili kaydet",
- "vaultProfileValidationError": "Ad, Vault adresi ve SSH imzalama rolü gereklidir",
- "vaultNoProfiles": "Henüz Vault profili yok.",
- "vaultSharedBadge": "paylaşılan",
"keyTypeLabel": "Anahtar Türü",
"keyTypeAuto": "Otomatik Algıla",
"keyPasteTab": "Yapıştır",
diff --git a/src/ui/locales/translated/uk_UA.json b/src/ui/locales/translated/uk_UA.json
index ec62a06c9..6c44a4877 100644
--- a/src/ui/locales/translated/uk_UA.json
+++ b/src/ui/locales/translated/uk_UA.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Виберіть один метод, який Termix використовуватиме для автентифікації цього SSH-з’єднання. Інші методи не перевірятимуться автоматично.",
"storedCredential": "Збережені облікові дані",
"selectACredential": "Виберіть облікові дані...",
- "vaultProfile": "Профіль підписанта сховища",
- "selectAVaultProfile": "Виберіть профіль Сховища...",
- "vaultProfileHint": "Налаштування надходять зі спільного профілю; ви входите у Сейф через OIDC під час підключення. Секрети не зберігаються.",
- "vaultNewProfile": "Новий профіль",
"secretRefHint": "Ви можете вставити посилання на 1Password (op://vault/item/field) замість секретного коду; він отримується з вашого секретного джерела під час підключення.",
"secretSourcesManage": "Керування секретними джерелами",
"secretSourcesTitle": "Секретні джерела",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Ім'я, URL-адреса сервера та токен обов'язкові",
"secretSourceSaved": "Секретне джерело збережено",
"secretSourceDeleted": "Секретне джерело видалено",
- "vaultManageProfiles": "Керування профілями Сховища",
- "vaultAddrLabel": "Адреса сховища",
- "vaultNamespaceLabel": "Простір імен",
- "vaultOidcMountLabel": "Монтування автентифікації OIDC",
- "vaultOidcRoleLabel": "Роль OIDC",
- "vaultSshMountLabel": "SSH-секрети кріплення",
- "vaultSshRoleLabel": "Роль підписувача SSH",
- "vaultValidPrincipalsLabel": "Дійсні принципали",
- "vaultKeyTypeLabel": "Тип тимчасового ключа",
- "vaultSharedLabel": "Поділитися з усіма користувачами",
- "vaultCreateProfile": "Створити профіль",
- "vaultProfileCreated": "Профіль сховища створено",
- "vaultProfileSaved": "Профіль сховища збережено",
- "vaultProfileDeleted": "Профіль сховища видалено",
- "vaultProfileSaveFailed": "Не вдалося зберегти профіль Сейфа",
- "vaultProfileDeleteFailed": "Не вдалося видалити профіль Сейфа",
- "vaultSaveProfile": "Зберегти профіль",
- "vaultProfileValidationError": "Ім'я, адреса сховища та роль підписувача SSH обов'язкові",
- "vaultNoProfiles": "Поки що немає профілів Сейфа.",
- "vaultSharedBadge": "спільний",
"keyTypeLabel": "Тип ключа",
"keyTypeAuto": "Автоматичне виявлення",
"keyPasteTab": "Вставити",
diff --git a/src/ui/locales/translated/vi_VN.json b/src/ui/locales/translated/vi_VN.json
index 7f365598c..b935f7488 100644
--- a/src/ui/locales/translated/vi_VN.json
+++ b/src/ui/locales/translated/vi_VN.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "Hãy chọn phương thức mà Termix sẽ sử dụng để xác thực kết nối SSH này. Các phương thức khác sẽ không được thử tự động.",
"storedCredential": "Thông tin xác thực đã lưu",
"selectACredential": "Chọn thông tin xác thực...",
- "vaultProfile": "Hồ sơ người ký Vault",
- "selectAVaultProfile": "Chọn hồ sơ Vault...",
- "vaultProfileHint": "Cài đặt từ hồ sơ dùng chung; bạn sẽ đăng nhập Vault qua OIDC khi kết nối. Không lưu bí mật.",
- "vaultNewProfile": "Hồ sơ mới",
"secretRefHint": "Bạn có thể dán mã tham chiếu 1Password (op://vault/item/field) thay cho mã bí mật; mã này sẽ được lấy từ nguồn mã bí mật của bạn khi kết nối.",
"secretSourcesManage": "Quản lý các nguồn tin mật",
"secretSourcesTitle": "Nguồn tin bí mật",
@@ -739,26 +735,6 @@
"secretSourceRequired": "Tên người dùng, URL máy chủ và mã thông báo là bắt buộc.",
"secretSourceSaved": "Nguồn bí mật đã được lưu",
"secretSourceDeleted": "Nguồn tin bí mật đã bị xóa.",
- "vaultManageProfiles": "Quản lý hồ sơ Vault",
- "vaultAddrLabel": "Địa chỉ Vault",
- "vaultNamespaceLabel": "Không gian tên",
- "vaultOidcMountLabel": "Điểm gắn xác thực OIDC",
- "vaultOidcRoleLabel": "Vai trò OIDC",
- "vaultSshMountLabel": "Điểm gắn bí mật SSH",
- "vaultSshRoleLabel": "Vai trò ký SSH",
- "vaultValidPrincipalsLabel": "Các danh tính hợp lệ",
- "vaultKeyTypeLabel": "Loại khóa tạm thời",
- "vaultSharedLabel": "Chia sẻ với tất cả người dùng",
- "vaultCreateProfile": "Tạo hồ sơ",
- "vaultProfileCreated": "Đã tạo hồ sơ Vault",
- "vaultProfileSaved": "Đã lưu hồ sơ Vault",
- "vaultProfileDeleted": "Đã xóa hồ sơ Vault",
- "vaultProfileSaveFailed": "Không thể lưu hồ sơ Vault",
- "vaultProfileDeleteFailed": "Không thể xóa hồ sơ Vault",
- "vaultSaveProfile": "Lưu hồ sơ",
- "vaultProfileValidationError": "Tên, địa chỉ Vault và vai trò ký SSH là bắt buộc",
- "vaultNoProfiles": "Chưa có hồ sơ Vault nào.",
- "vaultSharedBadge": "được chia sẻ",
"keyTypeLabel": "Loại khóa",
"keyTypeAuto": "Tự động phát hiện",
"keyPasteTab": "Dán",
diff --git a/src/ui/locales/translated/zh_CN.json b/src/ui/locales/translated/zh_CN.json
index 780d678af..7276eafaa 100644
--- a/src/ui/locales/translated/zh_CN.json
+++ b/src/ui/locales/translated/zh_CN.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "请选择 Termix 用于验证此 SSH 连接的一种方法。其他方法不会自动尝试。",
"storedCredential": "已存储凭据",
"selectACredential": "选择凭据...",
- "vaultProfile": "Vault 签名者配置文件",
- "selectAVaultProfile": "选择 Vault 配置文件...",
- "vaultProfileHint": "设置来自共享配置文件;连接时将使用 OIDC 登录 Vault。不会存储任何机密。",
- "vaultNewProfile": "新建配置文件",
"secretRefHint": "您可以粘贴 1Password 引用(op://vault/item/field)而不是密钥;连接时会从您的密钥源中获取该引用。",
"secretSourcesManage": "管理秘密来源",
"secretSourcesTitle": "秘密消息来源",
@@ -739,26 +735,6 @@
"secretSourceRequired": "需要提供名称、服务器 URL 和令牌。",
"secretSourceSaved": "秘密来源已保存",
"secretSourceDeleted": "秘密来源已删除",
- "vaultManageProfiles": "管理 Vault 配置文件",
- "vaultAddrLabel": "Vault 地址",
- "vaultNamespaceLabel": "命名空间",
- "vaultOidcMountLabel": "OIDC 认证挂载点",
- "vaultOidcRoleLabel": "OIDC 角色",
- "vaultSshMountLabel": "SSH 机密挂载点",
- "vaultSshRoleLabel": "SSH 签名者角色",
- "vaultValidPrincipalsLabel": "有效主体",
- "vaultKeyTypeLabel": "临时密钥类型",
- "vaultSharedLabel": "与所有用户共享",
- "vaultCreateProfile": "创建配置文件",
- "vaultProfileCreated": "Vault 配置文件已创建",
- "vaultProfileSaved": "Vault 配置文件已保存",
- "vaultProfileDeleted": "Vault 配置文件已删除",
- "vaultProfileSaveFailed": "保存 Vault 配置文件失败",
- "vaultProfileDeleteFailed": "删除 Vault 配置文件失败",
- "vaultSaveProfile": "保存配置文件",
- "vaultProfileValidationError": "名称、Vault 地址和 SSH 签名者角色为必填项",
- "vaultNoProfiles": "暂无 Vault 配置文件。",
- "vaultSharedBadge": "共享",
"keyTypeLabel": "密钥类型",
"keyTypeAuto": "自动检测",
"keyPasteTab": "粘贴",
diff --git a/src/ui/locales/translated/zh_TW.json b/src/ui/locales/translated/zh_TW.json
index 5de1dd566..51873042e 100644
--- a/src/ui/locales/translated/zh_TW.json
+++ b/src/ui/locales/translated/zh_TW.json
@@ -719,10 +719,6 @@
"authenticationMethodDesc": "請選擇 Termix 用於驗證此 SSH 連線的一種方法。其他方法不會自動嘗試。",
"storedCredential": "儲存的憑證",
"selectACredential": "選取憑證...",
- "vaultProfile": "Vault 簽署者設定檔",
- "selectAVaultProfile": "選取 Vault 設定檔...",
- "vaultProfileHint": "設定來自共用設定檔;連線時您將透過 OIDC 登入 Vault。不會儲存任何機密。",
- "vaultNewProfile": "新增設定檔",
"secretRefHint": "您可以貼上 1Password 引用(op://vault/item/field)而不是密鑰;連接時會從您的密鑰來源中獲取該引用。",
"secretSourcesManage": "管理秘密來源",
"secretSourcesTitle": "秘密消息來源",
@@ -739,26 +735,6 @@
"secretSourceRequired": "需要提供名稱、伺服器 URL 和令牌。",
"secretSourceSaved": "秘密來源已儲存",
"secretSourceDeleted": "秘密來源已刪除",
- "vaultManageProfiles": "管理 Vault 設定檔",
- "vaultAddrLabel": "Vault 位址",
- "vaultNamespaceLabel": "命名空間",
- "vaultOidcMountLabel": "OIDC 驗證掛載",
- "vaultOidcRoleLabel": "OIDC 角色",
- "vaultSshMountLabel": "SSH 機密掛載",
- "vaultSshRoleLabel": "SSH 簽署者角色",
- "vaultValidPrincipalsLabel": "有效主體",
- "vaultKeyTypeLabel": "暫時金鑰類型",
- "vaultSharedLabel": "與所有使用者共用",
- "vaultCreateProfile": "建立設定檔",
- "vaultProfileCreated": "Vault 設定檔已建立",
- "vaultProfileSaved": "Vault 設定檔已儲存",
- "vaultProfileDeleted": "Vault 設定檔已刪除",
- "vaultProfileSaveFailed": "無法儲存 Vault 設定檔",
- "vaultProfileDeleteFailed": "無法刪除 Vault 設定檔",
- "vaultSaveProfile": "儲存設定檔",
- "vaultProfileValidationError": "名稱、Vault 位址和 SSH 簽署者角色為必填",
- "vaultNoProfiles": "尚無 Vault 設定檔。",
- "vaultSharedBadge": "已共用",
"keyTypeLabel": "金鑰類型",
"keyTypeAuto": "自動偵測",
"keyPasteTab": "貼上",
diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts
index 371328604..a72603fe8 100644
--- a/src/ui/main-axios.ts
+++ b/src/ui/main-axios.ts
@@ -1783,14 +1783,6 @@ export {
deployCredentialToHost,
} from "@/api/credentials-api";
-export {
- getVaultProfiles,
- createVaultProfile,
- updateVaultProfile,
- deleteVaultProfile,
- type VaultProfilePayload,
-} from "@/api/vault-profiles-api";
-
// ============================================================================
// SNIPPETS API
// ============================================================================
diff --git a/src/ui/sidebar/HostEditor.tsx b/src/ui/sidebar/HostEditor.tsx
index d06535cfe..634a6651b 100644
--- a/src/ui/sidebar/HostEditor.tsx
+++ b/src/ui/sidebar/HostEditor.tsx
@@ -41,7 +41,6 @@ import {
updateSSHHost,
getSnippets,
getUserInfo,
- getVaultProfiles,
getHostPassword,
adminCreateUserHost,
adminUpdateUserHost,
@@ -57,7 +56,7 @@ import {
parseCustomThemes,
type SavedCustomTheme,
} from "@/api/open-tabs-api";
-import type { Host, VaultProfile } from "@/types/ui-types";
+import type { Host } from "@/types/ui-types";
import type { SSHHost } from "@/types";
import { useTabsSafe } from "@/shell/TabContext";
import { updatePluginHostSettings } from "@/api/plugins-api";
@@ -78,7 +77,6 @@ import { useConnectionDefaults } from "@/contexts/ConnectionDefaultsContext";
import { HostEditorGeneralTab } from "./HostEditorGeneralTab";
import { withProtocolSettings } from "./host-protocols";
import { canEditHost } from "./host-permissions";
-import { VaultProfileManager } from "./VaultProfileManager";
import {
getHostEditorSection,
isSshGroupTab,
@@ -167,8 +165,6 @@ export function HostEditor({
const [saving, setSaving] = useState(false);
const [snippets, setSnippets] = useState<{ id: number; name: string }[]>([]);
const [isOidcUser, setIsOidcUser] = useState(false);
- const [vaultProfiles, setVaultProfiles] = useState([]);
- const [showVaultManager, setShowVaultManager] = useState(false);
const [showSecretSources, setShowSecretSources] = useState(false);
const [quickCredentialName, setQuickCredentialName] = useState("");
const [creatingQuickCredential, setCreatingQuickCredential] = useState(false);
@@ -232,12 +228,6 @@ export function HostEditor({
setField("customThemeColors", { ...theme.colors });
};
- const reloadVaultProfiles = () => {
- getVaultProfiles()
- .then((res) => setVaultProfiles(res as unknown as VaultProfile[]))
- .catch(() => {});
- };
-
useEffect(() => {
getUserInfo()
.then((info) => setIsOidcUser(info.is_oidc))
@@ -251,7 +241,6 @@ export function HostEditor({
loadSnippets
.then((res) => setSnippets(mapSnippetResponse(res)))
.catch(() => {});
- reloadVaultProfiles();
}, [adminTargetUserId]);
useEffect(() => {
@@ -465,7 +454,7 @@ export function HostEditor({
};
// Shared hosts: view-level recipients see a read-only editor; edit-level
- // recipients may change the host but never its credential/vault references
+ // recipients may change the host but never its credential references
// or auth type (owner-only, enforced server-side too).
const isSharedHost = !!host?.isShared;
const readOnly = isSharedHost && host !== null && !canEditHost(host);
@@ -873,58 +862,6 @@ export function HostEditor({
>
)}
- {authMethod === "vault" && (
-
- )}
- {authMethod === "vault" && showVaultManager && (
-
setShowVaultManager(false)}
- />
- )}
{authMethod === "credential" && (
<>
diff --git a/src/ui/sidebar/HostEditorData.ts b/src/ui/sidebar/HostEditorData.ts
index 147050a5e..ad9cf9d03 100644
--- a/src/ui/sidebar/HostEditorData.ts
+++ b/src/ui/sidebar/HostEditorData.ts
@@ -134,7 +134,6 @@ export function createHostEditorForm(
: d?.credentialId != null
? String(d.credentialId)
: "",
- vaultProfileId: host?.vaultProfileId ?? "",
overrideCredentialUsername: host?.overrideCredentialUsername ?? false,
folder: host?.folder ?? "",
parentHostId: host?.parentHostId ?? "",
@@ -294,7 +293,6 @@ export function omitOwnerSshAuthFromSharedEdit(
keyType: _keyType,
sudoPassword: _sudoPassword,
credentialId: _credentialId,
- vaultProfileId: _vaultProfileId,
overrideCredentialUsername: _overrideCredentialUsername,
shareSshAuth: _shareSshAuth,
...editableFields
@@ -325,7 +323,6 @@ export function buildHostEditorPayload(
const usesKey = form.authType === "key";
const usesPassword = form.authType === "password";
const usesAgent = form.authType === "agent";
- const usesVault = form.authType === "vault";
// With SSH off, the host's primary protocol is the first plugin protocol
// switched on, and its port stands in for the host port.
const primaryProtocol = protocols.enableSsh
@@ -410,8 +407,6 @@ export function buildHostEditorPayload(
keyType: usesKey && form.keyType !== "auto" ? form.keyType : null,
credentialId:
usesCredential && form.credentialId ? Number(form.credentialId) : null,
- vaultProfileId:
- usesVault && form.vaultProfileId ? Number(form.vaultProfileId) : null,
overrideCredentialUsername: form.overrideCredentialUsername,
notes: form.notes,
enableTerminal: form.enableTerminal,
diff --git a/src/ui/sidebar/HostManagerData.ts b/src/ui/sidebar/HostManagerData.ts
index aed5b8a24..d0fd96de7 100644
--- a/src/ui/sidebar/HostManagerData.ts
+++ b/src/ui/sidebar/HostManagerData.ts
@@ -78,10 +78,6 @@ export function sshHostToHost(h: SSHHostWithStatus): Host {
keyPassword: h.keyPassword,
keyType: h.keyType,
credentialId: h.credentialId != null ? String(h.credentialId) : undefined,
- vaultProfileId:
- (h as { vaultProfileId?: number | string | null }).vaultProfileId != null
- ? String((h as { vaultProfileId?: number | string }).vaultProfileId)
- : undefined,
notes: h.notes,
pin: h.pin ?? false,
sortOrder: h.sortOrder ?? null,
diff --git a/src/ui/tests/sidebar/HostEditorData.test.ts b/src/ui/tests/sidebar/HostEditorData.test.ts
index 8eb3ccfa8..eec0c2c53 100644
--- a/src/ui/tests/sidebar/HostEditorData.test.ts
+++ b/src/ui/tests/sidebar/HostEditorData.test.ts
@@ -50,7 +50,6 @@ describe("omitOwnerSshAuthFromSharedEdit", () => {
key: "owner-key",
keyPassword: "owner-passphrase",
keyType: "ssh-ed25519",
- vaultProfileId: "9",
overrideCredentialUsername: true,
shareSshAuth: true,
sudoPassword: "owner-sudo",
@@ -70,7 +69,6 @@ describe("omitOwnerSshAuthFromSharedEdit", () => {
for (const field of [
"authType",
"credentialId",
- "vaultProfileId",
"overrideCredentialUsername",
"shareSshAuth",
"password",
@@ -128,32 +126,6 @@ describe("buildHostEditorPayload auth field isolation", () => {
expect(payload.password).toBe("newpass");
});
- it("drops the vaultProfileId when switching a host away from vault auth", () => {
- const form = {
- ...createHostEditorForm(null),
- authType: "password" as const,
- password: "newpass",
- vaultProfileId: "9",
- };
-
- const payload = buildHostEditorPayload(form, sshOnly);
-
- expect(payload.vaultProfileId).toBeNull();
- expect(payload.password).toBe("newpass");
- });
-
- it("sends vaultProfileId when authType is vault", () => {
- const form = {
- ...createHostEditorForm(null),
- authType: "vault" as const,
- vaultProfileId: "9",
- };
-
- const payload = buildHostEditorPayload(form, sshOnly);
-
- expect(payload.vaultProfileId).toBe(9);
- });
-
it("sends credentialId and optional password when authType is credential", () => {
const form = {
...createHostEditorForm(null),