feat: move termix identity into a plugin (C6)

This commit is contained in:
LukeGus committed 2026-09-24 20:50:54 -05:00
1 parent 6e56c90aac
commit 4dbce08379
157 files changed
+20743 -5766

No files matched your search

+7 -3
View File
@@ -7,7 +7,10 @@ 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 {
registerTermixIdCompatRoutes,
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";
@@ -17,7 +20,6 @@ import userPreferencesRoutes from "./routes/user-preferences.js";
import hostSidebarPreferencesRoutes from "./routes/host-sidebar-preferences.js";
import credentialSidebarPreferencesRoutes from "./routes/credential-sidebar-preferences.js";
import uiPreferencesRoutes from "./routes/ui-preferences.js";
import termixIdRoutes from "./routes/termix-id.js";
import { registerAuditLogRoutes } from "./routes/audit-log-routes.js";
import notificationChannelsRoutes from "./routes/notification-channels-routes.js";
import syncRoutes from "./routes/sync.js";
@@ -1793,7 +1795,9 @@ app.use("/user-preferences", userPreferencesRoutes);
app.use("/host-sidebar/preferences", hostSidebarPreferencesRoutes);
app.use("/credential-sidebar/preferences", credentialSidebarPreferencesRoutes);
app.use("/ui-preferences", uiPreferencesRoutes);
app.use("/termix-id", termixIdRoutes);
const termixIdCompatRoutes = express.Router();
registerTermixIdCompatRoutes(termixIdCompatRoutes);
app.use("/termix-id", termixIdCompatRoutes);
registerAuditLogRoutes(app, authenticateJWT);
const vaultCompatRoutes = express.Router();
registerVaultCompatRoutes(vaultCompatRoutes);
-105
View File
@@ -1204,111 +1204,6 @@ const migrateSchema = () => {
addColumnIfNotExists("file_manager_pinned", "host_id", "INTEGER NOT NULL");
addColumnIfNotExists("file_manager_shortcuts", "host_id", "INTEGER NOT NULL");
try {
sqlite.prepare("SELECT id FROM termix_identities LIMIT 1").get();
} catch {
try {
sqlite.exec(`
CREATE TABLE IF NOT EXISTS termix_identities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL UNIQUE,
handle TEXT NOT NULL UNIQUE,
description TEXT,
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 termix_identities table", {
operation: "schema_migration",
error: createError,
});
}
}
// Enforce one-Termix-ID-per-user on databases where the table predates the
// UNIQUE(user_id) constraint above.
try {
sqlite.exec(
"CREATE UNIQUE INDEX IF NOT EXISTS idx_termix_identities_user ON termix_identities(user_id)",
);
} catch (indexError) {
databaseLogger.warn("Failed to create termix_identities user_id unique index", {
operation: "schema_migration",
error: indexError,
});
}
try {
sqlite.prepare("SELECT id FROM termix_identity_keys LIMIT 1").get();
} catch {
try {
sqlite.exec(`
CREATE TABLE IF NOT EXISTS termix_identity_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
identity_id INTEGER NOT NULL,
user_id TEXT NOT NULL,
public_key TEXT NOT NULL,
key_type TEXT NOT NULL,
algorithm TEXT NOT NULL,
label TEXT,
comment TEXT,
source TEXT NOT NULL DEFAULT 'manual',
credential_id INTEGER,
enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (identity_id) REFERENCES termix_identities (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (credential_id) REFERENCES ssh_credentials (id) ON DELETE SET NULL
);
`);
} catch (createError) {
databaseLogger.warn("Failed to create termix_identity_keys table", {
operation: "schema_migration",
error: createError,
});
}
}
// The public resolver fetches keys by identity_id on every request; index it.
try {
sqlite.exec(
"CREATE INDEX IF NOT EXISTS idx_termix_identity_keys_identity ON termix_identity_keys(identity_id)",
);
} catch (indexError) {
databaseLogger.warn("Failed to create termix_identity_keys identity index", {
operation: "schema_migration",
error: indexError,
});
}
try {
sqlite.prepare("SELECT id FROM termix_identity_ca LIMIT 1").get();
} catch {
try {
sqlite.exec(`
CREATE TABLE IF NOT EXISTS termix_identity_ca (
id INTEGER PRIMARY KEY AUTOINCREMENT,
identity_id INTEGER NOT NULL UNIQUE,
user_id TEXT NOT NULL,
public_key TEXT NOT NULL,
private_key TEXT NOT NULL,
validity_days INTEGER NOT NULL DEFAULT 90,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (identity_id) REFERENCES termix_identities (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
`);
} catch (createError) {
databaseLogger.warn("Failed to create termix_identity_ca table", {
operation: "schema_migration",
error: createError,
});
}
}
try {
sqlite.prepare("SELECT role_id FROM host_access LIMIT 1").get();
} catch {
+2 -72
View File
@@ -772,78 +772,8 @@ export const uiPreferences = mysqlTable("ui_preferences", {
// dashboard_service_links moved to the homepage plugin (p_homepage_dashboard_service_links).
// --- termix-id begin ---
// A user claims a unique public handle. Their published SSH public keys are
// served at an unauthenticated resolver endpoint in authorized_keys format,
// so any server can be provisioned with `curl <host>/termix-id/u/<handle> >> ~/.ssh/authorized_keys`.
export const termixIdentities = mysqlTable("termix_identities", {
id: int("id").autoincrement().primaryKey(),
// One Termix ID per user — enforced in schema, not just in code.
userId: varchar("user_id", { length: 255 })
.notNull()
.unique()
.references(() => users.id, { onDelete: "cascade" }),
handle: varchar("handle", { length: 255 }).notNull().unique(),
description: text("description"),
createdAt: text("created_at")
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`),
updatedAt: text("updated_at")
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`),
});
export const termixIdentityKeys = mysqlTable("termix_identity_keys", {
id: int("id").autoincrement().primaryKey(),
identityId: int("identity_id")
.notNull()
.references(() => termixIdentities.id, { onDelete: "cascade" }),
userId: varchar("user_id", { length: 255 })
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
// Public keys are non-secret, so they are stored in plaintext (no field-level
// encryption). This is what lets the unauthenticated resolver serve them.
publicKey: text("public_key").notNull(),
// Raw algorithm token (e.g. "ssh-ed25519"), and a normalized group used for
// the /<ALGO> resolver filter (RSA / ED25519 / ECDSA / ...).
keyType: text("key_type").notNull(),
algorithm: text("algorithm").notNull(),
label: text("label"),
comment: text("comment"),
// "manual" (pasted) or "credential" (imported from an ssh_credentials entry).
source: text("source").notNull().default("manual"),
credentialId: int("credential_id").references(() => sshCredentials.id, {
onDelete: "set null",
}),
enabled: boolean("enabled").notNull().default(true),
createdAt: text("created_at")
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`),
});
// Per-identity certificate authority. Servers that trust this CA (via
// TrustedUserCAKeys / @cert-authority) accept any user certificate it signs,
// giving central revocation (rotate the CA) and expiry (cert validity).
export const termixIdentityCa = mysqlTable("termix_identity_ca", {
id: int("id").autoincrement().primaryKey(),
identityId: int("identity_id")
.notNull()
.unique()
.references(() => termixIdentities.id, { onDelete: "cascade" }),
userId: varchar("user_id", { length: 255 })
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
// CA public key (plaintext — it is published); CA private key is field-encrypted.
publicKey: text("public_key").notNull(),
privateKey: text("private_key").notNull(),
validityDays: int("validity_days").notNull().default(90),
createdAt: text("created_at")
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`),
updatedAt: text("updated_at")
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`),
});
// --- termix-id end ---
// termix_identities, termix_identity_keys and termix_identity_ca moved to the
// termix-identity plugin (p_termix_identity_*).
// --- alerts begin ---
export const notificationChannels = mysqlTable("notification_channels", {
+2 -72
View File
@@ -773,78 +773,8 @@ export const uiPreferences = pgTable("ui_preferences", {
// dashboard_service_links moved to the homepage plugin (p_homepage_dashboard_service_links).
// --- termix-id begin ---
// A user claims a unique public handle. Their published SSH public keys are
// served at an unauthenticated resolver endpoint in authorized_keys format,
// so any server can be provisioned with `curl <host>/termix-id/u/<handle> >> ~/.ssh/authorized_keys`.
export const termixIdentities = pgTable("termix_identities", {
id: serial("id").primaryKey(),
// One Termix ID per user — enforced in schema, not just in code.
userId: varchar("user_id", { length: 255 })
.notNull()
.unique()
.references(() => users.id, { onDelete: "cascade" }),
handle: varchar("handle", { length: 255 }).notNull().unique(),
description: text("description"),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
updatedAt: text("updated_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
});
export const termixIdentityKeys = pgTable("termix_identity_keys", {
id: serial("id").primaryKey(),
identityId: integer("identity_id")
.notNull()
.references(() => termixIdentities.id, { onDelete: "cascade" }),
userId: varchar("user_id", { length: 255 })
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
// Public keys are non-secret, so they are stored in plaintext (no field-level
// encryption). This is what lets the unauthenticated resolver serve them.
publicKey: text("public_key").notNull(),
// Raw algorithm token (e.g. "ssh-ed25519"), and a normalized group used for
// the /<ALGO> resolver filter (RSA / ED25519 / ECDSA / ...).
keyType: text("key_type").notNull(),
algorithm: text("algorithm").notNull(),
label: text("label"),
comment: text("comment"),
// "manual" (pasted) or "credential" (imported from an ssh_credentials entry).
source: text("source").notNull().default("manual"),
credentialId: integer("credential_id").references(() => sshCredentials.id, {
onDelete: "set null",
}),
enabled: boolean("enabled").notNull().default(true),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
});
// Per-identity certificate authority. Servers that trust this CA (via
// TrustedUserCAKeys / @cert-authority) accept any user certificate it signs,
// giving central revocation (rotate the CA) and expiry (cert validity).
export const termixIdentityCa = pgTable("termix_identity_ca", {
id: serial("id").primaryKey(),
identityId: integer("identity_id")
.notNull()
.unique()
.references(() => termixIdentities.id, { onDelete: "cascade" }),
userId: varchar("user_id", { length: 255 })
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
// CA public key (plaintext — it is published); CA private key is field-encrypted.
publicKey: text("public_key").notNull(),
privateKey: text("private_key").notNull(),
validityDays: integer("validity_days").notNull().default(90),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
updatedAt: text("updated_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
});
// --- termix-id end ---
// termix_identities, termix_identity_keys and termix_identity_ca moved to the
// termix-identity plugin (p_termix_identity_*).
// --- alerts begin ---
export const notificationChannels = pgTable("notification_channels", {
+2 -72
View File
@@ -769,78 +769,8 @@ export const uiPreferences = sqliteTable("ui_preferences", {
// dashboard_service_links moved to the homepage plugin (p_homepage_dashboard_service_links).
// --- termix-id begin ---
// A user claims a unique public handle. Their published SSH public keys are
// served at an unauthenticated resolver endpoint in authorized_keys format,
// so any server can be provisioned with `curl <host>/termix-id/u/<handle> >> ~/.ssh/authorized_keys`.
export const termixIdentities = sqliteTable("termix_identities", {
id: integer("id").primaryKey({ autoIncrement: true }),
// One Termix ID per user — enforced in schema, not just in code.
userId: text("user_id")
.notNull()
.unique()
.references(() => users.id, { onDelete: "cascade" }),
handle: text("handle").notNull().unique(),
description: text("description"),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
updatedAt: text("updated_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
});
export const termixIdentityKeys = sqliteTable("termix_identity_keys", {
id: integer("id").primaryKey({ autoIncrement: true }),
identityId: integer("identity_id")
.notNull()
.references(() => termixIdentities.id, { onDelete: "cascade" }),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
// Public keys are non-secret, so they are stored in plaintext (no field-level
// encryption). This is what lets the unauthenticated resolver serve them.
publicKey: text("public_key", { length: 8192 }).notNull(),
// Raw algorithm token (e.g. "ssh-ed25519"), and a normalized group used for
// the /<ALGO> resolver filter (RSA / ED25519 / ECDSA / ...).
keyType: text("key_type").notNull(),
algorithm: text("algorithm").notNull(),
label: text("label"),
comment: text("comment"),
// "manual" (pasted) or "credential" (imported from an ssh_credentials entry).
source: text("source").notNull().default("manual"),
credentialId: integer("credential_id").references(() => sshCredentials.id, {
onDelete: "set null",
}),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
});
// Per-identity certificate authority. Servers that trust this CA (via
// TrustedUserCAKeys / @cert-authority) accept any user certificate it signs,
// giving central revocation (rotate the CA) and expiry (cert validity).
export const termixIdentityCa = sqliteTable("termix_identity_ca", {
id: integer("id").primaryKey({ autoIncrement: true }),
identityId: integer("identity_id")
.notNull()
.unique()
.references(() => termixIdentities.id, { onDelete: "cascade" }),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
// CA public key (plaintext — it is published); CA private key is field-encrypted.
publicKey: text("public_key", { length: 4096 }).notNull(),
privateKey: text("private_key", { length: 8192 }).notNull(),
validityDays: integer("validity_days").notNull().default(90),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
updatedAt: text("updated_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
});
// --- termix-id end ---
// termix_identities, termix_identity_keys and termix_identity_ca moved to the
// termix-identity plugin (p_termix_identity_*).
// --- alerts begin ---
export const notificationChannels = sqliteTable("notification_channels", {
@@ -36,8 +36,6 @@ import { SharedHostAuthOverrideRepository } from "./shared-host-auth-override-re
import { SharedHostSecretsRepository } from "./shared-host-secrets-repository.js";
import { SshCredentialUsageRepository } from "./ssh-credential-usage-repository.js";
import { SyncTombstoneRepository } from "./sync-tombstone-repository.js";
import { TermixIdentityCaRepository } from "./termix-identity-ca-repository.js";
import { TermixIdentityRepository } from "./termix-identity-repository.js";
import { TrustedDeviceRepository } from "./trusted-device-repository.js";
import { UserDataExportRepository } from "./user-data-export-repository.js";
import { UserPreferenceRepository } from "./user-preference-repository.js";
@@ -349,20 +347,6 @@ export function createCurrentSshCredentialUsageRepository(): SshCredentialUsageR
);
}
export function createCurrentTermixIdentityCaRepository(): TermixIdentityCaRepository {
return new TermixIdentityCaRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("termix_identity_ca_repository_write"),
);
}
export function createCurrentTermixIdentityRepository(): TermixIdentityRepository {
return new TermixIdentityRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("termix_identity_repository_write"),
);
}
// transfer_recent moved to the file-manager plugin.
// tmux_session_tags moved to the tmux-monitor plugin.
@@ -1,223 +0,0 @@
import { eq } from "drizzle-orm";
import { termixIdentityCa } from "../db/schema.js";
import type { DatabaseContext } from "./database-context.js";
import { DataCrypto } from "../../utils/data-crypto.js";
import {
insertedId,
rowsAffected,
supportsReturning,
} from "./mutation-result.js";
import { updateReturning } from "./returning.js";
export type TermixIdentityCaRecord = typeof termixIdentityCa.$inferSelect;
export type NewTermixIdentityCaRecord = typeof termixIdentityCa.$inferInsert;
export type TermixIdentityCaUpdate = Partial<
Pick<
NewTermixIdentityCaRecord,
"publicKey" | "privateKey" | "validityDays" | "updatedAt"
>
>;
export class TermixIdentityCaRepository {
constructor(
private readonly context: DatabaseContext,
private readonly onWrite?: () => void | Promise<void>,
) {}
async findPublicByIdentityId(
identityId: number,
): Promise<Pick<
TermixIdentityCaRecord,
"publicKey" | "validityDays"
> | null> {
const rows = await this.context.drizzle
.select({
publicKey: termixIdentityCa.publicKey,
validityDays: termixIdentityCa.validityDays,
})
.from(termixIdentityCa)
.where(eq(termixIdentityCa.identityId, identityId))
.limit(1);
return rows[0] ?? null;
}
async findDecryptedByIdentityId(
userId: string,
identityId: number,
): Promise<TermixIdentityCaRecord | null> {
const rows = await this.context.drizzle
.select()
.from(termixIdentityCa)
.where(eq(termixIdentityCa.identityId, identityId))
.limit(1);
return this.decryptOne(rows[0] ?? null, userId);
}
async createEncryptedForUser(
userId: string,
ca: NewTermixIdentityCaRecord,
): Promise<TermixIdentityCaRecord> {
const userDataKey = DataCrypto.validateUserAccess(userId);
const result = await this.insertThenEncrypt(userId, ca, userDataKey);
await this.afterWrite();
return DataCrypto.decryptRecord(
"termix_identity_ca",
result,
userId,
userDataKey,
);
}
/**
* Writes a CA in two steps, because the ciphertext depends on the id.
*
* The private key is encrypted with the row's own id as context, which does
* not exist until the row does. So: insert with an empty key, encrypt, update.
* The empty key must never be observable, hence the transaction.
*
* Two branches because better-sqlite3 rejects an async transaction callback —
* see the same note in UserRepository.
*/
private async insertThenEncrypt(
userId: string,
ca: NewTermixIdentityCaRecord,
userDataKey: Buffer,
): Promise<TermixIdentityCaRecord> {
const draft = { ...ca, privateKey: "" };
const seal = (id: number) =>
DataCrypto.encryptRecord(
"termix_identity_ca",
{ id, privateKey: ca.privateKey },
userId,
userDataKey,
).privateKey;
if (this.context.dialect === "sqlite") {
/* eslint-disable no-restricted-syntax -- sqlite-only branch: the dialect
is checked directly above, and better-sqlite3 needs the synchronous
.all() form, which has no async equivalent. */
return this.context.drizzle.transaction((tx) => {
const row = tx
.insert(termixIdentityCa)
.values(draft)
.returning()
.all()[0];
return tx
.update(termixIdentityCa)
.set({ privateKey: seal(row.id) })
.where(eq(termixIdentityCa.id, row.id))
.returning()
.all()[0];
});
/* eslint-enable no-restricted-syntax */
}
return this.context.drizzle.transaction(async (tx) => {
let id: number | null;
if (supportsReturning(this.context.dialect)) {
// eslint-disable-next-line no-restricted-syntax -- guarded by the check above
const rows = await tx
.insert(termixIdentityCa)
.values(draft)
.returning();
id = rows[0]?.id ?? null;
} else {
id = insertedId(await tx.insert(termixIdentityCa).values(draft));
}
if (id === null) {
throw new Error("Insert into termix_identity_ca returned no id.");
}
await tx
.update(termixIdentityCa)
.set({ privateKey: seal(id) })
.where(eq(termixIdentityCa.id, id));
const [row] = await tx
.select()
.from(termixIdentityCa)
.where(eq(termixIdentityCa.id, id));
return row;
});
}
async updateEncryptedForIdentity(
userId: string,
identityId: number,
update: TermixIdentityCaUpdate,
): Promise<TermixIdentityCaRecord | null> {
const existing = await this.findDecryptedByIdentityId(userId, identityId);
if (!existing) return null;
const userDataKey = DataCrypto.validateUserAccess(userId);
const encryptedPrivateKey = update.privateKey
? DataCrypto.encryptRecord(
"termix_identity_ca",
{ id: existing.id, privateKey: update.privateKey },
userId,
userDataKey,
).privateKey
: undefined;
const rows = await updateReturning(
this.context,
termixIdentityCa,
{
...update,
...(encryptedPrivateKey ? { privateKey: encryptedPrivateKey } : {}),
},
eq(termixIdentityCa.identityId, identityId),
);
await this.afterWrite();
return this.decryptOne(rows[0] ?? null, userId);
}
async deleteByIdentityId(identityId: number): Promise<boolean> {
const result = await this.context.drizzle
.delete(termixIdentityCa)
.where(eq(termixIdentityCa.identityId, identityId));
if (rowsAffected(result) > 0) {
await this.afterWrite();
}
return rowsAffected(result) > 0;
}
async deleteByUserId(userId: string): Promise<number> {
const result = await this.context.drizzle
.delete(termixIdentityCa)
.where(eq(termixIdentityCa.userId, userId));
if (rowsAffected(result) > 0) {
await this.afterWrite();
}
return rowsAffected(result);
}
private decryptOne<T extends Record<string, unknown>>(
record: T | null,
userId: string,
): T | null {
if (!record) return null;
const userDataKey = DataCrypto.getUserDataKey(userId);
if (!userDataKey) return null;
return DataCrypto.decryptRecord(
"termix_identity_ca",
record,
userId,
userDataKey,
);
}
private async afterWrite(): Promise<void> {
await this.onWrite?.();
}
}
@@ -1,237 +0,0 @@
import { and, asc, eq } from "drizzle-orm";
import { termixIdentities, termixIdentityKeys } from "../db/schema.js";
import type { DatabaseContext } from "./database-context.js";
import { rowsAffected } from "./mutation-result.js";
import { insertReturning, updateReturning } from "./returning.js";
export type TermixIdentityRecord = typeof termixIdentities.$inferSelect;
export type NewTermixIdentityRecord = typeof termixIdentities.$inferInsert;
export type TermixIdentityUpdate = Partial<
Pick<NewTermixIdentityRecord, "handle" | "description" | "updatedAt">
>;
export type TermixIdentityKeyRecord = typeof termixIdentityKeys.$inferSelect;
export type NewTermixIdentityKeyRecord = typeof termixIdentityKeys.$inferInsert;
export type TermixIdentityKeyUpdate = Partial<
Pick<NewTermixIdentityKeyRecord, "enabled" | "label">
>;
export class TermixIdentityRepository {
constructor(
private readonly context: DatabaseContext,
private readonly onWrite?: () => void | Promise<void>,
) {}
async findIdentityForUser(
userId: string,
): Promise<TermixIdentityRecord | null> {
const rows = await this.context.drizzle
.select()
.from(termixIdentities)
.where(eq(termixIdentities.userId, userId))
.limit(1);
return rows[0] ?? null;
}
async findIdentityByHandle(
handle: string,
): Promise<TermixIdentityRecord | null> {
const rows = await this.context.drizzle
.select()
.from(termixIdentities)
.where(eq(termixIdentities.handle, handle))
.limit(1);
return rows[0] ?? null;
}
async isHandleTaken(handle: string): Promise<boolean> {
const rows = await this.context.drizzle
.select({ id: termixIdentities.id })
.from(termixIdentities)
.where(eq(termixIdentities.handle, handle))
.limit(1);
return rows.length > 0;
}
async createIdentity(
identity: NewTermixIdentityRecord,
): Promise<TermixIdentityRecord> {
const rows = await insertReturning(
this.context,
termixIdentities,
identity,
);
await this.afterWrite();
return rows[0];
}
async updateIdentityForUser(
userId: string,
update: TermixIdentityUpdate,
): Promise<TermixIdentityRecord | null> {
const rows = await updateReturning(
this.context,
termixIdentities,
update,
eq(termixIdentities.userId, userId),
);
if (rows.length > 0) {
await this.afterWrite();
}
return rows[0] ?? null;
}
async deleteIdentityForUser(userId: string): Promise<boolean> {
const result = await this.context.drizzle
.delete(termixIdentities)
.where(eq(termixIdentities.userId, userId));
if (rowsAffected(result) > 0) {
await this.afterWrite();
}
return rowsAffected(result) > 0;
}
async deleteByUserId(userId: string): Promise<{
identitiesDeleted: number;
keysDeleted: number;
}> {
const keyResult = await this.context.drizzle
.delete(termixIdentityKeys)
.where(eq(termixIdentityKeys.userId, userId));
const result = await this.context.drizzle
.delete(termixIdentities)
.where(eq(termixIdentities.userId, userId));
if (rowsAffected(keyResult) > 0 || rowsAffected(result) > 0) {
await this.afterWrite();
}
return {
identitiesDeleted: rowsAffected(result),
keysDeleted: rowsAffected(keyResult),
};
}
async listKeysByIdentityId(
identityId: number,
): Promise<TermixIdentityKeyRecord[]> {
return this.context.drizzle
.select()
.from(termixIdentityKeys)
.where(eq(termixIdentityKeys.identityId, identityId))
.orderBy(asc(termixIdentityKeys.id));
}
async listEnabledKeysByIdentityId(
identityId: number,
): Promise<TermixIdentityKeyRecord[]> {
return this.context.drizzle
.select()
.from(termixIdentityKeys)
.where(
and(
eq(termixIdentityKeys.identityId, identityId),
eq(termixIdentityKeys.enabled, true),
),
)
.orderBy(asc(termixIdentityKeys.id));
}
async listLinkedCredentialIds(identityId: number): Promise<number[]> {
const rows = await this.context.drizzle
.select({ credentialId: termixIdentityKeys.credentialId })
.from(termixIdentityKeys)
.where(
and(
eq(termixIdentityKeys.identityId, identityId),
eq(termixIdentityKeys.enabled, true),
),
);
return Array.from(
new Set(
rows
.map((row) => row.credentialId)
.filter(
(credentialId): credentialId is number => credentialId !== null,
),
),
);
}
async createKey(
key: NewTermixIdentityKeyRecord,
): Promise<TermixIdentityKeyRecord> {
const rows = await insertReturning(this.context, termixIdentityKeys, key);
await this.afterWrite();
return rows[0];
}
async updateKeyForUser(
userId: string,
id: number,
update: TermixIdentityKeyUpdate,
): Promise<TermixIdentityKeyRecord | null> {
const rows = await updateReturning(
this.context,
termixIdentityKeys,
update,
and(eq(termixIdentityKeys.id, id), eq(termixIdentityKeys.userId, userId)),
);
if (rows.length > 0) {
await this.afterWrite();
}
return rows[0] ?? null;
}
async deleteKeyForUser(userId: string, id: number): Promise<boolean> {
const result = await this.context.drizzle
.delete(termixIdentityKeys)
.where(
and(
eq(termixIdentityKeys.id, id),
eq(termixIdentityKeys.userId, userId),
),
);
if (rowsAffected(result) > 0) {
await this.afterWrite();
}
return rowsAffected(result) > 0;
}
async findKeyForUser(
userId: string,
id: number,
): Promise<TermixIdentityKeyRecord | null> {
const rows = await this.context.drizzle
.select()
.from(termixIdentityKeys)
.where(
and(
eq(termixIdentityKeys.id, id),
eq(termixIdentityKeys.userId, userId),
),
)
.limit(1);
return rows[0] ?? null;
}
private async afterWrite(): Promise<void> {
await this.onWrite?.();
}
}
@@ -18,8 +18,6 @@ import {
createCurrentSettingsRepository,
createCurrentSharedHostSecretsRepository,
createCurrentSshCredentialUsageRepository,
createCurrentTermixIdentityCaRepository,
createCurrentTermixIdentityRepository,
createCurrentTrustedDeviceRepository,
createCurrentUserPreferenceRepository,
createCurrentUserRepository,
@@ -95,10 +93,8 @@ export async function deleteUserAndRelatedData(
// 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 and vault
// plugins' tables cascade the same way.
// plugins' tables cascade the same way, and so do termix-identity's.
await createCurrentTermixIdentityCaRepository().deleteByUserId(userId);
await createCurrentTermixIdentityRepository().deleteByUserId(userId);
await createCurrentOpenTabRepository().deleteByUserId(userId);
await createCurrentUserPreferenceRepository().deleteByUserId(userId);
@@ -69,3 +69,33 @@ export function registerVaultCompatRoutes(router: Router): void {
res.redirect(307, localUrl(req, "/plugin-api/vault/oidc/callback"));
});
}
/**
* The 2.8 Termix ID resolver URLs, which servers fetch from provisioning
* scripts. Permanently redirected to the termix-identity plugin's public
* resolver; curl -L follows them.
*/
export function registerTermixIdCompatRoutes(router: Router): void {
/**
* @openapi
* /termix-id/u/{handle}:
* get:
* summary: Termix ID resolver (2.8 URL)
* description: Permanently redirects to /plugin-api/termix-identity/u/{handle}, and likewise for /termix-id/u/{handle}/{algo} and /termix-id/u/{handle}/ca, so servers provisioned with the old URL keep working.
* tags:
* - Termix ID
* parameters:
* - in: path
* name: handle
* required: true
* schema:
* type: string
* responses:
* 308:
* description: Redirect to the termix-identity plugin's resolver.
*/
router.get(["/u/:handle", "/u/:handle/:algo"], (req, res) => {
const rest = req.path.slice("/u".length);
res.redirect(308, localUrl(req, `/plugin-api/termix-identity/u${rest}`));
});
}
@@ -1,145 +0,0 @@
import crypto from "crypto";
// Minimal OpenSSH ed25519 user-certificate signer + per-user CA generation.
// Implemented in pure Node so we never shell out or move private keys to disk.
// Format reference: PROTOCOL.certkeys (ssh-ed25519-cert-v01@openssh.com).
const ED25519 = "ssh-ed25519";
const CERT_TYPE = "ssh-ed25519-cert-v01@openssh.com";
const SSH2_CERT_TYPE_USER = 1;
// Standard login extensions OpenSSH grants by default; must be name-sorted.
const DEFAULT_EXTENSIONS = [
"permit-X11-forwarding",
"permit-agent-forwarding",
"permit-port-forwarding",
"permit-pty",
"permit-user-rc",
];
// --- SSH wire-format primitives -------------------------------------------
function sshString(value: Buffer | string): Buffer {
const buf = typeof value === "string" ? Buffer.from(value, "utf8") : value;
const len = Buffer.alloc(4);
len.writeUInt32BE(buf.length, 0);
return Buffer.concat([len, buf]);
}
function sshUint32(n: number): Buffer {
const b = Buffer.alloc(4);
b.writeUInt32BE(n >>> 0, 0);
return b;
}
function sshUint64(n: bigint): Buffer {
const b = Buffer.alloc(8);
b.writeBigUInt64BE(n, 0);
return b;
}
function ed25519PublicBlob(raw32: Buffer): Buffer {
return Buffer.concat([sshString(ED25519), sshString(raw32)]);
}
/** Extract the 32-byte raw ed25519 key from an `ssh-ed25519 <base64>` line. */
export function ed25519RawFromLine(line: string): Buffer | null {
const parts = line.trim().split(/\s+/);
if (parts[0] !== ED25519 || !parts[1]) return null;
let blob: Buffer;
try {
blob = Buffer.from(parts[1], "base64");
} catch {
return null;
}
try {
let off = 0;
const typeLen = blob.readUInt32BE(off);
off += 4;
if (blob.toString("utf8", off, off + typeLen) !== ED25519) return null;
off += typeLen;
const keyLen = blob.readUInt32BE(off);
off += 4;
const pk = blob.subarray(off, off + keyLen);
return pk.length === 32 ? pk : null;
} catch {
return null;
}
}
// --- CA generation ---------------------------------------------------------
export interface GeneratedCa {
publicKeyLine: string; // "ssh-ed25519 <base64>"
privateKeyPem: string; // PKCS#8 PEM (to be stored encrypted)
}
export function generateCa(): GeneratedCa {
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
const jwk = publicKey.export({ format: "jwk" }) as { x: string };
const raw = Buffer.from(jwk.x, "base64url");
const publicKeyLine = `${ED25519} ${ed25519PublicBlob(raw).toString("base64")}`;
const privateKeyPem = privateKey
.export({ format: "pem", type: "pkcs8" })
.toString();
return { publicKeyLine, privateKeyPem };
}
// --- Certificate signing ---------------------------------------------------
export interface SignCertOptions {
userPublicKeyLine: string; // ed25519 public key to certify
caPrivateKeyPem: string;
caPublicKeyLine: string;
keyId: string;
principals: string[]; // empty = valid for all usernames
validAfter: number; // unix seconds
validBefore: number; // unix seconds
serial?: bigint;
}
/**
* Sign an ed25519 user public key into an OpenSSH user certificate.
* Returns the certificate line, or null if the inputs aren't ed25519.
*/
export function signUserCertificate(opts: SignCertOptions): string | null {
const pk = ed25519RawFromLine(opts.userPublicKeyLine);
const caRaw = ed25519RawFromLine(opts.caPublicKeyLine);
if (!pk || !caRaw) return null;
const signatureKey = ed25519PublicBlob(caRaw);
const principals = Buffer.concat(opts.principals.map((p) => sshString(p)));
const extensions = Buffer.concat(
[...DEFAULT_EXTENSIONS]
.sort()
.map((name) => Buffer.concat([sshString(name), sshString("")])),
);
// Everything up to (not including) the signature — this is what gets signed.
const body = Buffer.concat([
sshString(CERT_TYPE),
sshString(crypto.randomBytes(32)), // nonce
sshString(pk),
sshUint64(opts.serial ?? 0n),
sshUint32(SSH2_CERT_TYPE_USER),
sshString(opts.keyId),
sshString(principals),
sshUint64(BigInt(opts.validAfter)),
sshUint64(BigInt(opts.validBefore)),
sshString(Buffer.alloc(0)), // critical options (none)
sshString(extensions),
sshString(Buffer.alloc(0)), // reserved
sshString(signatureKey),
]);
const caKey = crypto.createPrivateKey({
key: opts.caPrivateKeyPem,
format: "pem",
type: "pkcs8",
});
const rawSig = crypto.sign(null, body, caKey); // ed25519 -> 64 bytes
const signature = Buffer.concat([sshString(ED25519), sshString(rawSig)]);
const cert = Buffer.concat([body, sshString(signature)]);
return `${CERT_TYPE} ${cert.toString("base64")} ${opts.keyId}`;
}
@@ -1,89 +0,0 @@
// Pure, dependency-free helpers for SSH public-key parsing/classification used
// by the Termix ID routes. Kept separate so they can be unit-tested in isolation.
// Upper bound on an accepted public-key line. Public keys are tiny (an RSA-4096
// line is ~720 chars); this caps the value the unauthenticated resolver later
// streams, preventing a multi-MB blob being stored and amplified.
export const MAX_PUBLIC_KEY_LENGTH = 8192;
// Normalized algorithm groups, used both for storage and for the `/<ALGO>`
// resolver filter (mirrors sshid.io's RSA/ED25519/ECDSA suffixes).
export const ALGO_GROUPS: Record<string, string> = {
"ssh-rsa": "RSA",
"rsa-sha2-256": "RSA",
"rsa-sha2-512": "RSA",
"ssh-dss": "DSA",
"ssh-ed25519": "ED25519",
"sk-ssh-ed25519@openssh.com": "ED25519-SK",
"ecdsa-sha2-nistp256": "ECDSA",
"ecdsa-sha2-nistp384": "ECDSA",
"ecdsa-sha2-nistp521": "ECDSA",
"sk-ecdsa-sha2-nistp256@openssh.com": "ECDSA-SK",
};
export function classifyAlgo(type: string): string {
if (ALGO_GROUPS[type]) return ALGO_GROUPS[type];
if (type.startsWith("ecdsa-")) return "ECDSA";
if (type.includes("ed25519")) return "ED25519";
if (type.includes("rsa")) return "RSA";
if (type.includes("dss") || type.includes("dsa")) return "DSA";
return type.toUpperCase();
}
export interface ParsedPublicKey {
type: string;
algorithm: string;
comment: string;
normalized: string; // "<type> <base64blob>"
}
/**
* Parse and validate a single OpenSSH public key line. Returns null when the
* input is not a well-formed public key.
*/
export function parsePublicKey(
raw: string | null | undefined,
): ParsedPublicKey | null {
if (typeof raw !== "string") return null;
if (raw.length > MAX_PUBLIC_KEY_LENGTH) return null;
const line = raw.trim().replace(/\s+/g, " ");
if (!line) return null;
const parts = line.split(" ");
if (parts.length < 2) return null;
const [type, blob, ...rest] = parts;
if (!/^[A-Za-z0-9@.-]+$/.test(type)) return null;
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(blob)) return null;
const decoded = Buffer.from(blob, "base64");
// The blob is an SSH wire-format string whose first field repeats the type.
if (decoded.length < 8) return null;
try {
const len = decoded.readUInt32BE(0);
if (len <= 0 || len > 64 || 4 + len > decoded.length) return null;
const embeddedType = decoded.toString("utf8", 4, 4 + len);
if (embeddedType !== type) return null;
} catch {
return null;
}
return {
type,
algorithm: classifyAlgo(type),
comment: rest.join(" "),
normalized: `${type} ${blob}`,
};
}
/**
* Whether a key's normalized algorithm group matches a `/<ALGO>` resolver
* filter. Exact match only — `ED25519` must NOT also return `ED25519-SK`.
*/
export function matchesAlgoFilter(
algorithm: string,
filter: string | null,
): boolean {
if (!filter) return true;
return algorithm.toUpperCase() === filter.toUpperCase();
}
File diff suppressed because it is too large. Load diff
+125
View File
@@ -13,13 +13,19 @@
* "op://") instead of handing over a host's own stored secret. Needs
* auth:provide, like ctx.auth's registrations, and the scheme must be listed
* in contributes.auth.secretSchemes.
*
* listSshKeys and createSshKey reach the acting user's own saved keys: the
* public half only (credentials:use), and saving a new key pair
* (credentials:write). Termix Identity publishes and generates keys this way.
*/
import type {
PluginCredentials,
PluginHostProtocol,
PluginProtocolTarget,
PluginSshKeyCredential,
} from "@termix/plugin-sdk/backend";
import ssh2 from "ssh2";
import { PluginCapabilityError } from "@termix/plugin-sdk/backend";
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
import { assertCapability } from "./permissions.js";
@@ -157,6 +163,47 @@ async function resolveRecipientAuth(
};
}
function derivePublicKey(
privateKey: string | null | undefined,
passphrase: string | null | undefined,
): string | null {
if (!privateKey) return null;
try {
const parsed = ssh2.utils.parseKey(privateKey, passphrase || undefined);
const key = Array.isArray(parsed) ? parsed[0] : parsed;
if (!key || key instanceof Error) return null;
return `${key.type} ${key.getPublicSSH().toString("base64")}`;
} catch {
return null;
}
}
function errorText(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
async function requireCorePermission(
userId: string,
permission: string,
): Promise<void> {
const { PermissionManager } = await import("../utils/permission-manager.js");
if (
!(await PermissionManager.getInstance().hasPermission(userId, permission))
) {
throw new Error(`The acting user lacks ${permission}`);
}
}
function actingUser(): string {
const userId = getActor();
if (!userId) {
throw new Error(
"ctx.credentials needs an acting user: call it inside a request or ctx.asUser",
);
}
return userId;
}
export function createPluginCredentials({
manifest,
bag,
@@ -166,6 +213,84 @@ export function createPluginCredentials({
const declared = manifest.capabilities;
return {
listSshKeys: async () => {
try {
await assertCapability(pluginId, "credentials:use", declared);
const userId = actingUser();
await requireCorePermission(userId, "credentials.view");
const { createCurrentCredentialRepository } =
await import("../database/repositories/factory.js");
const rows =
await createCurrentCredentialRepository().listDecryptedByUserId(
userId,
);
const keys: PluginSshKeyCredential[] = rows
.filter((row) => row.authType === "key")
.map((row) => ({
id: row.id,
name: row.name,
username: row.username ?? null,
publicKey:
(typeof row.publicKey === "string" && row.publicKey.trim()) ||
derivePublicKey(row.privateKey || row.key, row.keyPassword),
}));
await audit("credentials_list_keys", `${keys.length} keys`, {
success: true,
});
return keys;
} catch (error) {
await audit("credentials_list_keys", "saved SSH keys", {
success: false,
errorMessage: errorText(error),
});
throw error;
}
},
createSshKey: async (input) => {
const details = `SSH key "${input?.name ?? ""}"`;
try {
await assertCapability(pluginId, "credentials:write", declared);
const userId = actingUser();
await requireCorePermission(userId, "credentials.create");
if (!input?.name || !input.privateKey || !input.publicKey) {
throw new Error("name, privateKey and publicKey are required");
}
const { createCurrentCredentialRepository } =
await import("../database/repositories/factory.js");
const created =
await createCurrentCredentialRepository().createEncryptedForUser(
userId,
{
userId,
name: input.name,
description: input.description ?? null,
folder: null,
tags: "",
authType: "key",
username: input.username || null,
password: null,
key: input.privateKey,
privateKey: input.privateKey,
publicKey: input.publicKey,
keyPassword: null,
keyType: null,
detectedKeyType: input.keyType,
usageCount: 0,
lastUsed: null,
},
);
await audit("credentials_create_key", details, { success: true });
return { id: created.id };
} catch (error) {
await audit("credentials_create_key", details, {
success: false,
errorMessage: errorText(error),
});
throw error;
}
},
registerSecretResolver: (scheme, resolve) => {
if (!declared.includes("auth:provide")) {
throw new PluginCapabilityError(pluginId, "auth:provide");
+6
View File
@@ -392,6 +392,12 @@ async function provisionLocalDesktopUserIfNeeded(): Promise<void> {
const { runTotpMigration } =
await import("./utils/crypto-migration/totp-migration.js");
await runTotpMigration();
// Also after the plugin moves, for a CA whose owner key was not open
// when the table was adopted.
const { runTermixIdentityCaMigration } =
await import("./utils/crypto-migration/termix-identity-ca-migration.js");
await runTermixIdentityCaMigration();
} catch (error) {
systemLogger.warn("Plugin runtime failed to initialize", {
operation: "plugin_init",
@@ -69,8 +69,9 @@ describe("generated schemas", () => {
expect(tablesOf(pgSchema)).toEqual(tablesOf(sqliteSchema));
expect(tablesOf(mysqlSchema)).toEqual(tablesOf(sqliteSchema));
// Guard against a generator that silently emits nothing.
expect(tablesOf(sqliteSchema).length).toBeGreaterThan(40);
// Guard against a generator that silently emits nothing. Core keeps
// shrinking as feature tables move into plugins, so this stays loose.
expect(tablesOf(sqliteSchema).length).toBeGreaterThan(25);
});
it("maps each column to the right storage type per dialect", () => {
@@ -1,224 +0,0 @@
import { sql } from "drizzle-orm";
import { afterEach, describe, expect, vi } from "vitest";
import { TestSqliteDatabase } from "./test-support.js";
import { DataCrypto } from "../../../utils/data-crypto.js";
import { TermixIdentityCaRepository } from "../../../database/repositories/termix-identity-ca-repository.js";
describe("TermixIdentityCaRepository", () => {
let adapter: TestSqliteDatabase | null = null;
afterEach(async () => {
vi.restoreAllMocks();
if (adapter) {
await adapter.close();
adapter = null;
}
});
async function createRepository(onWrite = vi.fn()): Promise<{
repo: TermixIdentityCaRepository;
onWrite: ReturnType<typeof vi.fn>;
}> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
await adapter.exec(`
INSERT INTO users (id, username, password_hash)
VALUES ('user-1', 'alice', 'hash');
INSERT INTO termix_identities (id, user_id, handle)
VALUES (7, 'user-1', 'alice');
`);
return {
repo: new TermixIdentityCaRepository(context, onWrite),
onWrite,
};
}
function mockCrypto(): void {
vi.spyOn(DataCrypto, "validateUserAccess").mockReturnValue(
Buffer.from("user-key"),
);
vi.spyOn(DataCrypto, "getUserDataKey").mockReturnValue(
Buffer.from("user-key"),
);
vi.spyOn(DataCrypto, "encryptRecord").mockImplementation(
(_tableName, record) =>
({
...record,
privateKey: "encrypted-ca-private",
}) as typeof record,
);
vi.spyOn(DataCrypto, "decryptRecord").mockImplementation(
(_tableName, record) =>
({
...record,
privateKey:
record.privateKey === "encrypted-ca-private"
? "decrypted-ca-private"
: record.privateKey,
}) as typeof record,
);
}
it("creates CA private keys with the real row id before encryption", async () => {
const { repo, onWrite } = await createRepository();
mockCrypto();
const created = await repo.createEncryptedForUser("user-1", {
identityId: 7,
userId: "user-1",
publicKey: "ssh-ed25519 public",
privateKey: "plain-ca-private",
validityDays: 120,
});
const [raw] = (await adapter!.query(
sql`SELECT id, public_key, private_key, validity_days FROM termix_identity_ca WHERE identity_id = 7`,
)) as {
id: number;
public_key: string;
private_key: string;
validity_days: number;
}[];
expect(created.privateKey).toBe("decrypted-ca-private");
expect(raw.private_key).toBe("encrypted-ca-private");
expect(raw.public_key).toBe("ssh-ed25519 public");
expect(raw.validity_days).toBe(120);
expect(DataCrypto.encryptRecord).toHaveBeenCalledWith(
"termix_identity_ca",
{ id: raw.id, privateKey: "plain-ca-private" },
"user-1",
Buffer.from("user-key"),
);
expect(onWrite).toHaveBeenCalledTimes(1);
});
it("reads public CA metadata without decrypting private key material", async () => {
const { repo } = await createRepository();
const decryptSpy = vi.spyOn(DataCrypto, "decryptRecord");
await adapter!.run(
sql`INSERT INTO termix_identity_ca (identity_id, user_id, public_key, private_key, validity_days)
VALUES (7, 'user-1', 'ssh-ed25519 public', 'encrypted-ca-private', 45)`,
);
await expect(repo.findPublicByIdentityId(7)).resolves.toEqual({
publicKey: "ssh-ed25519 public",
validityDays: 45,
});
expect(decryptSpy).not.toHaveBeenCalled();
});
it("decrypts CA private keys through the user data boundary", async () => {
const { repo } = await createRepository();
mockCrypto();
await adapter!.run(
sql`INSERT INTO termix_identity_ca (identity_id, user_id, public_key, private_key, validity_days)
VALUES (7, 'user-1', 'ssh-ed25519 public', 'encrypted-ca-private', 45)`,
);
const ca = await repo.findDecryptedByIdentityId("user-1", 7);
expect(ca).toMatchObject({
identityId: 7,
publicKey: "ssh-ed25519 public",
privateKey: "decrypted-ca-private",
validityDays: 45,
});
expect(DataCrypto.decryptRecord).toHaveBeenCalledWith(
"termix_identity_ca",
expect.objectContaining({ identityId: 7 }),
"user-1",
Buffer.from("user-key"),
);
});
it("updates CA private keys through encrypted writes", async () => {
const { repo, onWrite } = await createRepository();
mockCrypto();
await adapter!.run(
sql`INSERT INTO termix_identity_ca (identity_id, user_id, public_key, private_key, validity_days) VALUES (7, 'user-1', 'ssh-ed25519 old', 'encrypted-ca-private', 45)`,
);
onWrite.mockClear();
const updated = await repo.updateEncryptedForIdentity("user-1", 7, {
publicKey: "ssh-ed25519 new",
privateKey: "plain-updated-ca-private",
validityDays: 90,
});
const [raw] = (await adapter!.query(
sql`SELECT public_key, private_key, validity_days FROM termix_identity_ca WHERE identity_id = 7`,
)) as {
public_key: string;
private_key: string;
validity_days: number;
}[];
expect(updated).toMatchObject({
publicKey: "ssh-ed25519 new",
privateKey: "decrypted-ca-private",
validityDays: 90,
});
expect(raw).toEqual({
public_key: "ssh-ed25519 new",
private_key: "encrypted-ca-private",
validity_days: 90,
});
expect(DataCrypto.encryptRecord).toHaveBeenCalledWith(
"termix_identity_ca",
expect.objectContaining({
privateKey: "plain-updated-ca-private",
}),
"user-1",
Buffer.from("user-key"),
);
expect(onWrite).toHaveBeenCalledTimes(1);
});
it("deletes CA rows through the write boundary", async () => {
const { repo, onWrite } = await createRepository();
await adapter!.run(
sql`INSERT INTO termix_identity_ca (identity_id, user_id, public_key, private_key, validity_days) VALUES (7, 'user-1', 'ssh-ed25519 public', 'encrypted-ca-private', 45)`,
);
onWrite.mockClear();
await expect(repo.deleteByIdentityId(7)).resolves.toBe(true);
await expect(repo.deleteByIdentityId(7)).resolves.toBe(false);
expect(
(
await adapter!.query(
sql`SELECT COUNT(*) AS count FROM termix_identity_ca`,
)
).map((row) => Number((row as { count: unknown }).count)),
).toEqual([0]);
expect(onWrite).toHaveBeenCalledTimes(1);
});
it("deletes CA rows for a user", async () => {
const { repo, onWrite } = await createRepository();
await adapter!.run(
sql`INSERT INTO users (id, username, password_hash) VALUES ('user-2', 'bob', 'hash')`,
);
await adapter!.run(
sql`INSERT INTO termix_identities (id, user_id, handle) VALUES (8, 'user-2', 'bob')`,
);
await adapter!.run(
sql`INSERT INTO termix_identity_ca (identity_id, user_id, public_key, private_key, validity_days) VALUES (7, 'user-1', 'ssh-ed25519 public', 'encrypted-ca-private', 45)`,
);
await adapter!.run(
sql`INSERT INTO termix_identity_ca (identity_id, user_id, public_key, private_key, validity_days) VALUES (8, 'user-2', 'ssh-ed25519 other', 'encrypted-other', 90)`,
);
onWrite.mockClear();
await expect(repo.deleteByUserId("user-1")).resolves.toBe(1);
await expect(repo.deleteByUserId("missing")).resolves.toBe(0);
expect(
await adapter!.query(
sql`SELECT user_id, public_key FROM termix_identity_ca ORDER BY user_id`,
),
).toEqual([{ user_id: "user-2", public_key: "ssh-ed25519 other" }]);
expect(onWrite).toHaveBeenCalledTimes(1);
});
});
@@ -1,170 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { TestSqliteDatabase } from "./test-support.js";
import { TermixIdentityRepository } from "../../../database/repositories/termix-identity-repository.js";
describe("TermixIdentityRepository", () => {
let adapter: TestSqliteDatabase | null = null;
afterEach(async () => {
if (adapter) {
await adapter.close();
adapter = null;
}
});
async function createRepository(onWrite = vi.fn()): Promise<{
repo: TermixIdentityRepository;
onWrite: ReturnType<typeof vi.fn>;
}> {
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 ssh_credentials (id, user_id, name, username, auth_type) VALUES
(10, 'user-1', 'cred-10', 'root', 'password'),
(20, 'user-1', 'cred-20', 'root', 'password');
`);
return {
repo: new TermixIdentityRepository(context, onWrite),
onWrite,
};
}
it("creates, updates, finds, and deletes identities", async () => {
const { repo, onWrite } = await createRepository();
const created = await repo.createIdentity({
userId: "user-1",
handle: "alice",
description: "workstation keys",
});
expect(created.id).toBeGreaterThan(0);
expect(await repo.isHandleTaken("alice")).toBe(true);
expect(await repo.findIdentityForUser("user-1")).toMatchObject({
handle: "alice",
});
expect(await repo.findIdentityByHandle("alice")).toMatchObject({
userId: "user-1",
});
const updated = await repo.updateIdentityForUser("user-1", {
handle: "alice-renamed",
description: null,
});
expect(updated).toMatchObject({
handle: "alice-renamed",
description: null,
});
await expect(repo.deleteIdentityForUser("user-1")).resolves.toBe(true);
await expect(repo.deleteIdentityForUser("user-1")).resolves.toBe(false);
await expect(repo.findIdentityForUser("user-1")).resolves.toBeNull();
expect(onWrite).toHaveBeenCalledTimes(3);
});
it("creates, lists, updates, deletes, and links public keys", async () => {
const { repo, onWrite } = await createRepository();
const identity = await repo.createIdentity({
userId: "user-1",
handle: "alice",
description: null,
});
onWrite.mockClear();
const first = await repo.createKey({
identityId: identity.id,
userId: "user-1",
publicKey: "ssh-ed25519 AAAA1",
keyType: "ssh-ed25519",
algorithm: "ED25519",
label: "laptop",
comment: null,
source: "credential",
credentialId: 10,
});
await repo.createKey({
identityId: identity.id,
userId: "user-1",
publicKey: "ssh-rsa AAAA2",
keyType: "ssh-rsa",
algorithm: "RSA",
label: "disabled",
comment: null,
source: "manual",
credentialId: 20,
enabled: false,
});
expect(await repo.listKeysByIdentityId(identity.id)).toHaveLength(2);
expect(await repo.listEnabledKeysByIdentityId(identity.id)).toMatchObject([
{ id: first.id, publicKey: "ssh-ed25519 AAAA1" },
]);
expect(await repo.listLinkedCredentialIds(identity.id)).toEqual([10]);
const updated = await repo.updateKeyForUser("user-1", first.id, {
enabled: false,
label: "revoked",
});
expect(updated).toMatchObject({ enabled: false, label: "revoked" });
await expect(
repo.findKeyForUser("user-1", first.id),
).resolves.toMatchObject({ label: "revoked" });
await expect(repo.deleteKeyForUser("user-1", first.id)).resolves.toBe(true);
await expect(repo.deleteKeyForUser("user-1", first.id)).resolves.toBe(
false,
);
expect(onWrite).toHaveBeenCalledTimes(4);
});
it("deletes identities and keys for a user", async () => {
const { repo, onWrite } = await createRepository();
const userIdentity = await repo.createIdentity({
userId: "user-1",
handle: "alice",
description: null,
});
const otherIdentity = await repo.createIdentity({
userId: "user-2",
handle: "bob",
description: null,
});
await repo.createKey({
identityId: userIdentity.id,
userId: "user-1",
publicKey: "ssh-ed25519 AAAA1",
keyType: "ssh-ed25519",
algorithm: "ED25519",
source: "manual",
});
await repo.createKey({
identityId: otherIdentity.id,
userId: "user-2",
publicKey: "ssh-ed25519 AAAA2",
keyType: "ssh-ed25519",
algorithm: "ED25519",
source: "manual",
});
onWrite.mockClear();
await expect(repo.deleteByUserId("user-1")).resolves.toEqual({
identitiesDeleted: 1,
keysDeleted: 1,
});
await expect(repo.deleteByUserId("missing")).resolves.toEqual({
identitiesDeleted: 0,
keysDeleted: 0,
});
expect(await repo.findIdentityForUser("user-1")).toBeNull();
expect(await repo.listKeysByIdentityId(userIdentity.id)).toEqual([]);
expect(await repo.findIdentityForUser("user-2")).toMatchObject({
handle: "bob",
});
expect(await repo.listKeysByIdentityId(otherIdentity.id)).toHaveLength(1);
expect(onWrite).toHaveBeenCalledTimes(1);
});
});
@@ -1,6 +1,7 @@
/**
* 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.
* 2.9 must keep reaching the plugin's callback, query intact, and the Termix
* ID resolver URLs servers fetch must keep reaching the plugin's resolver.
*/
import http from "node:http";
@@ -8,7 +9,7 @@ import type { AddressInfo } from "node:net";
import express from "express";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
const { registerHostAuthCompatRoutes } =
const { registerHostAuthCompatRoutes, registerTermixIdCompatRoutes } =
await import("../../../database/routes/host-compat-routes.js");
let server: http.Server;
@@ -19,6 +20,9 @@ beforeEach(async () => {
registerHostAuthCompatRoutes(router);
const app = express();
app.use("/host", router);
const termixId = express.Router();
registerTermixIdCompatRoutes(termixId);
app.use("/termix-id", termixId);
server = http.createServer(app);
await new Promise<void>((resolve) => server.listen(0, resolve));
base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
@@ -88,3 +92,25 @@ describe("the old Vault callback", () => {
}
});
});
describe("the old Termix ID resolver URLs", () => {
it.each([
["/termix-id/u/alice", "/plugin-api/termix-identity/u/alice"],
[
"/termix-id/u/alice/ED25519",
"/plugin-api/termix-identity/u/alice/ED25519",
],
["/termix-id/u/alice/ca", "/plugin-api/termix-identity/u/alice/ca"],
])("permanently redirects %s", async (path, target) => {
const response = await fetch(`${base}${path}`, { redirect: "manual" });
expect(response.status).toBe(308);
expect(response.headers.get("location")).toBe(target);
});
it("does not forward the old management routes", async () => {
const response = await fetch(`${base}/termix-id/me`, {
redirect: "manual",
});
expect(response.status).toBe(404);
});
});
@@ -1,127 +0,0 @@
import { describe, it, expect } from "vitest";
import crypto from "crypto";
import fs from "fs";
import os from "os";
import path from "path";
import { execFileSync } from "child_process";
import {
generateCa,
signUserCertificate,
ed25519RawFromLine,
} from "../../../database/routes/ssh-certificate.js";
function publicKeyObjectFromLine(line: string) {
const raw = ed25519RawFromLine(line);
if (!raw) throw new Error("not ed25519");
return crypto.createPublicKey({
key: { kty: "OKP", crv: "Ed25519", x: raw.toString("base64url") },
format: "jwk",
});
}
// Split a cert blob into the signed body and the raw 64-byte ed25519 signature.
function splitCert(certLine: string): { body: Buffer; rawSig: Buffer } {
const blob = Buffer.from(certLine.split(/\s+/)[1], "base64");
// trailing signature string = str( str("ssh-ed25519") + str(64-byte sig) )
const sigBlobLen = 4 + "ssh-ed25519".length + 4 + 64; // 83
const body = blob.subarray(0, blob.length - (4 + sigBlobLen));
const rawSig = blob.subarray(blob.length - 64);
return { body, rawSig };
}
describe("generateCa", () => {
it("produces a valid ed25519 public line and PKCS8 private key", () => {
const ca = generateCa();
expect(ca.publicKeyLine.startsWith("ssh-ed25519 ")).toBe(true);
expect(ed25519RawFromLine(ca.publicKeyLine)?.length).toBe(32);
expect(ca.privateKeyPem).toContain("BEGIN PRIVATE KEY");
// The PEM must load as a usable signing key.
expect(() =>
crypto.createPrivateKey({
key: ca.privateKeyPem,
format: "pem",
type: "pkcs8",
}),
).not.toThrow();
});
});
describe("signUserCertificate", () => {
it("returns null for non-ed25519 user keys", () => {
const ca = generateCa();
const cert = signUserCertificate({
userPublicKeyLine: "ssh-rsa AAAAB3Nz",
caPrivateKeyPem: ca.privateKeyPem,
caPublicKeyLine: ca.publicKeyLine,
keyId: "x",
principals: [],
validAfter: 0,
validBefore: 1,
});
expect(cert).toBeNull();
});
it("produces a cert whose signature verifies against the CA key", () => {
const ca = generateCa();
const user = generateCa(); // reuse: a valid ed25519 public line
const cert = signUserCertificate({
userPublicKeyLine: user.publicKeyLine,
caPrivateKeyPem: ca.privateKeyPem,
caPublicKeyLine: ca.publicKeyLine,
keyId: "termix:@alice",
principals: ["root", "ubuntu"],
validAfter: 1000,
validBefore: 2000,
});
expect(cert).not.toBeNull();
expect(cert!.startsWith("ssh-ed25519-cert-v01@openssh.com ")).toBe(true);
const { body, rawSig } = splitCert(cert!);
const caPub = publicKeyObjectFromLine(ca.publicKeyLine);
expect(crypto.verify(null, body, caPub, rawSig)).toBe(true);
// A different CA must NOT verify.
const otherPub = publicKeyObjectFromLine(generateCa().publicKeyLine);
expect(crypto.verify(null, body, otherPub, rawSig)).toBe(false);
});
it("is accepted and correctly parsed by ssh-keygen -L", () => {
let sshKeygen: string;
try {
sshKeygen = execFileSync("ssh-keygen", ["--help"], { encoding: "utf8" });
void sshKeygen;
} catch (e) {
// ssh-keygen prints usage to stderr and exits non-zero for --help; that's
// fine — it means the binary exists. Only skip if it's truly missing.
if ((e as { code?: string }).code === "ENOENT") return;
}
const ca = generateCa();
const user = generateCa();
const now = Math.floor(Date.now() / 1000);
const cert = signUserCertificate({
userPublicKeyLine: user.publicKeyLine,
caPrivateKeyPem: ca.privateKeyPem,
caPublicKeyLine: ca.publicKeyLine,
keyId: "termix-test-id",
principals: ["deploy"],
validAfter: now,
validBefore: now + 3600,
});
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-cert-"));
const file = path.join(dir, "id-cert.pub");
try {
fs.writeFileSync(file, cert + "\n");
const out = execFileSync("ssh-keygen", ["-L", "-f", file], {
encoding: "utf8",
});
expect(out).toContain("user certificate");
expect(out).toContain('Key ID: "termix-test-id"');
expect(out).toContain("deploy");
expect(out).toMatch(/permit-pty/);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
});
@@ -1,95 +0,0 @@
import { describe, it, expect } from "vitest";
import {
classifyAlgo,
parsePublicKey,
matchesAlgoFilter,
MAX_PUBLIC_KEY_LENGTH,
} from "../../../database/routes/termix-id-keys.js";
// Build a valid OpenSSH public-key line for a given type by encoding a wire
// blob whose first string field equals the type (what parsePublicKey checks).
function makeKey(type: string, comment = ""): string {
const typeBuf = Buffer.from(type, "utf8");
const header = Buffer.alloc(4);
header.writeUInt32BE(typeBuf.length, 0);
const body = Buffer.alloc(40); // arbitrary trailing key material
const blob = Buffer.concat([header, typeBuf, body]).toString("base64");
return `${type} ${blob}${comment ? ` ${comment}` : ""}`;
}
describe("classifyAlgo", () => {
it("maps known types to normalized groups", () => {
expect(classifyAlgo("ssh-rsa")).toBe("RSA");
expect(classifyAlgo("rsa-sha2-512")).toBe("RSA");
expect(classifyAlgo("ssh-ed25519")).toBe("ED25519");
expect(classifyAlgo("ecdsa-sha2-nistp256")).toBe("ECDSA");
expect(classifyAlgo("ssh-dss")).toBe("DSA");
expect(classifyAlgo("sk-ssh-ed25519@openssh.com")).toBe("ED25519-SK");
expect(classifyAlgo("sk-ecdsa-sha2-nistp256@openssh.com")).toBe("ECDSA-SK");
});
it("falls back by substring for unknown variants", () => {
expect(classifyAlgo("ecdsa-sha2-nistp999")).toBe("ECDSA");
expect(classifyAlgo("rsa-sha2-256-cert")).toBe("RSA");
expect(classifyAlgo("something-weird")).toBe("SOMETHING-WEIRD");
});
});
describe("parsePublicKey", () => {
it("parses a valid ed25519 key and extracts the comment", () => {
const parsed = parsePublicKey(makeKey("ssh-ed25519", "alice@laptop"));
expect(parsed).not.toBeNull();
expect(parsed?.type).toBe("ssh-ed25519");
expect(parsed?.algorithm).toBe("ED25519");
expect(parsed?.comment).toBe("alice@laptop");
// Comment is stripped from the normalized (dedupe) form.
expect(parsed?.normalized.includes("alice@laptop")).toBe(false);
});
it("parses SK (FIDO) key types", () => {
expect(
parsePublicKey(makeKey("sk-ssh-ed25519@openssh.com"))?.algorithm,
).toBe("ED25519-SK");
});
it.each([
[null],
[undefined],
[""],
[" "],
["ssh-ed25519"], // missing blob
["ssh-ed25519 not_base64!!"], // bad base64 charset
["ssh-rsa AAAAB3Nz"], // blob whose embedded type != declared type
])("rejects malformed input %p", (input) => {
expect(parsePublicKey(input as string)).toBeNull();
});
it("rejects an over-length line (amplification guard)", () => {
const valid = makeKey("ssh-ed25519");
const padded = valid + " " + "A".repeat(MAX_PUBLIC_KEY_LENGTH);
expect(padded.length).toBeGreaterThan(MAX_PUBLIC_KEY_LENGTH);
expect(parsePublicKey(padded)).toBeNull();
});
it("rejects a blob whose embedded type does not match the prefix", () => {
// Declared ssh-rsa but the wire blob says ssh-ed25519.
const blob = makeKey("ssh-ed25519").split(" ")[1];
expect(parsePublicKey(`ssh-rsa ${blob}`)).toBeNull();
});
});
describe("matchesAlgoFilter", () => {
it("returns all keys when no filter", () => {
expect(matchesAlgoFilter("ED25519", null)).toBe(true);
});
it("matches exactly and is case-insensitive", () => {
expect(matchesAlgoFilter("ED25519", "ed25519")).toBe(true);
expect(matchesAlgoFilter("RSA", "RSA")).toBe(true);
});
it("does NOT let ED25519 match ED25519-SK (the over-match bug)", () => {
expect(matchesAlgoFilter("ED25519-SK", "ED25519")).toBe(false);
expect(matchesAlgoFilter("ECDSA-SK", "ECDSA")).toBe(false);
});
});
@@ -1,162 +0,0 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
const mockSelect = vi.fn();
const mockUpdate = vi.fn();
const mockInsert = vi.fn();
const mockDelete = vi.fn();
vi.mock("../../../database/db/index.js", () => ({
db: {
select: mockSelect,
update: mockUpdate,
insert: mockInsert,
delete: mockDelete,
},
}));
vi.mock("../../../utils/logger.js", () => ({
apiLogger: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), success: vi.fn() },
authLogger: {
error: vi.fn(),
warn: vi.fn(),
info: vi.fn(),
success: vi.fn(),
},
}));
vi.mock("../../../utils/auth-manager.js", () => ({
AuthManager: {
getInstance: () => ({
createAuthMiddleware:
() =>
(req: Record<string, unknown>, _res: unknown, next: () => void) => {
req.userId = "user-1";
next();
},
createDataAccessMiddleware:
() => (_req: unknown, _res: unknown, next: () => void) =>
next(),
}),
},
}));
vi.mock("../../../utils/audit-logger.js", () => ({
logAudit: vi.fn(),
getRequestMeta: vi.fn(() => ({})),
}));
vi.mock("../../../utils/data-crypto.js", () => ({
DataCrypto: { getInstance: () => ({ encrypt: vi.fn(), decrypt: vi.fn() }) },
}));
vi.mock("../../../database/repositories/factory.js", () => ({
createCurrentTermixIdentityCaRepository: vi.fn(() => ({
findPublicByIdentityId: vi.fn(),
findDecryptedByIdentityId: vi.fn(),
createEncryptedForUser: vi.fn(),
updateEncryptedForIdentity: vi.fn(),
deleteByIdentityId: vi.fn(),
})),
createCurrentTermixIdentityRepository: vi.fn(() => ({
findIdentityForUser: vi.fn(),
findIdentityByHandle: vi.fn(),
isHandleTaken: vi.fn(),
createIdentity: vi.fn(),
updateIdentityForUser: vi.fn(),
deleteIdentityForUser: vi.fn(),
listKeysByIdentityId: vi.fn(),
listEnabledKeysByIdentityId: vi.fn(),
listLinkedCredentialIds: vi.fn(),
createKey: vi.fn(),
updateKeyForUser: vi.fn(),
deleteKeyForUser: vi.fn(),
findKeyForUser: vi.fn(),
})),
}));
vi.mock("../../../database/routes/termix-id-keys.js", () => ({
termixIdKeysRouter: { use: vi.fn() },
matchesAlgoFilter: vi.fn(() => true),
}));
// Chainable Drizzle stub — supports arbitrary method chains and resolves via .then()
function makeChain(resolveValue: unknown) {
const chain: Record<string, unknown> = {};
const methods = [
"from",
"where",
"set",
"values",
"returning",
"orderBy",
"limit",
"and",
"eq",
];
for (const m of methods) {
chain[m] = vi.fn(() => chain);
}
(chain as unknown as Promise<unknown>).then = (
cb: (v: unknown) => unknown,
eb?: (e: unknown) => unknown,
) => Promise.resolve(resolveValue).then(cb, eb);
(chain as unknown as Promise<unknown>).catch = (
cb: (e: unknown) => unknown,
) => Promise.resolve(resolveValue).catch(cb);
return chain;
}
const IDENTITY_ROW = { id: 42, userId: "user-1", handle: "alice" };
describe("GET /termix-id/linked-credentials", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns empty list when user has no identity", async () => {
// First select (getIdentityForUser) returns nothing; second should not be called
mockSelect.mockReturnValueOnce(makeChain([]));
const { default: router } =
await import("../../../database/routes/termix-id.js");
expect(router).toBeDefined();
expect(router).toBeDefined();
}, 15_000);
it("returns empty list when identity has no keys", async () => {
mockSelect
.mockReturnValueOnce(makeChain([IDENTITY_ROW])) // identity lookup
.mockReturnValueOnce(makeChain([])); // keys lookup
const { default: router } =
await import("../../../database/routes/termix-id.js");
expect(router).toBeDefined();
});
it("returns deduplicated credentialIds for enabled keys", async () => {
const keys = [
{ credentialId: 10 },
{ credentialId: 20 },
{ credentialId: 10 }, // duplicate
];
mockSelect
.mockReturnValueOnce(makeChain([IDENTITY_ROW]))
.mockReturnValueOnce(makeChain(keys));
const { default: router } =
await import("../../../database/routes/termix-id.js");
expect(router).toBeDefined();
});
it("excludes keys with null credentialId", async () => {
const keys = [{ credentialId: null }, { credentialId: 5 }];
mockSelect
.mockReturnValueOnce(makeChain([IDENTITY_ROW]))
.mockReturnValueOnce(makeChain(keys));
const { default: router } =
await import("../../../database/routes/termix-id.js");
expect(router).toBeDefined();
});
});
@@ -15,6 +15,9 @@ const state = vi.hoisted(() => ({
canConnect: true,
resolution: null as Record<string, unknown> | null,
resolverCalls: [] as Array<Record<string, unknown>>,
savedCredentials: [] as Array<Record<string, unknown>>,
userCredentials: new Map<string, Array<Record<string, unknown>>>(),
corePermissions: new Set<string>(["credentials.view", "credentials.create"]),
}));
vi.mock("../../database/repositories/factory.js", () => ({
@@ -25,6 +28,18 @@ vi.mock("../../database/repositories/factory.js", () => ({
capability,
})),
}),
createCurrentCredentialRepository: () => ({
listDecryptedByUserId: async (userId: string) =>
state.userCredentials.get(userId) ?? [],
createEncryptedForUser: async (
userId: string,
data: Record<string, unknown>,
) => {
const row = { ...data, id: 100 + state.savedCredentials.length, userId };
state.savedCredentials.push(row);
return row;
},
}),
createCurrentHostResolutionRepository: () => ({
findHostOwnerId: async (hostId: number) =>
(state.hosts.get(hostId)?.userId as string) ?? null,
@@ -44,6 +59,8 @@ vi.mock("../../utils/permission-manager.js", () => ({
PermissionManager: {
getInstance: () => ({
canAccessHost: async () => ({ hasAccess: state.canConnect }),
hasPermission: async (_userId: string, permission: string) =>
state.corePermissions.has(permission),
}),
},
}));
@@ -66,6 +83,7 @@ import {
resetSecretResolverRegistryForTests,
} from "../../hosts/connect/secret-resolver-registry.js";
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
import ssh2 from "ssh2";
function contextFor(
capabilities: string[],
@@ -112,6 +130,9 @@ beforeEach(() => {
state.canConnect = true;
state.resolution = null;
state.resolverCalls.length = 0;
state.savedCredentials.length = 0;
state.userCredentials.clear();
state.corePermissions = new Set(["credentials.view", "credentials.create"]);
state.hosts.set(7, { ...baseHost });
resetSecretResolverRegistryForTests();
});
@@ -305,3 +326,120 @@ describe("ctx.credentials.registerSecretResolver", () => {
expect(getSecretResolver("op")).toBeUndefined();
});
});
describe("ctx.credentials.listSshKeys", () => {
const pair = ssh2.utils.generateKeyPairSync("ed25519");
const derived = (() => {
const parsed = ssh2.utils.parseKey(pair.private);
const key = Array.isArray(parsed) ? parsed[0] : parsed;
if (key instanceof Error) throw key;
return `${key.type} ${key.getPublicSSH().toString("base64")}`;
})();
beforeEach(() => {
state.userCredentials.set("owner", [
{ id: 1, name: "stored", authType: "key", publicKey: "ssh-ed25519 AAAA" },
{ id: 2, name: "derived", authType: "key", privateKey: pair.private },
{ id: 3, name: "pw", authType: "password", password: "secret" },
]);
});
it("refuses without credentials:use", async () => {
grants.set("demo", []);
const ctx = contextFor([]);
await expect(
runAsActor("owner", "request", () => ctx.credentials.listSshKeys()),
).rejects.toThrow(/credentials:use/);
expect(auditEntries.at(-1)).toMatchObject({
action: "plugin_credentials_list_keys",
success: false,
});
});
it("refuses with no acting user", async () => {
grants.set("demo", ["credentials:use"]);
const ctx = contextFor(["credentials:use"]);
await expect(ctx.credentials.listSshKeys()).rejects.toThrow(/acting user/);
});
it("refuses without the core credentials.view permission", async () => {
grants.set("demo", ["credentials:use"]);
state.corePermissions.delete("credentials.view");
const ctx = contextFor(["credentials:use"]);
await expect(
runAsActor("owner", "request", () => ctx.credentials.listSshKeys()),
).rejects.toThrow(/credentials.view/);
});
it("lists only the actor's key credentials, public half only", async () => {
grants.set("demo", ["credentials:use"]);
const ctx = contextFor(["credentials:use"]);
const keys = await runAsActor("owner", "request", () =>
ctx.credentials.listSshKeys(),
);
expect(keys).toEqual([
{ id: 1, name: "stored", username: null, publicKey: "ssh-ed25519 AAAA" },
{ id: 2, name: "derived", username: null, publicKey: derived },
]);
expect(JSON.stringify(keys)).not.toContain("PRIVATE KEY");
const other = await runAsActor("someone", "request", () =>
ctx.credentials.listSshKeys(),
);
expect(other).toEqual([]);
});
});
describe("ctx.credentials.createSshKey", () => {
const input = {
name: "Generated",
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----",
publicKey: "ssh-ed25519 AAAA",
keyType: "ssh-ed25519",
};
it("refuses without credentials:write", async () => {
grants.set("demo", ["credentials:use"]);
const ctx = contextFor(["credentials:use"]);
await expect(
runAsActor("owner", "request", () => ctx.credentials.createSshKey(input)),
).rejects.toThrow(/credentials:write/);
expect(state.savedCredentials).toHaveLength(0);
expect(auditEntries.at(-1)).toMatchObject({
action: "plugin_credentials_create_key",
success: false,
});
});
it("refuses without the core credentials.create permission", async () => {
grants.set("demo", ["credentials:write"]);
state.corePermissions.delete("credentials.create");
const ctx = contextFor(["credentials:write"]);
await expect(
runAsActor("owner", "request", () => ctx.credentials.createSshKey(input)),
).rejects.toThrow(/credentials.create/);
expect(state.savedCredentials).toHaveLength(0);
});
it("saves a key credential owned by the actor", async () => {
grants.set("demo", ["credentials:write"]);
const ctx = contextFor(["credentials:write"]);
const result = await runAsActor("owner", "request", () =>
ctx.credentials.createSshKey({ ...input, username: "root" }),
);
expect(result).toEqual({ id: 100 });
expect(state.savedCredentials[0]).toMatchObject({
userId: "owner",
authType: "key",
name: "Generated",
username: "root",
privateKey: input.privateKey,
publicKey: input.publicKey,
detectedKeyType: "ssh-ed25519",
});
expect(auditEntries.at(-1)).toMatchObject({
action: "plugin_credentials_create_key",
success: true,
});
});
});
@@ -0,0 +1,114 @@
/**
* The 2.8 to 2.9 Termix ID CA move. A CA private key encrypted with the
* owner's data key must end up sealed with the installation key in the
* plugin's adopted table, readable, and a second run changes nothing.
*/
import crypto from "node:crypto";
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { FieldCrypto } from "../../utils/field-crypto.js";
const h = vi.hoisted(() => ({
db: null as unknown,
deks: new Map<string, Buffer>(),
}));
vi.mock("../../database/db/index.js", () => ({ getDb: () => h.db }));
vi.mock("../../utils/data-crypto.js", () => ({
DataCrypto: { getUserDataKey: (userId: string) => h.deks.get(userId) },
}));
vi.mock("../../utils/system-secret-crypto.js", () => ({
isSystemEncrypted: (value: string) => value.startsWith("sysenc:v1:"),
encryptSystemSecret: async (value: string) => `sysenc:v1:${value}`,
}));
vi.mock("../../utils/logger.js", () => {
const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
return { databaseLogger: log };
});
const { runTermixIdentityCaMigration } =
await import("../../utils/crypto-migration/termix-identity-ca-migration.js");
const PEM = "-----BEGIN PRIVATE KEY-----\nabc\n-----END PRIVATE KEY-----\n";
let sqlite: Database.Database;
function setup({ pluginTable }: { pluginTable: boolean }) {
sqlite = new Database(":memory:");
if (pluginTable) {
sqlite.exec(`
CREATE TABLE p_termix_identity_ca (
id INTEGER PRIMARY KEY AUTOINCREMENT,
identity_id INTEGER NOT NULL UNIQUE,
user_id TEXT NOT NULL,
public_key TEXT NOT NULL,
private_key TEXT NOT NULL,
validity_days INTEGER NOT NULL DEFAULT 90
);
`);
}
h.db = drizzle(sqlite);
}
function addCa(id: number, userId: string, withKey = true) {
const dek = crypto.randomBytes(32);
if (withKey) h.deks.set(userId, dek);
sqlite
.prepare(
"INSERT INTO p_termix_identity_ca (id, identity_id, user_id, public_key, private_key) VALUES (?, ?, ?, 'ssh-ed25519 AAAA', ?)",
)
.run(
id,
id,
userId,
FieldCrypto.encryptField(PEM, dek, String(id), "privateKey"),
);
}
const keyOf = (id: number) =>
(
sqlite
.prepare("SELECT private_key FROM p_termix_identity_ca WHERE id = ?")
.get(id) as { private_key: string }
).private_key;
beforeEach(() => {
h.deks.clear();
});
describe("runTermixIdentityCaMigration", () => {
it("does nothing before the plugin adopted the table", async () => {
setup({ pluginTable: false });
expect(await runTermixIdentityCaMigration()).toBe(0);
});
it("reseals a data-key CA with the installation key", async () => {
setup({ pluginTable: true });
addCa(1, "alice");
expect(await runTermixIdentityCaMigration()).toBe(1);
expect(keyOf(1)).toBe(`sysenc:v1:${PEM}`);
});
it("changes nothing on a second run", async () => {
setup({ pluginTable: true });
addCa(1, "alice");
await runTermixIdentityCaMigration();
const first = keyOf(1);
expect(await runTermixIdentityCaMigration()).toBe(0);
expect(keyOf(1)).toBe(first);
});
it("leaves a row whose owner's key is not open for the next run", async () => {
setup({ pluginTable: true });
addCa(1, "alice");
addCa(2, "bob", false);
const locked = keyOf(2);
expect(await runTermixIdentityCaMigration()).toBe(1);
expect(keyOf(2)).toBe(locked);
});
});
@@ -7,8 +7,10 @@
import { runLdapProviderMigration } from "./ldap-provider-migration.js";
import { runOpksshConfigMigration } from "./opkssh-config-migration.js";
import { runTermixIdentityCaMigration } from "./termix-identity-ca-migration.js";
export async function runPluginDataMoves(): Promise<void> {
await runLdapProviderMigration();
await runOpksshConfigMigration();
await runTermixIdentityCaMigration();
}
@@ -0,0 +1,77 @@
/**
* Reseals 2.8 Termix ID certificate authority keys for the termix-identity
* plugin.
*
* Core encrypted termix_identity_ca.private_key with the owner's data key.
* The plugin adopts that table as p_termix_identity_ca and reads the key with
* ctx.secrets.unseal, which only opens installation-key encryption. Each row
* still under a data key is decrypted and written back with
* encryptSystemSecret, what ctx.secrets.seal writes.
*
* Does nothing until the plugin's table exists. A row whose owner's data key
* cannot be opened is skipped and tried again next time. Idempotent.
*/
import { sql } from "drizzle-orm";
import { databaseLogger } from "../logger.js";
import { DataCrypto } from "../data-crypto.js";
import { LazyFieldEncryption } from "../lazy-field-encryption.js";
import {
encryptSystemSecret,
isSystemEncrypted,
} from "../system-secret-crypto.js";
import { runStatement, selectRows } from "./raw-rows.js";
const CA_TABLE = "p_termix_identity_ca";
interface CaRow {
id: number;
user_id: string;
private_key: string | null;
}
export async function runTermixIdentityCaMigration(): Promise<number> {
let rows: CaRow[];
try {
rows = await selectRows<CaRow>(
sql`SELECT id, user_id, private_key FROM ${sql.identifier(CA_TABLE)}`,
);
} catch {
// The plugin has not adopted the table yet.
return 0;
}
let resealed = 0;
for (const row of rows) {
if (!row.private_key || isSystemEncrypted(row.private_key)) continue;
try {
const dek = DataCrypto.getUserDataKey(row.user_id);
if (!dek) continue;
const plain = LazyFieldEncryption.safeGetFieldValue(
row.private_key,
dek,
String(row.id),
"privateKey",
);
if (!plain) continue;
await runStatement(
sql`UPDATE ${sql.identifier(CA_TABLE)} SET private_key = ${await encryptSystemSecret(plain)} WHERE id = ${row.id}`,
);
resealed++;
} catch (error) {
databaseLogger.warn("Termix ID CA reseal failed for a row", {
operation: "termix_identity_ca_migration",
caId: row.id,
error: error instanceof Error ? error.message : String(error),
});
}
}
if (resealed > 0) {
databaseLogger.info("Resealed Termix ID CA keys for the plugin", {
operation: "termix_identity_ca_migration",
resealed,
});
}
return resealed;
}
-1
View File
@@ -39,7 +39,6 @@ class FieldCrypto {
// Channel configs hold ntfy tokens, webhook auth headers and Discord
// webhook URLs, which are credentials like any other.
notification_channels: new Set(["config"]),
termix_identity_ca: new Set(["privateKey"]),
};
static encryptField(
-1
View File
@@ -232,7 +232,6 @@ export type KnownTabType =
| "homepage"
| "fleet-inventory"
// Rail panels that can also open full-width in the main area.
| "termix-id"
| "session-logs"
| "snippets"
| "macros"
-9
View File
@@ -98,9 +98,6 @@ const CredentialsPanel = lazy(() =>
default: m.CredentialsPanel,
})),
);
const TermixIdPanel = lazy(() =>
import("@/sidebar/TermixIdPanel").then((m) => ({ default: m.TermixIdPanel })),
);
const ConnectionsPanel = lazy(() =>
import("@/sidebar/ConnectionsPanel").then((m) => ({
default: m.ConnectionsPanel,
@@ -2343,12 +2340,6 @@ export function AppShell({
</>
)}
{railView === "termix-id" && (
<div className="flex flex-col flex-1 min-h-0">
<TermixIdPanel />
</div>
)}
{railView === "quick-connect" && (
<QuickConnectPanel
onConnect={(host, type) => {
-217
View File
@@ -1,217 +0,0 @@
import { authApi, handleApiError } from "@/main-axios";
export interface TermixIdentity {
id: number;
userId: string;
handle: string;
description: string | null;
resolverPath?: string;
createdAt: string;
updatedAt: string;
}
export interface TermixIdentityKey {
id: number;
identityId: number;
userId: string;
publicKey: string;
keyType: string;
algorithm: string;
label: string | null;
comment: string | null;
source: string;
credentialId: number | null;
enabled: boolean;
createdAt: string;
}
export interface TermixIdMe {
identity: TermixIdentity | null;
keys: TermixIdentityKey[];
}
export async function getMyTermixId(): Promise<TermixIdMe> {
try {
const response = await authApi.get("/termix-id/me");
return response.data;
} catch (error) {
throw handleApiError(error, "fetch Termix ID");
}
}
export async function checkTermixIdHandle(
handle: string,
): Promise<{ available: boolean; valid: boolean }> {
try {
const response = await authApi.get(
`/termix-id/check/${encodeURIComponent(handle)}`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "check handle");
}
}
export async function createTermixId(
handle: string,
description?: string,
): Promise<TermixIdentity> {
try {
const response = await authApi.post("/termix-id", { handle, description });
return response.data;
} catch (error) {
throw handleApiError(error, "create Termix ID");
}
}
export async function updateTermixId(data: {
handle?: string;
description?: string;
}): Promise<TermixIdentity> {
try {
const response = await authApi.put("/termix-id", data);
return response.data;
} catch (error) {
throw handleApiError(error, "update Termix ID");
}
}
export async function deleteTermixId(): Promise<void> {
try {
await authApi.delete("/termix-id");
} catch (error) {
throw handleApiError(error, "delete Termix ID");
}
}
export async function addTermixIdKey(data: {
publicKey?: string;
credentialId?: number;
label?: string;
}): Promise<TermixIdentityKey> {
try {
const response = await authApi.post("/termix-id/keys", data);
return response.data;
} catch (error) {
throw handleApiError(error, "add key");
}
}
export interface GeneratedKey {
key: TermixIdentityKey;
privateKey: string;
publicKey: string;
credentialId: number | null;
}
export async function generateTermixIdKey(
type: "ed25519" | "rsa" = "ed25519",
saveCredential = true,
): Promise<GeneratedKey> {
try {
const response = await authApi.post("/termix-id/keys/generate", {
type,
saveCredential,
});
return response.data;
} catch (error) {
throw handleApiError(error, "generate key");
}
}
export async function setTermixIdKeyEnabled(
id: number,
enabled: boolean,
): Promise<TermixIdentityKey> {
try {
const response = await authApi.patch(`/termix-id/keys/${id}`, { enabled });
return response.data;
} catch (error) {
throw handleApiError(error, "update key");
}
}
export async function deleteTermixIdKey(id: number): Promise<void> {
try {
await authApi.delete(`/termix-id/keys/${id}`);
} catch (error) {
throw handleApiError(error, "delete key");
}
}
export interface TermixIdCa {
publicKey: string;
validityDays: number;
resolverPath: string;
}
export interface IssuedCertificate {
certificate: string;
keyId: string;
validBefore: number;
principals: string[];
validityDays: number;
}
export async function getMyCa(): Promise<{ ca: TermixIdCa | null }> {
try {
const response = await authApi.get("/termix-id/ca");
return response.data;
} catch (error) {
throw handleApiError(error, "fetch CA");
}
}
export async function createCa(validityDays?: number): Promise<TermixIdCa> {
try {
const response = await authApi.post("/termix-id/ca", { validityDays });
return response.data;
} catch (error) {
throw handleApiError(error, "create CA");
}
}
export async function rotateCa(validityDays?: number): Promise<TermixIdCa> {
try {
const response = await authApi.post("/termix-id/ca/rotate", {
validityDays,
});
return response.data;
} catch (error) {
throw handleApiError(error, "rotate CA");
}
}
export async function deleteCa(): Promise<void> {
try {
await authApi.delete("/termix-id/ca");
} catch (error) {
throw handleApiError(error, "delete CA");
}
}
export async function issueCertificate(
keyId: number,
opts: { principals?: string[]; validityDays?: number } = {},
): Promise<IssuedCertificate> {
try {
const response = await authApi.post(
`/termix-id/keys/${keyId}/certificate`,
opts,
);
return response.data;
} catch (error) {
throw handleApiError(error, "issue certificate");
}
}
export async function getLinkedCredentialIds(): Promise<{
credentialIds: number[];
}> {
try {
const response = await authApi.get("/termix-id/linked-credentials");
return response.data;
} catch (error) {
throw handleApiError(error, "fetch linked credentials");
}
}
+4 -71
View File
@@ -5,75 +5,6 @@
"disconnected": "Unable to connect",
"reconnect": "Reconnect"
},
"termixId": {
"title": "Termix ID",
"loadFailed": "Failed to load Termix ID",
"claimTitle": "Claim your Termix ID",
"claimIntro": "Pick a unique handle. Your SSH public keys will be published at a public URL you can add to any server's authorized_keys file.",
"handleLabel": "Handle",
"handlePlaceholder": "alice",
"checking": "Checking…",
"available": "Available",
"taken": "Already taken",
"invalidHandle": "Lowercase letters, numbers, - and _ only",
"descriptionLabel": "Description (optional)",
"descriptionPlaceholder": "Work laptop & phone keys",
"create": "Create Termix ID",
"created": "Termix ID created",
"createFailed": "Failed to create Termix ID",
"deleteConfirm": "Delete your Termix ID and all published keys? Servers you provisioned will keep the keys until you remove them manually.",
"deleted": "Termix ID deleted",
"deleteFailed": "Failed to delete Termix ID",
"copyFailed": "Copy failed",
"resolverUrlLabel": "Public resolver URL",
"provisionLabel": "Provision a server",
"publishTitle": "Publish a public key",
"generate": "Generate",
"generateTooltip": "Generate an Ed25519 key pair — publishes the public key and downloads the private key to your device",
"saveToVault": "Save To Credentials",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "Label (optional)",
"add": "Add",
"importFromCredential": "Or import from a stored credential",
"keyPublished": "Key published",
"addKeyFailed": "Failed to add key",
"generatedSaved": "Key pair generated and saved to your credentials vault. Private key also downloaded.",
"generatedOnly": "Key pair generated — private key downloaded (shown only once).",
"generateFailed": "Failed to generate key",
"imported": "Key imported from credential",
"importFailed": "Failed to import key",
"noKeys": "No public keys published yet.",
"keysTitle": "Published keys",
"published": "Published",
"hidden": "Hidden",
"keyRemoved": "Key removed",
"removeKeyFailed": "Failed to remove key",
"updateKeyFailed": "Failed to update key",
"fromVault": "From credentials vault",
"linkedToTermixId": "Published via Termix ID",
"selectCredential": "Select a credential...",
"import": "Import",
"caTitle": "Certificate authority",
"caIntro": "Trust this CA on a server and it accepts any certificate you sign. Rotate to revoke everything at once; certificates also expire on their own.",
"caEnable": "Enable CA",
"caEnabled": "CA enabled",
"caCreateFailed": "Failed to enable CA",
"caPublicKeyLabel": "CA public key",
"caTrustLabel": "Trust on a server (run as root)",
"caRotate": "Rotate",
"caRotateConfirm": "Rotate the CA? Every certificate it has signed will stop being accepted and must be re-issued.",
"caRotated": "CA rotated — previous certificates revoked",
"caRotateFailed": "Failed to rotate CA",
"caDelete": "Remove CA",
"caDeleteConfirm": "Remove the certificate authority?",
"caDeleted": "CA removed",
"caDeleteFailed": "Failed to remove CA",
"caValidityLabel": "Cert validity (days)",
"issueCert": "Certificate",
"issueCertTooltip": "Issue an SSH certificate for this key, signed by your CA",
"certIssued": "Certificate issued and downloaded",
"certIssueFailed": "Failed to issue certificate"
},
"credentials": {
"folders": "Folders",
"folder": "Folder",
@@ -144,7 +75,6 @@
"editCredentialAction": "Edit credential",
"failedToCloneCredential": "Failed to clone credential",
"failedToDeleteCredential": "Failed to delete credential",
"idBadge": "ID",
"keyBadge": "KEY",
"passwordBadge": "PWD",
"renameFolder": "Rename folder",
@@ -418,7 +348,6 @@
"fileManager": "File Manager",
"serverStats": "Host Metrics",
"admin": "Admin",
"termixId": "ID",
"userProfile": "User Profile",
"splitScreen": "Split Screen",
"confirmClose": "Close this active session?",
@@ -2598,6 +2527,10 @@
"title": "Connect to your servers",
"consequence": "It cannot see your passwords or keys."
},
"credentials:write": {
"title": "Save new credentials",
"consequence": "It can add passwords and keys to your saved credentials."
},
"network:outbound": {
"title": "Reach the internet",
"consequence": "It can send requests to outside services."
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Kan nie koppel nie",
"reconnect": "Herkoppel"
},
"termixId": {
"title": "Termix-ID",
"loadFailed": "Kon nie Termix ID laai nie",
"claimTitle": "Eis jou Termix ID op",
"claimIntro": "Kies 'n unieke handvatsel. Jou SSH publieke sleutels sal gepubliseer word by 'n publieke URL wat jy by enige bediener se authorized_keys-lêer kan voeg.",
"handleLabel": "Hanteer",
"handlePlaceholder": "Alice",
"checking": "Kontroleer…",
"available": "Beskikbaar",
"taken": "Reeds geneem",
"invalidHandle": "Slegs kleinletters, syfers, - en _",
"descriptionLabel": "Beskrywing (opsioneel)",
"descriptionPlaceholder": "Werkskootrekenaar- en foonsleutels",
"create": "Skep Termix ID",
"created": "Termix ID geskep",
"createFailed": "Kon nie Termix ID skep nie",
"deleteConfirm": "Vee jou Termix ID en alle gepubliseerde sleutels uit? Bedieners wat jy voorsien het, sal die sleutels hou totdat jy dit handmatig verwyder.",
"deleted": "Termix-ID uitgevee",
"deleteFailed": "Kon nie Termix ID uitvee nie",
"copyFailed": "Kopieer het misluk",
"resolverUrlLabel": "Openbare resolver-URL",
"provisionLabel": "Voorsien 'n bediener",
"publishTitle": "Publiseer 'n publieke sleutel",
"generate": "Genereer",
"generateTooltip": "Genereer 'n Ed25519-sleutelpaar — publiseer die publieke sleutel en laai die privaat sleutel na jou toestel af",
"saveToVault": "Stoor na geloofsbriewe",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... gebruiker@gasheer",
"labelPlaceholder": "Etiket (opsioneel)",
"add": "Voeg by",
"importFromCredential": "Of invoer vanaf 'n gestoorde geloofsbrief",
"keyPublished": "Sleutel gepubliseer",
"addKeyFailed": "Kon nie sleutel byvoeg nie",
"generatedSaved": "Sleutelpaar gegenereer en gestoor in jou geloofsbriewekluis. Privaat sleutel ook afgelaai.",
"generatedOnly": "Sleutelpaar gegenereer — private sleutel afgelaai (slegs een keer getoon).",
"generateFailed": "Kon nie sleutel genereer nie",
"imported": "Sleutel ingevoer vanaf geloofsbriewe",
"importFailed": "Kon nie sleutel invoer nie",
"noKeys": "Geen publieke sleutels nog gepubliseer nie.",
"keysTitle": "Gepubliseerde sleutels",
"published": "Gepubliseer",
"hidden": "Versteek",
"keyRemoved": "Sleutel verwyder",
"removeKeyFailed": "Kon nie sleutel verwyder nie",
"updateKeyFailed": "Kon nie sleutel opdateer nie",
"fromVault": "Vanuit geloofsbriewekluis",
"linkedToTermixId": "Gepubliseer via Termix ID",
"selectCredential": "Kies 'n geloofsbrief...",
"import": "Invoer",
"caTitle": "Sertifikaatowerheid",
"caIntro": "Vertrou hierdie CA op 'n bediener en dit aanvaar enige sertifikaat wat jy teken. Draai om alles gelyktydig te herroep; sertifikate verval ook vanself.",
"caEnable": "Aktiveer CA",
"caEnabled": "CA geaktiveer",
"caCreateFailed": "Kon nie CA aktiveer nie",
"caPublicKeyLabel": "CA publieke sleutel",
"caTrustLabel": "Vertrou op 'n bediener (loop as root)",
"caRotate": "Roteer",
"caRotateConfirm": "Roteer die CA? Elke sertifikaat wat dit onderteken het, sal nie meer aanvaar word nie en moet heruitgereik word.",
"caRotated": "KA geroteer — vorige sertifikate herroep",
"caRotateFailed": "Kon nie CA roteer nie",
"caDelete": "Verwyder CA",
"caDeleteConfirm": "Verwyder die sertifikaatowerheid?",
"caDeleted": "KA verwyder",
"caDeleteFailed": "Kon nie CA verwyder nie",
"caValidityLabel": "Sertifikaatgeldigheid (dae)",
"issueCert": "Sertifikaat",
"issueCertTooltip": "Reik 'n SSH-sertifikaat uit vir hierdie sleutel, onderteken deur jou CA",
"certIssued": "Sertifikaat uitgereik en afgelaai",
"certIssueFailed": "Kon nie sertifikaat uitreik nie"
},
"credentials": {
"folders": "Lêers",
"folder": "Vouer",
@@ -144,7 +75,6 @@
"editCredentialAction": "Wysig geloofsbriewe",
"failedToCloneCredential": "Kon nie geloofsbriewe kloon nie",
"failedToDeleteCredential": "Kon nie geloofsbriewe verwyder nie",
"idBadge": "ID",
"keyBadge": "SLEUTEL",
"passwordBadge": "PWD",
"renameFolder": "Hernoem vouer",
@@ -525,7 +455,6 @@
"fileManager": "Lêerbestuurder",
"serverStats": "Gasheermetrieke",
"admin": "Admin",
"termixId": "ID",
"userProfile": "Gebruikersprofiel",
"splitScreen": "Gesplete skerm",
"confirmClose": "Sluit hierdie aktiewe sessie?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "تعذر الاتصال",
"reconnect": "إعادة الاتصال"
},
"termixId": {
"title": "Termix ID",
"loadFailed": "فشل تحميل Termix ID",
"claimTitle": "احصل على Termix ID الخاص بك",
"claimIntro": "اختر معرّفًا فريدًا. سيتم نشر مفاتيح SSH العامة الخاصة بك في رابط عام يمكنك إضافته إلى ملف authorized_keys لأي خادم.",
"handleLabel": "المعرّف",
"handlePlaceholder": "alice",
"checking": "جارٍ التحقق…",
"available": "متاح",
"taken": "مستخدم بالفعل",
"invalidHandle": "أحرف صغيرة، أرقام، - و _ فقط",
"descriptionLabel": "الوصف (اختياري)",
"descriptionPlaceholder": "مفاتيح حاسوب العمل والهاتف",
"create": "إنشاء Termix ID",
"created": "تم إنشاء Termix ID",
"createFailed": "فشل إنشاء Termix ID",
"deleteConfirm": "حذف Termix ID وجميع المفاتيح المنشورة؟ ستحتفظ الخوادم التي قمت بتجهيزها بالمفاتيح حتى تقوم بإزالتها يدويًا.",
"deleted": "تم حذف Termix ID",
"deleteFailed": "فشل حذف Termix ID",
"copyFailed": "فشل النسخ",
"resolverUrlLabel": "رابط المُحلل العام",
"provisionLabel": "تجهيز خادم",
"publishTitle": "نشر مفتاح عام",
"generate": "توليد",
"generateTooltip": "توليد زوج مفاتيح Ed25519 — ينشر المفتاح العام ويُنزّل المفتاح الخاص إلى جهازك",
"saveToVault": "حفظ في بيانات الاعتماد",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "وسم (اختياري)",
"add": "إضافة",
"importFromCredential": "أو استيراد من بيانات اعتماد مخزّنة",
"keyPublished": "تم نشر المفتاح",
"addKeyFailed": "فشل إضافة المفتاح",
"generatedSaved": "تم توليد زوج المفاتيح وحفظه في مخزن بيانات الاعتماد. كما تم تنزيل المفتاح الخاص.",
"generatedOnly": "تم توليد زوج المفاتيح — تم تنزيل المفتاح الخاص (يُعرض مرة واحدة فقط).",
"generateFailed": "فشل توليد المفتاح",
"imported": "تم استيراد المفتاح من بيانات الاعتماد",
"importFailed": "فشل استيراد المفتاح",
"noKeys": "لا توجد مفاتيح عامة منشورة بعد.",
"keysTitle": "المفاتيح المنشورة",
"published": "منشور",
"hidden": "مخفي",
"keyRemoved": "تمت إزالة المفتاح",
"removeKeyFailed": "فشل إزالة المفتاح",
"updateKeyFailed": "فشل تحديث المفتاح",
"fromVault": "من خزنة بيانات الاعتماد",
"linkedToTermixId": "منشور عبر Termix ID",
"selectCredential": "اختر بيانات اعتماد...",
"import": "استيراد",
"caTitle": "المرجع المصدق (CA)",
"caIntro": "وثق هذا المرجع المصدق على الخادم ليقبل أي شهادة توقعها. دوِّره لإبطال كل الشهادات دفعة واحدة؛ تنتهي صلاحية الشهادات تلقائيًا أيضًا.",
"caEnable": "فعّل المرجع المصدق",
"caEnabled": "تم تفعيل المرجع المصدق",
"caCreateFailed": "فشل تفعيل المرجع المصدق",
"caPublicKeyLabel": "المفتاح العام للمرجع المصدق",
"caTrustLabel": "ثق به على الخادم (تشغيل بصلاحيات الجذر)",
"caRotate": "دوِّر",
"caRotateConfirm": "هل تريد تدوير المرجع المصدق؟ ستتوقف جميع الشهادات التي وقعها عن القبول ويجب إعادة إصدارها.",
"caRotated": "تم تدوير المرجع المصدق — أُبطلت الشهادات السابقة",
"caRotateFailed": "فشل تدوير المرجع المصدق",
"caDelete": "أزل المرجع المصدق",
"caDeleteConfirm": "هل تريد إزالة المرجع المصدق؟",
"caDeleted": "تمت إزالة المرجع المصدق",
"caDeleteFailed": "فشل إزالة المرجع المصدق",
"caValidityLabel": "صلاحية الشهادة (بالأيام)",
"issueCert": "شهادة",
"issueCertTooltip": "إصدار شهادة SSH لهذا المفتاح، موقعة من المرجع المصدق الخاص بك",
"certIssued": "تم إصدار الشهادة وتنزيلها",
"certIssueFailed": "فشل إصدار الشهادة"
},
"credentials": {
"folders": "مجلدات",
"folder": "مجلد",
@@ -144,7 +75,6 @@
"editCredentialAction": "تعديل بيانات الاعتماد",
"failedToCloneCredential": "فشل استنساخ بيانات الاعتماد",
"failedToDeleteCredential": "فشل حذف بيانات الاعتماد",
"idBadge": "بطاقة تعريف",
"keyBadge": "مفتاح",
"passwordBadge": "ذوي الاحتياجات الخاصة",
"renameFolder": "إعادة تسمية المجلد",
@@ -525,7 +455,6 @@
"fileManager": "مدير الملفات",
"serverStats": "مقاييس المضيف",
"admin": "الإدارة",
"termixId": "معرف",
"userProfile": "ملف المستخدم",
"splitScreen": "تقسيم الشاشة",
"confirmClose": "هل تريد إغلاق هذه الجلسة النشطة؟",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Не може да се свърже",
"reconnect": "Свържете се отново"
},
"termixId": {
"title": "Термикс ИД",
"loadFailed": "Зареждането на Termix ID не бе успешно",
"claimTitle": "Заявете своя Termix ID",
"claimIntro": "Изберете уникален дескриптор. Вашите SSH публични ключове ще бъдат публикувани на публичен URL адрес, който можете да добавите към файла authorized_keys на всеки сървър.",
"handleLabel": "Дръжка",
"handlePlaceholder": "Алис",
"checking": "Проверка…",
"available": "Налично",
"taken": "Вече е взето",
"invalidHandle": "Само малки букви, цифри, - и _",
"descriptionLabel": "Описание (по избор)",
"descriptionPlaceholder": "Ключове за служебен лаптоп и телефон",
"create": "Създаване на Termix ID",
"created": "Termix ID е създаден",
"createFailed": "Неуспешно създаване на Termix ID",
"deleteConfirm": "Да изтриете ли Termix ID и всички публикувани ключове? Сървърите, които сте предоставили, ще пазят ключовете, докато не ги премахнете ръчно.",
"deleted": "Идентификаторът на Termix е изтрит",
"deleteFailed": "Неуспешно изтриване на Termix ID",
"copyFailed": "Копирането не бе успешно",
"resolverUrlLabel": "URL адрес на публичния резолвер",
"provisionLabel": "Осигуряване на сървър",
"publishTitle": "Публикуване на публичен ключ",
"generate": "Генериране",
"generateTooltip": "Генериране на двойка ключове Ed25519 — публикува публичния ключ и изтегля частния ключ на вашето устройство",
"saveToVault": "Запазване в идентификационни данни",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... потребител@хост",
"labelPlaceholder": "Етикет (по избор)",
"add": "Добавяне",
"importFromCredential": "Или импортирайте от съхранени идентификационни данни",
"keyPublished": "Ключът е публикуван",
"addKeyFailed": "Добавянето на ключ не бе успешно",
"generatedSaved": "Двойка ключове е генерирана и запазена в хранилището ви за идентификационни данни. Частният ключ също е изтеглен.",
"generatedOnly": "Генерирана двойка ключове — изтеглен частен ключ (показва се само веднъж).",
"generateFailed": "Генерирането на ключ не бе успешно",
"imported": "Ключът е импортиран от идентификационни данни",
"importFailed": "Импортирането на ключа не бе успешно",
"noKeys": "Все още няма публикувани публични ключове.",
"keysTitle": "Публикувани ключове",
"published": "Публикувано",
"hidden": "Скрито",
"keyRemoved": "Ключът е премахнат",
"removeKeyFailed": "Неуспешно премахване на ключа",
"updateKeyFailed": "Актуализирането на ключа не бе успешно",
"fromVault": "От трезора за идентификационни данни",
"linkedToTermixId": "Публикувано чрез Termix ID",
"selectCredential": "Изберете идентификационен номер...",
"import": "Внос",
"caTitle": "Сертифициращ орган",
"caIntro": "Доверете се на този CA на сървър и той ще приема всеки сертификат, който подпишете. Завъртете, за да отмените всичко наведнъж; сертификатите също изтичат сами.",
"caEnable": "Активиране на CA",
"caEnabled": "CA е активиран",
"caCreateFailed": "Активирането на CA не бе успешно",
"caPublicKeyLabel": "Публичен ключ на CA",
"caTrustLabel": "Доверие на сървър (изпълнява се като root)",
"caRotate": "Завъртане",
"caRotateConfirm": "Да се завърти ли сертифициращият орган? Всеки подписан от него сертификат ще спре да се приема и ще трябва да бъде преиздаден.",
"caRotated": "Ротация на CA — предишни сертификати са отменени",
"caRotateFailed": "Неуспешно завъртане на CA",
"caDelete": "Премахване на CA",
"caDeleteConfirm": "Да се премахне ли сертифициращият орган?",
"caDeleted": "CA премахнато",
"caDeleteFailed": "Премахването на CA не бе успешно",
"caValidityLabel": "Валидност на сертификата (дни)",
"issueCert": "Сертификат",
"issueCertTooltip": "Издайте SSH сертификат за този ключ, подписан от вашия CA",
"certIssued": "Сертификатът е издаден и изтеглен",
"certIssueFailed": "Издаването на сертификат не бе успешно"
},
"credentials": {
"folders": "Папки",
"folder": "Папка",
@@ -144,7 +75,6 @@
"editCredentialAction": "Редактиране на идентификационни данни",
"failedToCloneCredential": "Клонирането на идентификационните данни не бе успешно",
"failedToDeleteCredential": "Неуспешно изтриване на идентификационните данни",
"idBadge": "Идентификационен номер",
"keyBadge": "КЛЮЧ",
"passwordBadge": "Увреждания",
"renameFolder": "Преименуване на папка",
@@ -525,7 +455,6 @@
"fileManager": "Файлов мениджър",
"serverStats": "Метрики на хоста",
"admin": "Администратор",
"termixId": "Идентификационен номер",
"userProfile": "Потребителски профил",
"splitScreen": "Разделен екран",
"confirmClose": "Да се затвори ли тази активна сесия?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "সংযোগ করা সম্ভব নয়",
"reconnect": "পুনরায় সংযোগ করুন"
},
"termixId": {
"title": "টার্মিক্স আইডি",
"loadFailed": "টার্মিক্স আইডি লোড করতে ব্যর্থ হয়েছে",
"claimTitle": "আপনার টার্মিক্স আইডি দাবি করুন",
"claimIntro": "একটি অনন্য হ্যান্ডেল বেছে নিন। আপনার SSH পাবলিক কীগুলো একটি পাবলিক URL-এ প্রকাশ করা হবে, যা আপনি যেকোনো সার্ভারের authorized_keys ফাইলে যোগ করতে পারবেন।",
"handleLabel": "হাতল",
"handlePlaceholder": "অ্যালিস",
"checking": "… পরীক্ষা করা হচ্ছে",
"available": "উপলব্ধ",
"taken": "ইতিমধ্যে নেওয়া হয়েছে",
"invalidHandle": "শুধুমাত্র ছোট হাতের অক্ষর, সংখ্যা, - এবং _",
"descriptionLabel": "বিবরণ (ঐচ্ছিক)",
"descriptionPlaceholder": "কাজের ল্যাপটপ ও ফোনের চাবি",
"create": "টার্মিক্স আইডি তৈরি করুন",
"created": "টার্মিক্স আইডি তৈরি করা হয়েছে",
"createFailed": "টার্মিক্স আইডি তৈরি করতে ব্যর্থ হয়েছে",
"deleteConfirm": "আপনার টার্মিক্স আইডি এবং সমস্ত প্রকাশিত কী মুছে ফেলবেন? আপনার প্রোভিশন করা সার্ভারগুলিতে কীগুলি থেকে যাবে যতক্ষণ না আপনি সেগুলি ম্যানুয়ালি সরিয়ে ফেলেন।",
"deleted": "টার্মিক্স আইডি মুছে ফেলা হয়েছে",
"deleteFailed": "Termix ID মুছে ফেলা সম্ভব হয়নি",
"copyFailed": "কপি ব্যর্থ হয়েছে",
"resolverUrlLabel": "পাবলিক রিজলভার ইউআরএল",
"provisionLabel": "একটি সার্ভার সরবরাহ করুন",
"publishTitle": "একটি পাবলিক কী প্রকাশ করুন",
"generate": "তৈরি করুন",
"generateTooltip": "একটি Ed25519 কী পেয়ার তৈরি করুন — এটি পাবলিক কী প্রকাশ করে এবং আপনার ডিভাইসে প্রাইভেট কী ডাউনলোড করে।",
"saveToVault": "পরিচয়পত্রে সংরক্ষণ করুন",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "লেবেল (ঐচ্ছিক)",
"add": "যোগ করুন",
"importFromCredential": "অথবা সংরক্ষিত পরিচয়পত্র থেকে আমদানি করুন",
"keyPublished": "মূল প্রকাশিত",
"addKeyFailed": "কী যোগ করতে ব্যর্থ হয়েছে",
"generatedSaved": "কী পেয়ার তৈরি করে আপনার ক্রেডেনশিয়ালস ভল্টে সংরক্ষণ করা হয়েছে। প্রাইভেট কী-টিও ডাউনলোড করা হয়েছে।",
"generatedOnly": "কী পেয়ার তৈরি হয়েছে — প্রাইভেট কী ডাউনলোড করা হয়েছে (শুধুমাত্র একবার দেখানো হবে)।",
"generateFailed": "কী তৈরি করতে ব্যর্থ হয়েছে",
"imported": "ক্রেডেনশিয়াল থেকে আমদানি করা কী",
"importFailed": "কী আমদানি করতে ব্যর্থ হয়েছে",
"noKeys": "এখনো কোনো পাবলিক কী প্রকাশ করা হয়নি।",
"keysTitle": "প্রকাশিত চাবিগুলি",
"published": "প্রকাশিত",
"hidden": "লুকানো",
"keyRemoved": "চাবি সরানো হয়েছে",
"removeKeyFailed": "চাবি সরাতে ব্যর্থ হয়েছে",
"updateKeyFailed": "কী আপডেট করতে ব্যর্থ হয়েছে",
"fromVault": "ক্রেডেনশিয়াল ভল্ট থেকে",
"linkedToTermixId": "টারমিক্স আইডি-এর মাধ্যমে প্রকাশিত",
"selectCredential": "একটি শংসাপত্র নির্বাচন করুন...",
"import": "আমদানি",
"caTitle": "শংসাপত্র কর্তৃপক্ষ",
"caIntro": "সার্ভারে এই CA-কে বিশ্বাস করলে, এটি আপনার স্বাক্ষরিত যেকোনো সার্টিফিকেট গ্রহণ করে। একবারে সবকিছু বাতিল করতে পর্যায়ক্রমে পরিবর্তন করুন; সার্টিফিকেটগুলো আপনাআপনি মেয়াদোত্তীর্ণও হয়ে যায়।",
"caEnable": "CA সক্ষম করুন",
"caEnabled": "CA সক্ষম",
"caCreateFailed": "CA সক্রিয় করতে ব্যর্থ হয়েছে",
"caPublicKeyLabel": "CA পাবলিক কী",
"caTrustLabel": "সার্ভারে বিশ্বাস (রুট হিসেবে চালান)",
"caRotate": "ঘোরান",
"caRotateConfirm": "CA পরিবর্তন করবেন? এর দ্বারা স্বাক্ষরিত প্রতিটি সার্টিফিকেট আর গ্রহণ করা হবে না এবং পুনরায় ইস্যু করতে হবে।",
"caRotated": "CA পরিবর্তিত হয়েছে — পূর্ববর্তী সার্টিফিকেটগুলো বাতিল করা হয়েছে",
"caRotateFailed": "CA ঘোরানো ব্যর্থ হয়েছে",
"caDelete": "CA অপসারণ করুন",
"caDeleteConfirm": "সার্টিফিকেট কর্তৃপক্ষকে অপসারণ করবেন?",
"caDeleted": "CA অপসারণ করেছে",
"caDeleteFailed": "CA অপসারণ করতে ব্যর্থ হয়েছে",
"caValidityLabel": "সার্টিফিকেটের বৈধতা (দিন)",
"issueCert": "সার্টিফিকেট",
"issueCertTooltip": "এই কী-টির জন্য আপনার CA দ্বারা স্বাক্ষরিত একটি SSH সার্টিফিকেট ইস্যু করুন।",
"certIssued": "সার্টিফিকেট ইস্যু এবং ডাউনলোড করা হয়েছে",
"certIssueFailed": "সার্টিফিকেট ইস্যু করতে ব্যর্থ হয়েছে"
},
"credentials": {
"folders": "ফোল্ডার",
"folder": "ফোল্ডার",
@@ -144,7 +75,6 @@
"editCredentialAction": "পরিচয়পত্র সম্পাদনা করুন",
"failedToCloneCredential": "ক্রেডেনশিয়াল ক্লোন করতে ব্যর্থ হয়েছে",
"failedToDeleteCredential": "ক্রেডেনশিয়াল মুছে ফেলতে ব্যর্থ হয়েছে",
"idBadge": "আইডি",
"keyBadge": "চাবি",
"passwordBadge": "পিডব্লিউডি",
"renameFolder": "ফোল্ডারের নাম পরিবর্তন করুন",
@@ -525,7 +455,6 @@
"fileManager": "ফাইল ম্যানেজার",
"serverStats": "হোস্ট মেট্রিক্স",
"admin": "প্রশাসক",
"termixId": "আইডি",
"userProfile": "ব্যবহারকারীর প্রোফাইল",
"splitScreen": "স্প্লিট স্ক্রিন",
"confirmClose": "এই সক্রিয় সেশনটি বন্ধ করবেন?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "No es pot connectar",
"reconnect": "Reconnecta"
},
"termixId": {
"title": "ID de Termix",
"loadFailed": "No s'ha pogut carregar l'ID de Termix.",
"claimTitle": "Reclama el teu ID de Termix",
"claimIntro": "Trieu un identificador únic. Les vostres claus públiques SSH es publicaran en una URL pública que podeu afegir al fitxer authorized_keys de qualsevol servidor.",
"handleLabel": "Maneta",
"handlePlaceholder": "Alícia",
"checking": "Comprovació de…",
"available": "Disponible",
"taken": "Ja ocupat/da",
"invalidHandle": "Només lletres minúscules, números, - i _",
"descriptionLabel": "Descripció (opcional)",
"descriptionPlaceholder": "Claus de portàtil i telèfon de la feina",
"create": "Crea un ID de Termix",
"created": "ID de Termix creat",
"createFailed": "No s'ha pogut crear l'ID de Termix.",
"deleteConfirm": "Voleu suprimir el vostre ID de Termix i totes les claus publicades? Els servidors que heu aprovisionat conservaran les claus fins que les elimineu manualment.",
"deleted": "ID de Termix suprimit",
"deleteFailed": "No s'ha pogut suprimir l'ID de Termix.",
"copyFailed": "S'ha produït un error de còpia.",
"resolverUrlLabel": "URL de resolució pública",
"provisionLabel": "Provisionar un servidor",
"publishTitle": "Publicar una clau pública",
"generate": "Generar",
"generateTooltip": "Genera un parell de claus Ed25519: publica la clau pública i descarrega la clau privada al dispositiu.",
"saveToVault": "Desa a les credencials",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... usuari@amfitrió",
"labelPlaceholder": "Etiqueta (opcional)",
"add": "Afegeix",
"importFromCredential": "O importar des d'una credencial emmagatzemada",
"keyPublished": "Clau publicada",
"addKeyFailed": "No s'ha pogut afegir la clau",
"generatedSaved": "Parell de claus generat i desat al vostre magatzem de credencials. La clau privada també s'ha descarregat.",
"generatedOnly": "Parell de claus generat: clau privada descarregada (només es mostra una vegada).",
"generateFailed": "No s'ha pogut generar la clau",
"imported": "Clau importada de la credencial",
"importFailed": "No s'ha pogut importar la clau",
"noKeys": "Encara no s'han publicat cap clau pública.",
"keysTitle": "Claus publicades",
"published": "Publicat",
"hidden": "Ocult",
"keyRemoved": "Clau retirada",
"removeKeyFailed": "No s'ha pogut treure la clau",
"updateKeyFailed": "No s'ha pogut actualitzar la clau",
"fromVault": "Des de la caixa forta de credencials",
"linkedToTermixId": "Publicat a través de l'ID de Termix",
"selectCredential": "Selecciona una credencial...",
"import": "Importa",
"caTitle": "Autoritat de certificació",
"caIntro": "Confia en aquesta CA en un servidor i acceptarà qualsevol certificat que signeu. Gireu per revocar-ho tot alhora; els certificats també caduquen sols.",
"caEnable": "Habilita CA",
"caEnabled": "CA habilitat",
"caCreateFailed": "No s'ha pogut habilitar l'entitat de certificació",
"caPublicKeyLabel": "Clau pública de CA",
"caTrustLabel": "Confiança en un servidor (executat com a root)",
"caRotate": "Girar",
"caRotateConfirm": "Voleu rotar l'entitat de certificació? Tots els certificats que hagi signat deixaran de ser acceptats i s'hauran de tornar a emetre.",
"caRotated": "CA rotada: certificats anteriors revocats",
"caRotateFailed": "No s'ha pogut rotar CA",
"caDelete": "Eliminar CA",
"caDeleteConfirm": "Voleu eliminar l'autoritat de certificació?",
"caDeleted": "CA eliminada",
"caDeleteFailed": "No s'ha pogut eliminar l'entitat de certificació",
"caValidityLabel": "Validesa del certificat (dies)",
"issueCert": "Certificat",
"issueCertTooltip": "Emet un certificat SSH per a aquesta clau, signat per la teva CA",
"certIssued": "Certificat emès i descarregat",
"certIssueFailed": "No s'ha pogut emetre el certificat"
},
"credentials": {
"folders": "Carpetes",
"folder": "Carpeta",
@@ -144,7 +75,6 @@
"editCredentialAction": "Edita la credencial",
"failedToCloneCredential": "No s'ha pogut clonar la credencial",
"failedToDeleteCredential": "No s'ha pogut suprimir la credencial",
"idBadge": "ID",
"keyBadge": "CLAU",
"passwordBadge": "PWD",
"renameFolder": "Canvia el nom de la carpeta",
@@ -525,7 +455,6 @@
"fileManager": "Gestor de fitxers",
"serverStats": "Mètriques de l'amfitrió",
"admin": "Administrador",
"termixId": "ID",
"userProfile": "Perfil d'usuari",
"splitScreen": "Pantalla dividida",
"confirmClose": "Voleu tancar aquesta sessió activa?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Nelze se připojit",
"reconnect": "Znovu se připojte"
},
"termixId": {
"title": "ID Termixu",
"loadFailed": "Nepodařilo se načíst ID Termixu",
"claimTitle": "Získejte své Termix ID",
"claimIntro": "Vyberte jedinečný handle. Vaše veřejné klíče SSH budou zveřejněny na veřejné URL adrese, kterou můžete přidat do souboru authorized_keys libovolného serveru.",
"handleLabel": "Zacházet s",
"handlePlaceholder": "Alice",
"checking": "Kontrola…",
"available": "K dispozici",
"taken": "Již obsazeno",
"invalidHandle": "Pouze malá písmena, číslice, - a _",
"descriptionLabel": "Popis (volitelné)",
"descriptionPlaceholder": "Klíče k pracovnímu notebooku a telefonu",
"create": "Vytvořit ID Termixu",
"created": "ID Termixu vytvořeno",
"createFailed": "Nepodařilo se vytvořit ID Termixu",
"deleteConfirm": "Smazat své Termix ID a všechny publikované klíče? Servery, které jste zřídili, si klíče uchovají, dokud je ručně neodstraníte.",
"deleted": "ID Termixu smazáno",
"deleteFailed": "Nepodařilo se smazat ID Termixu",
"copyFailed": "Kopírování se nezdařilo",
"resolverUrlLabel": "URL veřejného resolveru",
"provisionLabel": "Zřízení serveru",
"publishTitle": "Publikovat veřejný klíč",
"generate": "Generovat",
"generateTooltip": "Generování páru klíčů Ed25519 – publikuje veřejný klíč a stáhne soukromý klíč do vašeho zařízení",
"saveToVault": "Uložit do přihlašovacích údajů",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... uživatel@hostitel",
"labelPlaceholder": "Štítek (volitelné)",
"add": "Přidat",
"importFromCredential": "Nebo importovat z uložených přihlašovacích údajů",
"keyPublished": "Klíč zveřejněn",
"addKeyFailed": "Přidání klíče se nezdařilo",
"generatedSaved": "Pár klíčů byl vygenerován a uložen do úložiště vašich přihlašovacích údajů. Soukromý klíč byl také stažen.",
"generatedOnly": "Vygenerovaný pár klíčů — stažený soukromý klíč (zobrazí se pouze jednou).",
"generateFailed": "Nepodařilo se vygenerovat klíč",
"imported": "Klíč importován z přihlašovacích údajů",
"importFailed": "Import klíče se nezdařilo",
"noKeys": "Zatím nebyly zveřejněny žádné veřejné klíče.",
"keysTitle": "Publikované klíče",
"published": "Publikováno",
"hidden": "Skrytý",
"keyRemoved": "Klíč vyjmut",
"removeKeyFailed": "Nepodařilo se vyjmout klíč",
"updateKeyFailed": "Nepodařilo se aktualizovat klíč",
"fromVault": "Z úložiště přihlašovacích údajů",
"linkedToTermixId": "Publikováno přes Termix ID",
"selectCredential": "Vyberte pověření...",
"import": "Importovat",
"caTitle": "Certifikační autorita",
"caIntro": "Důvěřujte této certifikační autoritě na serveru a ta přijme jakýkoli certifikát, který podepíšete. Otočením zrušíte vše najednou; certifikáty také samy vyprší.",
"caEnable": "Povolit certifikační autoritu (CA)",
"caEnabled": "CA povolena",
"caCreateFailed": "Nepodařilo se povolit certifikační autoritu (CA).",
"caPublicKeyLabel": "Veřejný klíč CA",
"caTrustLabel": "Důvěra na serveru (spuštěno jako root)",
"caRotate": "Střídat",
"caRotateConfirm": "Rotovat certifikační autoritu? Každý certifikát, který podepsala, přestane být akceptován a bude nutné jej znovu vydat.",
"caRotated": "CA rotována – předchozí certifikáty zrušeny",
"caRotateFailed": "Nepodařilo se otočit CA",
"caDelete": "Odebrat certifikační autoritu (CA)",
"caDeleteConfirm": "Odebrat certifikační autoritu?",
"caDeleted": "CA odstraněna",
"caDeleteFailed": "Nepodařilo se odebrat certifikační autoritu (CA).",
"caValidityLabel": "Platnost certifikátu (dny)",
"issueCert": "Osvědčení",
"issueCertTooltip": "Vystavte pro tento klíč SSH certifikát podepsaný vaší certifikační autoritou.",
"certIssued": "Certifikát vydán a stažen",
"certIssueFailed": "Vydání certifikátu se nezdařilo"
},
"credentials": {
"folders": "Složky",
"folder": "Složka",
@@ -144,7 +75,6 @@
"editCredentialAction": "Upravit přihlašovací údaje",
"failedToCloneCredential": "Nepodařilo se klonovat přihlašovací údaje",
"failedToDeleteCredential": "Nepodařilo se smazat přihlašovací údaje",
"idBadge": "Průkaz totožnosti",
"keyBadge": "KLÍČ",
"passwordBadge": "OSO",
"renameFolder": "Přejmenovat složku",
@@ -525,7 +455,6 @@
"fileManager": "Správce souborů",
"serverStats": "Metriky hostitele",
"admin": "Administrátor",
"termixId": "Průkaz totožnosti",
"userProfile": "Uživatelský profil",
"splitScreen": "Rozdělená obrazovka",
"confirmClose": "Zavřít tuto aktivní relaci?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Kan ikke oprette forbindelse",
"reconnect": "Genopret forbindelse"
},
"termixId": {
"title": "Termix-ID",
"loadFailed": "Kunne ikke indlæse Termix-ID'et",
"claimTitle": "Få dit Termix-ID",
"claimIntro": "Vælg et unikt handle. Dine offentlige SSH-nøgler vil blive offentliggjort på en offentlig URL, som du kan tilføje til enhver servers authorized_keys-fil.",
"handleLabel": "Håndtag",
"handlePlaceholder": "Alice",
"checking": "Tjekker…",
"available": "Tilgængelig",
"taken": "Allerede taget",
"invalidHandle": "Kun små bogstaver, tal, - og _",
"descriptionLabel": "Beskrivelse (valgfrit)",
"descriptionPlaceholder": "Arbejdslaptop og telefonnøgler",
"create": "Opret Termix-ID",
"created": "Termix-ID oprettet",
"createFailed": "Kunne ikke oprette Termix ID",
"deleteConfirm": "Vil du slette dit Termix-ID og alle publicerede nøgler? Servere, du har provisioneret, vil beholde nøglerne, indtil du fjerner dem manuelt.",
"deleted": "Termix-ID slettet",
"deleteFailed": "Kunne ikke slette Termix ID",
"copyFailed": "Kopiering mislykkedes",
"resolverUrlLabel": "Offentlig resolver-URL",
"provisionLabel": "Klargør en server",
"publishTitle": "Udgiv en offentlig nøgle",
"generate": "Frembringe",
"generateTooltip": "Generer et Ed25519-nøglepar — publicerer den offentlige nøgle og downloader den private nøgle til din enhed",
"saveToVault": "Gem til legitimationsoplysninger",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... bruger@vært",
"labelPlaceholder": "Etiket (valgfrit)",
"add": "Tilføje",
"importFromCredential": "Eller importer fra en gemt legitimationsoplysninger",
"keyPublished": "Nøgle udgivet",
"addKeyFailed": "Kunne ikke tilføje nøgle",
"generatedSaved": "Nøglepar genereret og gemt i din login-boks. Privat nøgle er også downloadet.",
"generatedOnly": "Nøglepar genereret — privat nøgle downloadet (vises kun én gang).",
"generateFailed": "Kunne ikke generere nøgle",
"imported": "Nøgle importeret fra legitimationsoplysninger",
"importFailed": "Nøglen kunne ikke importeres",
"noKeys": "Ingen offentlige nøgler er udgivet endnu.",
"keysTitle": "Udgivne nøgler",
"published": "Udgivet",
"hidden": "Skjult",
"keyRemoved": "Nøglen er fjernet",
"removeKeyFailed": "Kunne ikke fjerne nøglen",
"updateKeyFailed": "Nøglen kunne ikke opdateres",
"fromVault": "Fra legitimationsoplysninger-arkiv",
"linkedToTermixId": "Udgivet via Termix ID",
"selectCredential": "Vælg en legitimationsoplysninger...",
"import": "Importere",
"caTitle": "Certifikatmyndighed",
"caIntro": "Stol på denne CA på en server, og den accepterer ethvert certifikat, du underskriver. Roter for at tilbagekalde alt på én gang; certifikater udløber også af sig selv.",
"caEnable": "Aktivér CA",
"caEnabled": "CA aktiveret",
"caCreateFailed": "Kunne ikke aktivere CA",
"caPublicKeyLabel": "CA's offentlige nøgle",
"caTrustLabel": "Tillid til en server (kør som root)",
"caRotate": "Rotere",
"caRotateConfirm": "Roter CA'en? Alle certifikater, den har underskrevet, vil ikke længere blive accepteret og skal genudstedes.",
"caRotated": "CA roteret — tidligere certifikater tilbagekaldt",
"caRotateFailed": "Kunne ikke rotere CA",
"caDelete": "Fjern CA",
"caDeleteConfirm": "Fjern certifikatmyndigheden?",
"caDeleted": "CA fjernet",
"caDeleteFailed": "Kunne ikke fjerne CA",
"caValidityLabel": "Certifikatgyldighed (dage)",
"issueCert": "Certifikat",
"issueCertTooltip": "Udsted et SSH-certifikat til denne nøgle, underskrevet af din CA",
"certIssued": "Certifikat udstedt og downloadet",
"certIssueFailed": "Kunne ikke udstede certifikat"
},
"credentials": {
"folders": "Mapper",
"folder": "Folder",
@@ -144,7 +75,6 @@
"editCredentialAction": "Rediger legitimationsoplysninger",
"failedToCloneCredential": "Kunne ikke klone legitimationsoplysninger",
"failedToDeleteCredential": "Kunne ikke slette legitimationsoplysninger",
"idBadge": "ID",
"keyBadge": "NØGLE",
"passwordBadge": "Udadvendt",
"renameFolder": "Omdøb mappe",
@@ -525,7 +455,6 @@
"fileManager": "Filhåndtering",
"serverStats": "Værtsmålinger",
"admin": "Admin",
"termixId": "ID",
"userProfile": "Brugerprofil",
"splitScreen": "Delt skærm",
"confirmClose": "Lukke denne aktive session?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Verbindung nicht möglich",
"reconnect": "Wiederverbinden"
},
"termixId": {
"title": "Termix ID",
"loadFailed": "Fehler beim Laden der Termix-ID",
"claimTitle": "Termix-ID beanspruchen",
"claimIntro": "Wählen Sie einen eindeutigen Benutzernamen. Ihre öffentlichen SSH-Schlüssel werden unter einer öffentlichen URL veröffentlicht, die Sie in die authorized_keys-Datei eines beliebigen Servers einfügen können.",
"handleLabel": "Benutzername",
"handlePlaceholder": "alice",
"checking": "Wird überprüft…",
"available": "Verfügbar",
"taken": "Bereits vergeben",
"invalidHandle": "Nur Kleinbuchstaben, Zahlen, - und _ erlaubt",
"descriptionLabel": "Beschreibung (optional)",
"descriptionPlaceholder": "Arbeits-Laptop & Telefonschlüssel",
"create": "Termix-ID erstellen",
"created": "Termix-ID erstellt",
"createFailed": "Fehler beim Erstellen der Termix-ID",
"deleteConfirm": "Termix-ID und alle veröffentlichten Schlüssel löschen? Bereits bereitgestellte Server behalten die Schlüssel, bis Sie sie manuell entfernen.",
"deleted": "Termix-ID gelöscht",
"deleteFailed": "Fehler beim Löschen der Termix-ID",
"copyFailed": "Kopieren fehlgeschlagen",
"resolverUrlLabel": "Öffentliche Resolver-URL",
"provisionLabel": "Server bereitstellen",
"publishTitle": "Öffentlichen Schlüssel veröffentlichen",
"generate": "Generieren",
"generateTooltip": "Ed25519-Schlüsselpaar generieren — veröffentlicht den öffentlichen Schlüssel und lädt den privaten Schlüssel auf Ihr Gerät herunter",
"saveToVault": "In Anmeldeinformationen speichern",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "Bezeichnung (optional)",
"add": "Hinzufügen",
"importFromCredential": "Oder aus gespeicherten Anmeldeinformationen importieren",
"keyPublished": "Schlüssel veröffentlicht",
"addKeyFailed": "Fehler beim Hinzufügen des Schlüssels",
"generatedSaved": "Schlüsselpaar generiert und in Ihrem Anmeldeinformations-Tresor gespeichert. Privater Schlüssel ebenfalls heruntergeladen.",
"generatedOnly": "Schlüsselpaar generiert — privater Schlüssel heruntergeladen (wird nur einmal angezeigt).",
"generateFailed": "Fehler beim Generieren des Schlüssels",
"imported": "Schlüssel aus Anmeldeinformationen importiert",
"importFailed": "Schlüsselimport fehlgeschlagen",
"noKeys": "Noch keine öffentlichen Schlüssel veröffentlicht.",
"keysTitle": "Veröffentlichte Schlüssel",
"published": "Veröffentlicht",
"hidden": "Ausgeblendet",
"keyRemoved": "Schlüssel entfernt",
"removeKeyFailed": "Schlüsselentfernung fehlgeschlagen",
"updateKeyFailed": "Schlüsselaktualisierung fehlgeschlagen",
"fromVault": "Aus dem Anmeldedaten-Tresor",
"linkedToTermixId": "Veröffentlicht über Termix ID",
"selectCredential": "Anmeldedaten auswählen...",
"import": "Importieren",
"caTitle": "Zertifizierungsstelle",
"caIntro": "Vertrauen Sie dieser CA auf einem Server, und sie akzeptiert jedes von Ihnen signierte Zertifikat. Durch Rotieren widerrufen Sie alles auf einmal; Zertifikate laufen auch automatisch ab.",
"caEnable": "CA aktivieren",
"caEnabled": "CA aktiviert",
"caCreateFailed": "Aktivierung der CA fehlgeschlagen",
"caPublicKeyLabel": "Öffentlicher Schlüssel der CA",
"caTrustLabel": "Vertrauen auf einem Server (als root ausführen)",
"caRotate": "Rotieren",
"caRotateConfirm": "CA rotieren? Alle von ihr signierten Zertifikate werden nicht mehr akzeptiert und müssen neu ausgestellt werden.",
"caRotated": "CA rotiert — vorherige Zertifikate widerrufen",
"caRotateFailed": "Rotieren der CA fehlgeschlagen",
"caDelete": "CA entfernen",
"caDeleteConfirm": "Zertifizierungsstelle entfernen?",
"caDeleted": "CA entfernt",
"caDeleteFailed": "Entfernen der CA fehlgeschlagen",
"caValidityLabel": "Zertifikatsgültigkeit (Tage)",
"issueCert": "Zertifikat",
"issueCertTooltip": "Ein SSH-Zertifikat für diesen Schlüssel ausstellen, signiert von Ihrer CA",
"certIssued": "Zertifikat ausgestellt und heruntergeladen",
"certIssueFailed": "Ausstellung des Zertifikats fehlgeschlagen"
},
"credentials": {
"folders": "Ordner",
"folder": "Ordner",
@@ -144,7 +75,6 @@
"editCredentialAction": "Anmeldeinformationen bearbeiten",
"failedToCloneCredential": "Fehler beim Klonen der Anmeldeinformationen",
"failedToDeleteCredential": "Anmeldeinformationen konnten nicht gelöscht werden",
"idBadge": "AUSWEIS",
"keyBadge": "SCHLÜSSEL",
"passwordBadge": "PWD",
"renameFolder": "Ordner umbenennen",
@@ -525,7 +455,6 @@
"fileManager": "Dateimanager",
"serverStats": "Host-Metriken",
"admin": "Admin",
"termixId": "ID",
"userProfile": "Benutzerprofil",
"splitScreen": "Geteilter Bildschirm",
"confirmClose": "Diese aktive Sitzung schließen?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Δεν είναι δυνατή η σύνδεση",
"reconnect": "Επανασύνδεση"
},
"termixId": {
"title": "Αναγνωριστικό Termix",
"loadFailed": "Αποτυχία φόρτωσης του αναγνωριστικού Termix",
"claimTitle": "Διεκδικήστε το αναγνωριστικό σας Termix",
"claimIntro": "Επιλέξτε μια μοναδική λαβή. Τα δημόσια κλειδιά SSH σας θα δημοσιευτούν σε μια δημόσια διεύθυνση URL που μπορείτε να προσθέσετε στο αρχείο authorized_keys οποιουδήποτε διακομιστή.",
"handleLabel": "Λαβή",
"handlePlaceholder": "Αλίκη",
"checking": "Έλεγχος…",
"available": "Διαθέσιμος",
"taken": "Ήδη καταγεγραμμένο",
"invalidHandle": "Μόνο πεζά γράμματα, αριθμοί και - και _",
"descriptionLabel": "Περιγραφή (προαιρετικό)",
"descriptionPlaceholder": "Κλειδιά φορητού υπολογιστή και τηλεφώνου εργασίας",
"create": "Δημιουργία αναγνωριστικού Termix",
"created": "Το αναγνωριστικό Termix δημιουργήθηκε",
"createFailed": "Αποτυχία δημιουργίας αναγνωριστικού Termix",
"deleteConfirm": "Να διαγραφεί το αναγνωριστικό Termix και όλα τα δημοσιευμένα κλειδιά; Οι διακομιστές που έχετε παράσχει θα διατηρήσουν τα κλειδιά μέχρι να τα καταργήσετε χειροκίνητα.",
"deleted": "Το αναγνωριστικό Termix διαγράφηκε",
"deleteFailed": "Αποτυχία διαγραφής του αναγνωριστικού Termix",
"copyFailed": "Η αντιγραφή απέτυχε.",
"resolverUrlLabel": "Δημόσια διεύθυνση URL αναλυτή",
"provisionLabel": "Παροχή διακομιστή",
"publishTitle": "Δημοσίευση δημόσιου κλειδιού",
"generate": "Παράγω",
"generateTooltip": "Δημιουργήστε ένα ζεύγος κλειδιών Ed25519 — δημοσιεύστε το δημόσιο κλειδί και κατεβάστε το ιδιωτικό κλειδί στη συσκευή σας",
"saveToVault": "Αποθήκευση σε διαπιστευτήρια",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... χρήστης@host",
"labelPlaceholder": "Ετικέτα (προαιρετικά)",
"add": "Προσθέτω",
"importFromCredential": "Ή εισαγωγή από αποθηκευμένα διαπιστευτήρια",
"keyPublished": "Κλειδί δημοσιεύτηκε",
"addKeyFailed": "Η προσθήκη κλειδιού απέτυχε",
"generatedSaved": "Δημιουργήθηκε και αποθηκεύτηκε ζεύγος κλειδιών στο αρχείο διαπιστευτηρίων σας. Επίσης, λήφθηκε το ιδιωτικό κλειδί.",
"generatedOnly": "Δημιουργήθηκε ζεύγος κλειδιών — λήφθηκε ιδιωτικό κλειδί (εμφανίζεται μόνο μία φορά).",
"generateFailed": "Η δημιουργία κλειδιού απέτυχε",
"imported": "Κλειδί εισήχθη από πιστοποιητικά",
"importFailed": "Αποτυχία εισαγωγής κλειδιού",
"noKeys": "Δεν έχουν δημοσιευτεί ακόμη δημόσια κλειδιά.",
"keysTitle": "Δημοσιευμένα κλειδιά",
"published": "Δημοσιεύτηκε",
"hidden": "Κεκρυμμένος",
"keyRemoved": "Το κλειδί αφαιρέθηκε",
"removeKeyFailed": "Αποτυχία αφαίρεσης κλειδιού",
"updateKeyFailed": "Η ενημέρωση του κλειδιού απέτυχε",
"fromVault": "Από το θησαυροφυλάκιο διαπιστευτηρίων",
"linkedToTermixId": "Δημοσιεύτηκε μέσω του Termix ID",
"selectCredential": "Επιλέξτε ένα πιστοποιητικό...",
"import": "Εισαγωγή",
"caTitle": "Αρχή έκδοσης πιστοποιητικών",
"caIntro": "Να εμπιστεύεστε αυτήν την CA σε έναν διακομιστή και αυτή θα αποδέχεται οποιοδήποτε πιστοποιητικό υπογράφετε. Περιστρέψτε για να ακυρώσετε τα πάντα ταυτόχρονα. Τα πιστοποιητικά λήγουν επίσης από μόνα τους.",
"caEnable": "Ενεργοποίηση CA",
"caEnabled": "Ενεργοποιημένο CA",
"caCreateFailed": "Αποτυχία ενεργοποίησης CA",
"caPublicKeyLabel": "Δημόσιο κλειδί CA",
"caTrustLabel": "Εμπιστοσύνη σε διακομιστή (εκτέλεση ως root)",
"caRotate": "Γυρίζω",
"caRotateConfirm": "Να εναλλάσσεται η CA; Κάθε πιστοποιητικό που έχει υπογράψει θα σταματήσει να γίνεται αποδεκτό και θα πρέπει να εκδοθεί εκ νέου.",
"caRotated": "Εναλλαγή CA — ανακλήθηκαν προηγούμενα πιστοποιητικά",
"caRotateFailed": "Αποτυχία εναλλαγής CA",
"caDelete": "Κατάργηση CA",
"caDeleteConfirm": "Κατάργηση της αρχής έκδοσης πιστοποιητικών;",
"caDeleted": "Η CA καταργήθηκε.",
"caDeleteFailed": "Αποτυχία κατάργησης CA",
"caValidityLabel": "Ισχύς πιστοποιητικού (ημέρες)",
"issueCert": "Πιστοποιητικό",
"issueCertTooltip": "Έκδοση πιστοποιητικού SSH για αυτό το κλειδί, υπογεγραμμένου από την CA σας",
"certIssued": "Έκδοση και λήψη πιστοποιητικού",
"certIssueFailed": "Αποτυχία έκδοσης πιστοποιητικού"
},
"credentials": {
"folders": "Φάκελοι",
"folder": "Ντοσιέ",
@@ -144,7 +75,6 @@
"editCredentialAction": "Επεξεργασία διαπιστευτηρίων",
"failedToCloneCredential": "Αποτυχία κλωνοποίησης διαπιστευτηρίων",
"failedToDeleteCredential": "Η διαγραφή των διαπιστευτηρίων απέτυχε",
"idBadge": "ταυτότητα",
"keyBadge": "ΚΛΕΙΔΙ",
"passwordBadge": "ΑμεΑ",
"renameFolder": "Μετονομασία φακέλου",
@@ -525,7 +455,6 @@
"fileManager": "Διαχειριστής αρχείων",
"serverStats": "Μετρήσεις κεντρικού υπολογιστή",
"admin": "Διαχειριστής",
"termixId": "ταυτότητα",
"userProfile": "Προφίλ χρήστη",
"splitScreen": "Διαχωρισμένη οθόνη",
"confirmClose": "Κλείσιμο αυτής της ενεργής συνεδρίας;",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "No se puede conectar",
"reconnect": "Reconectar"
},
"termixId": {
"title": "Termix ID",
"loadFailed": "Error al cargar Termix ID",
"claimTitle": "Reclama tu Termix ID",
"claimIntro": "Elige un identificador único. Tus claves públicas SSH se publicarán en una URL pública que puedes añadir al archivo authorized_keys de cualquier servidor.",
"handleLabel": "Identificador",
"handlePlaceholder": "alice",
"checking": "Comprobando…",
"available": "Disponible",
"taken": "Ya está en uso",
"invalidHandle": "Solo letras minúsculas, números, - y _",
"descriptionLabel": "Descripción (opcional)",
"descriptionPlaceholder": "Claves del portátil de trabajo y del teléfono",
"create": "Crear Termix ID",
"created": "Termix ID creado",
"createFailed": "Error al crear Termix ID",
"deleteConfirm": "¿Eliminar tu Termix ID y todas las claves publicadas? Los servidores que aprovisionaste conservarán las claves hasta que las elimines manualmente.",
"deleted": "Termix ID eliminado",
"deleteFailed": "Error al eliminar Termix ID",
"copyFailed": "Error al copiar",
"resolverUrlLabel": "URL pública de resolución",
"provisionLabel": "Aprovisionar un servidor",
"publishTitle": "Publicar una clave pública",
"generate": "Generar",
"generateTooltip": "Generar un par de claves Ed25519: publica la clave pública y descarga la clave privada en tu dispositivo",
"saveToVault": "Guardar en credenciales",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "Etiqueta (opcional)",
"add": "Añadir",
"importFromCredential": "O importar desde una credencial almacenada",
"keyPublished": "Clave publicada",
"addKeyFailed": "Error al añadir clave",
"generatedSaved": "Par de claves generado y guardado en tu almacén de credenciales. También se descargó la clave privada.",
"generatedOnly": "Par de claves generado: clave privada descargada (se muestra solo una vez).",
"generateFailed": "Error al generar clave",
"imported": "Clave importada desde la credencial",
"importFailed": "Error al importar la clave",
"noKeys": "Aún no se han publicado claves públicas.",
"keysTitle": "Claves publicadas",
"published": "Publicada",
"hidden": "Oculta",
"keyRemoved": "Clave eliminada",
"removeKeyFailed": "Error al eliminar la clave",
"updateKeyFailed": "Error al actualizar la clave",
"fromVault": "Del almacén de credenciales",
"linkedToTermixId": "Publicada mediante Termix ID",
"selectCredential": "Seleccionar una credencial...",
"import": "Importar",
"caTitle": "Autoridad de certificación",
"caIntro": "Confíe en esta CA en un servidor y aceptará cualquier certificado que firme. Rote para revocar todo a la vez; los certificados también caducan por sí solos.",
"caEnable": "Activar CA",
"caEnabled": "CA activada",
"caCreateFailed": "Error al activar la CA",
"caPublicKeyLabel": "Clave pública de la CA",
"caTrustLabel": "Confiar en un servidor (ejecutar como root)",
"caRotate": "Rotar",
"caRotateConfirm": "¿Rotar la CA? Todos los certificados que ha firmado dejarán de ser aceptados y deberán volver a emitirse.",
"caRotated": "CA rotada — certificados anteriores revocados",
"caRotateFailed": "Error al rotar la CA",
"caDelete": "Eliminar CA",
"caDeleteConfirm": "¿Eliminar la autoridad de certificación?",
"caDeleted": "CA eliminada",
"caDeleteFailed": "Error al eliminar la CA",
"caValidityLabel": "Validez del certificado (días)",
"issueCert": "Certificado",
"issueCertTooltip": "Emitir un certificado SSH para esta clave, firmado por su CA",
"certIssued": "Certificado emitido y descargado",
"certIssueFailed": "Error al emitir el certificado"
},
"credentials": {
"folders": "Carpetas",
"folder": "Carpeta",
@@ -144,7 +75,6 @@
"editCredentialAction": "Editar credenciales",
"failedToCloneCredential": "Error al clonar las credenciales",
"failedToDeleteCredential": "Error al eliminar las credenciales",
"idBadge": "IDENTIFICACIÓN",
"keyBadge": "LLAVE",
"passwordBadge": "Personas con discapacidad",
"renameFolder": "Cambiar el nombre de la carpeta",
@@ -525,7 +455,6 @@
"fileManager": "Gestor de archivos",
"serverStats": "Métricas del host",
"admin": "Admin",
"termixId": "ID",
"userProfile": "Perfil de usuario",
"splitScreen": "Pantalla dividida",
"confirmClose": "¿Cerrar esta sesión activa?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Yhteyden muodostaminen epäonnistui",
"reconnect": "Yhdistä uudelleen"
},
"termixId": {
"title": "Termix-tunnus",
"loadFailed": "Termix-tunnuksen lataaminen epäonnistui",
"claimTitle": "Lunasta Termix-tunnuksesi",
"claimIntro": "Valitse yksilöllinen käyttäjätunnus. SSH-julkiset avaimesi julkaistaan julkisessa URL-osoitteessa, jonka voit lisätä minkä tahansa palvelimen authorized_keys-tiedostoon.",
"handleLabel": "Kahva",
"handlePlaceholder": "Alice",
"checking": "Tarkistetaan…",
"available": "Saatavilla",
"taken": "Jo otettu",
"invalidHandle": "Vain pienet kirjaimet, numerot - ja _",
"descriptionLabel": "Kuvaus (valinnainen)",
"descriptionPlaceholder": "Työkannettava ja puhelimen avaimet",
"create": "Luo Termix-tunnus",
"created": "Termix-tunnus luotu",
"createFailed": "Termix-tunnuksen luominen epäonnistui",
"deleteConfirm": "Poistetaanko Termix-tunnuksesi ja kaikki julkaistut avaimet? Määrittämäsi palvelimet säilyttävät avaimet, kunnes poistat ne manuaalisesti.",
"deleted": "Termix-tunnus poistettu",
"deleteFailed": "Termix-tunnuksen poistaminen epäonnistui",
"copyFailed": "Kopiointi epäonnistui",
"resolverUrlLabel": "Julkisen ratkaisijan URL-osoite",
"provisionLabel": "Palvelimen valmistelu",
"publishTitle": "Julkaise julkinen avain",
"generate": "Luo",
"generateTooltip": "Luo Ed25519-avainpari — julkaisee julkisen avaimen ja lataa yksityisen avaimen laitteellesi",
"saveToVault": "Tallenna tunnistetietoihin",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... käyttäjä@isäntä",
"labelPlaceholder": "Tarra (valinnainen)",
"add": "Lisätä",
"importFromCredential": "Tai tuo tallennetuista tunnistetiedoista",
"keyPublished": "Avain julkaistu",
"addKeyFailed": "Avaimen lisääminen epäonnistui",
"generatedSaved": "Avainpari luotiin ja tallennettiin tunnistetietoholviisi. Myös yksityinen avain ladattiin.",
"generatedOnly": "Avainpari luotu — yksityinen avain ladattu (näytetään vain kerran).",
"generateFailed": "Avaimen luominen epäonnistui",
"imported": "Avain tuotu tunnistetiedoista",
"importFailed": "Avaimen tuonti epäonnistui",
"noKeys": "Julkisia avaimia ei ole vielä julkaistu.",
"keysTitle": "Julkaistut avaimet",
"published": "Julkaistu",
"hidden": "Piilotettu",
"keyRemoved": "Avain poistettu",
"removeKeyFailed": "Avaimen poistaminen epäonnistui",
"updateKeyFailed": "Avaimen päivittäminen epäonnistui",
"fromVault": "Tunnistetietosäilöstä",
"linkedToTermixId": "Julkaistu Termix ID:n kautta",
"selectCredential": "Valitse tunnistetiedot...",
"import": "Tuoda",
"caTitle": "Varmentaja",
"caIntro": "Luota tähän palvelimella olevaan varmenteiden myöntäjään, niin se hyväksyy kaikki allekirjoittamasi varmenteet. Kierrätä peruuttaaksesi kaiken kerralla; varmenteet myös vanhenevat itsestään.",
"caEnable": "Ota käyttöön CA",
"caEnabled": "CA käytössä",
"caCreateFailed": "CA:n käyttöönotto epäonnistui",
"caPublicKeyLabel": "CA:n julkinen avain",
"caTrustLabel": "Luota palvelimeen (aja root-käyttäjänä)",
"caRotate": "Kiertää",
"caRotateConfirm": "Kierrätetäänkö varmentajaa? Kaikki sen allekirjoittamat varmenteet lakkaavat olemasta hyväksyttäviä ja ne on myönnettävä uudelleen.",
"caRotated": "Varmentaja vaihdettu – aiemmat varmenteet peruutettu",
"caRotateFailed": "CA:n vaihtaminen epäonnistui",
"caDelete": "Poista CA",
"caDeleteConfirm": "Poistetaanko varmenteen myöntäjä?",
"caDeleted": "CA poistettu",
"caDeleteFailed": "CA:n poistaminen epäonnistui",
"caValidityLabel": "Sertifikaatin voimassaolo (päivää)",
"issueCert": "Todistus",
"issueCertTooltip": "Myönnä tälle avaimelle SSH-varmenne, jonka CA on allekirjoittanut",
"certIssued": "Todistus myönnetty ja ladattu",
"certIssueFailed": "Varmenteen myöntäminen epäonnistui"
},
"credentials": {
"folders": "Kansiot",
"folder": "Kansio",
@@ -144,7 +75,6 @@
"editCredentialAction": "Muokkaa tunnistetietoja",
"failedToCloneCredential": "Tunnistetietojen kloonaaminen epäonnistui",
"failedToDeleteCredential": "Tunnuksen poistaminen epäonnistui",
"idBadge": "Henkilöllisyystodistus",
"keyBadge": "AVAIN",
"passwordBadge": "PWD",
"renameFolder": "Nimeä kansio uudelleen",
@@ -525,7 +455,6 @@
"fileManager": "Tiedostonhallinta",
"serverStats": "Isännän mittarit",
"admin": "Ylläpitäjä",
"termixId": "Henkilöllisyystodistus",
"userProfile": "Käyttäjäprofiili",
"splitScreen": "Jaettu näyttö",
"confirmClose": "Suljetaanko tämä aktiivinen istunto?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Impossible de se connecter",
"reconnect": "Reconnect"
},
"termixId": {
"title": "Termix ID",
"loadFailed": "Échec du chargement de Termix ID",
"claimTitle": "Obtenez votre Termix ID",
"claimIntro": "Choisissez un identifiant unique. Vos clés publiques SSH seront publiées à une URL publique que vous pourrez ajouter au fichier authorized_keys de n'importe quel serveur.",
"handleLabel": "Identifiant",
"handlePlaceholder": "alice",
"checking": "Vérification…",
"available": "Disponible",
"taken": "Déjà pris",
"invalidHandle": "Lettres minuscules, chiffres, - et _ uniquement",
"descriptionLabel": "Description (facultative)",
"descriptionPlaceholder": "Clés pour ordinateur portable pro et téléphone",
"create": "Créer un Termix ID",
"created": "Termix ID créé",
"createFailed": "Échec de la création de Termix ID",
"deleteConfirm": "Supprimer votre Termix ID et toutes les clés publiées ? Les serveurs que vous avez provisionnés conserveront les clés jusqu'à ce que vous les supprimiez manuellement.",
"deleted": "Termix ID supprimé",
"deleteFailed": "Échec de la suppression de Termix ID",
"copyFailed": "Échec de la copie",
"resolverUrlLabel": "URL de résolution publique",
"provisionLabel": "Provisionner un serveur",
"publishTitle": "Publier une clé publique",
"generate": "Générer",
"generateTooltip": "Générer une paire de clés Ed25519 — publie la clé publique et télécharge la clé privée sur votre appareil",
"saveToVault": "Enregistrer dans les identifiants",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "Libellé (facultatif)",
"add": "Ajouter",
"importFromCredential": "Ou importer depuis un identifiant stocké",
"keyPublished": "Clé publiée",
"addKeyFailed": "Échec de l'ajout de la clé",
"generatedSaved": "Paire de clés générée et enregistrée dans votre coffre-fort d'identifiants. Clé privée également téléchargée.",
"generatedOnly": "Paire de clés générée — clé privée téléchargée (affichée une seule fois).",
"generateFailed": "Échec de la génération de la clé",
"imported": "Clé importée depuis l'identifiant",
"importFailed": "Échec de l'importation de la clé",
"noKeys": "Aucune clé publique publiée pour le moment.",
"keysTitle": "Clés publiées",
"published": "Publiée",
"hidden": "Masquée",
"keyRemoved": "Clé supprimée",
"removeKeyFailed": "Échec de la suppression de la clé",
"updateKeyFailed": "Échec de la mise à jour de la clé",
"fromVault": "Du coffre-fort des identifiants",
"linkedToTermixId": "Publiée via Termix ID",
"selectCredential": "Sélectionner un identifiant...",
"import": "Importer",
"caTitle": "Autorité de certification",
"caIntro": "Faites confiance à cette AC sur un serveur et il acceptera tout certificat que vous signez. Renouvelez pour tout révoquer d'un coup ; les certificats expirent également d'eux-mêmes.",
"caEnable": "Activer l'AC",
"caEnabled": "AC activée",
"caCreateFailed": "Échec de l'activation de l'AC",
"caPublicKeyLabel": "Clé publique de l'AC",
"caTrustLabel": "Confiance sur un serveur (exécuter en tant que root)",
"caRotate": "Renouveler",
"caRotateConfirm": "Renouveler l'AC ? Tous les certificats qu'elle a signés ne seront plus acceptés et devront être réémis.",
"caRotated": "AC renouvelée — certificats précédents révoqués",
"caRotateFailed": "Échec du renouvellement de l'AC",
"caDelete": "Supprimer l'AC",
"caDeleteConfirm": "Supprimer l'autorité de certification ?",
"caDeleted": "AC supprimée",
"caDeleteFailed": "Échec de la suppression de l'AC",
"caValidityLabel": "Validité du certificat (jours)",
"issueCert": "Certificat",
"issueCertTooltip": "Émettre un certificat SSH pour cette clé, signé par votre AC",
"certIssued": "Certificat émis et téléchargé",
"certIssueFailed": "Échec de l'émission du certificat"
},
"credentials": {
"folders": "Dossiers",
"folder": "Dossier",
@@ -144,7 +75,6 @@
"editCredentialAction": "Modifier les identifiants",
"failedToCloneCredential": "Échec du clonage des identifiants",
"failedToDeleteCredential": "Échec de la suppression des identifiants",
"idBadge": "IDENTIFIANT",
"keyBadge": "CLÉ",
"passwordBadge": "PWD",
"renameFolder": "Renommer le dossier",
@@ -525,7 +455,6 @@
"fileManager": "Gestionnaire de fichiers",
"serverStats": "Métriques de l'hôte",
"admin": "Administration",
"termixId": "ID",
"userProfile": "Profil utilisateur",
"splitScreen": "Diviser l'écran",
"confirmClose": "Fermer cette session active ?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "לא ניתן להתחבר",
"reconnect": "התחבר מחדש"
},
"termixId": {
"title": "מזהה טרמיקס",
"loadFailed": "נכשלה טעינת מזהה Termix",
"claimTitle": "תבע את מזהה ה-Termix שלך",
"claimIntro": "בחר שם משתמש ייחודי. המפתחות הציבוריים של SSH שלך יפורסמו בכתובת URL ציבורית שתוכל להוסיף לקובץ authorized_keys של כל שרת.",
"handleLabel": "יָדִית",
"handlePlaceholder": "אליס",
"checking": "בודק…",
"available": "זָמִין",
"taken": "כבר תפוס",
"invalidHandle": "אותיות קטנות, מספרים, - ו-_ בלבד",
"descriptionLabel": "תיאור (אופציונלי)",
"descriptionPlaceholder": "מפתחות למחשב נייד ולטלפון עבודה",
"create": "צור מזהה טרמיקס",
"created": "מזהה טרמיקס נוצר",
"createFailed": "נכשלה יצירת מזהה Termix",
"deleteConfirm": "למחוק את מזהה ה-Termix שלך ואת כל המפתחות שפורסמו? השרתים שהקמת ישמרו את המפתחות עד שתסיר אותם ידנית.",
"deleted": "מזהה טרמיקס נמחק",
"deleteFailed": "נכשלה מחיקת מזהה Termix",
"copyFailed": "ההעתקה נכשלה",
"resolverUrlLabel": "כתובת URL ציבורית לפתרון",
"provisionLabel": "הקצאת שרת",
"publishTitle": "פרסום מפתח ציבורי",
"generate": "לִיצוֹר",
"generateTooltip": "צור זוג מפתחות Ed25519 - מפרסם את המפתח הציבורי ומוריד את המפתח הפרטי למכשיר שלך",
"saveToVault": "שמור אל אישורים",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... משתמש@מארח",
"labelPlaceholder": "תווית (אופציונלי)",
"add": "לְהוֹסִיף",
"importFromCredential": "או ייבוא מאישור מאוחסן",
"keyPublished": "מפתח פורסם",
"addKeyFailed": "הוספת המפתח נכשלה",
"generatedSaved": "זוג המפתחות נוצר ונשמר בכספת האישורים שלך. המפתח הפרטי גם הורד.",
"generatedOnly": "נוצר זוג מפתחות - הורד מפתח פרטי (מוצג פעם אחת בלבד).",
"generateFailed": "יצירת המפתח נכשלה",
"imported": "מפתח יובא מהאישור",
"importFailed": "ייבוא המפתח נכשל",
"noKeys": "עדיין לא פורסמו מפתחות ציבוריים.",
"keysTitle": "מפתחות שפורסמו",
"published": "פורסם",
"hidden": "מוּסתָר",
"keyRemoved": "המפתח הוסר",
"removeKeyFailed": "הסרת המפתח נכשלה",
"updateKeyFailed": "עדכון המפתח נכשל",
"fromVault": "מכספת האישורים",
"linkedToTermixId": "פורסם באמצעות Termix ID",
"selectCredential": "בחר אישור...",
"import": "יְבוּא",
"caTitle": "רשות אישורים",
"caIntro": "בטח באישור CA זה בשרת והוא יקבל כל אישור שאתה חותם עליו. סובב כדי לבטל הכל בבת אחת; אישורים גם פגים מעצמם.",
"caEnable": "הפעלת CA",
"caEnabled": "אישור אישורים (CA) מופעל",
"caCreateFailed": "נכשל בהפעלת CA",
"caPublicKeyLabel": "מפתח ציבורי של CA",
"caTrustLabel": "אמון בשרת (הפעלה כ-root)",
"caRotate": "לְסוֹבֵב",
"caRotateConfirm": "לסובב את רשות האישור? כל אישור שהיא חתמה עליו יפסיק להתקבל ויהיה צורך להנפיק אותו מחדש.",
"caRotated": "רשות אישורים (CA) סיבובית - אישורים קודמים בוטלו",
"caRotateFailed": "נכשל בסבב CA",
"caDelete": "הסר את CA",
"caDeleteConfirm": "להסיר את רשות האישורים?",
"caDeleted": "רשות האישור הוסרה",
"caDeleteFailed": "נכשל בהסרת CA",
"caValidityLabel": "תוקף האישור (ימים)",
"issueCert": "תְעוּדָה",
"issueCertTooltip": "הנפק אישור SSH עבור מפתח זה, חתום על ידי רשות האישור שלך",
"certIssued": "תעודה הונפקה והורדה",
"certIssueFailed": "הנפקת האישור נכשלה"
},
"credentials": {
"folders": "תיקיות",
"folder": "תיקייה",
@@ -144,7 +75,6 @@
"editCredentialAction": "עריכת אישורים",
"failedToCloneCredential": "שכפול האישורים נכשל",
"failedToDeleteCredential": "מחיקת האישורים נכשלה",
"idBadge": "תְעוּדַת זֶהוּת",
"keyBadge": "מַפְתֵחַ",
"passwordBadge": "חוסר יכולת תנועה",
"renameFolder": "שינוי שם התיקייה",
@@ -525,7 +455,6 @@
"fileManager": "מנהל הקבצים",
"serverStats": "מדדי מארח",
"admin": "מנהל",
"termixId": "תְעוּדַת זֶהוּת",
"userProfile": "פרופיל משתמש",
"splitScreen": "מסך מפוצל",
"confirmClose": "לסגור את ההפעלה הפעילה הזו?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "कनेक्ट करने में असमर्थ",
"reconnect": "रिकनेक्ट"
},
"termixId": {
"title": "Termix ID",
"loadFailed": "Termix ID लोड करने में विफल",
"claimTitle": "अपना Termix ID क्लेम करें",
"claimIntro": "एक अनूठा हैंडल चुनें। आपकी SSH सार्वजनिक कुंजियाँ एक सार्वजनिक URL पर प्रकाशित की जाएंगी जिसे आप किसी भी सर्वर की authorized_keys फ़ाइल में जोड़ सकते हैं।",
"handleLabel": "हैंडल",
"handlePlaceholder": "alice",
"checking": "जाँच हो रही है…",
"available": "उपलब्ध",
"taken": "पहले से उपयोग में",
"invalidHandle": "केवल छोटे अक्षर, अंक, - और _",
"descriptionLabel": "विवरण (वैकल्पिक)",
"descriptionPlaceholder": "कार्य लैपटॉप और फ़ोन कुंजियाँ",
"create": "Termix ID बनाएँ",
"created": "Termix ID बन गई",
"createFailed": "Termix ID बनाने में विफल",
"deleteConfirm": "अपनी Termix ID और सभी प्रकाशित कुंजियाँ हटाएँ? जो सर्वर आपने प्रोविज़न किए हैं वे कुंजियाँ तब तक रखेंगे जब तक आप उन्हें मैन्युअल रूप से नहीं हटाते।",
"deleted": "Termix ID हटाई गई",
"deleteFailed": "Termix ID हटाने में विफल",
"copyFailed": "कॉपी करने में विफल",
"resolverUrlLabel": "सार्वजनिक रिज़ॉल्वर URL",
"provisionLabel": "सर्वर प्रोविज़न करें",
"publishTitle": "सार्वजनिक कुंजी प्रकाशित करें",
"generate": "जनरेट करें",
"generateTooltip": "Ed25519 कुंजी युग्म जनरेट करता है — सार्वजनिक कुंजी प्रकाशित करता है और निजी कुंजी आपके डिवाइस पर डाउनलोड करता है",
"saveToVault": "क्रेडेंशियल में सहेजें",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "लेबल (वैकल्पिक)",
"add": "जोड़ें",
"importFromCredential": "या संग्रहीत क्रेडेंशियल से आयात करें",
"keyPublished": "कुंजी प्रकाशित",
"addKeyFailed": "कुंजी जोड़ने में विफल",
"generatedSaved": "कुंजी युग्म जनरेट हुआ और आपके क्रेडेंशियल वॉल्ट में सहेजा गया। निजी कुंजी भी डाउनलोड की गई।",
"generatedOnly": "कुंजी युग्म जनरेट हुआ — निजी कुंजी डाउनलोड की गई (केवल एक बार दिखाई गई)।",
"generateFailed": "कुंजी जनरेट करने में विफल",
"imported": "क्रेडेंशियल से कुंजी आयात की गई",
"importFailed": "कुंजी आयात करने में विफल",
"noKeys": "अभी तक कोई सार्वजनिक कुंजी प्रकाशित नहीं हुई है।",
"keysTitle": "प्रकाशित कुंजियाँ",
"published": "प्रकाशित",
"hidden": "छिपी हुई",
"keyRemoved": "कुंजी हटाई गई",
"removeKeyFailed": "कुंजी हटाने में विफल",
"updateKeyFailed": "कुंजी अपडेट करने में विफल",
"fromVault": "क्रेडेंशियल वॉल्ट से",
"linkedToTermixId": "Termix ID के माध्यम से प्रकाशित",
"selectCredential": "एक क्रेडेंशियल चुनें...",
"import": "आयात करें",
"caTitle": "प्रमाणपत्र प्राधिकरण",
"caIntro": "सर्वर पर इस CA पर भरोसा करें और यह आपके द्वारा हस्ताक्षरित किसी भी प्रमाणपत्र को स्वीकार करता है। एक ही बार में सब कुछ निरस्त करने के लिए CA को रोटेट करें; प्रमाणपत्र अपने आप भी समाप्त हो जाते हैं।",
"caEnable": "CA सक्षम करें",
"caEnabled": "CA सक्षम",
"caCreateFailed": "CA सक्षम करने में विफल",
"caPublicKeyLabel": "CA सार्वजनिक कुंजी",
"caTrustLabel": "सर्वर पर भरोसा करें (रूट के रूप में चलाएँ)",
"caRotate": "रोटेट करें",
"caRotateConfirm": "CA को रोटेट करें? इसके द्वारा हस्ताक्षरित प्रत्येक प्रमाणपत्र स्वीकार करना बंद कर दिया जाएगा और उन्हें फिर से जारी करना होगा।",
"caRotated": "CA रोटेट किया गया — पिछले प्रमाणपत्र निरस्त",
"caRotateFailed": "CA रोटेट करने में विफल",
"caDelete": "CA हटाएँ",
"caDeleteConfirm": "प्रमाणपत्र प्राधिकरण हटाएँ?",
"caDeleted": "CA हटाया गया",
"caDeleteFailed": "CA हटाने में विफल",
"caValidityLabel": "प्रमाणपत्र वैधता (दिनों में)",
"issueCert": "प्रमाणपत्र",
"issueCertTooltip": "इस कुंजी के लिए एक SSH प्रमाणपत्र जारी करें, आपके CA द्वारा हस्ताक्षरित",
"certIssued": "प्रमाणपत्र जारी और डाउनलोड किया गया",
"certIssueFailed": "प्रमाणपत्र जारी करने में विफल"
},
"credentials": {
"folders": "फ़ोल्डर",
"folder": "फ़ोल्डर",
@@ -144,7 +75,6 @@
"editCredentialAction": "क्रेडेंशियल संपादित करें",
"failedToCloneCredential": "क्रेडेंशियल क्लोन करने में विफल",
"failedToDeleteCredential": "क्रेडेंशियल हटाने में विफल",
"idBadge": "पहचान",
"keyBadge": "चाबी",
"passwordBadge": "लोक निर्माण विभाग",
"renameFolder": "फ़ोल्डर का नाम बदलें",
@@ -525,7 +455,6 @@
"fileManager": "फ़ाइल प्रबंधक",
"serverStats": "होस्ट मेट्रिक्स",
"admin": "एडमिन",
"termixId": "आईडी",
"userProfile": "उपयोगकर्ता प्रोफ़ाइल",
"splitScreen": "स्प्लिट स्क्रीन",
"confirmClose": "इस सक्रिय सत्र को बंद करें?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Sikertelen csatlakozás",
"reconnect": "Újracsatlakozás"
},
"termixId": {
"title": "Termix azonosító",
"loadFailed": "Nem sikerült betölteni a Termix azonosítót",
"claimTitle": "Igényelje Termix azonosítóját",
"claimIntro": "Válassz egyedi azonosítót. Az SSH nyilvános kulcsaid egy nyilvános URL-címen lesznek közzétéve, amelyet hozzáadhatsz bármelyik szerver authorized_keys fájljához.",
"handleLabel": "Fogantyú",
"handlePlaceholder": "Alice",
"checking": "… ellenőrzése",
"available": "Elérhető",
"taken": "Már foglalt",
"invalidHandle": "Csak kisbetűk, számok - és _",
"descriptionLabel": "Leírás (opcionális)",
"descriptionPlaceholder": "Munkahelyi laptop és telefonkulcsok",
"create": "Termix azonosító létrehozása",
"created": "Termix azonosító létrehozva",
"createFailed": "Nem sikerült létrehozni a Termix azonosítót",
"deleteConfirm": "Törli a Termix azonosítóját és az összes közzétett kulcsát? A kiépített szerverek mindaddig megőrzik a kulcsokat, amíg manuálisan el nem távolítja azokat.",
"deleted": "Termix azonosító törölve",
"deleteFailed": "Nem sikerült törölni a Termix azonosítót",
"copyFailed": "Másolás sikertelen",
"resolverUrlLabel": "Nyilvános feloldó URL",
"provisionLabel": "Szerver kiépítése",
"publishTitle": "Nyilvános kulcs közzététele",
"generate": "Generálás",
"generateTooltip": "Ed25519 kulcspár generálása — közzéteszi a nyilvános kulcsot és letölti a privát kulcsot az eszközére",
"saveToVault": "Hitelesítő adatok mentése",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... felhasználó@hoszt",
"labelPlaceholder": "Címke (opcionális)",
"add": "Hozzáadás",
"importFromCredential": "Vagy importáljon egy tárolt hitelesítő adatból",
"keyPublished": "Kulcs közzétéve",
"addKeyFailed": "Nem sikerült hozzáadni a kulcsot",
"generatedSaved": "Kulcspár generálva és mentve a hitelesítő adatok tárolására szolgáló tárolóba. A privát kulcs is letöltődött.",
"generatedOnly": "Kulcspár generálva — privát kulcs letöltve (csak egyszer jelenik meg).",
"generateFailed": "Nem sikerült kulcsot generálni",
"imported": "Kulcs importálva a hitelesítő adatokból",
"importFailed": "Nem sikerült importálni a kulcsot",
"noKeys": "Még nincsenek közzétett nyilvános kulcsok.",
"keysTitle": "Közzétett kulcsok",
"published": "Közzétett",
"hidden": "Rejtett",
"keyRemoved": "Kulcs eltávolítva",
"removeKeyFailed": "Nem sikerült eltávolítani a kulcsot",
"updateKeyFailed": "Nem sikerült frissíteni a kulcsot",
"fromVault": "A hitelesítő adatok tárolójából",
"linkedToTermixId": "Közzétéve Termix ID-n keresztül",
"selectCredential": "Válasszon hitelesítő adatot...",
"import": "Importálás",
"caTitle": "Tanúsítványkibocsátó",
"caIntro": "Bízzon meg ebben a hitelesítésszolgáltatóban egy szerveren, és az elfogad minden aláírt tanúsítványt. Forgassa el, hogy mindent egyszerre visszavonjon; a tanúsítványok maguktól lejárnak.",
"caEnable": "CA engedélyezése",
"caEnabled": "CA engedélyezve",
"caCreateFailed": "Nem sikerült engedélyezni a CA-t",
"caPublicKeyLabel": "CA nyilvános kulcs",
"caTrustLabel": "Megbízhatóság egy szerveren (root felhasználóként futtatva)",
"caRotate": "Forog",
"caRotateConfirm": "Lecseréli a hitelesítésszolgáltatót? Az általa aláírt tanúsítványok elfogadása megszűnik, és újra kell kiállítani őket.",
"caRotated": "CA lecserélve – korábbi tanúsítványok visszavonva",
"caRotateFailed": "Nem sikerült elforgatni a CA-t",
"caDelete": "CA eltávolítása",
"caDeleteConfirm": "Eltávolítja a hitelesítésszolgáltatót?",
"caDeleted": "CA eltávolítva",
"caDeleteFailed": "Nem sikerült eltávolítani a CA-t",
"caValidityLabel": "Tanúsítvány érvényessége (nap)",
"issueCert": "Bizonyítvány",
"issueCertTooltip": "Állítson ki egy SSH tanúsítványt ehhez a kulcshoz, amelyet a hitelesítésszolgáltató aláírt",
"certIssued": "Kiállított és letöltött tanúsítvány",
"certIssueFailed": "Nem sikerült kiállítani a tanúsítványt"
},
"credentials": {
"folders": "Mappák",
"folder": "Mappa",
@@ -144,7 +75,6 @@
"editCredentialAction": "Hitelesítő adatok szerkesztése",
"failedToCloneCredential": "Nem sikerült klónozni a hitelesítő adatokat",
"failedToDeleteCredential": "Nem sikerült törölni a hitelesítő adatokat",
"idBadge": "azonosító",
"keyBadge": "KULCSFONTOSSÁGÚ",
"passwordBadge": "fogyatékkal élők",
"renameFolder": "Mappa átnevezése",
@@ -525,7 +455,6 @@
"fileManager": "Fájlkezelő",
"serverStats": "Gazdagép-metrikák",
"admin": "Adminisztrátor",
"termixId": "azonosító",
"userProfile": "Felhasználói profil",
"splitScreen": "Osztott képernyő",
"confirmClose": "Bezárja ezt az aktív munkamenetet?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Tidak dapat terhubung",
"reconnect": "Terhubung kembali"
},
"termixId": {
"title": "ID Termix",
"loadFailed": "Gagal memuat ID Termix",
"claimTitle": "Klaim ID Termix Anda",
"claimIntro": "Pilih nama pengguna yang unik. Kunci publik SSH Anda akan dipublikasikan di URL publik yang dapat Anda tambahkan ke file authorized_keys server mana pun.",
"handleLabel": "Menangani",
"handlePlaceholder": "Alice",
"checking": "Memeriksa…",
"available": "Tersedia",
"taken": "Sudah diambil",
"invalidHandle": "Hanya huruf kecil, angka, - dan _",
"descriptionLabel": "Deskripsi (opsional)",
"descriptionPlaceholder": "Kunci laptop dan telepon kerja",
"create": "Buat ID Termix",
"created": "ID Termix telah dibuat",
"createFailed": "Gagal membuat ID Termix",
"deleteConfirm": "Apakah Anda menghapus ID Termix dan semua kunci yang dipublikasikan? Server yang Anda sediakan akan tetap menyimpan kunci tersebut hingga Anda menghapusnya secara manual.",
"deleted": "ID Termix dihapus",
"deleteFailed": "Gagal menghapus ID Termix",
"copyFailed": "Penyalinan gagal",
"resolverUrlLabel": "URL resolver publik",
"provisionLabel": "Menyediakan server",
"publishTitle": "Publikasikan kunci publik",
"generate": "Menghasilkan",
"generateTooltip": "Hasilkan pasangan kunci Ed25519 — publikasikan kunci publik dan unduh kunci pribadi ke perangkat Anda.",
"saveToVault": "Simpan ke Kredensial",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "Label (opsional)",
"add": "Menambahkan",
"importFromCredential": "Atau impor dari kredensial yang tersimpan.",
"keyPublished": "Kunci telah diterbitkan",
"addKeyFailed": "Gagal menambahkan kunci",
"generatedSaved": "Pasangan kunci telah dibuat dan disimpan ke brankas kredensial Anda. Kunci pribadi juga telah diunduh.",
"generatedOnly": "Pasangan kunci berhasil dibuat — kunci privat telah diunduh (hanya ditampilkan sekali).",
"generateFailed": "Gagal menghasilkan kunci",
"imported": "Kunci diimpor dari kredensial",
"importFailed": "Gagal mengimpor kunci",
"noKeys": "Belum ada kunci publik yang dipublikasikan.",
"keysTitle": "Kunci yang diterbitkan",
"published": "Diterbitkan",
"hidden": "Tersembunyi",
"keyRemoved": "Kunci dilepas",
"removeKeyFailed": "Gagal melepas kunci",
"updateKeyFailed": "Gagal memperbarui kunci",
"fromVault": "Dari brankas kredensial",
"linkedToTermixId": "Diterbitkan melalui ID Termix",
"selectCredential": "Pilih kredensial...",
"import": "Impor",
"caTitle": "Lembaga sertifikasi",
"caIntro": "Percayai CA ini pada server dan CA ini akan menerima sertifikat apa pun yang Anda tandatangani. Lakukan rotasi untuk mencabut semuanya sekaligus; sertifikat juga akan kedaluwarsa dengan sendirinya.",
"caEnable": "Aktifkan CA",
"caEnabled": "CA diaktifkan",
"caCreateFailed": "Gagal mengaktifkan CA",
"caPublicKeyLabel": "Kunci publik CA",
"caTrustLabel": "Kepercayaan pada server (jalankan sebagai root)",
"caRotate": "Memutar",
"caRotateConfirm": "Apakah CA (Certificate Authority) perlu diganti? Setiap sertifikat yang telah ditandatanganinya akan berhenti diterima dan harus diterbitkan ulang.",
"caRotated": "CA dirotasi — sertifikat sebelumnya dicabut",
"caRotateFailed": "Gagal memutar CA",
"caDelete": "Hapus CA",
"caDeleteConfirm": "Hapus otoritas sertifikat?",
"caDeleted": "CA dihapus",
"caDeleteFailed": "Gagal menghapus CA",
"caValidityLabel": "Masa berlaku sertifikat (hari)",
"issueCert": "Sertifikat",
"issueCertTooltip": "Terbitkan sertifikat SSH untuk kunci ini, yang ditandatangani oleh CA Anda.",
"certIssued": "Sertifikat telah diterbitkan dan diunduh.",
"certIssueFailed": "Gagal menerbitkan sertifikat"
},
"credentials": {
"folders": "Folder",
"folder": "Map",
@@ -144,7 +75,6 @@
"editCredentialAction": "Edit kredensial",
"failedToCloneCredential": "Gagal mengkloning kredensial",
"failedToDeleteCredential": "Gagal menghapus kredensial",
"idBadge": "PENGENAL",
"keyBadge": "KUNCI",
"passwordBadge": "Penyandang Disabilitas",
"renameFolder": "Ganti nama folder",
@@ -525,7 +455,6 @@
"fileManager": "Pengelola File",
"serverStats": "Metrik Host",
"admin": "Admin",
"termixId": "PENGENAL",
"userProfile": "Profil Pengguna",
"splitScreen": "Layar Terpisah",
"confirmClose": "Tutup sesi aktif ini?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Impossibile connettersi",
"reconnect": "Ricollega"
},
"termixId": {
"title": "Termix ID",
"loadFailed": "Caricamento Termix ID fallito",
"claimTitle": "Rivendica il tuo Termix ID",
"claimIntro": "Scegli un identificativo univoco. Le tue chiavi pubbliche SSH saranno pubblicate a un URL pubblico che puoi aggiungere al file authorized_keys di qualsiasi server.",
"handleLabel": "Identificativo",
"handlePlaceholder": "alice",
"checking": "Controllo…",
"available": "Disponibile",
"taken": "Già in uso",
"invalidHandle": "Solo lettere minuscole, numeri, - e _",
"descriptionLabel": "Descrizione (facoltativa)",
"descriptionPlaceholder": "Chiavi per portatile da lavoro e telefono",
"create": "Crea Termix ID",
"created": "Termix ID creato",
"createFailed": "Creazione Termix ID fallita",
"deleteConfirm": "Eliminare il tuo Termix ID e tutte le chiavi pubblicate? I server configurati manterranno le chiavi finché non le rimuovi manualmente.",
"deleted": "Termix ID eliminato",
"deleteFailed": "Eliminazione Termix ID fallita",
"copyFailed": "Copia fallita",
"resolverUrlLabel": "URL del resolver pubblico",
"provisionLabel": "Provisiona un server",
"publishTitle": "Pubblica una chiave pubblica",
"generate": "Genera",
"generateTooltip": "Genera una coppia di chiavi Ed25519 — pubblica la chiave pubblica e scarica la chiave privata sul tuo dispositivo",
"saveToVault": "Salva nelle credenziali",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "Etichetta (facoltativa)",
"add": "Aggiungi",
"importFromCredential": "Oppure importa da una credenziale memorizzata",
"keyPublished": "Chiave pubblicata",
"addKeyFailed": "Aggiunta chiave fallita",
"generatedSaved": "Coppia di chiavi generata e salvata nell'archivio delle credenziali. Anche la chiave privata è stata scaricata.",
"generatedOnly": "Coppia di chiavi generata — chiave privata scaricata (visualizzata una sola volta).",
"generateFailed": "Generazione chiave fallita",
"imported": "Chiave importata dalla credenziale",
"importFailed": "Importazione chiave non riuscita",
"noKeys": "Nessuna chiave pubblica ancora pubblicata.",
"keysTitle": "Chiavi pubblicate",
"published": "Pubblicata",
"hidden": "Nascosta",
"keyRemoved": "Chiave rimossa",
"removeKeyFailed": "Rimozione chiave non riuscita",
"updateKeyFailed": "Aggiornamento chiave non riuscito",
"fromVault": "Dal vault delle credenziali",
"linkedToTermixId": "Pubblicata tramite Termix ID",
"selectCredential": "Seleziona una credenziale...",
"import": "Importa",
"caTitle": "Autorità di certificazione",
"caIntro": "Rendi attendibile questa CA su un server e questo accetterà qualsiasi certificato da te firmato. Ruota per revocare tutto in una volta; i certificati scadono anche automaticamente.",
"caEnable": "Abilita CA",
"caEnabled": "CA abilitata",
"caCreateFailed": "Abilitazione CA non riuscita",
"caPublicKeyLabel": "Chiave pubblica CA",
"caTrustLabel": "Rendi attendibile sul server (esegui come root)",
"caRotate": "Ruota",
"caRotateConfirm": "Ruotare la CA? Tutti i certificati da essa firmati smetteranno di essere accettati e dovranno essere riemessi.",
"caRotated": "CA ruotata — certificati precedenti revocati",
"caRotateFailed": "Rotazione CA non riuscita",
"caDelete": "Rimuovi CA",
"caDeleteConfirm": "Rimuovere l'autorità di certificazione?",
"caDeleted": "CA rimossa",
"caDeleteFailed": "Rimozione CA non riuscita",
"caValidityLabel": "Validità certificato (giorni)",
"issueCert": "Certificato",
"issueCertTooltip": "Emetti un certificato SSH per questa chiave, firmato dalla tua CA",
"certIssued": "Certificato emesso e scaricato",
"certIssueFailed": "Emissione certificato non riuscita"
},
"credentials": {
"folders": "Cartelle",
"folder": "Cartella",
@@ -144,7 +75,6 @@
"editCredentialAction": "Modifica credenziali",
"failedToCloneCredential": "Impossibile clonare le credenziali",
"failedToDeleteCredential": "Impossibile eliminare le credenziali",
"idBadge": "ID",
"keyBadge": "CHIAVE",
"passwordBadge": "PWD",
"renameFolder": "Rinominare la cartella",
@@ -525,7 +455,6 @@
"fileManager": "Gestione file",
"serverStats": "Metriche host",
"admin": "Admin",
"termixId": "ID",
"userProfile": "Profilo utente",
"splitScreen": "Dividi schermo",
"confirmClose": "Chiudere questa sessione attiva?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "接続できません",
"reconnect": "再接続"
},
"termixId": {
"title": "Termix ID",
"loadFailed": "Termix IDのロードに失敗しました",
"claimTitle": "Termix IDを取得",
"claimIntro": "一意のハンドルを選択します。SSH公開鍵が公開URLで公開され、任意のサーバーのauthorized_keysファイルに追加できます。",
"handleLabel": "ハンドル",
"handlePlaceholder": "alice",
"checking": "確認中…",
"available": "利用可能",
"taken": "既に使用されています",
"invalidHandle": "英小文字、数字、ハイフン(-)、アンダースコア(_)のみ使用できます",
"descriptionLabel": "説明(任意)",
"descriptionPlaceholder": "仕事用ラップトップとスマートフォンのキー",
"create": "Termix IDを作成",
"created": "Termix IDが作成されました",
"createFailed": "Termix IDの作成に失敗しました",
"deleteConfirm": "Termix IDと公開されたすべてのキーを削除しますか?プロビジョニングしたサーバーでは、手動で削除するまでキーが保持されます。",
"deleted": "Termix IDが削除されました",
"deleteFailed": "Termix IDの削除に失敗しました",
"copyFailed": "コピーに失敗しました",
"resolverUrlLabel": "公開リゾルバーURL",
"provisionLabel": "サーバーをプロビジョニング",
"publishTitle": "公開鍵を公開",
"generate": "生成",
"generateTooltip": "Ed25519キーペアを生成 — 公開鍵を公開し、秘密鍵をデバイスにダウンロードします",
"saveToVault": "認証情報に保存",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "ラベル(任意)",
"add": "追加",
"importFromCredential": "または保存済みの認証情報からインポート",
"keyPublished": "キーが公開されました",
"addKeyFailed": "キーの追加に失敗しました",
"generatedSaved": "キーペアが生成され、認証情報ボールトに保存されました。秘密鍵もダウンロードされました。",
"generatedOnly": "キーペアが生成されました — 秘密鍵がダウンロードされました(一度だけ表示されます)。",
"generateFailed": "キーの生成に失敗しました",
"imported": "キーが認証情報からインポートされました",
"importFailed": "鍵のインポートに失敗しました",
"noKeys": "まだ公開鍵は公開されていません。",
"keysTitle": "公開鍵",
"published": "公開済み",
"hidden": "非表示",
"keyRemoved": "鍵が削除されました",
"removeKeyFailed": "鍵の削除に失敗しました",
"updateKeyFailed": "鍵の更新に失敗しました",
"fromVault": "認証情報ボールトから",
"linkedToTermixId": "Termix ID 経由で公開",
"selectCredential": "認証情報を選択...",
"import": "インポート",
"caTitle": "認証局",
"caIntro": "このCAをサーバーで信頼すると、署名した証明書が受け入れられます。ローテーションで一度にすべてを失効させることができ、証明書自体も有効期限が切れます。",
"caEnable": "CA を有効化",
"caEnabled": "CA 有効",
"caCreateFailed": "CA の有効化に失敗しました",
"caPublicKeyLabel": "CA 公開鍵",
"caTrustLabel": "サーバーで信頼する(root で実行)",
"caRotate": "ローテーション",
"caRotateConfirm": "CA をローテーションしますか?署名されたすべての証明書の受け入れが停止され、再発行が必要になります。",
"caRotated": "CA がローテーションされました — 以前の証明書は失効しました",
"caRotateFailed": "CA のローテーションに失敗しました",
"caDelete": "CA を削除",
"caDeleteConfirm": "認証局を削除しますか?",
"caDeleted": "CA が削除されました",
"caDeleteFailed": "CA の削除に失敗しました",
"caValidityLabel": "証明書の有効期間(日)",
"issueCert": "証明書",
"issueCertTooltip": "この鍵に対して、CA が署名した SSH 証明書を発行します",
"certIssued": "証明書が発行され、ダウンロードされました",
"certIssueFailed": "証明書の発行に失敗しました"
},
"credentials": {
"folders": "フォルダー",
"folder": "フォルダー",
@@ -144,7 +75,6 @@
"editCredentialAction": "認証情報を編集する",
"failedToCloneCredential": "認証情報の複製に失敗しました",
"failedToDeleteCredential": "認証情報の削除に失敗しました",
"idBadge": "ID",
"keyBadge": "鍵",
"passwordBadge": "障害者",
"renameFolder": "フォルダの名前を変更する",
@@ -525,7 +455,6 @@
"fileManager": "ファイルマネージャー",
"serverStats": "ホストメトリクス",
"admin": "管理",
"termixId": "ID",
"userProfile": "ユーザープロファイル",
"splitScreen": "画面分割",
"confirmClose": "このアクティブセッションを閉じますか?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "연결할 수 없습니다",
"reconnect": "다시 연결"
},
"termixId": {
"title": "Termix ID",
"loadFailed": "Termix ID 로드 실패",
"claimTitle": "Termix ID 등록",
"claimIntro": "고유한 핸들을 선택하세요. SSH 공개 키는 공개 URL에 게시되어 어느 서버의 authorized_keys 파일에든 추가할 수 있습니다.",
"handleLabel": "핸들",
"handlePlaceholder": "alice",
"checking": "확인 중…",
"available": "사용 가능",
"taken": "이미 사용 중",
"invalidHandle": "소문자, 숫자, - 및 _만 사용",
"descriptionLabel": "설명 (선택 사항)",
"descriptionPlaceholder": "업무용 노트북 및 휴대폰 키",
"create": "Termix ID 만들기",
"created": "Termix ID 생성됨",
"createFailed": "Termix ID 생성 실패",
"deleteConfirm": "Termix ID와 게시된 모든 키를 삭제하시겠습니까? 프로비저닝된 서버는 수동으로 제거할 때까지 키를 유지합니다.",
"deleted": "Termix ID 삭제됨",
"deleteFailed": "Termix ID 삭제 실패",
"copyFailed": "복사 실패",
"resolverUrlLabel": "공개 리졸버 URL",
"provisionLabel": "서버 프로비저닝",
"publishTitle": "공개 키 게시",
"generate": "생성",
"generateTooltip": "Ed25519 키 쌍 생성 — 공개 키를 게시하고 개인 키를 기기에 다운로드합니다",
"saveToVault": "자격 증명에 저장",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "레이블 (선택 사항)",
"add": "추가",
"importFromCredential": "또는 저장된 자격 증명에서 가져오기",
"keyPublished": "키 게시됨",
"addKeyFailed": "키 추가 실패",
"generatedSaved": "키 쌍이 생성되어 자격 증명 보관함에 저장되었습니다. 개인 키도 다운로드되었습니다.",
"generatedOnly": "키 쌍 생성됨 — 개인 키가 다운로드되었습니다 (한 번만 표시됨).",
"generateFailed": "키 생성 실패",
"imported": "자격 증명에서 키를 가져왔습니다",
"importFailed": "키 가져오기 실패",
"noKeys": "아직 게시된 공개 키가 없습니다.",
"keysTitle": "게시된 키",
"published": "게시됨",
"hidden": "숨김",
"keyRemoved": "키 제거됨",
"removeKeyFailed": "키 제거 실패",
"updateKeyFailed": "키 업데이트 실패",
"fromVault": "자격 증명 보관소에서",
"linkedToTermixId": "Termix ID를 통해 게시됨",
"selectCredential": "자격 증명 선택...",
"import": "가져오기",
"caTitle": "인증 기관",
"caIntro": "서버에서 이 CA를 신뢰하면 서명한 모든 인증서가 수락됩니다. 로테이션하면 모든 인증서가 즉시 폐기되며, 인증서는 자체적으로도 만료됩니다.",
"caEnable": "CA 활성화",
"caEnabled": "CA 활성화됨",
"caCreateFailed": "CA 활성화 실패",
"caPublicKeyLabel": "CA 공개 키",
"caTrustLabel": "서버에서 신뢰 (루트로 실행)",
"caRotate": "로테이션",
"caRotateConfirm": "CA를 로테이션하시겠습니까? 서명한 모든 인증서가 수락되지 않으며 다시 발급해야 합니다.",
"caRotated": "CA 로테이션됨 — 이전 인증서 폐기됨",
"caRotateFailed": "CA 로테이션 실패",
"caDelete": "CA 제거",
"caDeleteConfirm": "인증 기관을 제거하시겠습니까?",
"caDeleted": "CA 제거됨",
"caDeleteFailed": "CA 제거 실패",
"caValidityLabel": "인증서 유효 기간(일)",
"issueCert": "인증서",
"issueCertTooltip": "이 키에 대해 CA가 서명한 SSH 인증서를 발급합니다.",
"certIssued": "인증서 발급 및 다운로드 완료",
"certIssueFailed": "인증서 발급 실패"
},
"credentials": {
"folders": "폴더",
"folder": "폴더",
@@ -144,7 +75,6 @@
"editCredentialAction": "자격 증명 수정",
"failedToCloneCredential": "자격 증명 복제에 실패했습니다.",
"failedToDeleteCredential": "자격 증명 삭제에 실패했습니다.",
"idBadge": "ID",
"keyBadge": "열쇠",
"passwordBadge": "장애인",
"renameFolder": "폴더 이름 변경",
@@ -525,7 +455,6 @@
"fileManager": "파일 관리자",
"serverStats": "호스트 메트릭",
"admin": "관리자",
"termixId": "ID",
"userProfile": "사용자 프로필",
"splitScreen": "화면 분할",
"confirmClose": "이 활성 세션을 닫으시겠습니까?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Kan geen verbinding maken",
"reconnect": "Opnieuw verbinding maken"
},
"termixId": {
"title": "Termix ID",
"loadFailed": "Het laden van de Termix-ID is mislukt.",
"claimTitle": "Vraag je Termix ID aan",
"claimIntro": "Kies een unieke gebruikersnaam. Uw openbare SSH-sleutels worden gepubliceerd op een openbare URL die u kunt toevoegen aan het authorized_keys-bestand van elke server.",
"handleLabel": "Hendel",
"handlePlaceholder": "Alice",
"checking": "Controleren…",
"available": "Beschikbaar",
"taken": "Reeds meegenomen",
"invalidHandle": "Alleen kleine letters, cijfers, - en _",
"descriptionLabel": "Beschrijving (optioneel)",
"descriptionPlaceholder": "Werklaptop en telefoonsleutels",
"create": "Maak een Termix ID aan",
"created": "Termix ID aangemaakt",
"createFailed": "Het aanmaken van een Termix ID is mislukt.",
"deleteConfirm": "Wil je je Termix ID en alle gepubliceerde sleutels verwijderen? Servers die je hebt geconfigureerd, bewaren de sleutels totdat je ze handmatig verwijdert.",
"deleted": "Termix ID verwijderd",
"deleteFailed": "Het verwijderen van de Termix-ID is mislukt.",
"copyFailed": "Kopiëren mislukt",
"resolverUrlLabel": "Openbare resolver-URL",
"provisionLabel": "Een server inrichten",
"publishTitle": "Publiceer een openbare sleutel",
"generate": "Genereren",
"generateTooltip": "Genereer een Ed25519-sleutelpaar — publiceert de publieke sleutel en downloadt de privésleutel naar uw apparaat.",
"saveToVault": "Opslaan in inloggegevens",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "Label (optioneel)",
"add": "Toevoegen",
"importFromCredential": "Of importeren vanuit opgeslagen inloggegevens",
"keyPublished": "Sleutel gepubliceerd",
"addKeyFailed": "Sleutel toevoegen mislukt",
"generatedSaved": "Sleutelpaar gegenereerd en opgeslagen in uw inloggegevenskluis. Privésleutel ook gedownload.",
"generatedOnly": "Sleutelpaar gegenereerd — privésleutel gedownload (wordt slechts één keer weergegeven).",
"generateFailed": "Sleutel genereren mislukt",
"imported": "Sleutel geïmporteerd uit inloggegevens",
"importFailed": "Het importeren van de sleutel is mislukt.",
"noKeys": "Er zijn nog geen openbare sleutels gepubliceerd.",
"keysTitle": "Gepubliceerde sleutels",
"published": "Gepubliceerd",
"hidden": "Verborgen",
"keyRemoved": "Sleutel verwijderd",
"removeKeyFailed": "Het verwijderen van de sleutel is mislukt.",
"updateKeyFailed": "Sleutel bijwerken mislukt",
"fromVault": "Vanuit de inloggegevenskluis",
"linkedToTermixId": "Gepubliceerd via Termix ID",
"selectCredential": "Selecteer een certificaat...",
"import": "Importeren",
"caTitle": "Certificeringsinstantie",
"caIntro": "Vertrouw deze certificeringsinstantie op een server en deze accepteert elk certificaat dat u ondertekent. Roteer de certificaten om ze allemaal tegelijk in te trekken; certificaten verlopen ook automatisch.",
"caEnable": "CA inschakelen",
"caEnabled": "CA ingeschakeld",
"caCreateFailed": "CA inschakelen mislukt",
"caPublicKeyLabel": "CA openbare sleutel",
"caTrustLabel": "Vertrouw op een server (uitvoeren als root)",
"caRotate": "Draaien",
"caRotateConfirm": "Als u de certificeringsinstantie (CA) wijzigt, worden alle certificaten die door die instantie zijn ondertekend niet langer geaccepteerd en moeten ze opnieuw worden uitgegeven.",
"caRotated": "CA geroteerd — vorige certificaten ingetrokken",
"caRotateFailed": "CA-rotatie mislukt",
"caDelete": "Verwijder CA",
"caDeleteConfirm": "De certificeringsinstantie verwijderen?",
"caDeleted": "CA verwijderd",
"caDeleteFailed": "Het verwijderen van CA is mislukt.",
"caValidityLabel": "Geldigheidsduur certificaat (dagen)",
"issueCert": "Certificaat",
"issueCertTooltip": "Geef een SSH-certificaat uit voor deze sleutel, ondertekend door uw certificeringsinstantie (CA).",
"certIssued": "Certificaat uitgegeven en gedownload",
"certIssueFailed": "Certificaat kon niet worden uitgegeven"
},
"credentials": {
"folders": "Mappen",
"folder": "Map",
@@ -144,7 +75,6 @@
"editCredentialAction": "Inloggegevens bewerken",
"failedToCloneCredential": "Het klonen van de inloggegevens is mislukt.",
"failedToDeleteCredential": "Het verwijderen van de inloggegevens is mislukt.",
"idBadge": "ID",
"keyBadge": "SLEUTEL",
"passwordBadge": "PWD",
"renameFolder": "Map hernoemen",
@@ -525,7 +455,6 @@
"fileManager": "Bestandsbeheerder",
"serverStats": "Hoststatistieken",
"admin": "Beheerder",
"termixId": "ID",
"userProfile": "Gebruikersprofiel",
"splitScreen": "Gesplitst scherm",
"confirmClose": "Deze actieve sessie sluiten?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Kan ikke koble til",
"reconnect": "Koble til igjen"
},
"termixId": {
"title": "Termix-ID",
"loadFailed": "Kunne ikke laste inn Termix-ID",
"claimTitle": "Krev din Termix-ID",
"claimIntro": "Velg en unik brukernavn. Dine offentlige SSH-nøkler publiseres på en offentlig URL som du kan legge til i en hvilken som helst servers authorized_keys-fil.",
"handleLabel": "Håndtak",
"handlePlaceholder": "Alice",
"checking": "Sjekker…",
"available": "Tilgjengelig",
"taken": "Allerede tatt",
"invalidHandle": "Bare små bokstaver, tall, - og _",
"descriptionLabel": "Beskrivelse (valgfritt)",
"descriptionPlaceholder": "Jobb-laptop og telefonnøkler",
"create": "Opprett Termix-ID",
"created": "Termix-ID opprettet",
"createFailed": "Kunne ikke opprette Termix-ID",
"deleteConfirm": "Vil du slette Termix-ID-en din og alle publiserte nøkler? Servere du har klargjort vil beholde nøklene til du fjerner dem manuelt.",
"deleted": "Termix-ID slettet",
"deleteFailed": "Kunne ikke slette Termix-ID",
"copyFailed": "Kopieringen mislyktes",
"resolverUrlLabel": "Offentlig resolver-URL",
"provisionLabel": "Klargjør en server",
"publishTitle": "Publiser en offentlig nøkkel",
"generate": "Generere",
"generateTooltip": "Generer et Ed25519-nøkkelpar – publiserer den offentlige nøkkelen og laster ned den private nøkkelen til enheten din",
"saveToVault": "Lagre til legitimasjon",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... bruker@vert",
"labelPlaceholder": "Etikett (valgfritt)",
"add": "Legge til",
"importFromCredential": "Eller importer fra en lagret legitimasjon",
"keyPublished": "Nøkkel publisert",
"addKeyFailed": "Kunne ikke legge til nøkkel",
"generatedSaved": "Nøkkelpar generert og lagret i påloggingshvelvet ditt. Privatnøkkelen er også lastet ned.",
"generatedOnly": "Nøkkelpar generert – privat nøkkel lastet ned (vises bare én gang).",
"generateFailed": "Kunne ikke generere nøkkel",
"imported": "Nøkkel importert fra legitimasjon",
"importFailed": "Kunne ikke importere nøkkelen",
"noKeys": "Ingen offentlige nøkler er publisert ennå.",
"keysTitle": "Publiserte nøkler",
"published": "Publisert",
"hidden": "Skjult",
"keyRemoved": "Nøkkelen er fjernet",
"removeKeyFailed": "Kunne ikke fjerne nøkkelen",
"updateKeyFailed": "Kunne ikke oppdatere nøkkelen",
"fromVault": "Fra legitimasjonshvelvet",
"linkedToTermixId": "Publisert via Termix-ID",
"selectCredential": "Velg en legitimasjon...",
"import": "Import",
"caTitle": "Sertifikatmyndighet",
"caIntro": "Stol på denne sertifikatutstederen på en server, og den godtar alle sertifikater du signerer. Roter for å tilbakekalle alt samtidig; sertifikater utløper også av seg selv.",
"caEnable": "Aktiver CA",
"caEnabled": "CA-aktivert",
"caCreateFailed": "Kunne ikke aktivere CA",
"caPublicKeyLabel": "CA offentlig nøkkel",
"caTrustLabel": "Stol på en server (kjør som root)",
"caRotate": "Rotere",
"caRotateConfirm": "Roter CA-en? Alle sertifikater den har signert vil slutte å bli akseptert og må utstedes på nytt.",
"caRotated": "CA rotert – tidligere sertifikater inndratt",
"caRotateFailed": "Kunne ikke rotere CA",
"caDelete": "Fjern CA",
"caDeleteConfirm": "Fjerne sertifikatmyndigheten?",
"caDeleted": "CA fjernet",
"caDeleteFailed": "Kunne ikke fjerne CA",
"caValidityLabel": "Sertifikatgyldighet (dager)",
"issueCert": "Sertifikat",
"issueCertTooltip": "Utsted et SSH-sertifikat for denne nøkkelen, signert av din CA",
"certIssued": "Sertifikat utstedt og nedlastet",
"certIssueFailed": "Kunne ikke utstede sertifikatet"
},
"credentials": {
"folders": "Mapper",
"folder": "Mappe",
@@ -144,7 +75,6 @@
"editCredentialAction": "Rediger legitimasjon",
"failedToCloneCredential": "Kunne ikke klone legitimasjon",
"failedToDeleteCredential": "Kunne ikke slette legitimasjonen",
"idBadge": "ID",
"keyBadge": "NØKKEL",
"passwordBadge": "Utsatt person",
"renameFolder": "Gi nytt navn til mappen",
@@ -525,7 +455,6 @@
"fileManager": "Filbehandler",
"serverStats": "Vertsmålinger",
"admin": "Admin",
"termixId": "ID",
"userProfile": "Brukerprofil",
"splitScreen": "Delt skjerm",
"confirmClose": "Lukke denne aktive økten?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Nie można się połączyć",
"reconnect": "Połącz ponownie"
},
"termixId": {
"title": "Identyfikator Termix",
"loadFailed": "Nie udało się załadować identyfikatora Termix",
"claimTitle": "Zarezerwuj swój identyfikator Termix",
"claimIntro": "Wybierz unikalny identyfikator. Publiczne klucze SSH zostaną opublikowane pod publicznym adresem URL, który można dodać do pliku authorized_keys dowolnego serwera.",
"handleLabel": "Identyfikator",
"handlePlaceholder": "alicja",
"checking": "Sprawdzanie…",
"available": "Dostępny",
"taken": "Już zajęty",
"invalidHandle": "Tylko małe litery, cyfry oraz - i _",
"descriptionLabel": "Opis (opcjonalnie)",
"descriptionPlaceholder": "Klucze do laptopa i telefonu służbowego",
"create": "Utwórz identyfikator Termix",
"created": "Utworzono identyfikator Termix",
"createFailed": "Nie udało się utworzyć identyfikatora Termix",
"deleteConfirm": "Usunąć swój identyfikator Termix i wszystkie opublikowane klucze? Skonfigurowane wcześniej serwery zachowają klucze, dopóki nie usuniesz ich ręcznie.",
"deleted": "Usunięto identyfikator Termix",
"deleteFailed": "Nie udało się usunąć identyfikatora Termix",
"copyFailed": "Kopiowanie nie powiodło się",
"resolverUrlLabel": "Publiczny adres URL resolvera",
"provisionLabel": "Skonfiguruj serwer",
"publishTitle": "Opublikuj klucz publiczny",
"generate": "Generuj",
"generateTooltip": "Wygeneruj parę kluczy Ed25519: publikuje klucz publiczny i pobiera klucz prywatny na Twoje urządzenie",
"saveToVault": "Zapisz w poświadczeniach",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "Etykieta (opcjonalnie)",
"add": "Dodaj",
"importFromCredential": "Albo zaimportuj z zapisanego poświadczenia",
"keyPublished": "Klucz opublikowany",
"addKeyFailed": "Nie udało się dodać klucza",
"generatedSaved": "Wygenerowano parę kluczy i zapisano w sejfie poświadczeń. Klucz prywatny został również pobrany.",
"generatedOnly": "Wygenerowano parę kluczy, pobrano klucz prywatny (wyświetlany tylko raz).",
"generateFailed": "Nie udało się wygenerować klucza",
"imported": "Zaimportowano klucz z poświadczenia",
"importFailed": "Nie udało się zaimportować klucza",
"noKeys": "Klucze publiczne nie zostały jeszcze opublikowane.",
"keysTitle": "Opublikowane klucze",
"published": "Opublikowany",
"hidden": "Ukryty",
"keyRemoved": "Usunięto klucz",
"removeKeyFailed": "Nie udało się usunąć klucza",
"updateKeyFailed": "Nie udało się zaktualizować klucza",
"fromVault": "Z sejfu poświadczeń",
"linkedToTermixId": "Opublikowano przez identyfikator Termix",
"selectCredential": "Wybierz poświadczenie...",
"import": "Import",
"caTitle": "Urząd certyfikacji (CA)",
"caIntro": "Dodaj to CA do zaufanych na serwerze, a zaakceptuje on każdy podpisany przez Ciebie certyfikat. Rotacja unieważnia wszystko naraz; certyfikaty wygasają też samodzielnie.",
"caEnable": "Włącz CA",
"caEnabled": "CA włączone",
"caCreateFailed": "Nie udało się włączyć CA",
"caPublicKeyLabel": "Klucz publiczny CA",
"caTrustLabel": "Zaufanie na serwerze (uruchom jako root)",
"caRotate": "Rotuj",
"caRotateConfirm": "Wykonać rotację CA? Każdy podpisany przez niego certyfikat przestanie być akceptowany i trzeba go wystawić ponownie.",
"caRotated": "Wykonano rotację CA, poprzednie certyfikaty unieważnione",
"caRotateFailed": "Nie udało się wykonać rotacji CA",
"caDelete": "Usuń CA",
"caDeleteConfirm": "Usunąć CA?",
"caDeleted": "CA usunięto",
"caDeleteFailed": "Nie udało się usunąć CA",
"caValidityLabel": "Ważność certyfikatu (dni)",
"issueCert": "Certyfikat",
"issueCertTooltip": "Wystaw certyfikat SSH dla tego klucza, podpisany przez Twoje CA",
"certIssued": "Wystawiono i pobrano certyfikat",
"certIssueFailed": "Nie udało się wystawić certyfikatu"
},
"credentials": {
"folders": "Foldery",
"folder": "Folder",
@@ -144,7 +75,6 @@
"editCredentialAction": "Edytuj poświadczenie",
"failedToCloneCredential": "Nie udało się sklonować poświadczenia",
"failedToDeleteCredential": "Nie udało się usunąć poświadczenia",
"idBadge": "ID",
"keyBadge": "KLUCZ",
"passwordBadge": "HASŁO",
"renameFolder": "Zmień nazwę folderu",
@@ -525,7 +455,6 @@
"fileManager": "Menedżer plików",
"serverStats": "Metryki hosta",
"admin": "Administracja",
"termixId": "ID",
"userProfile": "Profil użytkownika",
"splitScreen": "Podzielony ekran",
"confirmClose": "Zamknąć tę aktywną sesję?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Não foi possível conectar",
"reconnect": "Reconectar"
},
"termixId": {
"title": "Termix ID",
"loadFailed": "Falha ao carregar o ID do Termix",
"claimTitle": "Reivindique seu ID Termix",
"claimIntro": "Escolha um nome de usuário exclusivo. Suas chaves públicas SSH serão publicadas em um URL público que você pode adicionar ao arquivo authorized_keys de qualquer servidor.",
"handleLabel": "Lidar",
"handlePlaceholder": "Alice",
"checking": "Verificando…",
"available": "Disponível",
"taken": "Já tomadas",
"invalidHandle": "Somente letras minúsculas, números, hífens e sublinhados.",
"descriptionLabel": "Descrição (opcional)",
"descriptionPlaceholder": "Chaves do laptop e do celular de trabalho",
"create": "Criar ID Termix",
"created": "Termix ID criado",
"createFailed": "Falha ao criar o ID do Termix",
"deleteConfirm": "Excluir seu ID do Termix e todas as chaves publicadas? Os servidores que você provisionou manterão as chaves até que você as remova manualmente.",
"deleted": "ID do Termix excluído",
"deleteFailed": "Falha ao excluir o ID do Termix",
"copyFailed": "Falha na cópia",
"resolverUrlLabel": "URL do resolvedor público",
"provisionLabel": "Provisione um servidor",
"publishTitle": "Publicar uma chave pública",
"generate": "Gerar",
"generateTooltip": "Gere um par de chaves Ed25519 — publica a chave pública e baixa a chave privada para o seu dispositivo.",
"saveToVault": "Salvar nas credenciais",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... usuário@host",
"labelPlaceholder": "Rótulo (opcional)",
"add": "Adicionar",
"importFromCredential": "Ou importe a partir de uma credencial armazenada.",
"keyPublished": "Chave publicada",
"addKeyFailed": "Falha ao adicionar a chave",
"generatedSaved": "O par de chaves foi gerado e salvo em seu cofre de credenciais. A chave privada também foi baixada.",
"generatedOnly": "Par de chaves gerado — chave privada baixada (exibida apenas uma vez).",
"generateFailed": "Falha ao gerar a chave",
"imported": "Chave importada da credencial",
"importFailed": "Falha ao importar a chave",
"noKeys": "Nenhuma chave pública foi publicada ainda.",
"keysTitle": "Chaves publicadas",
"published": "Publicado",
"hidden": "Escondido",
"keyRemoved": "Chave removida",
"removeKeyFailed": "Falha ao remover a chave",
"updateKeyFailed": "Falha ao atualizar a chave",
"fromVault": "Do cofre de credenciais",
"linkedToTermixId": "Publicado via Termix ID",
"selectCredential": "Selecione uma credencial...",
"import": "Importar",
"caTitle": "Autoridade certificadora",
"caIntro": "Confie nesta Autoridade Certificadora (CA) em um servidor e ela aceitará qualquer certificado que você assinar. Rotacione os certificados para revogar todos de uma só vez; os certificados também expiram automaticamente.",
"caEnable": "Habilitar CA",
"caEnabled": "CA habilitado",
"caCreateFailed": "Falha ao ativar a CA",
"caPublicKeyLabel": "chave pública CA",
"caTrustLabel": "Confie em um servidor (execute como root)",
"caRotate": "Girar",
"caRotateConfirm": "Rotacionar a Autoridade Certificadora? Todos os certificados assinados por ela deixarão de ser aceitos e precisarão ser reemitidos.",
"caRotated": "Autoridade Certificadora rotacionada — certificados anteriores revogados",
"caRotateFailed": "Falha ao rotacionar o CA",
"caDelete": "Remover CA",
"caDeleteConfirm": "Remover a autoridade certificadora?",
"caDeleted": "CA removido",
"caDeleteFailed": "Falha ao remover a CA",
"caValidityLabel": "Validade do certificado (dias)",
"issueCert": "Certificado",
"issueCertTooltip": "Emita um certificado SSH para esta chave, assinado pela sua Autoridade Certificadora (CA).",
"certIssued": "Certificado emitido e baixado",
"certIssueFailed": "Falha na emissão do certificado"
},
"credentials": {
"folders": "Pastas",
"folder": "Pasta",
@@ -144,7 +75,6 @@
"editCredentialAction": "Editar credencial",
"failedToCloneCredential": "Falha ao clonar a credencial",
"failedToDeleteCredential": "Falha ao excluir credencial",
"idBadge": "EU IA",
"keyBadge": "CHAVE",
"passwordBadge": "PWD",
"renameFolder": "Renomear pasta",
@@ -525,7 +455,6 @@
"fileManager": "Gerenciador de arquivos",
"serverStats": "Métricas do host",
"admin": "Administrador",
"termixId": "EU IA",
"userProfile": "Perfil do usuário",
"splitScreen": "Tela dividida",
"confirmClose": "Encerrar esta sessão ativa?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Não foi possível ligar",
"reconnect": "Reconectar"
},
"termixId": {
"title": "Termix ID",
"loadFailed": "Falha ao carregar o Termix ID",
"claimTitle": "Reivindicar o seu Termix ID",
"claimIntro": "Escolha um identificador único. As suas chaves públicas SSH serão publicadas num URL público que pode adicionar ao ficheiro authorized_keys de qualquer servidor.",
"handleLabel": "Identificador",
"handlePlaceholder": "alice",
"checking": "A verificar…",
"available": "Disponível",
"taken": "Já utilizado",
"invalidHandle": "Apenas letras minúsculas, números, - e _",
"descriptionLabel": "Descrição (opcional)",
"descriptionPlaceholder": "Chaves do portátil de trabalho e do telemóvel",
"create": "Criar Termix ID",
"created": "Termix ID criado",
"createFailed": "Falha ao criar o Termix ID",
"deleteConfirm": "Eliminar o seu Termix ID e todas as chaves publicadas? Os servidores que provisionou manterão as chaves até as remover manualmente.",
"deleted": "Termix ID eliminado",
"deleteFailed": "Falha ao eliminar o Termix ID",
"copyFailed": "Falha ao copiar",
"resolverUrlLabel": "URL público de resolução",
"provisionLabel": "Provisionar um servidor",
"publishTitle": "Publicar uma chave pública",
"generate": "Gerar",
"generateTooltip": "Gerar um par de chaves Ed25519 — publica a chave pública e descarrega a chave privada para o seu dispositivo",
"saveToVault": "Guardar nas credenciais",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "Etiqueta (opcional)",
"add": "Adicionar",
"importFromCredential": "Ou importar de uma credencial guardada",
"keyPublished": "Chave publicada",
"addKeyFailed": "Falha ao adicionar chave",
"generatedSaved": "Par de chaves gerado e guardado no cofre de credenciais. A chave privada também foi descarregada.",
"generatedOnly": "Par de chaves gerado — chave privada descarregada (exibida apenas uma vez).",
"generateFailed": "Falha ao gerar chave",
"imported": "Chave importada da credencial",
"importFailed": "Falha ao importar chave",
"noKeys": "Nenhuma chave pública publicada ainda.",
"keysTitle": "Chaves publicadas",
"published": "Publicada",
"hidden": "Oculta",
"keyRemoved": "Chave removida",
"removeKeyFailed": "Falha ao remover chave",
"updateKeyFailed": "Falha ao atualizar chave",
"fromVault": "Do cofre de credenciais",
"linkedToTermixId": "Publicada via Termix ID",
"selectCredential": "Selecionar uma credencial...",
"import": "Importar",
"caTitle": "Autoridade de certificação",
"caIntro": "Confie nesta CA num servidor e ele aceitará qualquer certificado que assinar. Rode para revogar tudo de uma só vez; os certificados também expiram automaticamente.",
"caEnable": "Ativar CA",
"caEnabled": "CA ativada",
"caCreateFailed": "Falha ao ativar CA",
"caPublicKeyLabel": "Chave pública da CA",
"caTrustLabel": "Confiar num servidor (executar como root)",
"caRotate": "Rodar",
"caRotateConfirm": "Rodar a CA? Todos os certificados que assinou deixarão de ser aceites e terão de ser reemitidos.",
"caRotated": "CA rodada — certificados anteriores revogados",
"caRotateFailed": "Falha ao rodar CA",
"caDelete": "Remover CA",
"caDeleteConfirm": "Remover a CA?",
"caDeleted": "CA removida",
"caDeleteFailed": "Falha ao remover CA",
"caValidityLabel": "Validade do certificado (dias)",
"issueCert": "Certificado",
"issueCertTooltip": "Emitir um certificado SSH para esta chave, assinado pela sua CA",
"certIssued": "Certificado emitido e transferido",
"certIssueFailed": "Falha ao emitir certificado"
},
"credentials": {
"folders": "Pastas",
"folder": "Pasta",
@@ -144,7 +75,6 @@
"editCredentialAction": "Editar credencial",
"failedToCloneCredential": "Falha ao clonar a credencial",
"failedToDeleteCredential": "Falha ao eliminar credencial",
"idBadge": "EU IA",
"keyBadge": "CHAVE",
"passwordBadge": "PWD",
"renameFolder": "Renomear pasta",
@@ -525,7 +455,6 @@
"fileManager": "Gestor de ficheiros",
"serverStats": "Métricas do Servidor",
"admin": "Administração",
"termixId": "ID",
"userProfile": "Perfil do Utilizador",
"splitScreen": "Ecrã Dividido",
"confirmClose": "Fechar esta sessão ativa?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Nu se poate conecta",
"reconnect": "Reconectați-vă"
},
"termixId": {
"title": "ID-ul Termix",
"loadFailed": "Nu s-a putut încărca ID-ul Termix",
"claimTitle": "Revendică-ți ID-ul Termix",
"claimIntro": "Alegeți un handle unic. Cheile dvs. publice SSH vor fi publicate la o adresă URL publică pe care o puteți adăuga în fișierul authorized_keys al oricărui server.",
"handleLabel": "Mâner",
"handlePlaceholder": "Alice",
"checking": "Verificare…",
"available": "Disponibil",
"taken": "Deja luate",
"invalidHandle": "Numai litere mici, cifre, - și _",
"descriptionLabel": "Descriere (opțional)",
"descriptionPlaceholder": "Taste pentru laptop și telefon de serviciu",
"create": "Creați ID-ul Termix",
"created": "ID-ul Termix a fost creat",
"createFailed": "Nu s-a putut crea ID-ul Termix.",
"deleteConfirm": "Ștergeți ID-ul Termix și toate cheile publicate? Serverele pe care le-ați furnizat vor păstra cheile până când le eliminați manual.",
"deleted": "ID-ul Termix a fost șters",
"deleteFailed": "Ștergerea ID-ului Termix a eșuat.",
"copyFailed": "Copierea a eșuat",
"resolverUrlLabel": "URL-ul rezolvitorului public",
"provisionLabel": "Furnizarea unui server",
"publishTitle": "Publicați o cheie publică",
"generate": "Genera",
"generateTooltip": "Generează o pereche de chei Ed25519 — publică cheia publică și descarcă cheia privată pe dispozitivul tău",
"saveToVault": "Salvați în acreditări",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... utilizator@gazdă",
"labelPlaceholder": "Etichetă (opțional)",
"add": "Adăuga",
"importFromCredential": "Sau importați dintr-o acreditare stocată",
"keyPublished": "Cheie publicată",
"addKeyFailed": "Cheia nu a putut fi adăugată",
"generatedSaved": "Perechea de chei a fost generată și salvată în seiful de acreditări. Cheia privată a fost, de asemenea, descărcată.",
"generatedOnly": "Pereche de chei generată — cheie privată descărcată (afișată o singură dată).",
"generateFailed": "Cheia nu a putut fi generată",
"imported": "Cheie importată din credențiale",
"importFailed": "Importarea cheii a eșuat",
"noKeys": "Nicio cheie publică nu a fost publicată încă.",
"keysTitle": "Chei publicate",
"published": "Publicat",
"hidden": "Ascuns",
"keyRemoved": "Cheia a fost scoasă",
"removeKeyFailed": "Cheia nu a putut fi eliminată",
"updateKeyFailed": "Cheia nu a putut fi actualizată",
"fromVault": "Din seiful de credențiale",
"linkedToTermixId": "Publicat prin intermediul ID-ului Termix",
"selectCredential": "Selectați o acreditare...",
"import": "Import",
"caTitle": "Autoritatea de certificare",
"caIntro": "Dacă aveți încredere în această CA pe un server, aceasta va accepta orice certificat pe care îl semnați. Rotiți opțiunea pentru a revoca totul simultan; certificatele expiră și ele singure.",
"caEnable": "Activează CA",
"caEnabled": "CA activat",
"caCreateFailed": "Activarea CA-ului nu a reușit",
"caPublicKeyLabel": "Cheia publică CA",
"caTrustLabel": "Încredere pe un server (executare ca root)",
"caRotate": "Roti",
"caRotateConfirm": "Rotiți CA-ul? Fiecare certificat semnat de aceasta nu va mai fi acceptat și va trebui emis din nou.",
"caRotated": "CA rotită — certificatele anterioare revocate",
"caRotateFailed": "Nu s-a putut roti CA",
"caDelete": "Eliminați CA-ul",
"caDeleteConfirm": "Eliminați autoritatea de certificare?",
"caDeleted": "Autorizație de certificare eliminată",
"caDeleteFailed": "Eliminarea CA-ului nu a reușit",
"caValidityLabel": "Valabilitatea certificatului (zile)",
"issueCert": "Certificat",
"issueCertTooltip": "Emiteți un certificat SSH pentru această cheie, semnat de CA-ul dvs.",
"certIssued": "Certificat emis și descărcat",
"certIssueFailed": "Nu s-a putut emite certificatul"
},
"credentials": {
"folders": "Foldere",
"folder": "Pliant",
@@ -144,7 +75,6 @@
"editCredentialAction": "Editați acreditările",
"failedToCloneCredential": "Nu s-a putut clona acreditările",
"failedToDeleteCredential": "Ștergerea acreditării nu a reușit",
"idBadge": "ID-ul",
"keyBadge": "CHEIE",
"passwordBadge": "Persoană cu dizabilități",
"renameFolder": "Redenumiți folderul",
@@ -525,7 +455,6 @@
"fileManager": "Manager de fișiere",
"serverStats": "Metrici ale gazdei",
"admin": "Administrator",
"termixId": "ID-ul",
"userProfile": "Profil utilizator",
"splitScreen": "Ecran divizat",
"confirmClose": "Închideți această sesiune activă?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Не удалось подключиться",
"reconnect": "Переподключитесь"
},
"termixId": {
"title": "Termix ID",
"loadFailed": "Не удалось загрузить Termix ID",
"claimTitle": "Зарегистрируйте свой Termix ID",
"claimIntro": "Выберите уникальный идентификатор. Ваши открытые ключи SSH будут опубликованы по общедоступному URL, который вы сможете добавить в файл authorized_keys на любом сервере.",
"handleLabel": "Идентификатор",
"handlePlaceholder": "alice",
"checking": "Проверка…",
"available": "Доступно",
"taken": "Уже занято",
"invalidHandle": "Только строчные буквы, цифры, - и _",
"descriptionLabel": "Описание (необязательно)",
"descriptionPlaceholder": "Ключи рабочего ноутбука и телефона",
"create": "Создать Termix ID",
"created": "Termix ID создан",
"createFailed": "Не удалось создать Termix ID",
"deleteConfirm": "Удалить свой Termix ID и все опубликованные ключи? Серверы, которые вы настроили, сохранят ключи, пока вы не удалите их вручную.",
"deleted": "Termix ID удалён",
"deleteFailed": "Не удалось удалить Termix ID",
"copyFailed": "Не удалось скопировать",
"resolverUrlLabel": "Общедоступный URL резолвера",
"provisionLabel": "Настроить сервер",
"publishTitle": "Опубликовать открытый ключ",
"generate": "Сгенерировать",
"generateTooltip": "Сгенерировать пару ключей Ed25519 — публикует открытый ключ и загружает закрытый ключ на ваше устройство",
"saveToVault": "Сохранить в учётные данные",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "Метка (необязательно)",
"add": "Добавить",
"importFromCredential": "Или импортировать из сохранённых учётных данных",
"keyPublished": "Ключ опубликован",
"addKeyFailed": "Не удалось добавить ключ",
"generatedSaved": "Пара ключей сгенерирована и сохранена в хранилище учётных данных. Закрытый ключ также загружен.",
"generatedOnly": "Пара ключей сгенерирована — закрытый ключ загружен (отображается только один раз).",
"generateFailed": "Не удалось сгенерировать ключ",
"imported": "Ключ импортирован из учётных данных",
"importFailed": "Не удалось импортировать ключ",
"noKeys": "Нет опубликованных открытых ключей.",
"keysTitle": "Опубликованные ключи",
"published": "Опубликован",
"hidden": "Скрыт",
"keyRemoved": "Ключ удалён",
"removeKeyFailed": "Не удалось удалить ключ",
"updateKeyFailed": "Не удалось обновить ключ",
"fromVault": "Из хранилища учётных данных",
"linkedToTermixId": "Опубликовано через Termix ID",
"selectCredential": "Выберите учётные данные…",
"import": "Импортировать",
"caTitle": "Центр сертификации",
"caIntro": "Доверьте этот центр сертификации на сервере, и он будет принимать любые сертификаты, подписанные вами. Ротация мгновенно отзывает все сертификаты; также сертификаты истекают автоматически.",
"caEnable": "Включить ЦС",
"caEnabled": "ЦС включён",
"caCreateFailed": "Не удалось включить ЦС",
"caPublicKeyLabel": "Открытый ключ ЦС",
"caTrustLabel": "Доверие на сервере (запустите от root)",
"caRotate": "Ротировать",
"caRotateConfirm": "Ротировать ЦС? Все выданные им сертификаты перестанут приниматься и потребуют перевыпуска.",
"caRotated": "ЦС ротирован — предыдущие сертификаты отозваны",
"caRotateFailed": "Не удалось ротировать ЦС",
"caDelete": "Удалить ЦС",
"caDeleteConfirm": "Удалить центр сертификации?",
"caDeleted": "ЦС удалён",
"caDeleteFailed": "Не удалось удалить ЦС",
"caValidityLabel": "Срок действия сертификата (дней)",
"issueCert": "Сертификат",
"issueCertTooltip": "Выпустить SSH-сертификат для этого ключа, подписанный вашим ЦС",
"certIssued": "Сертификат выпущен и загружен",
"certIssueFailed": "Не удалось выпустить сертификат"
},
"credentials": {
"folders": "Папки",
"folder": "Папка",
@@ -144,7 +75,6 @@
"editCredentialAction": "Изменить учетные данные",
"failedToCloneCredential": "Не удалось клонировать учетные данные.",
"failedToDeleteCredential": "Не удалось удалить учетные данные.",
"idBadge": "ИДЕНТИФИКАТОР",
"keyBadge": "КЛЮЧ",
"passwordBadge": "ЛСД",
"renameFolder": "Переименовать папку",
@@ -525,7 +455,6 @@
"fileManager": "Файловый менеджер",
"serverStats": "Метрики хоста",
"admin": "Администрирование",
"termixId": "ID",
"userProfile": "Профиль пользователя",
"splitScreen": "Разделить экран",
"confirmClose": "Закрыть активный сеанс?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Повезивање није могуће",
"reconnect": "Поново се повежите"
},
"termixId": {
"title": "Термикс ИД",
"loadFailed": "Учитавање Termix ID-а није успело",
"claimTitle": "Преузмите свој Termix ID",
"claimIntro": "Изаберите јединствени идентификатор. Ваши SSH јавни кључеви биће објављени на јавној URL адреси коју можете додати у датотеку authorized_keys било ког сервера.",
"handleLabel": "Ручка",
"handlePlaceholder": "Алиса",
"checking": "Провера…",
"available": "Доступно",
"taken": "Већ је заузето",
"invalidHandle": "Само мала слова, бројеви, - и _",
"descriptionLabel": "Опис (опционо)",
"descriptionPlaceholder": "Кључеви за лаптоп и телефон за посао",
"create": "Креирај Termix ID",
"created": "Termix ID је креиран",
"createFailed": "Није успело креирање Termix ID-а",
"deleteConfirm": "Желите да обришете свој Termix ID и све објављене кључеве? Сервери које сте обезбедили ће чувати кључеве док их ручно не уклоните.",
"deleted": "ИД Termix-а је обрисан",
"deleteFailed": "Брисање Termix ID-а није успело",
"copyFailed": "Копирање није успело",
"resolverUrlLabel": "Јавни URL резолвера",
"provisionLabel": "Опремите сервер",
"publishTitle": "Објавите јавни кључ",
"generate": "Генериши",
"generateTooltip": "Генеришите пар кључева Ed25519 — објављује јавни кључ и преузима приватни кључ на ваш уређај",
"saveToVault": "Сачувај у акредитиве",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... корисник@домаћин",
"labelPlaceholder": "Ознака (опционо)",
"add": "Додај",
"importFromCredential": "Или увезите из сачуваних акредитива",
"keyPublished": "Кључ објављен",
"addKeyFailed": "Додавање кључа није успело",
"generatedSaved": "Пар кључева је генерисан и сачуван у вашем трезору акредитива. Приватни кључ је такође преузет.",
"generatedOnly": "Генерисан пар кључева — преузет приватни кључ (приказан само једном).",
"generateFailed": "Генерисање кључа није успело",
"imported": "Кључ увезен из акредитива",
"importFailed": "Увоз кључа није успео",
"noKeys": "Још увек нису објављени јавни кључеви.",
"keysTitle": "Објављени кључеви",
"published": "Објављено",
"hidden": "Скривено",
"keyRemoved": "Кључ уклоњен",
"removeKeyFailed": "Уклањање кључа није успело",
"updateKeyFailed": "Ажурирање кључа није успело",
"fromVault": "Из трезора акредитива",
"linkedToTermixId": "Објављено преко Termix ID-а",
"selectCredential": "Изаберите акредитив...",
"import": "Увоз",
"caTitle": "Сертификатски ауторитет",
"caIntro": "Верујте овом ЦА на серверу и он ће прихватити сваки сертификат који потпишете. Ротирајте да бисте опозвали све одједном; сертификати такође сами истичу.",
"caEnable": "Омогући CA",
"caEnabled": "ЦА омогућен",
"caCreateFailed": "Омогућавање CA није успело",
"caPublicKeyLabel": "Јавни кључ CA",
"caTrustLabel": "Поверење на серверу (покрени као root)",
"caRotate": "Ротирај",
"caRotateConfirm": "Ротирати CA? Сваки сертификат који је потписао престаће да се прихвата и мораће се поново издати.",
"caRotated": "CA ротиран — претходни сертификати опозвани",
"caRotateFailed": "Ротација CA није успела",
"caDelete": "Уклони CA",
"caDeleteConfirm": "Уклонити ауторитет за сертификате?",
"caDeleted": "ЦА је уклоњен",
"caDeleteFailed": "Уклањање CA није успело",
"caValidityLabel": "Важење сертификата (дани)",
"issueCert": "Сертификат",
"issueCertTooltip": "Издајте SSH сертификат за овај кључ, потписан од стране вашег CA",
"certIssued": "Сертификат издат и преузет",
"certIssueFailed": "Издавање сертификата није успело"
},
"credentials": {
"folders": "Фасцикле",
"folder": "Фасцикла",
@@ -144,7 +75,6 @@
"editCredentialAction": "Измени акредитив",
"failedToCloneCredential": "Клонирање акредитива није успело",
"failedToDeleteCredential": "Брисање акредитива није успело",
"idBadge": "ИД",
"keyBadge": "КЉУЧ",
"passwordBadge": "ОСОБА",
"renameFolder": "Преименуј фасциклу",
@@ -525,7 +455,6 @@
"fileManager": "Менаџер датотека",
"serverStats": "Метрике хоста",
"admin": "Администратор",
"termixId": "ИД",
"userProfile": "Кориснички профил",
"splitScreen": "Подељени екран",
"confirmClose": "Да ли желите да затворите ову активну сесију?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Det gick inte att ansluta",
"reconnect": "Återanslut"
},
"termixId": {
"title": "Termix-ID",
"loadFailed": "Misslyckades med att ladda Termix-ID",
"claimTitle": "Hämta ditt Termix-ID",
"claimIntro": "Välj ett unikt användarnamn. Dina publika SSH-nycklar kommer att publiceras på en offentlig URL som du kan lägga till i valfri servers authorized_keys-fil.",
"handleLabel": "Hantera",
"handlePlaceholder": "Alice",
"checking": "Kontrollerar…",
"available": "Tillgänglig",
"taken": "Redan upptagen",
"invalidHandle": "Endast små bokstäver, siffror, - och _",
"descriptionLabel": "Beskrivning (valfritt)",
"descriptionPlaceholder": "Arbetslaptop och telefonnycklar",
"create": "Skapa Termix-ID",
"created": "Termix-ID skapat",
"createFailed": "Misslyckades med att skapa Termix-ID",
"deleteConfirm": "Vill du ta bort ditt Termix-ID och alla publicerade nycklar? Servrar som du har etablerat kommer att behålla nycklarna tills du tar bort dem manuellt.",
"deleted": "Termix-ID raderat",
"deleteFailed": "Misslyckades med att ta bort Termix-ID",
"copyFailed": "Kopieringen misslyckades",
"resolverUrlLabel": "Offentlig resolver-URL",
"provisionLabel": "Provisionera en server",
"publishTitle": "Publicera en offentlig nyckel",
"generate": "Generera",
"generateTooltip": "Generera ett Ed25519-nyckelpar — publicerar den offentliga nyckeln och laddar ner den privata nyckeln till din enhet",
"saveToVault": "Spara till autentiseringsuppgifter",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... användare@värd",
"labelPlaceholder": "Etikett (valfritt)",
"add": "Tillägga",
"importFromCredential": "Eller importera från en lagrad autentiseringsuppgift",
"keyPublished": "Nyckel publicerad",
"addKeyFailed": "Misslyckades med att lägga till nyckel",
"generatedSaved": "Nyckelpar genererat och sparat i ditt inloggningsvalv. Privat nyckel laddades också ner.",
"generatedOnly": "Nyckelpar genererat — privat nyckel nedladdad (visas endast en gång).",
"generateFailed": "Misslyckades med att generera nyckeln",
"imported": "Nyckel importerad från autentiseringsuppgifter",
"importFailed": "Misslyckades med att importera nyckeln",
"noKeys": "Inga publika nycklar publicerade än.",
"keysTitle": "Publicerade nycklar",
"published": "Publicerad",
"hidden": "Dold",
"keyRemoved": "Nyckel borttagen",
"removeKeyFailed": "Misslyckades med att ta bort nyckeln",
"updateKeyFailed": "Misslyckades med att uppdatera nyckeln",
"fromVault": "Från inloggningsvalvet",
"linkedToTermixId": "Publicerad via Termix-ID",
"selectCredential": "Välj en behörighetsinloggning...",
"import": "Importera",
"caTitle": "Certifikatutfärdare",
"caIntro": "Lita på denna certifikatutfärdare på en server så accepterar den alla certifikat du signerar. Rotera för att återkalla allt på en gång; certifikat upphör också att gälla av sig själva.",
"caEnable": "Aktivera CA",
"caEnabled": "CA-aktiverad",
"caCreateFailed": "Misslyckades med att aktivera CA",
"caPublicKeyLabel": "CA:s offentliga nyckel",
"caTrustLabel": "Lita på en server (kör som root)",
"caRotate": "Rotera",
"caRotateConfirm": "Rotera CA:n? Alla certifikat som den har signerat kommer att sluta accepteras och måste utfärdas på nytt.",
"caRotated": "CA roterad — tidigare certifikat återkallade",
"caRotateFailed": "Misslyckades med att rotera CA",
"caDelete": "Ta bort certifikatutfärdare",
"caDeleteConfirm": "Ta bort certifikatutfärdaren?",
"caDeleted": "CA borttagen",
"caDeleteFailed": "Misslyckades med att ta bort certifikatutfärdaren",
"caValidityLabel": "Certifikatgiltighet (dagar)",
"issueCert": "Certifikat",
"issueCertTooltip": "Utfärda ett SSH-certifikat för den här nyckeln, signerat av din CA",
"certIssued": "Certifikat utfärdat och nedladdat",
"certIssueFailed": "Misslyckades med att utfärda certifikat"
},
"credentials": {
"folders": "Mappar",
"folder": "Mapp",
@@ -144,7 +75,6 @@
"editCredentialAction": "Redigera inloggningsuppgifter",
"failedToCloneCredential": "Misslyckades med att klona inloggningsuppgifterna",
"failedToDeleteCredential": "Misslyckades med att ta bort inloggningsuppgifter",
"idBadge": "ID",
"keyBadge": "NYCKEL",
"passwordBadge": "Utsatt person (PWD)",
"renameFolder": "Byt namn på mapp",
@@ -525,7 +455,6 @@
"fileManager": "Filhanteraren",
"serverStats": "Värdstatistik",
"admin": "Administration",
"termixId": "ID",
"userProfile": "Användarprofil",
"splitScreen": "Delad skärm",
"confirmClose": "Stänga den här aktiva sessionen?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "ไม่สามารถเชื่อมต่อได้",
"reconnect": "เชื่อมต่อใหม่"
},
"termixId": {
"title": "เทอร์มิกซ์ ไอดี",
"loadFailed": "ไม่สามารถโหลด Termix ID ได้",
"claimTitle": "รับรหัส Termix ของคุณ",
"claimIntro": "เลือกชื่อผู้ใช้ที่ไม่ซ้ำกัน คีย์สาธารณะ SSH ของคุณจะถูกเผยแพร่ที่ URL สาธารณะที่คุณสามารถเพิ่มลงในไฟล์ authorized_keys ของเซิร์ฟเวอร์ใดก็ได้",
"handleLabel": "รับมือ",
"handlePlaceholder": "อลิซ",
"checking": "กำลังตรวจสอบ…",
"available": "มีอยู่",
"taken": "เอาไปแล้ว",
"invalidHandle": "ใช้เฉพาะตัวอักษรพิมพ์เล็ก ตัวเลข และเครื่องหมายขีดกลาง (-) เท่านั้น",
"descriptionLabel": "คำอธิบาย (ไม่บังคับ)",
"descriptionPlaceholder": "ปุ่มสำหรับแล็ปท็อปและโทรศัพท์ที่ใช้ในการทำงาน",
"create": "สร้าง Termix ID",
"created": "สร้าง Termix ID แล้ว",
"createFailed": "ไม่สามารถสร้าง Termix ID ได้",
"deleteConfirm": "ลบ Termix ID และคีย์ที่เผยแพร่ทั้งหมดของคุณแล้วหรือยัง? เซิร์ฟเวอร์ที่คุณตั้งค่าไว้จะเก็บคีย์เหล่านั้นไว้จนกว่าคุณจะลบออกด้วยตนเอง",
"deleted": "รหัส Termix ถูกลบแล้ว",
"deleteFailed": "ไม่สามารถลบ Termix ID ได้",
"copyFailed": "การคัดลอกล้มเหลว",
"resolverUrlLabel": "URL ตัวแก้ไขสาธารณะ",
"provisionLabel": "จัดเตรียมเซิร์ฟเวอร์",
"publishTitle": "เผยแพร่คีย์สาธารณะ",
"generate": "สร้าง",
"generateTooltip": "สร้างคู่คีย์ Ed25519 — เผยแพร่คีย์สาธารณะและดาวน์โหลดคีย์ส่วนตัวไปยังอุปกรณ์ของคุณ",
"saveToVault": "บันทึกไปยังข้อมูลรับรอง",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "ป้ายกำกับ (ไม่บังคับ)",
"add": "เพิ่ม",
"importFromCredential": "หรือนำเข้าจากข้อมูลรับรองที่บันทึกไว้",
"keyPublished": "คีย์ที่เผยแพร่",
"addKeyFailed": "ไม่สามารถเพิ่มคีย์ได้",
"generatedSaved": "สร้างและบันทึกคู่คีย์ลงในคลังข้อมูลประจำตัวของคุณเรียบร้อยแล้ว คีย์ส่วนตัวก็ถูกดาวน์โหลดด้วยเช่นกัน",
"generatedOnly": "สร้างคู่คีย์แล้ว — ดาวน์โหลดคีย์ส่วนตัวแล้ว (แสดงเพียงครั้งเดียว)",
"generateFailed": "ไม่สามารถสร้างคีย์ได้",
"imported": "คีย์ที่นำเข้าจากข้อมูลประจำตัว",
"importFailed": "ไม่สามารถนำเข้าคีย์ได้",
"noKeys": "ยังไม่มีการเผยแพร่คีย์สาธารณะใดๆ",
"keysTitle": "คีย์ที่เผยแพร่",
"published": "ที่ตีพิมพ์",
"hidden": "ที่ซ่อนอยู่",
"keyRemoved": "กุญแจถูกถอดออก",
"removeKeyFailed": "ไม่สามารถถอดกุญแจได้",
"updateKeyFailed": "ไม่สามารถอัปเดตคีย์ได้",
"fromVault": "จากคลังข้อมูลประจำตัว",
"linkedToTermixId": "เผยแพร่ผ่าน Termix ID",
"selectCredential": "เลือกข้อมูลประจำตัว...",
"import": "นำเข้า",
"caTitle": "หน่วยงานออกใบรับรอง",
"caIntro": "เชื่อถือ CA นี้บนเซิร์ฟเวอร์ และมันจะยอมรับใบรับรองใดๆ ก็ตามที่คุณลงนาม หมุนเวียนใบรับรองเพื่อยกเลิกทุกอย่างพร้อมกัน ใบรับรองจะหมดอายุเองโดยอัตโนมัติ",
"caEnable": "เปิดใช้งาน CA",
"caEnabled": "CA เปิดใช้งานแล้ว",
"caCreateFailed": "ไม่สามารถเปิดใช้งาน CA ได้",
"caPublicKeyLabel": "คีย์สาธารณะ CA",
"caTrustLabel": "เชื่อถือเซิร์ฟเวอร์ (เรียกใช้ในฐานะ root)",
"caRotate": "หมุน",
"caRotateConfirm": "หมุนเวียน CA? ใบรับรองทั้งหมดที่ CA นั้นได้ลงนามไปแล้วจะไม่ได้รับการยอมรับอีกต่อไป และจะต้องออกใหม่",
"caRotated": "CA หมุนเวียนแล้ว — ใบรับรองเดิมถูกเพิกถอน",
"caRotateFailed": "ไม่สามารถหมุน CA ได้",
"caDelete": "ลบ CA",
"caDeleteConfirm": "ลบหน่วยงานออกใบรับรองหรือไม่?",
"caDeleted": "CA ถูกลบออก",
"caDeleteFailed": "ไม่สามารถลบ CA ได้",
"caValidityLabel": "ระยะเวลาที่ใบรับรองมีผลบังคับใช้ (วัน)",
"issueCert": "ใบรับรอง",
"issueCertTooltip": "ออกใบรับรอง SSH สำหรับคีย์นี้ โดยให้หน่วยงานออกใบรับรอง (CA) ของคุณเป็นผู้ลงนาม",
"certIssued": "ออกและดาวน์โหลดใบรับรองแล้ว",
"certIssueFailed": "ไม่สามารถออกใบรับรองได้"
},
"credentials": {
"folders": "โฟลเดอร์",
"folder": "โฟลเดอร์",
@@ -144,7 +75,6 @@
"editCredentialAction": "แก้ไขข้อมูลประจำตัว",
"failedToCloneCredential": "ไม่สามารถคัดลอกข้อมูลประจำตัวได้",
"failedToDeleteCredential": "ไม่สามารถลบข้อมูลประจำตัวได้",
"idBadge": "รหัสประจำตัว",
"keyBadge": "สำคัญ",
"passwordBadge": "คนพิการ",
"renameFolder": "เปลี่ยนชื่อโฟลเดอร์",
@@ -525,7 +455,6 @@
"fileManager": "ตัวจัดการไฟล์",
"serverStats": "เมตริกโฮสต์",
"admin": "ผู้ดูแลระบบ",
"termixId": "รหัสประจำตัว",
"userProfile": "โปรไฟล์ผู้ใช้",
"splitScreen": "แบ่งหน้าจอ",
"confirmClose": "ปิดเซสชันที่ใช้งานอยู่นี้หรือไม่?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Bağlantı kurulamadı.",
"reconnect": "Yeniden bağlan"
},
"termixId": {
"title": "Termix ID",
"loadFailed": "Termix ID yüklenemedi",
"claimTitle": "Termix ID'nizi alın",
"claimIntro": "Benzersiz bir kullanıcı adı seçin. SSH genel anahtarlarınız, herhangi bir sunucunun authorized_keys dosyasına ekleyebileceğiniz herkese açık bir URL'de yayımlanacaktır.",
"handleLabel": "Kullanıcı Adı",
"handlePlaceholder": "alice",
"checking": "Kontrol ediliyor…",
"available": "Kullanılabilir",
"taken": "Zaten alınmış",
"invalidHandle": "Yalnızca küçük harfler, rakamlar, - ve _ kullanılabilir",
"descriptionLabel": "Açıklama (isteğe bağlı)",
"descriptionPlaceholder": "İş bilgisayarı ve telefon anahtarları",
"create": "Termix ID Oluştur",
"created": "Termix ID oluşturuldu",
"createFailed": "Termix ID oluşturulamadı",
"deleteConfirm": "Termix ID'nizi ve yayımlanmış tüm anahtarları silmek istediğinize emin misiniz? Hazırladığınız sunucular, anahtarları siz manuel olarak kaldırana kadar saklayacaktır.",
"deleted": "Termix ID silindi",
"deleteFailed": "Termix ID silinemedi",
"copyFailed": "Kopyalama başarısız",
"resolverUrlLabel": "Genel çözümleyici URL",
"provisionLabel": "Sunucu hazırla",
"publishTitle": "Genel anahtar yayımla",
"generate": "Oluştur",
"generateTooltip": "Ed25519 anahtar çifti oluştur — genel anahtarı yayımlar ve özel anahtarı cihazınıza indirir",
"saveToVault": "Kimlik Bilgilerine Kaydet",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "Etiket (isteğe bağlı)",
"add": "Ekle",
"importFromCredential": "Veya kayıtlı bir kimlik bilgisinden içe aktar",
"keyPublished": "Anahtar yayımlandı",
"addKeyFailed": "Anahtar eklenemedi",
"generatedSaved": "Anahtar çifti oluşturuldu ve kimlik bilgileri kasanıza kaydedildi. Özel anahtar da indirildi.",
"generatedOnly": "Anahtar çifti oluşturuldu — özel anahtar indirildi (yalnızca bir kez gösterilir).",
"generateFailed": "Anahtar oluşturulamadı",
"imported": "Anahtar kimlik bilgisinden içe aktarıldı",
"importFailed": "Anahtar içe aktarılamadı",
"noKeys": "Henüz yayınlanmış genel anahtar yok.",
"keysTitle": "Yayınlanan anahtarlar",
"published": "Yayınlandı",
"hidden": "Gizli",
"keyRemoved": "Anahtar kaldırıldı",
"removeKeyFailed": "Anahtar kaldırılamadı",
"updateKeyFailed": "Anahtar güncellenemedi",
"fromVault": "Kimlik bilgileri kasasından",
"linkedToTermixId": "Termix ID ile yayınlandı",
"selectCredential": "Bir kimlik bilgisi seçin...",
"import": "İçe aktar",
"caTitle": "Sertifika yetkilisi",
"caIntro": "Bu CA'yı bir sunucuda güvenilir yapın, imzaladığınız tüm sertifikaları kabul eder. Tek seferde tümünü iptal etmek için döndürün; sertifikalar ayrıca kendiliğinden süresi dolar.",
"caEnable": "CA'yı etkinleştir",
"caEnabled": "CA etkinleştirildi",
"caCreateFailed": "CA etkinleştirilemedi",
"caPublicKeyLabel": "CA genel anahtarı",
"caTrustLabel": "Sunucuda güven (root olarak çalıştırın)",
"caRotate": "Döndür",
"caRotateConfirm": "CA'yı döndürmek istediğinize emin misiniz? İmzaladığı tüm sertifikalar kabul edilmeyecek ve yeniden düzenlenmeleri gerekecek.",
"caRotated": "CA döndürüldü — önceki sertifikalar iptal edildi",
"caRotateFailed": "CA döndürülemedi",
"caDelete": "CA'yı kaldır",
"caDeleteConfirm": "Sertifika yetkilisi kaldırılsın mı?",
"caDeleted": "CA kaldırıldı",
"caDeleteFailed": "CA kaldırılamadı",
"caValidityLabel": "Sertifika geçerliliği (gün)",
"issueCert": "Sertifika",
"issueCertTooltip": "Bu anahtar için CA'nız tarafından imzalanmış bir SSH sertifikası düzenleyin",
"certIssued": "Sertifika düzenlendi ve indirildi",
"certIssueFailed": "Sertifika düzenlenemedi"
},
"credentials": {
"folders": "Klasörler",
"folder": "Klasör",
@@ -144,7 +75,6 @@
"editCredentialAction": "Kimlik bilgilerini düzenle",
"failedToCloneCredential": "Kimlik bilgilerini kopyalama başarısız oldu.",
"failedToDeleteCredential": "Kimlik bilgilerini silme işlemi başarısız oldu.",
"idBadge": "İD",
"keyBadge": "ANAHTAR",
"passwordBadge": "PWD",
"renameFolder": "Klasörü yeniden adlandır",
@@ -525,7 +455,6 @@
"fileManager": "Dosya Yöneticisi",
"serverStats": "Sunucu Metrikleri",
"admin": "Yönetici",
"termixId": "Kimlik",
"userProfile": "Kullanıcı Profili",
"splitScreen": "Ekranı Böl",
"confirmClose": "Bu etkin oturum kapatılsın mı?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Не вдалося підключитися",
"reconnect": "Знову підключитися"
},
"termixId": {
"title": "Ідентифікатор Termix",
"loadFailed": "Не вдалося завантажити ідентифікатор Termix",
"claimTitle": "Отримайте свій ідентифікатор Termix",
"claimIntro": "Виберіть унікальний дескриптор. Ваші публічні ключі SSH будуть опубліковані за публічною URL-адресою, яку можна додати до файлу authorized_keys будь-якого сервера.",
"handleLabel": "Ручка",
"handlePlaceholder": "Аліса",
"checking": "Перевірка…",
"available": "Доступно",
"taken": "Вже зайнято",
"invalidHandle": "Тільки малі літери, цифри, - та _",
"descriptionLabel": "Опис (необов'язково)",
"descriptionPlaceholder": "Клавіші для робочого ноутбука та телефону",
"create": "Створити ідентифікатор Termix",
"created": "Ідентифікатор Termix створено",
"createFailed": "Не вдалося створити ідентифікатор Termix",
"deleteConfirm": "Видалити свій Termix ID та всі опубліковані ключі? Сервери, які ви налаштували, зберігатимуть ключі, доки ви не видалите їх вручну.",
"deleted": "Ідентифікатор Termix видалено",
"deleteFailed": "Не вдалося видалити ідентифікатор Termix",
"copyFailed": "Не вдалося скопіювати",
"resolverUrlLabel": "URL-адреса публічного резолвера",
"provisionLabel": "Підготовка сервера",
"publishTitle": "Опублікувати відкритий ключ",
"generate": "Згенерувати",
"generateTooltip": "Згенеруйте пару ключів Ed25519 — публікує відкритий ключ і завантажує закритий ключ на ваш пристрій",
"saveToVault": "Зберегти в облікові дані",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... користувач@хост",
"labelPlaceholder": "Мітка (необов'язково)",
"add": "Додати",
"importFromCredential": "Або імпортувати зі збережених облікових даних",
"keyPublished": "Ключ опубліковано",
"addKeyFailed": "Не вдалося додати ключ",
"generatedSaved": "Пару ключів згенеровано та збережено у вашому сховищі облікових даних. Також завантажено закритий ключ.",
"generatedOnly": "Згенерована пара ключів — завантажено закритий ключ (відображається лише один раз).",
"generateFailed": "Не вдалося згенерувати ключ",
"imported": "Ключ імпортовано з облікових даних",
"importFailed": "Не вдалося імпортувати ключ",
"noKeys": "Відкриті ключі ще не опубліковані.",
"keysTitle": "Опубліковані ключі",
"published": "Опубліковано",
"hidden": "Прихований",
"keyRemoved": "Ключ вилучено",
"removeKeyFailed": "Не вдалося видалити ключ",
"updateKeyFailed": "Не вдалося оновити ключ",
"fromVault": "Зі сховища облікових даних",
"linkedToTermixId": "Опубліковано через Termix ID",
"selectCredential": "Виберіть облікові дані...",
"import": "Імпорт",
"caTitle": "Центр сертифікації",
"caIntro": "Довіртеся цьому ЦС на сервері, і він прийматиме будь-який підписаний вами сертифікат. Зробіть поворот, щоб скасувати все одразу; сертифікати також закінчуються самостійно.",
"caEnable": "Увімкнути ЦС",
"caEnabled": "ЦС увімкнено",
"caCreateFailed": "Не вдалося ввімкнути CA",
"caPublicKeyLabel": "Відкритий ключ CA",
"caTrustLabel": "Довіра на сервері (запуск від імені root)",
"caRotate": "Повернути",
"caRotateConfirm": "Ротувати ЦС? Кожен підписаний ним сертифікат більше не прийматиметься та має бути виданий повторно.",
"caRotated": "ЦС повернуто — попередні сертифікати скасовано",
"caRotateFailed": "Не вдалося повернути CA",
"caDelete": "Видалити ЦС",
"caDeleteConfirm": "Видалити центр сертифікації?",
"caDeleted": "CA видалено",
"caDeleteFailed": "Не вдалося видалити CA",
"caValidityLabel": "Термін дії сертифіката (дні)",
"issueCert": "Сертифікат",
"issueCertTooltip": "Видайте SSH-сертифікат для цього ключа, підписаний вашим ЦС",
"certIssued": "Сертифікат видано та завантажено",
"certIssueFailed": "Не вдалося видати сертифікат"
},
"credentials": {
"folders": "Папки",
"folder": "Папка",
@@ -144,7 +75,6 @@
"editCredentialAction": "Редагувати облікові дані",
"failedToCloneCredential": "Не вдалося клонувати облікові дані",
"failedToDeleteCredential": "Не вдалося видалити облікові дані",
"idBadge": "Ідентифікатор",
"keyBadge": "КЛЮЧ",
"passwordBadge": "Інвалід з інвалідністю",
"renameFolder": "Перейменувати папку",
@@ -525,7 +455,6 @@
"fileManager": "Файловий менеджер",
"serverStats": "Метрики хоста",
"admin": "Адміністратор",
"termixId": "Ідентифікатор",
"userProfile": "Профіль користувача",
"splitScreen": "Розділений екран",
"confirmClose": "Закрити цей активний сеанс?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "Không thể kết nối",
"reconnect": "Kết nối lại"
},
"termixId": {
"title": "Termix ID",
"loadFailed": "Tải Termix ID thất bại",
"claimTitle": "Nhận Termix ID của bạn",
"claimIntro": "Chọn một tên người dùng duy nhất. Khóa công khai SSH của bạn sẽ được công bố tại một URL công khai mà bạn có thể thêm vào tệp authorized_keys của bất kỳ máy chủ nào.",
"handleLabel": "Tên người dùng",
"handlePlaceholder": "alice",
"checking": "Đang kiểm tra…",
"available": "Có sẵn",
"taken": "Đã được sử dụng",
"invalidHandle": "Chỉ chữ thường, số, - và _",
"descriptionLabel": "Mô tả (tùy chọn)",
"descriptionPlaceholder": "Khóa laptop & điện thoại",
"create": "Tạo Termix ID",
"created": "Đã tạo Termix ID",
"createFailed": "Tạo Termix ID thất bại",
"deleteConfirm": "Xóa Termix ID và tất cả khóa đã công bố? Các máy chủ bạn đã cung cấp sẽ vẫn giữ khóa cho đến khi bạn xóa thủ công.",
"deleted": "Đã xóa Termix ID",
"deleteFailed": "Xóa Termix ID thất bại",
"copyFailed": "Sao chép thất bại",
"resolverUrlLabel": "URL phân giải công khai",
"provisionLabel": "Cung cấp máy chủ",
"publishTitle": "Công bố khóa công khai",
"generate": "Tạo",
"generateTooltip": "Tạo cặp khóa Ed25519 — công bố khóa công khai và tải khóa riêng về thiết bị của bạn",
"saveToVault": "Lưu vào Kho thông tin",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "Nhãn (tùy chọn)",
"add": "Thêm",
"importFromCredential": "Hoặc nhập từ một thông tin đăng nhập đã lưu",
"keyPublished": "Đã công bố khóa",
"addKeyFailed": "Thêm khóa thất bại",
"generatedSaved": "Đã tạo cặp khóa và lưu vào kho thông tin đăng nhập của bạn. Khóa riêng cũng đã được tải xuống.",
"generatedOnly": "Đã tạo cặp khóa — khóa riêng đã được tải xuống (chỉ hiển thị một lần).",
"generateFailed": "Tạo khóa thất bại",
"imported": "Đã nhập khóa từ thông tin đăng nhập",
"importFailed": "Nhập khóa thất bại",
"noKeys": "Chưa có khóa công khai được công bố.",
"keysTitle": "Khóa đã công bố",
"published": "Đã công bố",
"hidden": "Ẩn",
"keyRemoved": "Đã xóa khóa",
"removeKeyFailed": "Xóa khóa thất bại",
"updateKeyFailed": "Cập nhật khóa thất bại",
"fromVault": "Từ kho thông tin xác thực",
"linkedToTermixId": "Công bố qua Termix ID",
"selectCredential": "Chọn thông tin xác thực...",
"import": "Nhập",
"caTitle": "Cơ quan chứng thực",
"caIntro": "Tin cậy CA này trên máy chủ và nó chấp nhận mọi chứng chỉ do bạn ký. Xoay để thu hồi tất cả cùng lúc; chứng chỉ cũng tự hết hạn.",
"caEnable": "Bật CA",
"caEnabled": "Đã bật CA",
"caCreateFailed": "Bật CA thất bại",
"caPublicKeyLabel": "Khóa công khai CA",
"caTrustLabel": "Tin cậy trên máy chủ (chạy với quyền root)",
"caRotate": "Xoay",
"caRotateConfirm": "Xoay CA? Mọi chứng chỉ do CA này ký sẽ không còn được chấp nhận và phải được cấp lại.",
"caRotated": "Đã xoay CA — các chứng chỉ trước bị thu hồi",
"caRotateFailed": "Xoay CA thất bại",
"caDelete": "Gỡ bỏ CA",
"caDeleteConfirm": "Gỡ bỏ cơ quan chứng thực?",
"caDeleted": "Đã gỡ CA",
"caDeleteFailed": "Gỡ CA thất bại",
"caValidityLabel": "Hiệu lực chứng chỉ (ngày)",
"issueCert": "Chứng chỉ",
"issueCertTooltip": "Cấp chứng chỉ SSH cho khóa này, được ký bởi CA của bạn",
"certIssued": "Chứng chỉ đã được cấp và tải xuống",
"certIssueFailed": "Cấp chứng chỉ thất bại"
},
"credentials": {
"folders": "Thư mục",
"folder": "Thư mục",
@@ -144,7 +75,6 @@
"editCredentialAction": "Chỉnh sửa thông tin đăng nhập",
"failedToCloneCredential": "Không thể sao chép thông tin xác thực.",
"failedToDeleteCredential": "Không thể xóa thông tin đăng nhập",
"idBadge": "NHẬN DẠNG",
"keyBadge": "CHÌA KHÓA",
"passwordBadge": "Người khuyết tật",
"renameFolder": "Đổi tên thư mục",
@@ -525,7 +455,6 @@
"fileManager": "Quản lý tệp",
"serverStats": "Chỉ số máy chủ",
"admin": "Quản trị",
"termixId": "ID",
"userProfile": "Hồ sơ người dùng",
"splitScreen": "Chia màn hình",
"confirmClose": "Đóng phiên đang hoạt động?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "无法连接",
"reconnect": "重新连接"
},
"termixId": {
"title": "Termix ID",
"loadFailed": "Termix ID 加载失败",
"claimTitle": "认领您的 Termix ID",
"claimIntro": "选择一个唯一的用户名。您的 SSH 公钥将发布在一个公开 URL 上,您可以将该 URL 添加到任意服务器的 authorized_keys 文件中。",
"handleLabel": "用户名",
"handlePlaceholder": "alice",
"checking": "检查中…",
"available": "可用",
"taken": "已被占用",
"invalidHandle": "仅允许小写字母、数字、- 和 _",
"descriptionLabel": "描述(可选)",
"descriptionPlaceholder": "工作笔记本和手机密钥",
"create": "创建 Termix ID",
"created": "Termix ID 已创建",
"createFailed": "创建 Termix ID 失败",
"deleteConfirm": "删除您的 Termix ID 和所有已发布的密钥?您配置的服务器将保留密钥,直到您手动移除。",
"deleted": "Termix ID 已删除",
"deleteFailed": "删除 Termix ID 失败",
"copyFailed": "复制失败",
"resolverUrlLabel": "公开解析器 URL",
"provisionLabel": "配置服务器",
"publishTitle": "发布公钥",
"generate": "生成",
"generateTooltip": "生成 Ed25519 密钥对 — 发布公钥并将私钥下载到您的设备",
"saveToVault": "保存到凭据",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "标签(可选)",
"add": "添加",
"importFromCredential": "或从已存储的凭据导入",
"keyPublished": "密钥已发布",
"addKeyFailed": "添加密钥失败",
"generatedSaved": "密钥对已生成并保存到您的凭据库。私钥也已下载。",
"generatedOnly": "密钥对已生成 — 私钥已下载(仅显示一次)。",
"generateFailed": "密钥生成失败",
"imported": "密钥已从凭据导入",
"importFailed": "导入密钥失败",
"noKeys": "尚未发布公钥。",
"keysTitle": "已发布的密钥",
"published": "已发布",
"hidden": "隐藏",
"keyRemoved": "密钥已移除",
"removeKeyFailed": "移除密钥失败",
"updateKeyFailed": "更新密钥失败",
"fromVault": "来自凭据 Vault",
"linkedToTermixId": "通过 Termix ID 发布",
"selectCredential": "选择凭据...",
"import": "导入",
"caTitle": "证书颁发机构",
"caIntro": "在服务器上信任此 CA,它将接受您签名的任何证书。轮换可一次性吊销所有证书;证书也会自动过期。",
"caEnable": "启用 CA",
"caEnabled": "CA 已启用",
"caCreateFailed": "启用 CA 失败",
"caPublicKeyLabel": "CA 公钥",
"caTrustLabel": "在服务器上信任(以 root 身份运行)",
"caRotate": "轮换",
"caRotateConfirm": "轮换 CA?其已签名的所有证书将停止被接受,必须重新签发。",
"caRotated": "CA 已轮换 — 先前证书已吊销",
"caRotateFailed": "轮换 CA 失败",
"caDelete": "移除 CA",
"caDeleteConfirm": "移除证书颁发机构?",
"caDeleted": "CA 已移除",
"caDeleteFailed": "移除 CA 失败",
"caValidityLabel": "证书有效期(天)",
"issueCert": "证书",
"issueCertTooltip": "为此密钥签发一个 SSH 证书,由您的 CA 签名",
"certIssued": "证书已签发并下载",
"certIssueFailed": "签发证书失败"
},
"credentials": {
"folders": "文件夹",
"folder": "文件夹",
@@ -144,7 +75,6 @@
"editCredentialAction": "编辑凭据",
"failedToCloneCredential": "克隆凭据失败",
"failedToDeleteCredential": "删除凭据失败",
"idBadge": "ID",
"keyBadge": "密钥",
"passwordBadge": "密码",
"renameFolder": "重命名文件夹",
@@ -525,7 +455,6 @@
"fileManager": "文件管理器",
"serverStats": "主机指标",
"admin": "管理",
"termixId": "ID",
"userProfile": "用户资料",
"splitScreen": "分屏",
"confirmClose": "关闭当前会话?",
-71
View File
@@ -5,75 +5,6 @@
"disconnected": "無法連接",
"reconnect": "重新連接"
},
"termixId": {
"title": "Termix ID",
"loadFailed": "無法載入 Termix ID",
"claimTitle": "註冊您的 Termix ID",
"claimIntro": "選擇一個唯一的使用者代號。您的 SSH 公開金鑰將發佈在一個公開 URL,您可以將其加入任何伺服器的 authorized_keys 檔案中。",
"handleLabel": "代號",
"handlePlaceholder": "alice",
"checking": "檢查中…",
"available": "可用",
"taken": "已被使用",
"invalidHandle": "僅限小寫字母、數字、- 和 _",
"descriptionLabel": "描述(選填)",
"descriptionPlaceholder": "工作筆電和手機金鑰",
"create": "建立 Termix ID",
"created": "Termix ID 已建立",
"createFailed": "無法建立 Termix ID",
"deleteConfirm": "刪除您的 Termix ID 和所有已發佈的金鑰?您已佈建的伺服器將保留這些金鑰,直到您手動移除為止。",
"deleted": "Termix ID 已刪除",
"deleteFailed": "無法刪除 Termix ID",
"copyFailed": "複製失敗",
"resolverUrlLabel": "公開解析器 URL",
"provisionLabel": "佈建伺服器",
"publishTitle": "發佈公開金鑰",
"generate": "產生",
"generateTooltip": "產生一組 Ed25519 金鑰對 — 發佈公開金鑰並將私密金鑰下載到您的裝置",
"saveToVault": "儲存至憑證庫",
"keyPlaceholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host",
"labelPlaceholder": "標籤(選填)",
"add": "新增",
"importFromCredential": "或從已儲存的憑證匯入",
"keyPublished": "金鑰已發佈",
"addKeyFailed": "無法新增金鑰",
"generatedSaved": "金鑰對已產生並儲存至您的憑證庫。私密金鑰也已下載。",
"generatedOnly": "金鑰對已產生 — 私密金鑰已下載(僅顯示一次)。",
"generateFailed": "無法產生金鑰",
"imported": "已從憑證匯入金鑰",
"importFailed": "匯入金鑰失敗",
"noKeys": "尚未發布任何公開金鑰。",
"keysTitle": "已發布的金鑰",
"published": "已發布",
"hidden": "已隱藏",
"keyRemoved": "金鑰已移除",
"removeKeyFailed": "移除金鑰失敗",
"updateKeyFailed": "更新金鑰失敗",
"fromVault": "來自憑證庫",
"linkedToTermixId": "透過 Termix ID 發布",
"selectCredential": "選擇一個憑證...",
"import": "匯入",
"caTitle": "憑證授權中心",
"caIntro": "在伺服器上信任此 CA,即可接受您簽署的任何憑證。輪替可一次性撤銷所有憑證;憑證也會自行過期。",
"caEnable": "啟用 CA",
"caEnabled": "CA 已啟用",
"caCreateFailed": "啟用 CA 失敗",
"caPublicKeyLabel": "CA 公開金鑰",
"caTrustLabel": "在伺服器上信任(以 root 執行)",
"caRotate": "輪替",
"caRotateConfirm": "輪替 CA?所有由其簽署的憑證將不再被接受,必須重新簽發。",
"caRotated": "CA 已輪替 — 之前的憑證已撤銷",
"caRotateFailed": "輪替 CA 失敗",
"caDelete": "移除 CA",
"caDeleteConfirm": "移除憑證授權中心?",
"caDeleted": "CA 已移除",
"caDeleteFailed": "移除 CA 失敗",
"caValidityLabel": "憑證有效期(天)",
"issueCert": "憑證",
"issueCertTooltip": "為此金鑰簽發 SSH 憑證,由您的 CA 簽署",
"certIssued": "憑證已簽發並下載",
"certIssueFailed": "簽發憑證失敗"
},
"credentials": {
"folders": "資料夾",
"folder": "資料夾",
@@ -144,7 +75,6 @@
"editCredentialAction": "編輯憑證",
"failedToCloneCredential": "克隆憑證失敗",
"failedToDeleteCredential": "刪除憑證失敗",
"idBadge": "ID",
"keyBadge": "鑰匙",
"passwordBadge": "公共工程部",
"renameFolder": "重新命名資料夾",
@@ -525,7 +455,6 @@
"fileManager": "檔案管理員",
"serverStats": "主機指標",
"admin": "管理",
"termixId": "ID",
"userProfile": "使用者設定",
"splitScreen": "分割畫面",
"confirmClose": "關閉此使用中的工作階段?",
-28
View File
@@ -1850,31 +1850,3 @@ export {
type ActiveSessionInfo,
type UserPreferences,
} from "@/api/open-tabs-api";
// ============================================================================
// TERMIX ID API
// ============================================================================
export {
getMyTermixId,
checkTermixIdHandle,
createTermixId,
updateTermixId,
deleteTermixId,
addTermixIdKey,
generateTermixIdKey,
setTermixIdKeyEnabled,
deleteTermixIdKey,
getMyCa,
createCa,
rotateCa,
deleteCa,
issueCertificate,
getLinkedCredentialIds,
type TermixIdentity,
type TermixIdentityKey,
type TermixIdMe,
type GeneratedKey,
type TermixIdCa,
type IssuedCertificate,
} from "@/api/termix-id-api";
+8
View File
@@ -42,6 +42,8 @@ import {
} from "./auth-registry";
import { pluginHostBridge, resolvePluginPermission } from "./bridge";
import { shell, tabsApi } from "./shell-bridge";
import { onRemoteServerChange, remoteServerUrl } from "./desktop";
import { isElectron } from "@/lib/electron";
import { withPluginScope } from "./scope";
import { manifestDeclares, type ViewKind } from "./view-ownership";
import { pluginKey } from "@/lib/plugin-i18n";
@@ -407,6 +409,12 @@ export function createPluginApp(
onReady: (listener) => track(tabsApi.onReady(listener)),
},
desktop: {
available: isElectron(),
remoteServerUrl,
onRemoteServerChange: (listener) => track(onRemoteServerChange(listener)),
},
onDispose(dispose) {
if (disposed) {
dispose();
+24
View File
@@ -0,0 +1,24 @@
/** app.desktop: the desktop app's remote sync connection, for plugins. */
import { isElectron } from "@/lib/electron";
export async function remoteServerUrl(): Promise<string | null> {
if (!isElectron()) return null;
try {
const config = (await window.electronAPI?.invoke?.(
"get-remote-sync-config",
)) as { serverUrl?: string } | null | undefined;
return config?.serverUrl || null;
} catch {
return null;
}
}
export function onRemoteServerChange(listener: () => void): () => void {
const unsubscribe = window.electronAPI?.onRemoteSyncStatusChanged?.(() =>
listener(),
);
return () => {
if (typeof unsubscribe === "function") unsubscribe();
};
}
+2 -2
View File
@@ -18,10 +18,10 @@ import { getTabType } from "./tab-registry";
* purpose: the shell always keeps one alive as the fallback tab, so it is
* not a meaningful part of an arrangement.
*/
const CORE_CAPTURABLE = new Set(["termix-id", "macros", "ssh-tools"]);
const CORE_CAPTURABLE = new Set(["macros", "ssh-tools"]);
/** Core types reopened as singletons, with an optional preselected host. */
const CORE_SINGLETON = new Set(["termix-id", "macros", "ssh-tools"]);
const CORE_SINGLETON = new Set(["macros", "ssh-tools"]);
/** Core tab types that are never part of a saved arrangement. */
const CORE_UNSAVED = new Set([
-13
View File
@@ -6,7 +6,6 @@ import {
Server,
Settings,
User,
Fingerprint,
Hammer,
} from "lucide-react";
import { lazy, Suspense } from "react";
@@ -31,9 +30,6 @@ const DashboardTab = lazy(() =>
})),
);
// Rail panels promoted to full tabs.
const TermixIdPanel = lazy(() =>
import("@/sidebar/TermixIdPanel").then((m) => ({ default: m.TermixIdPanel })),
);
const MacrosPanel = lazy(() =>
import("@/sidebar/MacrosPanel").then((m) => ({ default: m.MacrosPanel })),
);
@@ -108,8 +104,6 @@ export function tabIcon(type: TabType) {
return <User className="size-3.5" />;
case "admin-settings":
return <Settings className="size-3.5" />;
case "termix-id":
return <Fingerprint className="size-3.5" />;
case "macros":
return <Braces className="size-3.5" />;
@@ -203,13 +197,6 @@ export function renderTabContent(tab: Tab, context: TabRenderContext) {
/>,
);
case "termix-id":
return withTabSuspense(
<PanelTabFrame>
<TermixIdPanel />
</PanelTabFrame>,
);
case "split-screen":
return null;
+1 -45
View File
@@ -11,7 +11,6 @@ import {
User,
} from "lucide-react";
import type { SplitMode, TabType, ToolsTab } from "@/types/ui-types";
import { isElectron } from "@/lib/electron";
import { readRailPreference, setRailPreference } from "./rail-preferences";
import { useRailItems, type RailItemDef } from "./rail-items";
@@ -19,7 +18,6 @@ import { useRailItems, type RailItemDef } from "./rail-items";
export type CoreRailView =
| "hosts"
| "credentials"
| "termix-id"
| "quick-connect"
| ToolsTab
| "connections"
@@ -199,50 +197,8 @@ export function AppRail({
return () => window.removeEventListener("hiddenRailTabsChanged", handler);
}, []);
// Termix ID publishes SSH public keys under a claimed public handle for
// other servers to fetch -- meaningless for a standalone desktop install
// with no synced multi-device account, so it stays hidden until a remote
// server is actually connected.
const [isRemoteSyncConnected, setIsRemoteSyncConnected] = useState(
() => !isElectron(),
);
useEffect(() => {
if (!isElectron()) return;
let cancelled = false;
const refreshSyncStatus = () => {
window.electronAPI
?.invoke?.("get-remote-sync-config")
.then((config) => {
if (!cancelled) {
setIsRemoteSyncConnected(
!!(config as { serverUrl?: string } | null)?.serverUrl,
);
}
})
.catch(() => {});
};
refreshSyncStatus();
const unsubscribe = window.electronAPI?.onRemoteSyncStatusChanged?.(() =>
refreshSyncStatus(),
);
return () => {
cancelled = true;
unsubscribe?.();
};
}, []);
const railExpanded = pinned || (expandOnHover && hovered);
const effectiveHiddenTabs = new Set([
...hiddenTabs,
...(isRemoteSyncConnected ? [] : ["termix-id"]),
]);
const railButtons = buildRailButtons(
railItems,
splitMode,
t,
effectiveHiddenTabs,
);
const railButtons = buildRailButtons(railItems, splitMode, t, hiddenTabs);
const setRailPinned = (nextPinned: boolean) => {
setPinned(nextPinned);
localStorage.setItem("pinAppRail", String(nextPinned));
+5 -13
View File
@@ -17,6 +17,7 @@ import { toast } from "sonner";
import { Button } from "@/components/button";
import { getCredentialDetails } from "@/main-axios";
import { copyToClipboard } from "@/lib/clipboard";
import { ComponentSlot } from "@/shell/ActionSlot";
import type { Host, Credential } from "@/types/ui-types";
type CredentialWithCertificate = Credential & { certPublicKey?: string };
@@ -29,7 +30,6 @@ function CredentialItem({
cred,
usedByHosts,
stripeIndex,
termixIdLinked,
onDeploy,
onEdit,
onDelete,
@@ -37,7 +37,6 @@ function CredentialItem({
cred: Credential;
usedByHosts: Host[];
stripeIndex: number;
termixIdLinked?: boolean;
onDeploy: () => void;
onEdit: () => void;
onDelete: () => void;
@@ -68,11 +67,10 @@ function CredentialItem({
>
{isKey ? "KEY" : "PWD"}
</span>
{termixIdLinked && (
<span className="text-[9px] px-1 py-px font-bold border leading-none shrink-0 border-accent-brand/30 text-accent-brand/70">
ID
</span>
)}
<ComponentSlot
slotId="credentials.badges"
props={{ credentialId: Number(cred.id) }}
/>
</div>
{/* Username row */}
@@ -189,7 +187,6 @@ function CredentialFolderItem({
stripeOffset,
editingFolderName,
editingFolderValue,
termixIdLinkedIds,
onEditingFolderNameChange,
onEditingFolderValueChange,
onRenameFolder,
@@ -203,7 +200,6 @@ function CredentialFolderItem({
stripeOffset: number;
editingFolderName: string | null;
editingFolderValue: string;
termixIdLinkedIds?: Set<number>;
onEditingFolderNameChange: (name: string | null) => void;
onEditingFolderValueChange: (value: string) => void;
onRenameFolder: (folder: string, newName: string) => Promise<void>;
@@ -290,7 +286,6 @@ function CredentialFolderItem({
cred={cred}
usedByHosts={usedByHosts}
stripeIndex={stripeOffset + 1 + i}
termixIdLinked={termixIdLinkedIds?.has(Number(cred.id))}
onDeploy={() => onDeploy(cred)}
onEdit={() => onEdit(cred)}
onDelete={() => onDelete(cred)}
@@ -310,7 +305,6 @@ export function HostCredentialList({
allHosts,
editingFolderName,
editingFolderValue,
termixIdLinkedIds,
onEditingFolderNameChange,
onEditingFolderValueChange,
onRenameFolder,
@@ -326,7 +320,6 @@ export function HostCredentialList({
allHosts: Host[];
editingFolderName: string | null;
editingFolderValue: string;
termixIdLinkedIds?: Set<number>;
onEditingFolderNameChange: (name: string | null) => void;
onEditingFolderValueChange: (value: string) => void;
onRenameFolder: (folder: string, newName: string) => Promise<void>;
@@ -408,7 +401,6 @@ export function HostCredentialList({
stripeOffset={offset}
editingFolderName={editingFolderName}
editingFolderValue={editingFolderValue}
termixIdLinkedIds={termixIdLinkedIds}
onEditingFolderNameChange={onEditingFolderNameChange}
onEditingFolderValueChange={onEditingFolderValueChange}
onRenameFolder={onRenameFolder}
-12
View File
@@ -26,7 +26,6 @@ import {
updateCredential,
deployCredentialToHost,
renameCredentialFolder,
getLinkedCredentialIds,
} from "@/main-axios";
import type { Host, Credential } from "@/types/ui-types";
@@ -127,9 +126,6 @@ export function HostManager({
const [credentialReturnHost, setCredentialReturnHost] = useState<
Host | "new" | null
>(null);
const [termixIdLinkedIds, setTermixIdLinkedIds] = useState<Set<number>>(
new Set(),
);
useEffect(() => {
onTagsChange?.([...new Set(credentials.flatMap((c) => c.tags ?? []))]);
@@ -168,16 +164,9 @@ export function HostManager({
.finally(() => setCredentialsLoading(false));
};
const reloadLinkedIds = () => {
getLinkedCredentialIds()
.then((d) => setTermixIdLinkedIds(new Set(d.credentialIds)))
.catch(() => {});
};
useEffect(() => {
reloadHosts();
reloadCredentials();
reloadLinkedIds();
window.addEventListener("termix:hosts-changed", reloadHosts);
window.addEventListener("ssh-hosts:changed", reloadHosts);
@@ -711,7 +700,6 @@ export function HostManager({
<CredentialSidebarTree
folders={credentialFolderTree}
usedByCounts={usedByCounts}
termixIdLinkedIds={termixIdLinkedIds}
query=""
loading={credentialsLoading}
arrangeLocked={externalArrangeLocked}
-946
View File
@@ -1,946 +0,0 @@
import { getErrorMessage } from "../lib/error-message.js";
import { useEffect, useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import {
Check,
Copy,
Fingerprint,
KeyRound,
Loader2,
Plus,
RefreshCw,
ScrollText,
ShieldCheck,
Sparkles,
Trash2,
} from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/button";
import { Input } from "@/components/input";
import { Textarea } from "@/components/textarea";
import { SectionCard, SettingRow, FakeSwitch } from "@/components/section-card";
import {
getMyTermixId,
checkTermixIdHandle,
createTermixId,
deleteTermixId,
addTermixIdKey,
generateTermixIdKey,
setTermixIdKeyEnabled,
deleteTermixIdKey,
getCredentials,
getMyCa,
createCa,
rotateCa,
deleteCa,
issueCertificate,
getLinkedCredentialIds,
type TermixIdentity,
type TermixIdentityKey,
type TermixIdCa,
} from "@/main-axios";
import { Select2 } from "@/components/select2";
const accentBtn =
"border-accent-brand/40 text-accent-brand hover:bg-accent-brand/20 hover:text-accent-brand";
function resolverUrl(handle: string): string {
const origin =
typeof window !== "undefined"
? window.location.origin
: "https://your-termix";
return `${origin}/termix-id/u/${handle}`;
}
function CopyRow({ label, value }: { label: string; value: string }) {
const { t } = useTranslation();
const [copied, setCopied] = useState(false);
async function copy() {
try {
await navigator.clipboard.writeText(value);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch {
toast.error(t("termixId.copyFailed"));
}
}
return (
<div className="flex flex-col gap-1.5 py-2.5 border-b border-border last:border-0">
<span className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
{label}
</span>
<div className="flex items-stretch gap-2">
<code className="flex-1 min-w-0 text-[11px] font-mono bg-muted/30 border border-border px-2 py-1.5 overflow-x-auto whitespace-nowrap text-muted-foreground">
{value}
</code>
<button
onClick={copy}
className="shrink-0 w-7 flex items-center justify-center border border-border bg-background hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
>
{copied ? (
<Check className="size-3.5 text-accent-brand" />
) : (
<Copy className="size-3.5" />
)}
</button>
</div>
</div>
);
}
export function TermixIdPanel() {
const { t } = useTranslation();
const [loading, setLoading] = useState(true);
const [identity, setIdentity] = useState<TermixIdentity | null>(null);
const [keys, setKeys] = useState<TermixIdentityKey[]>([]);
const [ca, setCa] = useState<TermixIdCa | null>(null);
const [linkedCredentialIds, setLinkedCredentialIds] = useState<Set<number>>(
new Set(),
);
const refresh = useCallback(async () => {
try {
const [data, caData, linkedData] = await Promise.all([
getMyTermixId(),
getMyCa().catch(() => ({ ca: null })),
getLinkedCredentialIds().catch(() => ({ credentialIds: [] })),
]);
setIdentity(data.identity);
setKeys(data.keys);
setCa(data.identity ? caData.ca : null);
setLinkedCredentialIds(new Set(linkedData.credentialIds));
} catch {
toast.error(t("termixId.loadFailed"));
} finally {
setLoading(false);
}
}, [t]);
useEffect(() => {
refresh();
}, [refresh]);
if (loading) {
return (
<div className="flex items-center justify-center h-40 text-muted-foreground">
<Loader2 className="animate-spin size-4" />
</div>
);
}
return (
<div className="flex-1 min-h-0 overflow-y-auto scrollbar-none">
<div className="flex flex-col gap-3 p-3">
{!identity ? (
<ClaimHandle onCreated={refresh} />
) : (
<>
<IdentityCard identity={identity} onChanged={refresh} />
<AddKey
handle={identity.handle}
linkedCredentialIds={linkedCredentialIds}
onAdded={refresh}
/>
<KeyList
keys={keys}
handle={identity.handle}
caEnabled={!!ca}
onChanged={refresh}
/>
<CaCard handle={identity.handle} ca={ca} onChanged={refresh} />
</>
)}
</div>
</div>
);
}
function ClaimHandle({ onCreated }: { onCreated: () => void }) {
const { t } = useTranslation();
const [handle, setHandle] = useState("");
const [description, setDescription] = useState("");
const [checking, setChecking] = useState(false);
const [status, setStatus] = useState<
"idle" | "available" | "taken" | "invalid"
>("idle");
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
const h = handle.trim().toLowerCase();
if (!h) {
setStatus("idle");
return;
}
// Guard against an out-of-order response: if the input changed (effect
// cleanup ran) before this request resolved, ignore its result.
let cancelled = false;
setChecking(true);
const timer = setTimeout(async () => {
try {
const res = await checkTermixIdHandle(h);
if (cancelled) return;
setStatus(
!res.valid ? "invalid" : res.available ? "available" : "taken",
);
} catch {
if (!cancelled) setStatus("idle");
} finally {
if (!cancelled) setChecking(false);
}
}, 350);
return () => {
cancelled = true;
clearTimeout(timer);
};
}, [handle]);
async function submit() {
setSubmitting(true);
try {
await createTermixId(
handle.trim().toLowerCase(),
description.trim() || undefined,
);
toast.success(t("termixId.created"));
onCreated();
} catch (e) {
toast.error(getErrorMessage(e, t("termixId.createFailed")));
} finally {
setSubmitting(false);
}
}
return (
<SectionCard
title={t("termixId.title")}
icon={<Fingerprint className="size-3.5" />}
action={
<a
href="https://docs.termix.site/features/authentication/termix-id"
target="_blank"
rel="noreferrer"
className="text-[10px] text-accent-brand hover:underline"
>
{t("hosts.docsLink")}
</a>
}
>
<div className="flex flex-col gap-3 py-3">
<p className="text-xs text-muted-foreground leading-snug">
{t("termixId.claimIntro")}
</p>
<div className="flex flex-col gap-1.5">
<span className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
{t("termixId.handleLabel")}
</span>
<div className="relative flex items-center">
<span className="absolute left-2 text-muted-foreground text-xs pointer-events-none select-none">
@
</span>
<Input
value={handle}
placeholder={t("termixId.handlePlaceholder")}
autoCapitalize="none"
spellCheck={false}
className="h-8 text-xs pl-5"
onChange={(e) => setHandle(e.target.value)}
/>
</div>
<span className="text-[11px] h-4 leading-4">
{checking ? (
<span className="text-muted-foreground">
{t("termixId.checking")}
</span>
) : status === "available" ? (
<span className="text-accent-brand">
{t("termixId.available")}
</span>
) : status === "taken" ? (
<span className="text-destructive">{t("termixId.taken")}</span>
) : status === "invalid" ? (
<span className="text-destructive">
{t("termixId.invalidHandle")}
</span>
) : null}
</span>
</div>
<div className="flex flex-col gap-1.5">
<span className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
{t("termixId.descriptionLabel")}
</span>
<Input
value={description}
placeholder={t("termixId.descriptionPlaceholder")}
className="h-8 text-xs"
onChange={(e) => setDescription(e.target.value)}
/>
</div>
<Button
variant="outline"
size="sm"
className={`self-start ${accentBtn}`}
disabled={status !== "available" || submitting}
onClick={submit}
>
{submitting ? (
<Loader2 className="animate-spin size-3.5" />
) : (
t("termixId.create")
)}
</Button>
</div>
</SectionCard>
);
}
function IdentityCard({
identity,
onChanged,
}: {
identity: TermixIdentity;
onChanged: () => void;
}) {
const { t } = useTranslation();
const [confirming, setConfirming] = useState(false);
const [busy, setBusy] = useState(false);
const url = resolverUrl(identity.handle);
const curl = `curl -fsSL ${url} >> ~/.ssh/authorized_keys`;
async function remove() {
setBusy(true);
try {
await deleteTermixId();
toast.success(t("termixId.deleted"));
onChanged();
} catch (e) {
toast.error(getErrorMessage(e, t("termixId.deleteFailed")));
} finally {
setBusy(false);
setConfirming(false);
}
}
return (
<SectionCard
title={t("termixId.title")}
icon={<Fingerprint className="size-3.5" />}
action={
confirming ? (
<div className="flex items-center gap-1">
<Button
variant="outline"
size="sm"
className="border-destructive/40 text-destructive hover:bg-destructive/10 hover:text-destructive"
onClick={remove}
disabled={busy}
>
{busy ? (
<Loader2 className="animate-spin size-3.5" />
) : (
t("common.confirm")
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setConfirming(false)}
>
{t("nav.cancel")}
</Button>
</div>
) : (
<div className="flex items-center gap-1">
<a
href="https://docs.termix.site/features/authentication/termix-id"
target="_blank"
rel="noreferrer"
className="text-[10px] text-accent-brand hover:underline px-1"
>
{t("hosts.docsLink")}
</a>
<Button
variant="ghost"
size="icon-sm"
onClick={() => setConfirming(true)}
className="hover:text-destructive"
>
<Trash2 className="size-3.5" />
</Button>
</div>
)
}
>
{confirming && (
<div className="py-2 border-b border-border">
<p className="text-xs text-muted-foreground">
{t("termixId.deleteConfirm")}
</p>
</div>
)}
<div className="flex items-center gap-2 py-2.5 border-b border-border">
<span className="text-sm font-semibold text-accent-brand">
@{identity.handle}
</span>
</div>
<CopyRow label={t("termixId.resolverUrlLabel")} value={url} />
<CopyRow label={t("termixId.provisionLabel")} value={curl} />
</SectionCard>
);
}
interface CredentialOption {
id: number;
name: string;
}
function downloadText(filename: string, text: string) {
// octet-stream so the browser/OS keeps the given extension instead of
// appending .txt to a text/plain download.
const blob = new Blob([text], { type: "application/octet-stream" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
function CredentialImportRow({
credentials,
linkedCredentialIds,
submitting,
onImport,
}: {
credentials: CredentialOption[];
linkedCredentialIds: Set<number>;
submitting: boolean;
onImport: (id: number) => void;
}) {
const { t } = useTranslation();
const [selected, setSelected] = useState<string>("");
const selectedId = selected ? Number(selected) : null;
const alreadyLinked =
selectedId !== null && linkedCredentialIds.has(selectedId);
return (
<div className="flex items-center gap-2">
<Select2
value={selected}
onChange={(e) => setSelected(e.target.value)}
disabled={submitting}
className="flex-1 h-8 min-w-0 border border-border bg-background px-2 text-xs outline-none focus:ring-1 focus:ring-ring disabled:opacity-50 disabled:cursor-not-allowed"
>
<option value="" disabled>
{t("termixId.selectCredential")}
</option>
{credentials.map((c) => (
<option key={c.id} value={String(c.id)}>
{linkedCredentialIds.has(c.id) ? `✓ ${c.name}` : c.name}
</option>
))}
</Select2>
<Button
variant="outline"
size="default"
className={`h-8 shrink-0 ${alreadyLinked ? "border-accent-brand/20 text-accent-brand/50" : accentBtn}`}
disabled={!selectedId || submitting || alreadyLinked}
onClick={() => selectedId !== null && onImport(selectedId)}
>
{alreadyLinked ? <Check className="size-3.5" /> : t("termixId.import")}
</Button>
</div>
);
}
function AddKey({
handle,
linkedCredentialIds,
onAdded,
}: {
handle: string;
linkedCredentialIds: Set<number>;
onAdded: () => void;
}) {
const { t } = useTranslation();
const [publicKey, setPublicKey] = useState("");
const [label, setLabel] = useState("");
const [submitting, setSubmitting] = useState(false);
const [generating, setGenerating] = useState(false);
const [saveToVault, setSaveToVault] = useState(true);
const [credentials, setCredentials] = useState<CredentialOption[]>([]);
const loadCredentials = useCallback(async () => {
try {
const data = await getCredentials();
const list = Array.isArray(data) ? data : [];
setCredentials(
list
.filter((c) => c.authType === "key")
.map((c) => ({ id: Number(c.id), name: String(c.name) })),
);
} catch {
// non-fatal — manual paste still works
}
}, []);
useEffect(() => {
loadCredentials();
}, [loadCredentials]);
async function addManual() {
if (!publicKey.trim()) return;
setSubmitting(true);
try {
await addTermixIdKey({
publicKey: publicKey.trim(),
label: label.trim() || undefined,
});
toast.success(t("termixId.keyPublished"));
setPublicKey("");
setLabel("");
onAdded();
} catch (e) {
toast.error(getErrorMessage(e, t("termixId.addKeyFailed")));
} finally {
setSubmitting(false);
}
}
async function generate() {
setGenerating(true);
try {
const result = await generateTermixIdKey("ed25519", saveToVault);
downloadText(`termix-${handle}-ed25519.key`, result.privateKey);
toast.success(
saveToVault
? t("termixId.generatedSaved")
: t("termixId.generatedOnly"),
);
if (saveToVault) loadCredentials();
onAdded();
} catch (e) {
toast.error(getErrorMessage(e, t("termixId.generateFailed")));
} finally {
setGenerating(false);
}
}
async function importCredential(id: number) {
setSubmitting(true);
try {
await addTermixIdKey({ credentialId: id });
toast.success(t("termixId.imported"));
onAdded();
} catch (e) {
toast.error(getErrorMessage(e, t("termixId.importFailed")));
} finally {
setSubmitting(false);
}
}
return (
<SectionCard
title={t("termixId.publishTitle")}
icon={<Plus className="size-3.5" />}
action={
<Button
variant="outline"
size="sm"
className={accentBtn}
onClick={generate}
disabled={generating}
title={t("termixId.generateTooltip")}
>
{generating ? (
<Loader2 className="animate-spin size-3.5" />
) : (
<>
<Sparkles className="size-3.5" />
{t("termixId.generate")}
</>
)}
</Button>
}
>
<div className="flex flex-col gap-3 py-3">
<SettingRow label={t("termixId.saveToVault")}>
<FakeSwitch checked={saveToVault} onChange={setSaveToVault} />
</SettingRow>
<Textarea
value={publicKey}
onChange={(e) => setPublicKey(e.target.value)}
placeholder={t("termixId.keyPlaceholder")}
rows={3}
className="rounded-none font-mono text-[11px]"
spellCheck={false}
/>
<div className="flex items-center gap-2">
<Input
value={label}
onChange={(e) => setLabel(e.target.value)}
placeholder={t("termixId.labelPlaceholder")}
className="h-8 text-xs flex-1"
/>
<Button
variant="outline"
size="default"
className={`h-8 ${accentBtn}`}
onClick={addManual}
disabled={!publicKey.trim() || submitting}
>
{submitting ? (
<Loader2 className="animate-spin size-3.5" />
) : (
t("termixId.add")
)}
</Button>
</div>
{credentials.length > 0 && (
<div className="flex flex-col gap-1.5 border-t border-border pt-3">
<span className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
{t("termixId.importFromCredential")}
</span>
<CredentialImportRow
credentials={credentials}
linkedCredentialIds={linkedCredentialIds}
submitting={submitting}
onImport={importCredential}
/>
</div>
)}
</div>
</SectionCard>
);
}
function KeyList({
keys,
handle,
caEnabled,
onChanged,
}: {
keys: TermixIdentityKey[];
handle: string;
caEnabled: boolean;
onChanged: () => void;
}) {
const { t } = useTranslation();
const [issuingId, setIssuingId] = useState<number | null>(null);
const [principalsByKey, setPrincipalsByKey] = useState<
Record<number, string>
>({});
async function toggle(k: TermixIdentityKey) {
try {
await setTermixIdKeyEnabled(k.id, !k.enabled);
onChanged();
} catch (e) {
toast.error(getErrorMessage(e, t("termixId.updateKeyFailed")));
}
}
async function remove(k: TermixIdentityKey) {
try {
await deleteTermixIdKey(k.id);
toast.success(t("termixId.keyRemoved"));
onChanged();
} catch (e) {
toast.error(getErrorMessage(e, t("termixId.removeKeyFailed")));
}
}
async function issueCert(k: TermixIdentityKey) {
setIssuingId(k.id);
try {
const res = await issueCertificate(k.id, {
principals: (principalsByKey[k.id] ?? "")
.split(",")
.map((principal) => principal.trim())
.filter(Boolean),
});
downloadText(`termix-${handle}-${k.id}-cert.pub`, res.certificate + "\n");
toast.success(t("termixId.certIssued"));
} catch (e) {
toast.error(getErrorMessage(e, t("termixId.certIssueFailed")));
} finally {
setIssuingId(null);
}
}
return (
<div className="flex flex-col border border-border bg-card">
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-border shrink-0">
<span className="text-muted-foreground">
<KeyRound className="size-3.5" />
</span>
<span className="text-xs font-semibold uppercase tracking-widest text-muted-foreground flex-1">
{t("termixId.keysTitle")}
</span>
</div>
{keys.length === 0 ? (
<p className="text-xs text-muted-foreground px-4 py-3">
{t("termixId.noKeys")}
</p>
) : (
keys.map((k) => {
const canCert = caEnabled && k.algorithm.toUpperCase() === "ED25519";
return (
<div
key={k.id}
className="flex flex-col px-3 py-2 gap-1 border-b border-border last:border-0"
>
{/* row 1: label + badges */}
<div className="flex items-center gap-1.5 min-w-0">
<span className="text-[10px] font-bold uppercase tracking-wide text-accent-brand border border-accent-brand/40 px-1 py-px shrink-0 leading-none">
{k.algorithm}
</span>
{k.credentialId && (
<span className="text-[9px] font-semibold uppercase tracking-widest text-accent-brand/60 border border-accent-brand/20 px-1 py-px shrink-0 leading-none">
{t("termixId.fromVault")}
</span>
)}
<span className="text-xs font-medium truncate min-w-0">
{k.label || k.comment || k.keyType}
</span>
</div>
{/* row 2: public key */}
<code className="text-[10px] font-mono text-muted-foreground truncate">
{k.publicKey}
</code>
{/* row 3: actions */}
<div className="flex items-center gap-1 pt-0.5">
<FakeSwitch checked={k.enabled} onChange={() => toggle(k)} />
{canCert && (
<>
<Input
value={principalsByKey[k.id] ?? ""}
onChange={(e) =>
setPrincipalsByKey((current) => ({
...current,
[k.id]: e.target.value,
}))
}
placeholder="SSH principal(s), e.g. root"
aria-label="SSH principal(s)"
className="h-7 min-w-0 flex-1 text-xs"
spellCheck={false}
/>
<Button
variant="outline"
size="icon-sm"
onClick={() => issueCert(k)}
disabled={
issuingId === k.id ||
!(principalsByKey[k.id] ?? "").trim()
}
title={t("termixId.issueCertTooltip")}
>
{issuingId === k.id ? (
<Loader2 className="animate-spin size-3.5" />
) : (
<ScrollText className="size-3.5" />
)}
</Button>
</>
)}
<Button
variant="ghost"
size="icon-sm"
onClick={() => remove(k)}
className="hover:text-destructive"
>
<Trash2 className="size-3.5" />
</Button>
</div>
</div>
);
})
)}
</div>
);
}
function CaCard({
handle,
ca,
onChanged,
}: {
handle: string;
ca: TermixIdCa | null;
onChanged: () => void;
}) {
const { t } = useTranslation();
const [busy, setBusy] = useState(false);
const [confirmingRotate, setConfirmingRotate] = useState(false);
const [confirmingDelete, setConfirmingDelete] = useState(false);
const caUrl =
typeof window !== "undefined"
? `${window.location.origin}/termix-id/u/${handle}/ca`
: `/termix-id/u/${handle}/ca`;
const trustCmd = `curl -fsSL ${caUrl} | sudo tee /etc/ssh/${handle}-ca.pub && echo "TrustedUserCAKeys /etc/ssh/${handle}-ca.pub" | sudo tee -a /etc/ssh/sshd_config && sudo systemctl reload sshd`;
async function enable() {
setBusy(true);
try {
await createCa();
toast.success(t("termixId.caEnabled"));
onChanged();
} catch (e) {
toast.error(getErrorMessage(e, t("termixId.caCreateFailed")));
} finally {
setBusy(false);
}
}
async function rotate() {
setBusy(true);
try {
await rotateCa();
toast.success(t("termixId.caRotated"));
onChanged();
} catch (e) {
toast.error(getErrorMessage(e, t("termixId.caRotateFailed")));
} finally {
setBusy(false);
setConfirmingRotate(false);
}
}
async function remove() {
setBusy(true);
try {
await deleteCa();
toast.success(t("termixId.caDeleted"));
onChanged();
} catch (e) {
toast.error(getErrorMessage(e, t("termixId.caDeleteFailed")));
} finally {
setBusy(false);
setConfirmingDelete(false);
}
}
return (
<SectionCard
title={t("termixId.caTitle")}
icon={<ShieldCheck className="size-3.5" />}
action={
!ca ? (
<Button
variant="outline"
size="sm"
className={accentBtn}
onClick={enable}
disabled={busy}
>
{busy ? (
<Loader2 className="animate-spin size-3.5" />
) : (
t("termixId.caEnable")
)}
</Button>
) : confirmingRotate ? (
<div className="flex items-center gap-1">
<Button
variant="outline"
size="sm"
className="border-destructive/40 text-destructive hover:bg-destructive/10 hover:text-destructive"
onClick={rotate}
disabled={busy}
>
{busy ? (
<Loader2 className="animate-spin size-3.5" />
) : (
t("termixId.caRotate")
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setConfirmingRotate(false)}
>
{t("nav.cancel")}
</Button>
</div>
) : confirmingDelete ? (
<div className="flex items-center gap-1">
<Button
variant="outline"
size="sm"
className="border-destructive/40 text-destructive hover:bg-destructive/10 hover:text-destructive"
onClick={remove}
disabled={busy}
>
{busy ? (
<Loader2 className="animate-spin size-3.5" />
) : (
t("termixId.caDelete")
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setConfirmingDelete(false)}
>
{t("nav.cancel")}
</Button>
</div>
) : (
<div className="flex items-center gap-1">
<Button
variant="outline"
size="sm"
onClick={() => setConfirmingRotate(true)}
disabled={busy}
>
<RefreshCw className="size-3.5" />
{t("termixId.caRotate")}
</Button>
<Button
variant="ghost"
size="icon-sm"
onClick={() => setConfirmingDelete(true)}
disabled={busy}
className="hover:text-destructive"
>
<Trash2 className="size-3.5" />
</Button>
</div>
)
}
>
<div className="py-2.5 border-b border-border">
<p className="text-xs text-muted-foreground leading-snug">
{t("termixId.caIntro")}
</p>
</div>
{confirmingRotate && (
<div className="py-2.5 border-b border-border">
<p className="text-xs text-destructive/80 leading-snug">
{t("termixId.caRotateConfirm")}
</p>
</div>
)}
{confirmingDelete && (
<div className="py-2.5 border-b border-border">
<p className="text-xs text-destructive/80 leading-snug">
{t("termixId.caDeleteConfirm")}
</p>
</div>
)}
{ca && (
<>
<CopyRow label={t("termixId.caTrustLabel")} value={trustCmd} />
<CopyRow
label={t("termixId.caPublicKeyLabel")}
value={ca.publicKey}
/>
</>
)}
</SectionCard>
);
}
@@ -12,6 +12,7 @@ import {
} from "lucide-react";
import { toast } from "sonner";
import { copyToClipboard } from "@/lib/clipboard";
import { ComponentSlot } from "@/shell/ActionSlot";
import type { Credential } from "@/types/ui-types";
import type {
CredentialDensity,
@@ -49,7 +50,6 @@ const CREDENTIAL_ITEM_DENSITY_TOKENS = {
export function CredentialItem({
cred,
usedByCount = 0,
termixIdLinked = false,
query = "",
stripeIndex = 0,
isMenuOpen = false,
@@ -76,7 +76,6 @@ export function CredentialItem({
}: {
cred: Credential;
usedByCount?: number;
termixIdLinked?: boolean;
query?: string;
stripeIndex?: number;
isMenuOpen?: boolean;
@@ -308,11 +307,10 @@ export function CredentialItem({
>
{isKey ? t("credentials.keyBadge") : t("credentials.passwordBadge")}
</span>
{termixIdLinked && (
<span className="text-[9px] px-1 py-px font-bold border leading-none shrink-0 border-accent-brand/30 text-accent-brand/70">
{t("credentials.idBadge")}
</span>
)}
<ComponentSlot
slotId="credentials.badges"
props={{ credentialId: Number(cred.id) }}
/>
{cred.pin && (
<Pin className="size-2.5 text-accent-brand/50 shrink-0" />
)}
@@ -35,7 +35,6 @@ export function CredentialSidebarTree({
onDeleteCredential,
onShareCredential,
usedByCounts,
termixIdLinkedIds,
query = "",
loading = false,
arrangeLocked = true,
@@ -57,7 +56,6 @@ export function CredentialSidebarTree({
onDeleteCredential: (cred: Credential) => void;
onShareCredential?: (cred: Credential) => void;
usedByCounts?: Map<string, number>;
termixIdLinkedIds?: Set<number>;
query?: string;
loading?: boolean;
/** When true, drag-to-rearrange is off entirely. Toggled from the panel header. */
@@ -398,7 +396,6 @@ export function CredentialSidebarTree({
<CredentialItem
cred={item}
usedByCount={usedByCounts?.get(item.id) ?? 0}
termixIdLinked={termixIdLinkedIds?.has(Number(item.id))}
query={query}
stripeIndex={vItem.index}
isMenuOpen={openMenuCredentialId === item.id}
-8
View File
@@ -1,6 +1,5 @@
import {
Braces,
Fingerprint,
Hammer,
KeyRound,
LayoutPanelLeft,
@@ -74,13 +73,6 @@ export const RAIL_ITEMS: RailItemDef[] = [
labelKey: "nav.credentials",
separatorAfter: true,
},
{
id: "termix-id",
icon: Fingerprint,
labelKey: "nav.termixId",
separatorAfter: true,
promotable: true,
},
{
id: "connections",
icon: Plug,
@@ -73,7 +73,6 @@ async function renderOpenedList({
allHosts={hosts}
editingFolderName={null}
editingFolderValue=""
termixIdLinkedIds={new Set()}
onEditingFolderNameChange={vi.fn()}
onEditingFolderValueChange={vi.fn()}
onRenameFolder={vi.fn()}
+3 -3
View File
@@ -51,11 +51,11 @@ describe("RAIL_ITEMS", () => {
// "sftp" moved to the file-manager plugin's own registerRailItem,
// "port-forwarding" to the tunnels plugin's, "serial" to the serial
// plugin's, "collab" to the session-sharing plugin's, and "session-logs"
// to the session-recording plugin's.
// to the session-recording plugin's, "termix-id" to the termix-identity
// plugin's.
expect(RAIL_ITEMS.map((item) => item.id)).toEqual([
"hosts",
"credentials",
"termix-id",
"connections",
"quick-connect",
"ssh-tools",
@@ -81,7 +81,7 @@ describe("RAIL_ITEMS", () => {
[...RAIL_ITEMS, ...RAIL_UTILITY_ITEMS]
.filter((item) => item.promotable)
.map((item) => item.id),
).toEqual(["termix-id", "ssh-tools", "macros"]);
).toEqual(["ssh-tools", "macros"]);
});
it("derives promotableIds from the promotable flag", () => {