feat: move vault ssh signing into a plugin (C5)

This commit is contained in:
LukeGus committed 2026-09-24 20:24:09 -05:00
1 parent b027717c46
commit 6e56c90aac
198 files changed
+19717 -3445

No files matched your search

+1 -1
View File
@@ -267,7 +267,7 @@ http {
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
}
location ~ ^/vault(/.*)?$ {
location = /vault/oidc/callback {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
+1 -1
View File
@@ -247,7 +247,7 @@ http {
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
}
location ~ ^/vault(/.*)?$ {
location = /vault/oidc/callback {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
@@ -0,0 +1,5 @@
-- vault_profiles and vault_tokens move to the vault plugin's adopted tables
-- at activation, and ssh_data.vault_profile_id is copied into the vault
-- plugin's host settings by a boot migration after this runs. Dropping any
-- of them here would lose the data first.
SELECT 1;
File diff suppressed because it is too large. Load diff
+7
View File
@@ -379,6 +379,13 @@
"when": 1790295071302,
"tag": "0053_magical_groot",
"breakpoints": true
},
{
"idx": 54,
"version": "5",
"when": 1790298804983,
"tag": "0054_wakeful_misty_knight",
"breakpoints": true
}
]
}
@@ -0,0 +1,5 @@
-- vault_profiles and vault_tokens move to the vault plugin's adopted tables
-- at activation, and ssh_data.vault_profile_id is copied into the vault
-- plugin's host settings by a boot migration after this runs. Dropping any
-- of them here would lose the data first.
SELECT 1;
File diff suppressed because it is too large. Load diff
+7
View File
@@ -386,6 +386,13 @@
"when": 1790295070561,
"tag": "0054_cloudy_galactus",
"breakpoints": true
},
{
"idx": 55,
"version": "7",
"when": 1790298804216,
"tag": "0055_jazzy_quasimodo",
"breakpoints": true
}
]
}
+5
View File
@@ -0,0 +1,5 @@
-- vault_profiles and vault_tokens move to the vault plugin's adopted tables
-- at activation, and ssh_data.vault_profile_id is copied into the vault
-- plugin's host settings by a boot migration after this runs. Dropping any
-- of them here would lose the data first.
SELECT 1;
File diff suppressed because it is too large. Load diff
+7
View File
@@ -358,6 +358,13 @@
"when": 1790295069807,
"tag": "0050_clear_white_queen",
"breakpoints": true
},
{
"idx": 51,
"version": "6",
"when": 1790298803438,
"tag": "0051_conscious_blur",
"breakpoints": true
}
]
}
+11
View File
@@ -7060,6 +7060,10 @@
"resolved": "plugins/tunnels",
"link": true
},
"node_modules/@termix-plugin/vault": {
"resolved": "plugins/vault",
"link": true
},
"node_modules/@termix-plugin/wake-on-lan": {
"resolved": "plugins/wake-on-lan",
"link": true
@@ -18772,6 +18776,13 @@
"@termix/plugin-sdk": "*"
}
},
"plugins/vault": {
"name": "@termix-plugin/vault",
"version": "1.0.0",
"devDependencies": {
"@termix/plugin-sdk": "*"
}
},
"plugins/wake-on-lan": {
"name": "@termix-plugin/wake-on-lan",
"version": "1.0.0",
+41 -11
View File
@@ -185,7 +185,7 @@ artifacts in lockstep: the `schema.ts` declaration, the `index.ts` DDL, and the
regenerated `schema.pg.ts`/`schema.mysql.ts` plus drizzle migrations.
**Sync entities are registered, not hardcoded.** `ctx.sync.registerEntity`
adds an entity to remote sync; core registers its own ten the same way in
adds an entity to remote sync; core registers its own the same way in
`database/routes/sync-entities.ts`. The wire names are unchanged, because
existing tombstones and rows on the far side match on those strings. The
Electron client asks the server for the ordered list and falls back to its
@@ -1008,12 +1008,10 @@ auto-session and trusted devices, plus the base SSH auth types password, key,
stored credential, agent and none. Every other login method, second factor and
SSH auth method is a plugin through `ctx.auth`.
In 2.9.0, Vault still lives in core. It registers through the same
interface from `src/backend/auth/legacy-providers.ts` with
`pluginId: "core"`. Its Phase C step moves its block out of that file; D1
deletes it. **C4** moved Step-CA out and deleted
`src/ui/auth/legacy-auth-ui.tsx`, which had no other editor left.
Nothing else in core branches on those type names.
**C5** moved Vault, the last SSH auth method core still registered, into
its plugin and deleted `src/backend/auth/legacy-providers.ts`. **C4** had
already deleted `src/ui/auth/legacy-auth-ui.tsx`. Core registers only the
base types and branches on no plugin type name.
**C1** moved TOTP and passkeys out:
@@ -1137,7 +1135,7 @@ Nothing else in core branches on those type names.
`requires_browser_sign_in` and continue on `connect-browser-sign-in`.
- `@termix/plugin-sdk/ssh-certs` (`applyCertificateAuth`) replaces core's
`opkssh-cert-auth.ts`: opkssh, step-ca, core key auth with a CA
certificate, and the Vault provider still in core use it.
certificate, and vault use it.
**C4** moved Step-CA out:
@@ -1159,6 +1157,39 @@ Nothing else in core branches on those type names.
- The sign-in dialog is a `terminal.overlay` contribution speaking
`stepca_*`.
**C5** moved HashiCorp Vault out:
- `vault` registers the `vault` SSH auth provider. It adopts
`vault_profiles` and `vault_tokens` as `p_vault_profiles` and
`p_vault_tokens`. Profiles keep their ids and sync ids; cached
certificates are sealed with `ctx.secrets.seal`, and the ones 2.8 wrote
with the user's data key do not unseal, so they are dropped and cost one
sign-in. A fresh install has no foreign key from a token to its profile
(the SDK has no builder for one to a plugin's own table), so deleting a
profile deletes its tokens in code.
- A host's profile is the `profileId` host setting (hidden, edited in the
plugin's `registerSshAuthEditor` editor, which also holds the profile
manager where the host editor always had it).
`vault-settings-migration.ts` copies `ssh_data.vault_profile_id` into it;
the column is out of `schema.ts` and its drizzle drop is a no-op.
- Vault is reached through `ctx.fetch`, with the profile's own Vault host
passed as `allowPrivateHosts`, since Vault servers are usually internal.
- Profile routes are `/plugin-api/vault/profiles`, gated on `vault.use`;
sharing a profile needs `vault.share` (admins by default), which replaces
2.8's admin check. Only the owner edits or deletes one.
- The sync entity keeps its wire name `vaultProfiles` and order 20. Hosts no
longer reference it, so core's host entity lost its `vaultProfileId`
reference and `sync-references.ts` has no Vault case.
- The callback is `/plugin-api/vault/oidc/callback` (public). The boot
migration decides `legacyCallback` once: on when the 2.8 database used
Vault, so Termix keeps sending `<base>/vault/oidc/callback`, the URI Vault
roles allow, which core answers with a 307 to the plugin
(`registerVaultCompatRoutes` in `host-compat-routes.ts`; nginx proxies
only that path now).
- The sign-in dialog is a `terminal.overlay` contribution. It keeps the 2.8
message names (`vault_auth_url`, `vault_completed`, `vault_error`) for
Termix-Mobile.
#### One SSH connect pipeline
Every SSH connection, core's and plugins', goes through
@@ -2298,8 +2329,7 @@ upgrade on all three engines, and the checks in step 15 are green.
(`ctx.disposables.add`). Nothing at module scope. `deactivate` is usually
empty.
4. **When the SDK does not have it, add it to the SDK.** Never work around a
gap with a core import, and never add to
`src/backend/auth/legacy-providers.ts`. A new ctx member is:
gap with a core import. A new ctx member is:
1. the type and a doc comment in `packages/plugin-sdk/src/backend.ts`;
2. the implementation in `src/backend/plugins/ctx.ts`, wrapped in `guarded()`
when it is privileged, so it checks the capability and writes an audit
@@ -2540,7 +2570,7 @@ Then, with the app running:
The bundled plugins predate the SDK, apart from workspaces (A9), snippets
(B2), remote-desktop (B14), docker (B15), host-metrics (B16), automations
(B17), ai (B18), homepage (B19), totp and webauthn (C1), sso and ldap (C2),
opkssh and warpgate (C3), step-ca (C4),
opkssh and warpgate (C3), step-ca (C4), vault (C5),
which import nothing from core. The others still reach core by relative
path (`../../../../src/backend/...`), which an esbuild plugin,
`packages/plugin-sdk/cli/lib/legacy-core-imports.mjs`, keeps out of the bundle
+33
View File
@@ -529,6 +529,31 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
rejects (it then downloads the pinned version instead). Pass
`OPKSSH_VERSION` and the matching `OPKSSH_SHA256` as `ENV` in the final
stage. Owner: D0.
- **C5 (vault):** the Termix docs still describe Vault profiles with the
`/vault/oidc/callback` redirect URI. They need the plugin settings page,
the new redirect URI (`/plugin-api/vault/oidc/callback`) and the
`legacyCallback` setting. Owner: D0 (docs repo).
- **C5 (vault):** a host's Vault profile is the plugin's `profileId` host
setting now, and host-scope plugin settings do not travel over remote sync
(the host entity used to carry `vaultProfileSyncId`). Profiles still sync
as `vaultProfiles`; the host on the other side has to pick its profile
again. Needs a generic way to sync host-scope plugin settings, the same gap
the B7 export/import line describes. Owner: D1.
- **C5 (vault), for D2:** `vaultProfileId` was an owner-private auth field a
shared-host recipient with edit access could not change. As a host setting
it is written through `/plugins/vault/settings/host/:hostId`, which only
checks edit access, so such a recipient can now point the host at another
profile. Same question as the B14 line on per-field sharing levels.
Owner: D2.
- **C5 (vault):** `ssh_data.vault_profile_id` stays in the database,
unused, after `vault-settings-migration.ts` copies it (kept so a downgrade
works, and the pg/mysql foreign key on it now points at
`p_vault_profiles`). Drop it in 3.0.0. Owner: 3.0.0.
- **Pre-existing, found in C5:** `npm run test -w plugins/ssh-terminal`
fails "ssh-terminal activate > serves the terminal socket": it expects
`mock.wsRoutes` to equal `[{ path, raw }]`, but `FakeWsRoute` now also
records `handler` and `options`. Use `toMatchObject` or
`expect.objectContaining`. Owner: D0.
## Manual checks after 2.9.0
@@ -624,3 +649,11 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
and the host says it needs it. A Warpgate host's toggle is on in the host
editor's Plugins group after the upgrade, and the terminal, file manager
and docker each show the sign-in dialog and connect after it.
- Vault: on a 2.8 database every Vault profile is there and each Vault host
still has its profile selected in the host editor; the admin page shows the
old redirect URI; a terminal connect opens Vault's sign-in, signs a
certificate and connects, and a second connect skips the browser until the
certificate expires; create, edit, share and delete a profile from the host
editor (sharing only offered with "Share Vault profiles"); switch to the new
redirect URI after allowing it in the Vault role; disable the vault plugin
and a Vault host says it needs it, then enable it again.
@@ -584,7 +584,6 @@ export async function activate(ctx: PluginContext) {
let resolvedSocks5Username = socks5Username;
let resolvedSocks5Password = socks5Password;
let resolvedSocks5ProxyChain = socks5ProxyChain;
let resolvedVaultProfileId: number | null = null;
const resolveFrom = async () => {
const resolvedHost = hostSyncId
@@ -607,8 +606,6 @@ export async function activate(ctx: PluginContext) {
resolvedTerminalConfig = resolvedHost.terminalConfig as unknown as
Record<string, unknown> | undefined;
resolvedScpLegacy = resolvedHost.scpLegacy ?? false;
resolvedVaultProfileId =
(resolvedHost.vaultProfile as { id?: number } | undefined)?.id ?? null;
if (resolvedHost.useSocks5) {
resolvedUseSocks5 = resolvedHost.useSocks5;
resolvedSocks5Host = resolvedHost.socks5Host;
@@ -717,9 +714,6 @@ export async function activate(ctx: PluginContext) {
keyPassword: resolvedCredentials.keyPassword,
certPublicKey: resolvedCredentials.certPublicKey,
terminalConfig: resolvedTerminalConfig ?? null,
vaultProfile: resolvedVaultProfileId
? { id: resolvedVaultProfileId }
: null,
jumpHosts: resolvedJumpHosts,
useSocks5: resolvedUseSocks5,
socks5Host: resolvedSocks5Host,
-4
View File
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} window",
"tmuxWindows": "Windows",
"unknownError": "Unknown error occurred",
"vaultAuthDescription": "A window has opened to sign in to HashiCorp Vault. Complete the sign-in there; this connection will continue automatically.",
"vaultAuthFailed": "Vault authentication failed. Please try again.",
"vaultAuthTitle": "Vault sign-in required",
"vaultReopen": "Reopen sign-in window",
"websocketAbnormalClose": "Connection closed unexpectedly. This may be due to a reverse proxy or SSL configuration issue. Please check server logs.",
"websocketError": "WebSocket connection error"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} venster",
"tmuxWindows": "Vensters",
"unknownError": "Onbekende fout het voorgekom",
"vaultAuthDescription": "'n Venster het oopgemaak om by HashiCorp Vault aan te meld. Voltooi die aanmelding daar; hierdie verbinding sal outomaties voortgaan.",
"vaultAuthFailed": "Kluisverifikasie het misluk. Probeer asseblief weer.",
"vaultAuthTitle": "Kluisaanmelding vereis",
"vaultReopen": "Maak aanmeldvenster weer oop",
"websocketAbnormalClose": "Verbinding het onverwags gesluit. Dit kan wees as gevolg van 'n omgekeerde proxy- of SSL-konfigurasieprobleem. Gaan asseblief bedienerlogboeke na.",
"websocketError": "WebSocket-verbindingsfout"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} نافذة",
"tmuxWindows": "النوافذ",
"unknownError": "حدث خطأ غير معروف",
"vaultAuthDescription": "تم فتح نافذة لتسجيل الدخول إلى HashiCorp Vault. أكمل تسجيل الدخول هناك؛ سيستمر هذا الاتصال تلقائيًا.",
"vaultAuthFailed": "فشلت مصادقة Vault. يرجى المحاولة مرة أخرى.",
"vaultAuthTitle": "مطلوب تسجيل الدخول إلى Vault",
"vaultReopen": "أعد فتح نافذة تسجيل الدخول",
"websocketAbnormalClose": "أُغلق الاتصال بشكل غير متوقع. قد يكون السبب مشكلة في الوكيل العكسي أو إعدادات SSL. يرجى مراجعة سجلات الخادم.",
"websocketError": "خطأ في اتصال WebSocket"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} прозорец",
"tmuxWindows": "Прозорци",
"unknownError": "Възникна неизвестна грешка",
"vaultAuthDescription": "Отвори се прозорец за влизане в HashiCorp Vault. Завършете влизането там; тази връзка ще продължи автоматично.",
"vaultAuthFailed": "Удостоверяването на трезора не бе успешно. Моля, опитайте отново.",
"vaultAuthTitle": "Изисква се влизане в трезора",
"vaultReopen": "Отваряне отново на прозореца за влизане",
"websocketAbnormalClose": "Връзката се прекъсна неочаквано. Това може да се дължи на проблем с обратен прокси или SSL конфигурация. Моля, проверете лог файловете на сървъра.",
"websocketError": "Грешка при свързване с WebSocket"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} জানালা",
"tmuxWindows": "উইন্ডোজ",
"unknownError": "অজানা ত্রুটি ঘটেছে",
"vaultAuthDescription": "হ্যাশিকর্প ভল্টে সাইন ইন করার জন্য একটি উইন্ডো খুলেছে। সেখানে সাইন-ইন সম্পন্ন করুন; এই সংযোগটি স্বয়ংক্রিয়ভাবে চলতে থাকবে।",
"vaultAuthFailed": "ভল্ট প্রমাণীকরণ ব্যর্থ হয়েছে। অনুগ্রহ করে আবার চেষ্টা করুন।",
"vaultAuthTitle": "ভল্টে সাইন-ইন করা আবশ্যক",
"vaultReopen": "সাইন-ইন উইন্ডো পুনরায় খুলুন",
"websocketAbnormalClose": "সংযোগটি অপ্রত্যাশিতভাবে বন্ধ হয়ে গেছে। এটি রিভার্স প্রক্সি বা SSL কনফিগারেশন সংক্রান্ত সমস্যার কারণে হতে পারে। অনুগ্রহ করে সার্ভার লগ পরীক্ষা করুন।",
"websocketError": "ওয়েবসকেট সংযোগ ত্রুটি"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "finestra {{count}}",
"tmuxWindows": "Finestres",
"unknownError": "S'ha produït un error desconegut",
"vaultAuthDescription": "S'ha obert una finestra per iniciar la sessió a HashiCorp Vault. Completeu l'inici de sessió allà; aquesta connexió continuarà automàticament.",
"vaultAuthFailed": "L'autenticació de la caixa forta ha fallat. Torna-ho a provar.",
"vaultAuthTitle": "Cal iniciar sessió a la caixa forta",
"vaultReopen": "Torna a obrir la finestra d'inici de sessió",
"websocketAbnormalClose": "La connexió s'ha tancat inesperadament. Això pot ser degut a un problema de configuració del proxy invers o de l'SSL. Si us plau, reviseu els registres del servidor.",
"websocketError": "Error de connexió de WebSocket"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} okno",
"tmuxWindows": "Okna",
"unknownError": "Došlo k neznámé chybě",
"vaultAuthDescription": "Otevřelo se okno pro přihlášení do HashiCorp Vault. Dokončete přihlášení tam; toto připojení bude automaticky pokračovat.",
"vaultAuthFailed": "Ověření v trezoru se nezdařilo. Zkuste to prosím znovu.",
"vaultAuthTitle": "Vyžaduje se přihlášení do trezoru",
"vaultReopen": "Znovu otevřít přihlašovací okno",
"websocketAbnormalClose": "Spojení se neočekávaně ukončilo. Může to být způsobeno problémem s reverzní proxy nebo konfigurací SSL. Zkontrolujte prosím protokoly serveru.",
"websocketError": "Chyba připojení k WebSocketu"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} vindue",
"tmuxWindows": "Vinduer",
"unknownError": "Der opstod en ukendt fejl",
"vaultAuthDescription": "Et vindue er åbnet, hvor du kan logge ind på HashiCorp Vault. Fuldfør loginprocessen der; forbindelsen fortsætter automatisk.",
"vaultAuthFailed": "Godkendelse af arkiv mislykkedes. Prøv igen.",
"vaultAuthTitle": "Login til Vault kræves",
"vaultReopen": "Genåbn loginvinduet",
"websocketAbnormalClose": "Forbindelsen blev lukket uventet. Dette kan skyldes et problem med reverse proxy eller SSL-konfigurationen. Tjek venligst serverlogfilerne.",
"websocketError": "WebSocket-forbindelsesfejl"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} Fenster",
"tmuxWindows": "Fenster",
"unknownError": "Unbekannter Fehler aufgetreten",
"vaultAuthDescription": "Ein Fenster zur Anmeldung bei HashiCorp Vault wurde geöffnet. Schließen Sie die Anmeldung dort ab; diese Verbindung wird automatisch fortgesetzt.",
"vaultAuthFailed": "Vault-Authentifizierung fehlgeschlagen. Bitte versuchen Sie es erneut.",
"vaultAuthTitle": "Vault-Anmeldung erforderlich",
"vaultReopen": "Anmeldefenster erneut öffnen",
"websocketAbnormalClose": "Verbindung unerwartet geschlossen. Dies kann auf ein Problem mit dem Reverse-Proxy oder der SSL-Konfiguration zurückzuführen sein. Bitte überprüfen Sie die Server-Logs.",
"websocketError": "WebSocket-Verbindungsfehler"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "παράθυρο {{count}}",
"tmuxWindows": "Παράθυρα",
"unknownError": "Παρουσιάστηκε άγνωστο σφάλμα",
"vaultAuthDescription": "Έχει ανοίξει ένα παράθυρο για να συνδεθείτε στο HashiCorp Vault. Ολοκληρώστε τη σύνδεση εκεί. Αυτή η σύνδεση θα συνεχιστεί αυτόματα.",
"vaultAuthFailed": "Ο έλεγχος ταυτότητας του vault απέτυχε. Δοκιμάστε ξανά.",
"vaultAuthTitle": "Απαιτείται σύνδεση στο Vault",
"vaultReopen": "Επανάνοιγμα παραθύρου σύνδεσης",
"websocketAbnormalClose": "Η σύνδεση έκλεισε απροσδόκητα. Αυτό μπορεί να οφείλεται σε πρόβλημα αντίστροφης μεσολάβησης ή ρύθμισης παραμέτρων SSL. Ελέγξτε τα αρχεία καταγραφής διακομιστή.",
"websocketError": "Σφάλμα σύνδεσης WebSocket"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} ventana",
"tmuxWindows": "Ventanas",
"unknownError": "Se ha producido un error desconocido.",
"vaultAuthDescription": "Se ha abierto una ventana para iniciar sesión en HashiCorp Vault. Completa el inicio de sesión allí; esta conexión continuará automáticamente.",
"vaultAuthFailed": "La autenticación de Vault falló. Inténtalo de nuevo.",
"vaultAuthTitle": "Inicio de sesión en Vault requerido",
"vaultReopen": "Reabrir ventana de inicio de sesión",
"websocketAbnormalClose": "La conexión se cerró de forma inesperada. Esto puede deberse a un problema con el proxy inverso o la configuración SSL. Revisa los registros del servidor.",
"websocketError": "Error de conexión WebSocket"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} ikkuna",
"tmuxWindows": "Ikkunat",
"unknownError": "Tuntematon virhe tapahtui",
"vaultAuthDescription": "Ikkuna HashiCorp-holviin kirjautumista varten on avautunut. Kirjaudu sisään siinä. Yhteys jatkuu automaattisesti.",
"vaultAuthFailed": "Holvin todennus epäonnistui. Yritä uudelleen.",
"vaultAuthTitle": "Holviin kirjautuminen vaaditaan",
"vaultReopen": "Avaa kirjautumisikkuna uudelleen",
"websocketAbnormalClose": "Yhteys katkesi odottamatta. Tämä voi johtua käänteisestä välityspalvelimesta tai SSL-määritysongelmasta. Tarkista palvelimen lokit.",
"websocketError": "WebSocket-yhteysvirhe"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} fenêtre(s)",
"tmuxWindows": "Fenêtres",
"unknownError": "Une erreur inconnue s'est produite",
"vaultAuthDescription": "Une fenêtre s'est ouverte pour vous connecter à HashiCorp Vault. Terminez la connexion dans cette fenêtre ; cette connexion continuera automatiquement.",
"vaultAuthFailed": "Échec de l'authentification Vault. Veuillez réessayer.",
"vaultAuthTitle": "Connexion à Vault requise",
"vaultReopen": "Rouvrir la fenêtre de connexion",
"websocketAbnormalClose": "Connexion fermée de manière inattendue. Cela peut être dû à un problème de proxy inverse ou de configuration SSL. Veuillez vérifier les journaux du serveur.",
"websocketError": "Erreur de connexion WebSocket"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "חלון {{count}}",
"tmuxWindows": "חלונות",
"unknownError": "אירעה שגיאה לא ידועה",
"vaultAuthDescription": "נפתח חלון לכניסה לכספת HashiCorp. השלם את תהליך הכניסה שם; חיבור זה ימשיך אוטומטית.",
"vaultAuthFailed": "אימות הכספת נכשל. אנא נסה שוב.",
"vaultAuthTitle": "נדרשת כניסה לכספת",
"vaultReopen": "פתח מחדש את חלון הכניסה",
"websocketAbnormalClose": "החיבור נסגר באופן בלתי צפוי. ייתכן שהדבר נובע מבעיית פרוקסי הפוך או מבעיית תצורת SSL. אנא בדוק את יומני השרת.",
"websocketError": "שגיאת חיבור WebSocket"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} विंडो",
"tmuxWindows": "विंडो",
"unknownError": "अज्ञात त्रुटि हुई",
"vaultAuthDescription": "HashiCorp Vault में साइन इन करने के लिए एक विंडो खुल गई है। वहाँ साइन-इन पूरा करें; यह कनेक्शन स्वचालित रूप से जारी रहेगा।",
"vaultAuthFailed": "Vault प्रमाणीकरण विफल। कृपया पुनः प्रयास करें।",
"vaultAuthTitle": "Vault साइन-इन आवश्यक है",
"vaultReopen": "साइन-इन विंडो फिर से खोलें",
"websocketAbnormalClose": "कनेक्शन अप्रत्याशित रूप से बंद हो गया। यह रिवर्स प्रॉक्सी या SSL कॉन्फ़िगरेशन समस्या के कारण हो सकता है। कृपया सर्वर लॉग जाँचें।",
"websocketError": "WebSocket कनेक्शन त्रुटि"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} ablak",
"tmuxWindows": "Ablakok",
"unknownError": "Ismeretlen hiba történt",
"vaultAuthDescription": "Megnyílt egy ablak a HashiCorp Vaultba való bejelentkezéshez. Fejezze be a bejelentkezést ott; a kapcsolat automatikusan folytatódik.",
"vaultAuthFailed": "A trezor hitelesítése sikertelen. Próbálja újra.",
"vaultAuthTitle": "Bejelentkezés szükséges a széfbe",
"vaultReopen": "Bejelentkezési ablak újranyitása",
"websocketAbnormalClose": "A kapcsolat váratlanul megszakadt. Ennek oka lehet fordított proxy vagy SSL konfigurációs probléma. Kérjük, ellenőrizze a szervernaplókat.",
"websocketError": "WebSocket csatlakozási hiba"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} jendela",
"tmuxWindows": "Windows",
"unknownError": "Terjadi kesalahan yang tidak diketahui.",
"vaultAuthDescription": "Jendela telah terbuka untuk masuk ke HashiCorp Vault. Selesaikan proses masuk di sana; koneksi ini akan berlanjut secara otomatis.",
"vaultAuthFailed": "Autentikasi Vault gagal. Silakan coba lagi.",
"vaultAuthTitle": "Login ke Vault diperlukan.",
"vaultReopen": "Buka kembali jendela masuk",
"websocketAbnormalClose": "Koneksi terputus secara tidak terduga. Ini mungkin disebabkan oleh masalah konfigurasi reverse proxy atau SSL. Silakan periksa log server.",
"websocketError": "Kesalahan koneksi WebSocket"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} finestra",
"tmuxWindows": "Finestre",
"unknownError": "Si è verificato un errore sconosciuto",
"vaultAuthDescription": "Una finestra si è aperta per accedere a HashiCorp Vault. Completa l'accesso lì; la connessione continuerà automaticamente.",
"vaultAuthFailed": "Autenticazione Vault fallita. Riprova.",
"vaultAuthTitle": "Accesso a Vault richiesto",
"vaultReopen": "Riapri la finestra di accesso",
"websocketAbnormalClose": "La connessione si è chiusa in modo imprevisto. Potrebbe trattarsi di un problema di reverse proxy o configurazione SSL. Controlla i log del server.",
"websocketError": "Errore di connessione WebSocket"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} ウィンドウ",
"tmuxWindows": "ウィンドウ",
"unknownError": "不明なエラーが発生しました",
"vaultAuthDescription": "HashiCorp Vaultへのサインイン用のウィンドウが開きました。そこでサインインを完了すると、この接続は自動的に続行されます。",
"vaultAuthFailed": "Vault 認証に失敗しました。もう一度お試しください。",
"vaultAuthTitle": "Vault サインインが必要です",
"vaultReopen": "サインインウィンドウを再表示",
"websocketAbnormalClose": "接続が予期せず切断されました。リバースプロキシまたはSSLの設定に問題がある可能性があります。サーバーログを確認してください。",
"websocketError": "WebSocket接続エラー"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}}개 윈도우",
"tmuxWindows": "윈도우",
"unknownError": "알 수 없는 오류가 발생했습니다.",
"vaultAuthDescription": "HashiCorp Vault 로그인을 위한 창이 열렸습니다. 해당 창에서 로그인을 완료하면 연결이 자동으로 계속됩니다.",
"vaultAuthFailed": "Vault 인증에 실패했습니다. 다시 시도하세요.",
"vaultAuthTitle": "Vault 로그인 필요",
"vaultReopen": "로그인 창 다시 열기",
"websocketAbnormalClose": "연결이 예기치 않게 종료되었습니다. 리버스 프록시 또는 SSL 구성 문제일 수 있습니다. 서버 로그를 확인하세요.",
"websocketError": "WebSocket 연결 오류"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} venster",
"tmuxWindows": "Windows",
"unknownError": "Er is een onbekende fout opgetreden.",
"vaultAuthDescription": "Er is een venster geopend om in te loggen bij HashiCorp Vault. Voltooi de aanmelding daar; deze verbinding wordt automatisch tot stand gebracht.",
"vaultAuthFailed": "Authenticatie voor de kluis is mislukt. Probeer het opnieuw.",
"vaultAuthTitle": "Aanmelden bij Vault is vereist.",
"vaultReopen": "Heropen het aanmeldvenster",
"websocketAbnormalClose": "De verbinding is onverwacht verbroken. Dit kan te wijten zijn aan een probleem met de reverse proxy of de SSL-configuratie. Controleer de serverlogboeken.",
"websocketError": "WebSocket-verbindingsfout"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} vindu",
"tmuxWindows": "Vinduer",
"unknownError": "Ukjent feil oppstod",
"vaultAuthDescription": "Et vindu har åpnet seg for å logge på HashiCorp Vault. Fullfør påloggingen der; denne tilkoblingen vil fortsette automatisk.",
"vaultAuthFailed": "Hvelv-autentisering mislyktes. Prøv på nytt.",
"vaultAuthTitle": "Pålogging til arkivet kreves",
"vaultReopen": "Åpne påloggingsvinduet på nytt",
"websocketAbnormalClose": "Tilkoblingen ble avsluttet uventet. Dette kan skyldes et problem med omvendt proxy eller SSL-konfigurasjon. Sjekk serverloggene.",
"websocketError": "WebSocket-tilkoblingsfeil"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} okno",
"tmuxWindows": "Okna",
"unknownError": "Wystąpił nieznany błąd",
"vaultAuthDescription": "Otworzyło się okno logowania do HashiCorp Vault. Dokończ logowanie, a połączenie zostanie nawiązane automatycznie.",
"vaultAuthFailed": "Uwierzytelnienie w Vault nie powiodło się. Spróbuj ponownie.",
"vaultAuthTitle": "Wymagane logowanie do Vault",
"vaultReopen": "Otwórz ponownie okno logowania",
"websocketAbnormalClose": "Połączenie zostało nieoczekiwanie zamknięte. Przyczyną może być reverse proxy lub problem z konfiguracją SSL. Sprawdź logi serwera.",
"websocketError": "Błąd połączenia WebSocket"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} janela",
"tmuxWindows": "Windows",
"unknownError": "Ocorreu um erro desconhecido.",
"vaultAuthDescription": "Uma janela foi aberta para fazer login no HashiCorp Vault. Conclua o login; esta conexão será estabelecida automaticamente.",
"vaultAuthFailed": "A autenticação do cofre falhou. Tente novamente.",
"vaultAuthTitle": "É necessário fazer login no cofre.",
"vaultReopen": "Reabrir janela de login",
"websocketAbnormalClose": "A conexão foi encerrada inesperadamente. Isso pode ter ocorrido devido a um problema de configuração de proxy reverso ou SSL. Verifique os registros do servidor.",
"websocketError": "Erro de conexão WebSocket"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} janela",
"tmuxWindows": "Janelas",
"unknownError": "Ocorreu um erro desconhecido.",
"vaultAuthDescription": "Foi aberta uma janela para iniciar sessão no HashiCorp Vault. Conclua o início de sessão aí; esta ligação continuará automaticamente.",
"vaultAuthFailed": "A autenticação no Vault falhou. Tente novamente.",
"vaultAuthTitle": "Início de sessão no Vault necessário",
"vaultReopen": "Reabrir janela de início de sessão",
"websocketAbnormalClose": "A ligação foi fechada inesperadamente. Isto pode dever-se a um problema de configuração do proxy reverso ou SSL. Verifique os registos do servidor.",
"websocketError": "Erro de ligação WebSocket"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "fereastră {{count}}",
"tmuxWindows": "Ferestre",
"unknownError": "A apărut o eroare necunoscută",
"vaultAuthDescription": "S-a deschis o fereastră pentru a vă conecta la HashiCorp Vault. Finalizați conectarea acolo; această conexiune va continua automat.",
"vaultAuthFailed": "Autentificarea în seif a eșuat. Vă rugăm să încercați din nou.",
"vaultAuthTitle": "Este necesară conectarea la seif",
"vaultReopen": "Redeschideți fereastra de conectare",
"websocketAbnormalClose": "Conexiunea s-a închis pe neașteptate. Acest lucru se poate datora unei probleme de configurare a proxy-ului invers sau a SSL. Vă rugăm să verificați jurnalele serverului.",
"websocketError": "Eroare de conexiune WebSocket"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} окно",
"tmuxWindows": "Окна",
"unknownError": "Произошла неизвестная ошибка",
"vaultAuthDescription": "Открыто окно для входа в HashiCorp Vault. Завершите вход там; подключение продолжится автоматически.",
"vaultAuthFailed": "Аутентификация Vault не удалась. Попробуйте снова.",
"vaultAuthTitle": "Требуется вход в Vault",
"vaultReopen": "Повторно открыть окно входа",
"websocketAbnormalClose": "Соединение неожиданно закрыто. Возможно, проблема с обратным прокси или конфигурацией SSL. Проверьте журналы сервера.",
"websocketError": "Ошибка подключения WebSocket"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} прозор",
"tmuxWindows": "Прозори",
"unknownError": "Дошло је до непознате грешке",
"vaultAuthDescription": "Отворио се прозор за пријављивање на HashiCorp Vault. Завршите пријављивање тамо; ова веза ће се аутоматски наставити.",
"vaultAuthFailed": "Аутентификација трезора није успела. Молимо покушајте поново.",
"vaultAuthTitle": "Потребно је пријављивање у трезор",
"vaultReopen": "Поново отвори прозор за пријављивање",
"websocketAbnormalClose": "Веза је неочекивано прекинута. Ово може бити због проблема са обрнутим проксијем или SSL конфигурацијом. Молимо вас да проверите логове сервера.",
"websocketError": "Грешка у повезивању са WebSocket-ом"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} fönster",
"tmuxWindows": "Fönster",
"unknownError": "Okänt fel inträffade",
"vaultAuthDescription": "Ett fönster har öppnats för att logga in på HashiCorp Vault. Slutför inloggningen där; anslutningen fortsätter automatiskt.",
"vaultAuthFailed": "Valvautentisering misslyckades. Försök igen.",
"vaultAuthTitle": "Inloggning till arkivet krävs",
"vaultReopen": "Öppna inloggningsfönstret igen",
"websocketAbnormalClose": "Anslutningen stängdes oväntat. Detta kan bero på ett problem med omvänd proxy eller SSL-konfiguration. Kontrollera serverloggarna.",
"websocketError": "WebSocket-anslutningsfel"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} หน้าต่าง",
"tmuxWindows": "วินโดวส์",
"unknownError": "เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุ",
"vaultAuthDescription": "หน้าต่างสำหรับเข้าสู่ระบบ HashiCorp Vault ได้เปิดขึ้นแล้ว โปรดทำการเข้าสู่ระบบให้เสร็จสมบูรณ์ การเชื่อมต่อนี้จะดำเนินการต่อโดยอัตโนมัติ",
"vaultAuthFailed": "การตรวจสอบสิทธิ์ Vault ล้มเหลว โปรดลองอีกครั้ง",
"vaultAuthTitle": "ต้องลงชื่อเข้าใช้ Vault",
"vaultReopen": "เปิดหน้าต่างเข้าสู่ระบบอีกครั้ง",
"websocketAbnormalClose": "การเชื่อมต่อถูกตัดโดยไม่คาดคิด อาจเกิดจากปัญหาเกี่ยวกับรีเวิร์สพร็อกซีหรือการกำหนดค่า SSL โปรดตรวจสอบบันทึกของเซิร์ฟเวอร์",
"websocketError": "ข้อผิดพลาดในการเชื่อมต่อ WebSocket"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} pencere",
"tmuxWindows": "Pencereler",
"unknownError": "Bilinmeyen bir hata oluştu",
"vaultAuthDescription": "HashiCorp Vault'ta oturum açmak için bir pencere açıldı. Orada oturum açmayı tamamlayın; bu bağlantı otomatik olarak devam edecek.",
"vaultAuthFailed": "Vault kimlik doğrulaması başarısız oldu. Lütfen tekrar deneyin.",
"vaultAuthTitle": "Vault Oturum Açması Gerekli",
"vaultReopen": "Oturum açma penceresini yeniden aç",
"websocketAbnormalClose": "Bağlantı beklenmedik şekilde kapandı. Bu, ters proxy veya SSL yapılandırma sorunundan kaynaklanabilir. Lütfen sunucu günlüklerini kontrol edin.",
"websocketError": "WebSocket bağlantı hatası"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "вікно {{count}}",
"tmuxWindows": "Вікна",
"unknownError": "Сталася невідома помилка",
"vaultAuthDescription": "Відкрилося вікно для входу в HashiCorp Vault. Завершіть вхід там; це з’єднання буде продовжено автоматично.",
"vaultAuthFailed": "Не вдалося виконати автентифікацію сховища. Спробуйте ще раз.",
"vaultAuthTitle": "Потрібен вхід до Сейфа",
"vaultReopen": "Знову відкрити вікно входу",
"websocketAbnormalClose": "З’єднання несподівано завершилося. Можливо, це пов’язано зі зворотним проксі-сервером або проблемою конфігурації SSL. Перевірте журнали сервера.",
"websocketError": "Помилка підключення WebSocket"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} cửa sổ",
"tmuxWindows": "Cửa sổ",
"unknownError": "Lỗi không xác định đã xảy ra",
"vaultAuthDescription": "Một cửa sổ đã mở để đăng nhập vào HashiCorp Vault. Hoàn tất đăng nhập ở đó; kết nối này sẽ tự động tiếp tục.",
"vaultAuthFailed": "Xác thực Vault thất bại. Vui lòng thử lại.",
"vaultAuthTitle": "Yêu cầu đăng nhập Vault",
"vaultReopen": "Mở lại cửa sổ đăng nhập",
"websocketAbnormalClose": "Kết nối đóng đột ngột. Có thể do sự cố cấu hình reverse proxy hoặc SSL. Vui lòng kiểm tra nhật ký máy chủ.",
"websocketError": "Lỗi kết nối WebSocket"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} 个窗口",
"tmuxWindows": "窗口",
"unknownError": "发生未知错误",
"vaultAuthDescription": "已打开窗口以登录 HashiCorp Vault。在该处完成登录;此连接将自动继续。",
"vaultAuthFailed": "Vault 认证失败,请重试。",
"vaultAuthTitle": "需要 Vault 登录",
"vaultReopen": "重新打开登录窗口",
"websocketAbnormalClose": "连接意外关闭。这可能是因为反向代理或SSL配置问题。请检查服务器日志。",
"websocketError": "WebSocket 连接错误"
},
@@ -74,10 +74,6 @@
"tmuxWindowCount": "{{count}} 個視窗",
"tmuxWindows": "視窗",
"unknownError": "發生未知錯誤",
"vaultAuthDescription": "已開啟視窗以登入 HashiCorp Vault。在該處完成登入;此連線將自動繼續。",
"vaultAuthFailed": "Vault 驗證失敗。請再試一次。",
"vaultAuthTitle": "需要 Vault 登入",
"vaultReopen": "重新開啟登入視窗",
"websocketAbnormalClose": "連線意外關閉。這可能是由於反向代理或 SSL 設定問題所導致。請檢查伺服器記錄。",
"websocketError": "WebSocket 連線錯誤"
},
@@ -1141,7 +1141,7 @@ export function createTerminalSocket(deps: TerminalSocketDeps) {
/**
* Starts the browser sign-in behind an "<interaction>_auth_required"
* message. The provider for the host's auth type wins; otherwise the first
* provider that owns this interaction (Vault attaches to any auth type).
* provider that owns this interaction.
*/
async function startAuthInteraction(
interaction: string,
@@ -342,15 +342,6 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
const currentHostIdRef = useRef<number | null>(null);
const currentHostConfigRef = useRef<TerminalHostConfig | null>(null);
// Vault SSH signer interactive OIDC flow
const [vaultDialog, setVaultDialog] = useState<{
stage: "waiting" | "error";
error?: string;
} | null>(null);
const vaultFailedRef = useRef(false);
const vaultPopupRef = useRef<Window | null>(null);
const vaultTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [hostKeyVerification, setHostKeyVerification] = useState<{
isOpen: boolean;
scenario: "new" | "changed";
@@ -1909,7 +1900,6 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
updateConnectionError(errorMessage);
setIsConnecting(false);
} else if (msg.type === "connected") {
vaultFailedRef.current = false;
wasConnectedRef.current = true;
setIsConnected(true);
setIsConnecting(false);
@@ -2104,86 +2094,6 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
webSocketRef.current.close();
}
}, 300000);
} else if (msg.type === "vault_auth_required") {
if (connectionTimeoutRef.current) {
clearTimeout(connectionTimeoutRef.current);
connectionTimeoutRef.current = null;
}
if (vaultFailedRef.current) {
setVaultDialog({
stage: "error",
error: t("terminal.vaultAuthFailed"),
});
updateConnectionError(t("terminal.vaultAuthFailed"));
addLog({
type: "error",
stage: "auth",
message: t("terminal.vaultAuthFailed"),
});
} else {
vaultFailedRef.current = true;
webSocketRef.current?.send(
JSON.stringify({
type: "vault_start_auth",
data: { hostId: msg.hostId },
}),
);
}
} else if (msg.type === "vault_auth_url") {
if (connectionErrorRef.current) return;
try {
vaultPopupRef.current = window.open(
msg.url,
"termix-vault-oidc",
"width=540,height=720",
);
} catch {
vaultPopupRef.current = null;
}
setVaultDialog({ stage: "waiting" });
if (vaultTimeoutRef.current) clearTimeout(vaultTimeoutRef.current);
vaultTimeoutRef.current = setTimeout(() => {
setVaultDialog(null);
webSocketRef.current?.close();
}, 300000);
} else if (msg.type === "vault_completed") {
if (vaultTimeoutRef.current) {
clearTimeout(vaultTimeoutRef.current);
vaultTimeoutRef.current = null;
}
try {
vaultPopupRef.current?.close();
} catch {
// popup may already be closed
}
setVaultDialog(null);
if (webSocketRef.current && terminal) {
webSocketRef.current.send(
JSON.stringify({
type: "vault_auth_completed",
data: {
hostId: currentHostIdRef.current,
cols: terminal.cols || 80,
rows: terminal.rows || 24,
hostConfig: currentHostConfigRef.current,
},
}),
);
}
} else if (msg.type === "vault_error") {
if (connectionErrorRef.current) return;
vaultFailedRef.current = true;
if (vaultTimeoutRef.current) {
clearTimeout(vaultTimeoutRef.current);
vaultTimeoutRef.current = null;
}
try {
vaultPopupRef.current?.close();
} catch {
// popup may already be closed
}
setVaultDialog({ stage: "error", error: msg.error });
setIsConnecting(false);
} else if (msg.type === "keyboard_interactive_available") {
setKeyboardInteractiveDetected(true);
setIsConnecting(false);
@@ -2240,7 +2150,6 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
}
} else if (msg.type === "sessionAttached") {
isAttachingSessionRef.current = false;
vaultFailedRef.current = false;
wasConnectedRef.current = true;
setIsConnected(true);
setIsConnecting(false);
@@ -3884,70 +3793,6 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
props={overlayProps as unknown as Record<string, unknown>}
/>
{vaultDialog && (
<div className="absolute inset-0 z-50 flex items-center justify-center bg-black/60">
<div className="w-[420px] max-w-[90%] border border-border bg-background p-5 shadow-lg">
<h3 className="text-sm font-bold mb-2 text-foreground">
{vaultDialog.stage === "error"
? t("terminal.vaultAuthFailed")
: t("terminal.vaultAuthTitle")}
</h3>
{vaultDialog.stage === "error" ? (
<p className="text-xs text-destructive mb-4 break-words">
{vaultDialog.error || t("terminal.vaultAuthFailed")}
</p>
) : (
<p className="text-xs text-muted-foreground mb-4">
{t("terminal.vaultAuthDescription")}
</p>
)}
<div className="flex justify-end gap-2">
{vaultDialog.stage === "waiting" && (
<Button
variant="outline"
size="sm"
onClick={() => {
try {
vaultPopupRef.current?.focus();
} catch {
// popup may be gone
}
}}
>
{t("terminal.vaultReopen")}
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={() => {
if (vaultTimeoutRef.current) {
clearTimeout(vaultTimeoutRef.current);
vaultTimeoutRef.current = null;
}
try {
vaultPopupRef.current?.close();
} catch {
// popup may already be closed
}
webSocketRef.current?.send(
JSON.stringify({
type: "vault_cancel",
data: { hostId: currentHostIdRef.current },
}),
);
setVaultDialog(null);
}}
>
{vaultDialog.stage === "error"
? t("common.close")
: t("hosts.cancelBtn")}
</Button>
</div>
</div>
</div>
)}
{hostKeyVerification?.isOpen && (
<HostKeyVerificationDialog
isOpen={true}
+17
View File
@@ -0,0 +1,17 @@
# Changelog
## 1.0.0
Bundled with Termix 2.9.0.
- Vault SSH signing moved out of core. The callback is now
`/plugin-api/vault/oidc/callback`; the 2.8 URI `/vault/oidc/callback`
still works and upgraded installs keep sending it until an admin turns
off "Use the old redirect URI".
- Profiles keep their ids and sync ids. A host's profile is now a host
setting of this plugin.
- Signed certificates are stored encrypted with the installation key.
Certificates cached by 2.8 are not carried over; the next connection asks
for one sign-in.
- Sharing a profile needs the "Share Vault profiles" permission, which
admins have by default.
+29
View File
@@ -0,0 +1,29 @@
# HashiCorp Vault
Adds the `vault` SSH auth type: Termix signs you in to Vault with OIDC in the
browser, has Vault's SSH secrets engine sign a fresh key, and keeps the
short-lived certificate until it expires.
Termix talks to Vault's HTTP API directly (`auth/<mount>/oidc/auth_url`,
`auth/<mount>/oidc/callback`, `<ssh mount>/sign/<role>`). No Vault token,
AppRole secret or long-lived key is ever stored.
## Profiles
A profile holds the connection settings only: the Vault address and
namespace, the OIDC mount and role, the SSH secrets mount and signer role,
the valid principals and the key type. Pick one in the host editor after
choosing "Vault" as the authentication type, and manage profiles from the
same place. Users with the "Share Vault profiles" permission can share a
profile with everyone.
## Settings
Admin settings, under Settings > Plugins > HashiCorp Vault:
- **Redirect URI**: add it to `allowed_redirect_uris` in each Vault OIDC
role: `<base URL>/plugin-api/vault/oidc/callback`, or
`<base URL>/vault/oidc/callback` while "Use the old redirect URI" is on.
An install upgraded from 2.8 that used Vault keeps the old redirect URI
turned on.
+69
View File
@@ -0,0 +1,69 @@
{
"editor": {
"profile": "Vault Signer Profile",
"selectProfile": "Select a Vault profile...",
"hint": "Settings come from the shared profile; you'll sign in to Vault via OIDC when you connect. No secrets are stored.",
"docs": "View docs",
"sharedProfile": "{{name}} (shared)"
},
"profiles": {
"new": "New profile",
"manage": "Manage Vault profiles",
"name": "Friendly Name",
"vaultAddr": "Vault Address",
"namespace": "Namespace",
"oidcMount": "OIDC Auth Mount",
"oidcRole": "OIDC Role",
"sshMount": "SSH Secrets Mount",
"sshRole": "SSH Signer Role",
"validPrincipals": "Valid Principals",
"keyType": "Ephemeral Key Type",
"shareWithAll": "Share with all users",
"create": "Create profile",
"created": "Vault profile created",
"saved": "Vault profile saved",
"deleted": "Vault profile deleted",
"saveFailed": "Failed to save Vault profile",
"deleteFailed": "Failed to delete Vault profile",
"save": "Save profile",
"validationError": "Name, Vault address and SSH signer role are required",
"none": "No Vault profiles yet.",
"sharedBadge": "shared",
"edit": "Edit",
"delete": "Delete"
},
"hosts": {
"filterAuthVault": "Vault"
},
"dialog": {
"title": "Vault sign-in required",
"description": "A window has opened to sign in to HashiCorp Vault. Complete the sign-in there; this connection will continue automatically.",
"failed": "Vault authentication failed. Please try again.",
"reopen": "Reopen sign-in window"
},
"permissions": {
"use": {
"title": "Use Vault SSH signing",
"description": "Create Vault signer profiles and connect to hosts that use Vault."
},
"share": {
"title": "Share Vault profiles",
"description": "Mark a Vault signer profile as shared so every user can pick it."
}
},
"settings": {
"redirectUri": {
"label": "Redirect URI",
"description": "Add this URL to allowed_redirect_uris in each Vault OIDC role that Termix signs in with."
},
"legacyCallback": {
"label": "Use the old redirect URI",
"description": "Keep sending /vault/oidc/callback, the redirect URI used before 2.9. Turn this off once your Vault roles allow the new one."
},
"host": {
"profileId": {
"label": "Vault signer profile"
}
}
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Kluis Ondertekenaar Profiel",
"selectProfile": "Kies 'n kluisprofiel...",
"hint": "Instellings kom van die gedeelde profiel; jy sal by Vault aanmeld via OIDC wanneer jy koppel. Geen geheime word gestoor nie.",
"docs": "Bekyk dokumente"
},
"profiles": {
"new": "Nuwe profiel",
"manage": "Bestuur Vault-profiele",
"name": "Vriendelike Naam",
"vaultAddr": "Kluisadres",
"namespace": "Naamruimte",
"oidcMount": "OIDC-magtigingsmontering",
"oidcRole": "OIDC-rol",
"sshMount": "SSH-geheime berg",
"sshRole": "SSH-ondertekenaarrol",
"validPrincipals": "Geldige Prinsipale",
"keyType": "Efemere Sleuteltipe",
"shareWithAll": "Deel met alle gebruikers",
"create": "Skep profiel",
"created": "Kluisprofiel geskep",
"saved": "Kluisprofiel gestoor",
"deleted": "Kluisprofiel uitgevee",
"saveFailed": "Kon nie kluisprofiel stoor nie",
"deleteFailed": "Kon nie kluisprofiel uitvee nie",
"save": "Stoor profiel",
"validationError": "Naam, kluisadres en SSH-ondertekenrol word vereis",
"none": "Geen Vault-profiele nog nie.",
"sharedBadge": "gedeel"
},
"dialog": {
"title": "Kluisaanmelding vereis",
"description": "'n Venster het oopgemaak om by HashiCorp Vault aan te meld. Voltooi die aanmelding daar; hierdie verbinding sal outomaties voortgaan.",
"failed": "Kluisverifikasie het misluk. Probeer asseblief weer.",
"reopen": "Maak aanmeldvenster weer oop"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "ملف توقيع Vault",
"selectProfile": "اختيار ملف توقيع Vault...",
"hint": "تأتي الإعدادات من الملف المشترك؛ ستقوم بتسجيل الدخول إلى Vault عبر OIDC عند الاتصال. لا يتم تخزين أي أسرار.",
"docs": "عرض المستندات"
},
"profiles": {
"new": "ملف جديد",
"manage": "إدارة ملفات Vault",
"name": "الاسم المألوف",
"vaultAddr": "عنوان Vault",
"namespace": "مساحة الاسم",
"oidcMount": "تثبيت مصادقة OIDC",
"oidcRole": "دور OIDC",
"sshMount": "تثبيت أسرار SSH",
"sshRole": "دور موقع SSH",
"validPrincipals": "المستخدمون المسموح بهم",
"keyType": "نوع المفتاح المؤقت",
"shareWithAll": "مشاركة مع جميع المستخدمين",
"create": "إنشاء ملف تعريف",
"created": "تم إنشاء ملف تعريف Vault",
"saved": "تم حفظ ملف تعريف Vault",
"deleted": "تم حذف ملف تعريف Vault",
"saveFailed": "فشل حفظ ملف تعريف Vault",
"deleteFailed": "فشل حذف ملف تعريف Vault",
"save": "حفظ ملف التعريف",
"validationError": "الاسم وعنوان Vault ودور الموقّع SSH مطلوب",
"none": "لا توجد ملفات تعريف Vault بعد.",
"sharedBadge": "مشترك"
},
"dialog": {
"title": "مطلوب تسجيل الدخول إلى Vault",
"description": "تم فتح نافذة لتسجيل الدخول إلى HashiCorp Vault. أكمل تسجيل الدخول هناك؛ سيستمر هذا الاتصال تلقائيًا.",
"failed": "فشلت مصادقة Vault. يرجى المحاولة مرة أخرى.",
"reopen": "أعد فتح نافذة تسجيل الدخول"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Профил на подписващ в трезора",
"selectProfile": "Изберете профил в трезора...",
"hint": "Настройките идват от споделения профил; ще влезете в Vault чрез OIDC, когато се свържете. Не се съхраняват тайни.",
"docs": "Преглед на документи"
},
"profiles": {
"new": "Нов профил",
"manage": "Управление на профили в хранилището",
"name": "Приятелско име",
"vaultAddr": "Адрес на трезора",
"namespace": "Именно пространство",
"oidcMount": "OIDC Authentication Mount",
"oidcRole": "Роля на OIDC",
"sshMount": "SSH Secrets Mount",
"sshRole": "Роля на SSH подписващ",
"validPrincipals": "Валидни принципали",
"keyType": "Тип ефимерен ключ",
"shareWithAll": "Споделяне с всички потребители",
"create": "Създаване на профил",
"created": "Профилът на трезора е създаден",
"saved": "Профилът на трезора е запазен",
"deleted": "Профилът в трезора е изтрит",
"saveFailed": "Запазването на профила в Трезора не бе успешно",
"deleteFailed": "Изтриването на профила в Vault не бе успешно",
"save": "Запазване на профил",
"validationError": "Име, адрес на трезора и роля на SSH подписващ са задължителни",
"none": "Все още няма профили в Трезора.",
"sharedBadge": "споделено"
},
"dialog": {
"title": "Изисква се влизане в трезора",
"description": "Отвори се прозорец за влизане в HashiCorp Vault. Завършете влизането там; тази връзка ще продължи автоматично.",
"failed": "Удостоверяването на трезора не бе успешно. Моля, опитайте отново.",
"reopen": "Отваряне отново на прозореца за влизане"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "ভল্ট স্বাক্ষরকারীর প্রোফাইল",
"selectProfile": "একটি ভল্ট প্রোফাইল নির্বাচন করুন...",
"hint": "সেটিংস শেয়ার করা প্রোফাইল থেকে আসে; সংযোগ করার সময় আপনি OIDC-এর মাধ্যমে ভল্টে সাইন ইন করবেন। কোনো গোপনীয় তথ্য সংরক্ষিত থাকে না।",
"docs": "নথি দেখুন"
},
"profiles": {
"new": "নতুন প্রোফাইল",
"manage": "ভল্ট প্রোফাইলগুলি পরিচালনা করুন",
"name": "বন্ধুত্বপূর্ণ নাম",
"vaultAddr": "ভল্ট ঠিকানা",
"namespace": "নেমস্পেস",
"oidcMount": "OIDC অথোরাইজেশন মাউন্ট",
"oidcRole": "OIDC ভূমিকা",
"sshMount": "SSH সিক্রেটস মাউন্ট",
"sshRole": "SSH স্বাক্ষরকারীর ভূমিকা",
"validPrincipals": "বৈধ অধ্যক্ষরা",
"keyType": "ক্ষণস্থায়ী কী টাইপ",
"shareWithAll": "সকল ব্যবহারকারীর সাথে শেয়ার করুন",
"create": "প্রোফাইল তৈরি করুন",
"created": "ভল্ট প্রোফাইল তৈরি করা হয়েছে",
"saved": "ভল্ট প্রোফাইল সংরক্ষিত হয়েছে",
"deleted": "ভল্ট প্রোফাইল মুছে ফেলা হয়েছে",
"saveFailed": "ভল্ট প্রোফাইল সংরক্ষণ করতে ব্যর্থ হয়েছে",
"deleteFailed": "ভল্ট প্রোফাইল মুছে ফেলা ব্যর্থ হয়েছে",
"save": "প্রোফাইল সংরক্ষণ করুন",
"validationError": "নাম, ভল্ট অ্যাড্রেস এবং SSH সাইনার রোল প্রয়োজন।",
"none": "এখনো কোনো ভল্ট প্রোফাইল তৈরি হয়নি।",
"sharedBadge": "শেয়ার করা"
},
"dialog": {
"title": "ভল্টে সাইন-ইন করা আবশ্যক",
"description": "হ্যাশিকর্প ভল্টে সাইন ইন করার জন্য একটি উইন্ডো খুলেছে। সেখানে সাইন-ইন সম্পন্ন করুন; এই সংযোগটি স্বয়ংক্রিয়ভাবে চলতে থাকবে।",
"failed": "ভল্ট প্রমাণীকরণ ব্যর্থ হয়েছে। অনুগ্রহ করে আবার চেষ্টা করুন।",
"reopen": "সাইন-ইন উইন্ডো পুনরায় খুলুন"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Perfil del signant de la caixa forta",
"selectProfile": "Seleccioneu un perfil de Vault...",
"hint": "La configuració prové del perfil compartit; iniciaràs la sessió a Vault mitjançant OIDC quan et connectis. No s'emmagatzemen secrets.",
"docs": "Veure documents"
},
"profiles": {
"new": "Nou perfil",
"manage": "Gestiona els perfils de Vault",
"name": "Nom amigable",
"vaultAddr": "Adreça de la caixa forta",
"namespace": "Espai de noms",
"oidcMount": "Muntatge d'autenticació OIDC",
"oidcRole": "Rol de l'OIDC",
"sshMount": "Muntanya de Secrets SSH",
"sshRole": "Rol de signant SSH",
"validPrincipals": "Principals vàlids",
"keyType": "Tipus de clau efímera",
"shareWithAll": "Comparteix amb tots els usuaris",
"create": "Crea un perfil",
"created": "Perfil de la caixa forta creat",
"saved": "Perfil de la caixa forta desat",
"deleted": "Perfil de la caixa forta suprimit",
"saveFailed": "No s'ha pogut desar el perfil de Vault",
"deleteFailed": "No s'ha pogut suprimir el perfil de Vault",
"save": "Desa el perfil",
"validationError": "El nom, l'adreça de la caixa forta i el rol de signant SSH són obligatoris.",
"none": "Encara no hi ha perfils de Vault.",
"sharedBadge": "compartit"
},
"dialog": {
"title": "Cal iniciar sessió a la caixa forta",
"description": "S'ha obert una finestra per iniciar la sessió a HashiCorp Vault. Completeu l'inici de sessió allà; aquesta connexió continuarà automàticament.",
"failed": "L'autenticació de la caixa forta ha fallat. Torna-ho a provar.",
"reopen": "Torna a obrir la finestra d'inici de sessió"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Profil podepisujícího trezoru",
"selectProfile": "Vyberte profil úložiště...",
"hint": "Nastavení pocházejí ze sdíleného profilu; po připojení se do úložiště přihlásíte přes OIDC. Žádné tajné kódy se neukládají.",
"docs": "Zobrazit dokumenty"
},
"profiles": {
"new": "Nový profil",
"manage": "Správa profilů úložiště",
"name": "Přátelský název",
"vaultAddr": "Adresa trezoru",
"namespace": "Jmenný prostor",
"oidcMount": "Připojení autorizace OIDC",
"oidcRole": "Role OIDC",
"sshMount": "SSH tajemství Mount",
"sshRole": "Role SSH podepisujícího",
"validPrincipals": "Platné principy",
"keyType": "Typ dočasného klíče",
"shareWithAll": "Sdílet se všemi uživateli",
"create": "Vytvořit profil",
"created": "Profil úložiště vytvořen",
"saved": "Profil trezoru uložen",
"deleted": "Profil trezoru smazán",
"saveFailed": "Uložení profilu úložiště se nezdařilo.",
"deleteFailed": "Profil v úložišti se nepodařilo smazat.",
"save": "Uložit profil",
"validationError": "Jméno, adresa trezoru a role SSH signatáře jsou povinné.",
"none": "Zatím žádné profily v úložišti Vault.",
"sharedBadge": "sdílené"
},
"dialog": {
"title": "Vyžaduje se přihlášení do trezoru",
"description": "Otevřelo se okno pro přihlášení do HashiCorp Vault. Dokončete přihlášení tam; toto připojení bude automaticky pokračovat.",
"failed": "Ověření v trezoru se nezdařilo. Zkuste to prosím znovu.",
"reopen": "Znovu otevřít přihlašovací okno"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Profil for underskriver af arkiv",
"selectProfile": "Vælg en Vault-profil...",
"hint": "Indstillingerne kommer fra den delte profil; du logger ind på Vault via OIDC, når du opretter forbindelse. Der gemmes ingen hemmeligheder.",
"docs": "Se dokumenter"
},
"profiles": {
"new": "Ny profil",
"manage": "Administrer Vault-profiler",
"name": "Venligt navn",
"vaultAddr": "Adresse i boksen",
"namespace": "Navnerum",
"oidcMount": "OIDC-godkendelsesmontering",
"oidcRole": "OIDC-rolle",
"sshMount": "SSH-hemmeligheder monteres",
"sshRole": "SSH-underskriverrolle",
"validPrincipals": "Gyldige principper",
"keyType": "Kortvarig nøgletype",
"shareWithAll": "Del med alle brugere",
"create": "Opret profil",
"created": "Vault-profil oprettet",
"saved": "Vault-profil gemt",
"deleted": "Vault-profil slettet",
"saveFailed": "Kunne ikke gemme Vault-profilen",
"deleteFailed": "Kunne ikke slette Vault-profilen",
"save": "Gem profil",
"validationError": "Navn, Vault-adresse og SSH-underskriverrolle er påkrævet",
"none": "Ingen Vault-profiler endnu.",
"sharedBadge": "delt"
},
"dialog": {
"title": "Login til Vault kræves",
"description": "Et vindue er åbnet, hvor du kan logge ind på HashiCorp Vault. Fuldfør loginprocessen der; forbindelsen fortsætter automatisk.",
"failed": "Godkendelse af arkiv mislykkedes. Prøv igen.",
"reopen": "Genåbn loginvinduet"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Vault-Signer-Profil",
"selectProfile": "Vault-Profil auswählen...",
"hint": "Die Einstellungen stammen aus dem freigegebenen Profil; Sie melden sich beim Verbinden per OIDC bei Vault an. Es werden keine Geheimnisse gespeichert.",
"docs": "Dokumentation anzeigen"
},
"profiles": {
"new": "Neues Profil",
"manage": "Vault-Profile verwalten",
"name": "Anzeigename",
"vaultAddr": "Vault-Adresse",
"namespace": "Namespace",
"oidcMount": "OIDC-Auth-Mount",
"oidcRole": "OIDC-Rolle",
"sshMount": "SSH-Secrets-Mount",
"sshRole": "SSH-Signer-Rolle",
"validPrincipals": "Gültige Principals",
"keyType": "Ephemerer Schlüsseltyp",
"shareWithAll": "Mit allen Benutzern teilen",
"create": "Profil erstellen",
"created": "Vault-Profil erstellt",
"saved": "Vault-Profil gespeichert",
"deleted": "Vault-Profil gelöscht",
"saveFailed": "Fehler beim Speichern des Vault-Profils",
"deleteFailed": "Fehler beim Löschen des Vault-Profils",
"save": "Profil speichern",
"validationError": "Name, Vault-Adresse und SSH-Signer-Rolle sind erforderlich",
"none": "Noch keine Vault-Profile.",
"sharedBadge": "geteilt"
},
"dialog": {
"title": "Vault-Anmeldung erforderlich",
"description": "Ein Fenster zur Anmeldung bei HashiCorp Vault wurde geöffnet. Schließen Sie die Anmeldung dort ab; diese Verbindung wird automatisch fortgesetzt.",
"failed": "Vault-Authentifizierung fehlgeschlagen. Bitte versuchen Sie es erneut.",
"reopen": "Anmeldefenster erneut öffnen"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Προφίλ Υπογράφοντος Vault",
"selectProfile": "Επιλέξτε ένα προφίλ Vault...",
"hint": "Οι ρυθμίσεις προέρχονται από το κοινόχρηστο προφίλ. Θα συνδεθείτε στο Vault μέσω OIDC όταν συνδεθείτε. Δεν αποθηκεύονται μυστικά.",
"docs": "Προβολή εγγράφων"
},
"profiles": {
"new": "Νέο προφίλ",
"manage": "Διαχείριση προφίλ Vault",
"name": "Φιλικό όνομα",
"vaultAddr": "Διεύθυνση θησαυροφυλακίου",
"namespace": "Χώρος ονομάτων",
"oidcMount": "Μοντάζ ελέγχου ταυτότητας OIDC",
"oidcRole": "Ρόλος OIDC",
"sshMount": "SSH Secrets Mount",
"sshRole": "Ρόλος Υπογράφοντος SSH",
"validPrincipals": "Έγκυρες Αρχές",
"keyType": "Εφήμερος Τύπος Κλειδιού",
"shareWithAll": "Κοινοποίηση με όλους τους χρήστες",
"create": "Δημιουργία προφίλ",
"created": "Δημιουργήθηκε προφίλ Vault",
"saved": "Το προφίλ του Vault αποθηκεύτηκε",
"deleted": "Το προφίλ Vault διαγράφηκε",
"saveFailed": "Αποτυχία αποθήκευσης προφίλ Vault",
"deleteFailed": "Αποτυχία διαγραφής προφίλ Vault",
"save": "Αποθήκευση προφίλ",
"validationError": "Απαιτούνται όνομα, διεύθυνση vault και ρόλος υπογράφοντος SSH",
"none": "Δεν υπάρχουν ακόμη προφίλ Vault.",
"sharedBadge": "κοινόχρηστο"
},
"dialog": {
"title": "Απαιτείται σύνδεση στο Vault",
"description": "Έχει ανοίξει ένα παράθυρο για να συνδεθείτε στο HashiCorp Vault. Ολοκληρώστε τη σύνδεση εκεί. Αυτή η σύνδεση θα συνεχιστεί αυτόματα.",
"failed": "Ο έλεγχος ταυτότητας του vault απέτυχε. Δοκιμάστε ξανά.",
"reopen": "Επανάνοιγμα παραθύρου σύνδεσης"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Perfil de firmante de Vault",
"selectProfile": "Seleccionar un perfil de Vault...",
"hint": "La configuración proviene del perfil compartido; se iniciará sesión en Vault a través de OIDC al conectarse. No se almacenan secretos.",
"docs": "Ver documentación"
},
"profiles": {
"new": "Nuevo perfil",
"manage": "Administrar perfiles de Vault",
"name": "Nombre descriptivo",
"vaultAddr": "Dirección de Vault",
"namespace": "Espacio de nombres",
"oidcMount": "Montaje de autenticación OIDC",
"oidcRole": "Rol OIDC",
"sshMount": "Montaje de secretos SSH",
"sshRole": "Rol de firmante SSH",
"validPrincipals": "Principals válidos",
"keyType": "Tipo de clave efímera",
"shareWithAll": "Compartir con todos los usuarios",
"create": "Crear perfil",
"created": "Perfil de Vault creado",
"saved": "Perfil de Vault guardado",
"deleted": "Perfil de Vault eliminado",
"saveFailed": "Error al guardar el perfil de Vault",
"deleteFailed": "Error al eliminar el perfil de Vault",
"save": "Guardar perfil",
"validationError": "Se requieren el nombre, la dirección de Vault y el rol de firmante SSH",
"none": "Aún no hay perfiles de Vault.",
"sharedBadge": "compartido"
},
"dialog": {
"title": "Inicio de sesión en Vault requerido",
"description": "Se ha abierto una ventana para iniciar sesión en HashiCorp Vault. Completa el inicio de sesión allí; esta conexión continuará automáticamente.",
"failed": "La autenticación de Vault falló. Inténtalo de nuevo.",
"reopen": "Reabrir ventana de inicio de sesión"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Varaston allekirjoittajan profiili",
"selectProfile": "Valitse holviprofiili...",
"hint": "Asetukset tulevat jaetusta profiilista; kirjaudut Holviin OIDC:n kautta, kun muodostat yhteyden. Salaisuuksia ei tallenneta.",
"docs": "Näytä dokumentit"
},
"profiles": {
"new": "Uusi profiili",
"manage": "Hallinnoi holviprofiileja",
"name": "Ystävällinen nimi",
"vaultAddr": "Holvin osoite",
"namespace": "Nimiavaruus",
"oidcMount": "OIDC-todennusliitos",
"oidcRole": "OIDC-rooli",
"sshMount": "SSH-salaisuuksien kiinnitys",
"sshRole": "SSH-allekirjoittajan rooli",
"validPrincipals": "Voimassa olevat pääasialliset",
"keyType": "Lyhytaikainen avaintyyppi",
"shareWithAll": "Jaa kaikkien käyttäjien kanssa",
"create": "Luo profiili",
"created": "Holvin profiili luotu",
"saved": "Holvin profiili tallennettu",
"deleted": "Holvin profiili poistettu",
"saveFailed": "Holvin profiilin tallentaminen epäonnistui",
"deleteFailed": "Holviprofiilin poistaminen epäonnistui",
"save": "Tallenna profiili",
"validationError": "Nimi, holvin osoite ja SSH-allekirjoittajan rooli vaaditaan",
"none": "Ei vielä holviprofiileja.",
"sharedBadge": "jaettu"
},
"dialog": {
"title": "Holviin kirjautuminen vaaditaan",
"description": "Ikkuna HashiCorp-holviin kirjautumista varten on avautunut. Kirjaudu sisään siinä. Yhteys jatkuu automaattisesti.",
"failed": "Holvin todennus epäonnistui. Yritä uudelleen.",
"reopen": "Avaa kirjautumisikkuna uudelleen"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Profil de signature Vault",
"selectProfile": "Sélectionner un profil Vault...",
"hint": "Les paramètres proviennent du profil partagé ; vous vous connecterez à Vault via OIDC lors de la connexion. Aucun secret n'est stocké.",
"docs": "Voir la documentation"
},
"profiles": {
"new": "Nouveau profil",
"manage": "Gérer les profils Vault",
"name": "Nom convivial",
"vaultAddr": "Adresse Vault",
"namespace": "Espace de noms",
"oidcMount": "Point de montage d'authentification OIDC",
"oidcRole": "Rôle OIDC",
"sshMount": "Point de montage des secrets SSH",
"sshRole": "Rôle de signature SSH",
"validPrincipals": "Principaux valides",
"keyType": "Type de clé éphémère",
"shareWithAll": "Partager avec tous les utilisateurs",
"create": "Créer un profil",
"created": "Profil Vault créé",
"saved": "Profil Vault enregistré",
"deleted": "Profil Vault supprimé",
"saveFailed": "Échec de l'enregistrement du profil Vault",
"deleteFailed": "Échec de la suppression du profil Vault",
"save": "Enregistrer le profil",
"validationError": "Le nom, l'adresse Vault et le rôle de signataire SSH sont requis",
"none": "Aucun profil Vault pour le moment",
"sharedBadge": "partagé"
},
"dialog": {
"title": "Connexion à Vault requise",
"description": "Une fenêtre s'est ouverte pour vous connecter à HashiCorp Vault. Terminez la connexion dans cette fenêtre ; cette connexion continuera automatiquement.",
"failed": "Échec de l'authentification Vault. Veuillez réessayer.",
"reopen": "Rouvrir la fenêtre de connexion"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "פרופיל חותם בכספת",
"selectProfile": "בחר פרופיל כספת...",
"hint": "ההגדרות מגיעות מהפרופיל המשותף; תיכנס לכספת דרך OIDC כשתתחבר. לא נשמרים סודות.",
"docs": "הצג מסמכים"
},
"profiles": {
"new": "פרופיל חדש",
"manage": "ניהול פרופילי כספת",
"name": "שם ידידותי",
"vaultAddr": "כתובת הכספת",
"namespace": "מרחב השמות",
"oidcMount": "אישור OIDC",
"oidcRole": "תפקיד OIDC",
"sshMount": "סודות SSH מאוורר",
"sshRole": "תפקיד חותם SSH",
"validPrincipals": "עקרונות תקפים",
"keyType": "סוג מפתח זמני",
"shareWithAll": "שתף עם כל המשתמשים",
"create": "צור פרופיל",
"created": "פרופיל הכספת נוצר",
"saved": "פרופיל הכספת נשמר",
"deleted": "פרופיל הכספת נמחק",
"saveFailed": "שמירת פרופיל הכספת נכשלה",
"deleteFailed": "נכשלה מחיקת פרופיל הכספת",
"save": "שמור פרופיל",
"validationError": "שם, כתובת כספת ותפקיד חתימה ב-SSH נדרשים",
"none": "אין עדיין פרופילים בכספת.",
"sharedBadge": "מְשׁוּתָף"
},
"dialog": {
"title": "נדרשת כניסה לכספת",
"description": "נפתח חלון לכניסה לכספת HashiCorp. השלם את תהליך הכניסה שם; חיבור זה ימשיך אוטומטית.",
"failed": "אימות הכספת נכשל. אנא נסה שוב.",
"reopen": "פתח מחדש את חלון הכניסה"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Vault साइनर प्रोफ़ाइल",
"selectProfile": "एक Vault प्रोफ़ाइल चुनें...",
"hint": "सेटिंग साझा प्रोफ़ाइल से आती हैं; कनेक्ट करते समय आप OIDC के माध्यम से Vault में साइन इन करेंगे। कोई सीक्रेट संग्रहीत नहीं किए जाते।",
"docs": "दस्तावेज़ देखें"
},
"profiles": {
"new": "नई प्रोफ़ाइल",
"manage": "Vault प्रोफ़ाइल प्रबंधित करें",
"name": "अनुकूल नाम",
"vaultAddr": "Vault पता",
"namespace": "नेमस्पेस",
"oidcMount": "OIDC ऑथ माउंट",
"oidcRole": "OIDC रोल",
"sshMount": "SSH सीक्रेट माउंट",
"sshRole": "SSH साइनर रोल",
"validPrincipals": "मान्य प्रिंसिपल",
"keyType": "एफेमरल कुंजी प्रकार",
"shareWithAll": "सभी उपयोगकर्ताओं के साथ साझा करें",
"create": "प्रोफ़ाइल बनाएँ",
"created": "Vault प्रोफ़ाइल बनाई गई",
"saved": "Vault प्रोफ़ाइल सहेजी गई",
"deleted": "Vault प्रोफ़ाइल हटाई गई",
"saveFailed": "Vault प्रोफ़ाइल सहेजने में विफल",
"deleteFailed": "Vault प्रोफ़ाइल हटाने में विफल",
"save": "प्रोफ़ाइल सहेजें",
"validationError": "नाम, Vault पता और SSH हस्ताक्षरकर्ता भूमिका आवश्यक हैं",
"none": "अभी तक कोई Vault प्रोफ़ाइल नहीं है।",
"sharedBadge": "साझा"
},
"dialog": {
"title": "Vault साइन-इन आवश्यक है",
"description": "HashiCorp Vault में साइन इन करने के लिए एक विंडो खुल गई है। वहाँ साइन-इन पूरा करें; यह कनेक्शन स्वचालित रूप से जारी रहेगा।",
"failed": "Vault प्रमाणीकरण विफल। कृपया पुनः प्रयास करें।",
"reopen": "साइन-इन विंडो फिर से खोलें"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Vault aláírói profil",
"selectProfile": "Válasszon ki egy Vault profilt...",
"hint": "A beállítások a megosztott profilból származnak; a csatlakozáskor OIDC-n keresztül fog bejelentkezni a Vaultba. A rendszer nem tárol titkos adatokat.",
"docs": "Dokumentumok megtekintése"
},
"profiles": {
"new": "Új profil",
"manage": "Vault profilok kezelése",
"name": "Barátságos név",
"vaultAddr": "Trezor címe",
"namespace": "Névtér",
"oidcMount": "OIDC hitelesítési csatolás",
"oidcRole": "OIDC szerepkör",
"sshMount": "SSH titkok felhúzása",
"sshRole": "SSH aláíró szerepkör",
"validPrincipals": "Érvényes megbízók",
"keyType": "Efemer kulcstípus",
"shareWithAll": "Megosztás az összes felhasználóval",
"create": "Profil létrehozása",
"created": "Létrehozva a trezor profilja",
"saved": "A trezor profilja mentve",
"deleted": "Tárolóprofil törölve",
"saveFailed": "Nem sikerült menteni a Vault profilját",
"deleteFailed": "Nem sikerült törölni a Vault profilját",
"save": "Profil mentése",
"validationError": "Név, trezor címe és SSH aláírói szerepkör megadása kötelező.",
"none": "Még nincsenek Vault-profilok.",
"sharedBadge": "megosztott"
},
"dialog": {
"title": "Bejelentkezés szükséges a széfbe",
"description": "Megnyílt egy ablak a HashiCorp Vaultba való bejelentkezéshez. Fejezze be a bejelentkezést ott; a kapcsolat automatikusan folytatódik.",
"failed": "A trezor hitelesítése sikertelen. Próbálja újra.",
"reopen": "Bejelentkezési ablak újranyitása"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Profil Penandatangan Vault",
"selectProfile": "Pilih profil Vault...",
"hint": "Pengaturan berasal dari profil bersama; Anda akan masuk ke Vault melalui OIDC saat terhubung. Tidak ada rahasia yang disimpan.",
"docs": "Lihat dokumen"
},
"profiles": {
"new": "Profil baru",
"manage": "Kelola profil Vault",
"name": "Nama yang Ramah",
"vaultAddr": "Alamat Brankas",
"namespace": "Ruang nama",
"oidcMount": "OIDC Otorisasi Pemasangan",
"oidcRole": "Peran OIDC",
"sshMount": "Pemasangan Rahasia SSH",
"sshRole": "Peran Penandatangan SSH",
"validPrincipals": "Prinsip-prinsip yang Sah",
"keyType": "Jenis Kunci Sementara",
"shareWithAll": "Bagikan dengan semua pengguna",
"create": "Buat profil",
"created": "Profil Vault telah dibuat.",
"saved": "Profil Vault tersimpan.",
"deleted": "Profil Vault dihapus",
"saveFailed": "Gagal menyimpan profil Vault",
"deleteFailed": "Gagal menghapus profil Vault",
"save": "Simpan profil",
"validationError": "Nama, alamat Vault, dan peran penandatangan SSH wajib diisi.",
"none": "Belum ada profil Vault.",
"sharedBadge": "dibagikan"
},
"dialog": {
"title": "Login ke Vault diperlukan.",
"description": "Jendela telah terbuka untuk masuk ke HashiCorp Vault. Selesaikan proses masuk di sana; koneksi ini akan berlanjut secara otomatis.",
"failed": "Autentikasi Vault gagal. Silakan coba lagi.",
"reopen": "Buka kembali jendela masuk"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Profilo firmatario Vault",
"selectProfile": "Seleziona un profilo Vault...",
"hint": "Le impostazioni provengono dal profilo condiviso; effettuerai l'accesso a Vault tramite OIDC quando ti connetti. Nessun segreto è memorizzato.",
"docs": "Visualizza documentazione"
},
"profiles": {
"new": "Nuovo profilo",
"manage": "Gestisci profili Vault",
"name": "Nome descrittivo",
"vaultAddr": "Indirizzo Vault",
"namespace": "Namespace",
"oidcMount": "Mount di autenticazione OIDC",
"oidcRole": "Ruolo OIDC",
"sshMount": "Mount dei segreti SSH",
"sshRole": "Ruolo firmatario SSH",
"validPrincipals": "Principal validi",
"keyType": "Tipo di chiave effimera",
"shareWithAll": "Condividi con tutti gli utenti",
"create": "Crea profilo",
"created": "Profilo Vault creato",
"saved": "Profilo Vault salvato",
"deleted": "Profilo Vault eliminato",
"saveFailed": "Salvataggio profilo Vault non riuscito",
"deleteFailed": "Eliminazione profilo Vault non riuscita",
"save": "Salva profilo",
"validationError": "Nome, indirizzo Vault e ruolo di firma SSH sono obbligatori",
"none": "Nessun profilo Vault ancora.",
"sharedBadge": "condiviso"
},
"dialog": {
"title": "Accesso a Vault richiesto",
"description": "Una finestra si è aperta per accedere a HashiCorp Vault. Completa l'accesso lì; la connessione continuerà automaticamente.",
"failed": "Autenticazione Vault fallita. Riprova.",
"reopen": "Riapri la finestra di accesso"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Vault 署名者プロファイル",
"selectProfile": "Vault プロファイルを選択...",
"hint": "設定は共有プロファイルから取得され、接続時に OIDC で Vault にサインインします。シークレットは保存されません。",
"docs": "ドキュメントを表示"
},
"profiles": {
"new": "新規プロファイル",
"manage": "Vault プロファイルの管理",
"name": "表示名",
"vaultAddr": "Vault アドレス",
"namespace": "名前空間",
"oidcMount": "OIDC 認証マウント",
"oidcRole": "OIDC ロール",
"sshMount": "SSH シークレットマウント",
"sshRole": "SSH 署名者ロール",
"validPrincipals": "有効なプリンシパル",
"keyType": "エフェメラルキータイプ",
"shareWithAll": "すべてのユーザーと共有",
"create": "プロファイルを作成",
"created": "Vaultプロファイルが作成されました",
"saved": "Vaultプロファイルが保存されました",
"deleted": "Vaultプロファイルが削除されました",
"saveFailed": "Vaultプロファイルの保存に失敗しました",
"deleteFailed": "Vaultプロファイルの削除に失敗しました",
"save": "プロファイルを保存",
"validationError": "名前、Vaultアドレス、SSH署名ロールは必須です",
"none": "まだVaultプロファイルがありません。",
"sharedBadge": "共有"
},
"dialog": {
"title": "Vault サインインが必要です",
"description": "HashiCorp Vaultへのサインイン用のウィンドウが開きました。そこでサインインを完了すると、この接続は自動的に続行されます。",
"failed": "Vault 認証に失敗しました。もう一度お試しください。",
"reopen": "サインインウィンドウを再表示"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Vault 서명자 프로필",
"selectProfile": "Vault 프로필 선택...",
"hint": "설정은 공유 프로필에서 가져옵니다. 연결 시 OIDC를 통해 Vault에 로그인하게 됩니다. 비밀은 저장되지 않습니다.",
"docs": "문서 보기"
},
"profiles": {
"new": "새 프로필",
"manage": "Vault 프로필 관리",
"name": "별칭",
"vaultAddr": "Vault 주소",
"namespace": "네임스페이스",
"oidcMount": "OIDC 인증 마운트",
"oidcRole": "OIDC 역할",
"sshMount": "SSH 시크릿 마운트",
"sshRole": "SSH 서명자 역할",
"validPrincipals": "유효한 프린시펄",
"keyType": "임시 키 유형",
"shareWithAll": "모든 사용자와 공유",
"create": "프로필 생성",
"created": "Vault 프로필 생성됨",
"saved": "Vault 프로필 저장됨",
"deleted": "Vault 프로필 삭제됨",
"saveFailed": "Vault 프로필 저장 실패",
"deleteFailed": "Vault 프로필 삭제 실패",
"save": "프로필 저장",
"validationError": "이름, Vault 주소 및 SSH 서명자 역할이 필요합니다.",
"none": "아직 Vault 프로필이 없습니다.",
"sharedBadge": "공유"
},
"dialog": {
"title": "Vault 로그인 필요",
"description": "HashiCorp Vault 로그인을 위한 창이 열렸습니다. 해당 창에서 로그인을 완료하면 연결이 자동으로 계속됩니다.",
"failed": "Vault 인증에 실패했습니다. 다시 시도하세요.",
"reopen": "로그인 창 다시 열기"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Vault-ondertekenaarprofiel",
"selectProfile": "Selecteer een Vault-profiel...",
"hint": "De instellingen zijn afkomstig van het gedeelde profiel; u meldt zich bij Vault aan via OIDC wanneer u verbinding maakt. Er worden geen geheimen opgeslagen.",
"docs": "Documenten bekijken"
},
"profiles": {
"new": "Nieuw profiel",
"manage": "Beheer Vault-profielen",
"name": "Vriendelijke naam",
"vaultAddr": "Kluisadres",
"namespace": "Naamruimte",
"oidcMount": "OIDC-authenticatiekoppeling",
"oidcRole": "OIDC-rol",
"sshMount": "SSH-geheimen Mount",
"sshRole": "SSH-ondertekenaarrol",
"validPrincipals": "Geldige principes",
"keyType": "Tijdelijk sleuteltype",
"shareWithAll": "Delen met alle gebruikers",
"create": "Profiel aanmaken",
"created": "Vault-profiel aangemaakt",
"saved": "Vault-profiel opgeslagen",
"deleted": "Kluisprofiel verwijderd",
"saveFailed": "Het opslaan van het Vault-profiel is mislukt.",
"deleteFailed": "Het verwijderen van het Vault-profiel is mislukt.",
"save": "Profiel opslaan",
"validationError": "Naam, kluisadres en SSH-ondertekeningsrol zijn vereist.",
"none": "Nog geen Vault-profielen beschikbaar.",
"sharedBadge": "gedeeld"
},
"dialog": {
"title": "Aanmelden bij Vault is vereist.",
"description": "Er is een venster geopend om in te loggen bij HashiCorp Vault. Voltooi de aanmelding daar; deze verbinding wordt automatisch tot stand gebracht.",
"failed": "Authenticatie voor de kluis is mislukt. Probeer het opnieuw.",
"reopen": "Heropen het aanmeldvenster"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Profil for hvelvsigner",
"selectProfile": "Velg en Vault-profil...",
"hint": "Innstillingene kommer fra den delte profilen. Du logger deg på Vault via OIDC når du kobler til. Ingen hemmeligheter lagres.",
"docs": "Vis dokumenter"
},
"profiles": {
"new": "Ny profil",
"manage": "Administrer Vault-profiler",
"name": "Vennlig navn",
"vaultAddr": "Hvelvadresse",
"namespace": "Navnerom",
"oidcMount": "OIDC-autentiseringsmontering",
"oidcRole": "OIDC-rolle",
"sshMount": "SSH-hemmeligheter monteres",
"sshRole": "SSH-signatørrolle",
"validPrincipals": "Gyldige prinsipaler",
"keyType": "Kortvarig nøkkeltype",
"shareWithAll": "Del med alle brukere",
"create": "Opprett profil",
"created": "Vault-profil opprettet",
"saved": "Vault-profilen er lagret",
"deleted": "Vault-profilen er slettet",
"saveFailed": "Kunne ikke lagre Vault-profilen",
"deleteFailed": "Kunne ikke slette Vault-profilen",
"save": "Lagre profil",
"validationError": "Navn, hvelvadresse og SSH-signerrolle er obligatorisk",
"none": "Ingen Vault-profiler ennå.",
"sharedBadge": "delt"
},
"dialog": {
"title": "Pålogging til arkivet kreves",
"description": "Et vindu har åpnet seg for å logge på HashiCorp Vault. Fullfør påloggingen der; denne tilkoblingen vil fortsette automatisk.",
"failed": "Hvelv-autentisering mislyktes. Prøv på nytt.",
"reopen": "Åpne påloggingsvinduet på nytt"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Profil podpisujący Vault",
"selectProfile": "Wybierz profil Vault...",
"hint": "Ustawienia pochodzą z profilu współdzielonego. Po połączeniu zalogujesz się do Vault przez OIDC. Nie są przechowywane żadne sekrety.",
"docs": "Zobacz dokumentację"
},
"profiles": {
"new": "Nowy profil",
"manage": "Zarządzaj profilami Vault",
"name": "Przyjazna nazwa",
"vaultAddr": "Adres Vault",
"namespace": "Przestrzeń nazw",
"oidcMount": "Mount uwierzytelniania OIDC",
"oidcRole": "Rola OIDC",
"sshMount": "Mount sekretów SSH",
"sshRole": "Rola podpisująca SSH",
"validPrincipals": "Dozwoleni użytkownicy (principals)",
"keyType": "Typ klucza efemerycznego",
"shareWithAll": "Udostępnij wszystkim użytkownikom",
"create": "Utwórz profil",
"created": "Utworzono profil Vault",
"saved": "Zapisano profil Vault",
"deleted": "Usunięto profil Vault",
"saveFailed": "Nie udało się zapisać profilu Vault",
"deleteFailed": "Nie udało się usunąć profilu Vault",
"save": "Zapisz profil",
"validationError": "Wymagane są nazwa, adres Vault i rola podpisująca SSH",
"none": "Brak profili Vault.",
"sharedBadge": "współdzielony"
},
"dialog": {
"title": "Wymagane logowanie do Vault",
"description": "Otworzyło się okno logowania do HashiCorp Vault. Dokończ logowanie, a połączenie zostanie nawiązane automatycznie.",
"failed": "Uwierzytelnienie w Vault nie powiodło się. Spróbuj ponownie.",
"reopen": "Otwórz ponownie okno logowania"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Perfil do assinante do Vault",
"selectProfile": "Selecione um perfil do Vault...",
"hint": "As configurações são provenientes do perfil compartilhado; você fará login no Vault via OIDC ao se conectar. Nenhum segredo é armazenado.",
"docs": "Ver documentos"
},
"profiles": {
"new": "Novo perfil",
"manage": "Gerenciar perfis do Vault",
"name": "Nome amigável",
"vaultAddr": "Endereço do cofre",
"namespace": "Espaço de nomes",
"oidcMount": "Montagem de autenticação OIDC",
"oidcRole": "Função OIDC",
"sshMount": "Montagem de segredos SSH",
"sshRole": "Função de assinatura SSH",
"validPrincipals": "Principais válidos",
"keyType": "Tipo de chave efêmera",
"shareWithAll": "Compartilhe com todos os usuários",
"create": "Criar perfil",
"created": "Perfil do Vault criado",
"saved": "Perfil do Vault salvo",
"deleted": "Perfil do Vault excluído",
"saveFailed": "Falha ao salvar o perfil do Vault",
"deleteFailed": "Falha ao excluir o perfil do Vault",
"save": "Salvar perfil",
"validationError": "Nome, endereço do Vault e função de assinatura SSH são obrigatórios.",
"none": "Ainda não há perfis no Vault.",
"sharedBadge": "compartilhado"
},
"dialog": {
"title": "É necessário fazer login no cofre.",
"description": "Uma janela foi aberta para fazer login no HashiCorp Vault. Conclua o login; esta conexão será estabelecida automaticamente.",
"failed": "A autenticação do cofre falhou. Tente novamente.",
"reopen": "Reabrir janela de login"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Perfil de assinatura Vault",
"selectProfile": "Selecionar um perfil Vault...",
"hint": "As definições provêm do perfil partilhado; ao ligar, iniciará sessão no Vault via OIDC. Nenhum segredo é armazenado.",
"docs": "Ver documentação"
},
"profiles": {
"new": "Novo perfil",
"manage": "Gerir perfis Vault",
"name": "Nome Amigável",
"vaultAddr": "Endereço Vault",
"namespace": "Namespace",
"oidcMount": "Mount de autenticação OIDC",
"oidcRole": "Função OIDC",
"sshMount": "Mount de segredos SSH",
"sshRole": "Função de assinatura SSH",
"validPrincipals": "Principais válidos",
"keyType": "Tipo de chave efémera",
"shareWithAll": "Partilhar com todos os utilizadores",
"create": "Criar perfil",
"created": "Perfil Vault criado",
"saved": "Perfil Vault guardado",
"deleted": "Perfil Vault eliminado",
"saveFailed": "Falha ao guardar perfil Vault",
"deleteFailed": "Falha ao eliminar perfil Vault",
"save": "Guardar perfil",
"validationError": "Nome, endereço Vault e função de signatário SSH são obrigatórios",
"none": "Ainda sem perfis Vault.",
"sharedBadge": "partilhado"
},
"dialog": {
"title": "Início de sessão no Vault necessário",
"description": "Foi aberta uma janela para iniciar sessão no HashiCorp Vault. Conclua o início de sessão aí; esta ligação continuará automaticamente.",
"failed": "A autenticação no Vault falhou. Tente novamente.",
"reopen": "Reabrir janela de início de sessão"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Profilul semnatarului din seif",
"selectProfile": "Selectați un profil Vault...",
"hint": "Setările provin din profilul partajat; te vei conecta la Vault prin OIDC când te conectezi. Nu sunt stocate secrete.",
"docs": "Vizualizați documentele"
},
"profiles": {
"new": "Profil nou",
"manage": "Gestionați profilurile din seif",
"name": "Nume prietenos",
"vaultAddr": "Adresă seif",
"namespace": "Spațiu de nume",
"oidcMount": "Montare autentificare OIDC",
"oidcRole": "Rolul OIDC",
"sshMount": "Secretele SSH Mount",
"sshRole": "Rolul semnatarului SSH",
"validPrincipals": "Principali validi",
"keyType": "Tip de cheie efemeră",
"shareWithAll": "Partajați cu toți utilizatorii",
"create": "Creați un profil",
"created": "Profilul seifului a fost creat",
"saved": "Profilul seifului a fost salvat",
"deleted": "Profilul seifului a fost șters",
"saveFailed": "Nu s-a putut salva profilul din Seif",
"deleteFailed": "Nu s-a putut șterge profilul Vault",
"save": "Salvați profilul",
"validationError": "Numele, adresa seifului și rolul semnatarului SSH sunt obligatorii.",
"none": "Încă nu există profiluri Vault.",
"sharedBadge": "partajat"
},
"dialog": {
"title": "Este necesară conectarea la seif",
"description": "S-a deschis o fereastră pentru a vă conecta la HashiCorp Vault. Finalizați conectarea acolo; această conexiune va continua automat.",
"failed": "Autentificarea în seif a eșuat. Vă rugăm să încercați din nou.",
"reopen": "Redeschideți fereastra de conectare"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Профиль подписчика Vault",
"selectProfile": "Выберите профиль Vault...",
"hint": "Настройки берутся из общего профиля; при подключении вы войдёте в Vault через OIDC. Секреты не хранятся.",
"docs": "Документация"
},
"profiles": {
"new": "Новый профиль",
"manage": "Управление профилями Vault",
"name": "Дружественное имя",
"vaultAddr": "Адрес Vault",
"namespace": "Пространство имён",
"oidcMount": "Монтирование аутентификации OIDC",
"oidcRole": "Роль OIDC",
"sshMount": "Монтирование секретов SSH",
"sshRole": "Роль подписчика SSH",
"validPrincipals": "Допустимые принципалы",
"keyType": "Тип эфемерного ключа",
"shareWithAll": "Общий доступ для всех пользователей",
"create": "Создать профиль",
"created": "Профиль Vault создан",
"saved": "Профиль Vault сохранён",
"deleted": "Профиль Vault удалён",
"saveFailed": "Не удалось сохранить профиль Vault",
"deleteFailed": "Не удалось удалить профиль Vault",
"save": "Сохранить профиль",
"validationError": "Требуются имя, адрес Vault и роль SSH подписанта",
"none": "Пока нет профилей Vault.",
"sharedBadge": "общий"
},
"dialog": {
"title": "Требуется вход в Vault",
"description": "Открыто окно для входа в HashiCorp Vault. Завершите вход там; подключение продолжится автоматически.",
"failed": "Аутентификация Vault не удалась. Попробуйте снова.",
"reopen": "Повторно открыть окно входа"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Профил потписника трезора",
"selectProfile": "Изаберите профил трезора...",
"hint": "Подешавања долазе из дељеног профила; пријавићете се у Vault преко OIDC-а када се повежете. Ниједне тајне се не чувају.",
"docs": "Прикажи документе"
},
"profiles": {
"new": "Нови профил",
"manage": "Управљање профилима трезора",
"name": "Пријатељско име",
"vaultAddr": "Адреса трезора",
"namespace": "Именски простор",
"oidcMount": "OIDC аутентификација монтирања",
"oidcRole": "Улога OIDC-а",
"sshMount": "SSH тајне носач",
"sshRole": "Улога SSH потписника",
"validPrincipals": "Важећи принципали",
"keyType": "Тип ефемерног кључа",
"shareWithAll": "Подели са свим корисницима",
"create": "Направи профил",
"created": "Профил трезора је креиран",
"saved": "Профил трезора је сачуван",
"deleted": "Профил трезора је обрисан",
"saveFailed": "Чување профила у трезору није успело",
"deleteFailed": "Брисање профила у Трезору није успело",
"save": "Сачувај профил",
"validationError": "Име, адреса трезора и улога SSH потписника су обавезни",
"none": "Још нема профила у Трезору.",
"sharedBadge": "дељено"
},
"dialog": {
"title": "Потребно је пријављивање у трезор",
"description": "Отворио се прозор за пријављивање на HashiCorp Vault. Завршите пријављивање тамо; ова веза ће се аутоматски наставити.",
"failed": "Аутентификација трезора није успела. Молимо покушајте поново.",
"reopen": "Поново отвори прозор за пријављивање"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Profil för valvsignerare",
"selectProfile": "Välj en Vault-profil...",
"hint": "Inställningarna kommer från den delade profilen; du loggar in på Vault via OIDC när du ansluter. Inga hemligheter lagras.",
"docs": "Visa dokument"
},
"profiles": {
"new": "Ny profil",
"manage": "Hantera Vault-profiler",
"name": "Vänligt namn",
"vaultAddr": "Valvadress",
"namespace": "Namnrymd",
"oidcMount": "OIDC-autentiseringsmontering",
"oidcRole": "OIDC-roll",
"sshMount": "SSH-hemligheter monteras",
"sshRole": "SSH-signerarroll",
"validPrincipals": "Giltiga principer",
"keyType": "Tillfällig nyckeltyp",
"shareWithAll": "Dela med alla användare",
"create": "Skapa profil",
"created": "Arkivprofil skapad",
"saved": "Arkivprofilen har sparats",
"deleted": "Arkivprofil raderad",
"saveFailed": "Kunde inte spara arkivprofilen",
"deleteFailed": "Misslyckades med att ta bort Arkiv-profilen",
"save": "Spara profil",
"validationError": "Namn, valvadress och SSH-signerarroll krävs",
"none": "Inga Vault-profiler ännu.",
"sharedBadge": "delad"
},
"dialog": {
"title": "Inloggning till arkivet krävs",
"description": "Ett fönster har öppnats för att logga in på HashiCorp Vault. Slutför inloggningen där; anslutningen fortsätter automatiskt.",
"failed": "Valvautentisering misslyckades. Försök igen.",
"reopen": "Öppna inloggningsfönstret igen"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "โปรไฟล์ผู้ลงนาม Vault",
"selectProfile": "เลือกโปรไฟล์ Vault...",
"hint": "การตั้งค่าต่างๆ มาจากโปรไฟล์ที่ใช้ร่วมกัน คุณจะเข้าสู่ระบบ Vault ผ่าน OIDC เมื่อเชื่อมต่อ ไม่มีข้อมูลลับใดๆ ถูกจัดเก็บไว้",
"docs": "ดูเอกสาร"
},
"profiles": {
"new": "โปรไฟล์ใหม่",
"manage": "จัดการโปรไฟล์ Vault",
"name": "ชื่อที่เป็นมิตร",
"vaultAddr": "ที่อยู่ห้องนิรภัย",
"namespace": "เนมสเปซ",
"oidcMount": "OIDC Auth Mount",
"oidcRole": "บทบาทของ OIDC",
"sshMount": "SSH Secrets Mount",
"sshRole": "บทบาทผู้ลงนาม SSH",
"validPrincipals": "หลักการที่ถูกต้อง",
"keyType": "ประเภทคีย์ชั่วคราว",
"shareWithAll": "แชร์กับผู้ใช้ทั้งหมด",
"create": "สร้างโปรไฟล์",
"created": "สร้างโปรไฟล์ Vault แล้ว",
"saved": "บันทึกโปรไฟล์ Vault แล้ว",
"deleted": "โปรไฟล์ Vault ถูกลบแล้ว",
"saveFailed": "ไม่สามารถบันทึกโปรไฟล์ Vault ได้",
"deleteFailed": "ไม่สามารถลบโปรไฟล์ Vault ได้",
"save": "บันทึกโปรไฟล์",
"validationError": "จำเป็นต้องระบุชื่อ ที่อยู่ Vault และบทบาทผู้ลงนาม SSH",
"none": "ยังไม่มีโปรไฟล์ Vault ใดๆ",
"sharedBadge": "แชร์"
},
"dialog": {
"title": "ต้องลงชื่อเข้าใช้ Vault",
"description": "หน้าต่างสำหรับเข้าสู่ระบบ HashiCorp Vault ได้เปิดขึ้นแล้ว โปรดทำการเข้าสู่ระบบให้เสร็จสมบูรณ์ การเชื่อมต่อนี้จะดำเนินการต่อโดยอัตโนมัติ",
"failed": "การตรวจสอบสิทธิ์ Vault ล้มเหลว โปรดลองอีกครั้ง",
"reopen": "เปิดหน้าต่างเข้าสู่ระบบอีกครั้ง"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Vault İmzalayıcı Profili",
"selectProfile": "Bir Vault profili seçin...",
"hint": "Ayarlar paylaşılan profilden gelir; bağlandığınızda OIDC ile Vault'ta oturum açarsınız. Hiçbir gizli bilgi saklanmaz.",
"docs": "Dokümanları görüntüle"
},
"profiles": {
"new": "Yeni profil",
"manage": "Vault Profillerini Yönet",
"name": "Gösterim Adı",
"vaultAddr": "Vault Adresi",
"namespace": "Ad Alanı",
"oidcMount": "OIDC Kimlik Doğrulama Bağlama",
"oidcRole": "OIDC Rolü",
"sshMount": "SSH Gizli Dizin Bağlama",
"sshRole": "SSH İmzalayıcı Rolü",
"validPrincipals": "Geçerli Asıllar",
"keyType": "Geçici Anahtar Türü",
"shareWithAll": "Tüm Kullanıcılarla Paylaş",
"create": "Profil oluştur",
"created": "Vault profili oluşturuldu",
"saved": "Vault profili kaydedildi",
"deleted": "Vault profili silindi",
"saveFailed": "Vault profili kaydedilemedi",
"deleteFailed": "Vault profili silinemedi",
"save": "Profili kaydet",
"validationError": "Ad, Vault adresi ve SSH imzalama rolü gereklidir",
"none": "Henüz Vault profili yok.",
"sharedBadge": "paylaşılan"
},
"dialog": {
"title": "Vault Oturum Açması Gerekli",
"description": "HashiCorp Vault'ta oturum açmak için bir pencere açıldı. Orada oturum açmayı tamamlayın; bu bağlantı otomatik olarak devam edecek.",
"failed": "Vault kimlik doğrulaması başarısız oldu. Lütfen tekrar deneyin.",
"reopen": "Oturum açma penceresini yeniden aç"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Профіль підписанта сховища",
"selectProfile": "Виберіть профіль Сховища...",
"hint": "Налаштування надходять зі спільного профілю; ви входите у Сейф через OIDC під час підключення. Секрети не зберігаються.",
"docs": "Переглянути документи"
},
"profiles": {
"new": "Новий профіль",
"manage": "Керування профілями Сховища",
"name": "Зрозуміле ім'я",
"vaultAddr": "Адреса сховища",
"namespace": "Простір імен",
"oidcMount": "Монтування автентифікації OIDC",
"oidcRole": "Роль OIDC",
"sshMount": "SSH-секрети кріплення",
"sshRole": "Роль підписувача SSH",
"validPrincipals": "Дійсні принципали",
"keyType": "Тип тимчасового ключа",
"shareWithAll": "Поділитися з усіма користувачами",
"create": "Створити профіль",
"created": "Профіль сховища створено",
"saved": "Профіль сховища збережено",
"deleted": "Профіль сховища видалено",
"saveFailed": "Не вдалося зберегти профіль Сейфа",
"deleteFailed": "Не вдалося видалити профіль Сейфа",
"save": "Зберегти профіль",
"validationError": "Ім'я, адреса сховища та роль підписувача SSH обов'язкові",
"none": "Поки що немає профілів Сейфа.",
"sharedBadge": "спільний"
},
"dialog": {
"title": "Потрібен вхід до Сейфа",
"description": "Відкрилося вікно для входу в HashiCorp Vault. Завершіть вхід там; це з’єднання буде продовжено автоматично.",
"failed": "Не вдалося виконати автентифікацію сховища. Спробуйте ще раз.",
"reopen": "Знову відкрити вікно входу"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Hồ sơ người ký Vault",
"selectProfile": "Chọn hồ sơ Vault...",
"hint": "Cài đặt từ hồ sơ dùng chung; bạn sẽ đăng nhập Vault qua OIDC khi kết nối. Không lưu bí mật.",
"docs": "Xem tài liệu"
},
"profiles": {
"new": "Hồ sơ mới",
"manage": "Quản lý hồ sơ Vault",
"name": "Tên hiển thị",
"vaultAddr": "Địa chỉ Vault",
"namespace": "Không gian tên",
"oidcMount": "Điểm gắn xác thực OIDC",
"oidcRole": "Vai trò OIDC",
"sshMount": "Điểm gắn bí mật SSH",
"sshRole": "Vai trò ký SSH",
"validPrincipals": "Các danh tính hợp lệ",
"keyType": "Loại khóa tạm thời",
"shareWithAll": "Chia sẻ với tất cả người dùng",
"create": "Tạo hồ sơ",
"created": "Đã tạo hồ sơ Vault",
"saved": "Đã lưu hồ sơ Vault",
"deleted": "Đã xóa hồ sơ Vault",
"saveFailed": "Không thể lưu hồ sơ Vault",
"deleteFailed": "Không thể xóa hồ sơ Vault",
"save": "Lưu hồ sơ",
"validationError": "Tên, địa chỉ Vault và vai trò ký SSH là bắt buộc",
"none": "Chưa có hồ sơ Vault nào.",
"sharedBadge": "được chia sẻ"
},
"dialog": {
"title": "Yêu cầu đăng nhập Vault",
"description": "Một cửa sổ đã mở để đăng nhập vào HashiCorp Vault. Hoàn tất đăng nhập ở đó; kết nối này sẽ tự động tiếp tục.",
"failed": "Xác thực Vault thất bại. Vui lòng thử lại.",
"reopen": "Mở lại cửa sổ đăng nhập"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Vault 签名者配置文件",
"selectProfile": "选择 Vault 配置文件...",
"hint": "设置来自共享配置文件;连接时将使用 OIDC 登录 Vault。不会存储任何机密。",
"docs": "查看文档"
},
"profiles": {
"new": "新建配置文件",
"manage": "管理 Vault 配置文件",
"name": "友好名称",
"vaultAddr": "Vault 地址",
"namespace": "命名空间",
"oidcMount": "OIDC 认证挂载点",
"oidcRole": "OIDC 角色",
"sshMount": "SSH 机密挂载点",
"sshRole": "SSH 签名者角色",
"validPrincipals": "有效主体",
"keyType": "临时密钥类型",
"shareWithAll": "与所有用户共享",
"create": "创建配置文件",
"created": "Vault 配置文件已创建",
"saved": "Vault 配置文件已保存",
"deleted": "Vault 配置文件已删除",
"saveFailed": "保存 Vault 配置文件失败",
"deleteFailed": "删除 Vault 配置文件失败",
"save": "保存配置文件",
"validationError": "名称、Vault 地址和 SSH 签名者角色为必填项",
"none": "暂无 Vault 配置文件。",
"sharedBadge": "共享"
},
"dialog": {
"title": "需要 Vault 登录",
"description": "已打开窗口以登录 HashiCorp Vault。在该处完成登录;此连接将自动继续。",
"failed": "Vault 认证失败,请重试。",
"reopen": "重新打开登录窗口"
}
}
@@ -0,0 +1,38 @@
{
"editor": {
"profile": "Vault 簽署者設定檔",
"selectProfile": "選取 Vault 設定檔...",
"hint": "設定來自共用設定檔;連線時您將透過 OIDC 登入 Vault。不會儲存任何機密。",
"docs": "檢視文件"
},
"profiles": {
"new": "新增設定檔",
"manage": "管理 Vault 設定檔",
"name": "易記名稱",
"vaultAddr": "Vault 位址",
"namespace": "命名空間",
"oidcMount": "OIDC 驗證掛載",
"oidcRole": "OIDC 角色",
"sshMount": "SSH 機密掛載",
"sshRole": "SSH 簽署者角色",
"validPrincipals": "有效主體",
"keyType": "暫時金鑰類型",
"shareWithAll": "與所有使用者共用",
"create": "建立設定檔",
"created": "Vault 設定檔已建立",
"saved": "Vault 設定檔已儲存",
"deleted": "Vault 設定檔已刪除",
"saveFailed": "無法儲存 Vault 設定檔",
"deleteFailed": "無法刪除 Vault 設定檔",
"save": "儲存設定檔",
"validationError": "名稱、Vault 位址和 SSH 簽署者角色為必填",
"none": "尚無 Vault 設定檔。",
"sharedBadge": "已共用"
},
"dialog": {
"title": "需要 Vault 登入",
"description": "已開啟視窗以登入 HashiCorp Vault。在該處完成登入;此連線將自動繼續。",
"failed": "Vault 驗證失敗。請再試一次。",
"reopen": "重新開啟登入視窗"
}
}
+72
View File
@@ -0,0 +1,72 @@
{
"id": "vault",
"name": "HashiCorp Vault",
"version": "1.0.0",
"description": "Sign in to hosts with short-lived SSH certificates from a HashiCorp Vault SSH secrets engine, after an OIDC sign-in to Vault in the browser.",
"author": {
"name": "Termix"
},
"license": "MIT",
"repository": "https://github.com/Termix-SSH/Termix",
"category": "Access & Security",
"icon": "KeyRound",
"engine": {
"termix": ">=2.9.0",
"api": "1"
},
"capabilities": [
"db:own",
"secrets:own",
"network:outbound",
"network:serve",
"auth:provide",
"ui:surface"
],
"contributes": {
"auth": {
"sshAuthTypes": ["vault"]
},
"permissions": [
{
"name": "use",
"titleKey": "permissions.use.title",
"descriptionKey": "permissions.use.description",
"defaultRoles": ["admin", "user"]
},
{
"name": "share",
"titleKey": "permissions.share.title",
"descriptionKey": "permissions.share.description",
"defaultRoles": ["admin"]
}
],
"settings": {
"admin": [
{
"key": "redirectUri",
"type": "custom",
"component": "redirectUri"
},
{
"key": "legacyCallback",
"type": "boolean",
"labelKey": "settings.legacyCallback.label",
"descriptionKey": "settings.legacyCallback.description",
"default": false
}
],
"host": {
"fields": [
{
"key": "profileId",
"type": "number",
"labelKey": "settings.host.profileId.label",
"hidden": true
}
]
}
}
},
"locales": "locales",
"platforms": ["linux", "win32", "darwin"]
}
@@ -0,0 +1,37 @@
-- vault 0001: adopt_vault_tables
-- Generated by termix-plugin migrations. Review before committing.
CREATE TABLE IF NOT EXISTS `vault_profiles` (
`id` int AUTO_INCREMENT PRIMARY KEY,
`user_id` varchar(255) NOT NULL,
`name` text NOT NULL,
`description` text,
`folder` text,
`tags` text,
`vault_addr` text NOT NULL,
`vault_namespace` text,
`oidc_mount` text,
`oidc_role` text,
`ssh_mount` text,
`ssh_role` text NOT NULL,
`valid_principals` text,
`key_type` text,
`shared` boolean NOT NULL DEFAULT false,
`sync_id` varchar(255) UNIQUE,
`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
);
RENAME TABLE `vault_profiles` TO `p_vault_profiles`;
CREATE TABLE IF NOT EXISTS `vault_tokens` (
`id` int AUTO_INCREMENT PRIMARY KEY,
`user_id` varchar(255) NOT NULL,
`profile_id` int NOT NULL,
`ssh_cert` text NOT NULL,
`private_key` text NOT NULL,
`created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP),
`expires_at` varchar(255) NOT NULL,
`last_used` text,
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
);
RENAME TABLE `vault_tokens` TO `p_vault_tokens`;
@@ -0,0 +1,38 @@
-- vault 0001: adopt_vault_tables
-- Generated by termix-plugin migrations. Review before committing.
CREATE TABLE IF NOT EXISTS "vault_profiles" (
"id" serial PRIMARY KEY,
"user_id" varchar(255) NOT NULL,
"name" text NOT NULL,
"description" text,
"folder" text,
"tags" text,
"vault_addr" text NOT NULL,
"vault_namespace" text,
"oidc_mount" text,
"oidc_role" text,
"ssh_mount" text,
"ssh_role" text NOT NULL,
"valid_principals" text,
"key_type" text,
"shared" boolean NOT NULL DEFAULT false,
"sync_id" varchar(255) UNIQUE,
"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
);
ALTER TABLE "vault_profiles" RENAME TO "p_vault_profiles";
CREATE TABLE IF NOT EXISTS "vault_tokens" (
"id" serial PRIMARY KEY,
"user_id" varchar(255) NOT NULL,
"profile_id" integer NOT NULL,
"ssh_cert" text NOT NULL,
"private_key" text NOT NULL,
"created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expires_at" varchar(255) NOT NULL,
"last_used" text,
FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS "idx_vault_tokens_user_profile" ON "vault_tokens" ("user_id", "profile_id");
ALTER TABLE "vault_tokens" RENAME TO "p_vault_tokens";
+127
View File
@@ -0,0 +1,127 @@
{
"version": 1,
"tables": {
"profiles": {
"columns": {
"id": {
"type": "id",
"primaryKey": true,
"notNull": true
},
"userId": {
"type": "refUser",
"length": 255,
"notNull": true
},
"name": {
"type": "text",
"notNull": true
},
"description": {
"type": "text"
},
"folder": {
"type": "text"
},
"tags": {
"type": "text"
},
"vaultAddr": {
"type": "text",
"notNull": true
},
"vaultNamespace": {
"type": "text"
},
"oidcMount": {
"type": "text"
},
"oidcRole": {
"type": "text"
},
"sshMount": {
"type": "text"
},
"sshRole": {
"type": "text",
"notNull": true
},
"validPrincipals": {
"type": "text"
},
"keyType": {
"type": "text"
},
"shared": {
"type": "boolean",
"notNull": true,
"defaultValue": false
},
"syncId": {
"type": "varchar",
"length": 255,
"unique": true
},
"createdAt": {
"type": "text",
"notNull": true,
"defaultNow": true
},
"updatedAt": {
"type": "text",
"notNull": true,
"defaultNow": true
}
},
"indexes": [],
"adopts": "vault_profiles"
},
"tokens": {
"columns": {
"id": {
"type": "id",
"primaryKey": true,
"notNull": true
},
"userId": {
"type": "refUser",
"length": 255,
"notNull": true
},
"profileId": {
"type": "integer",
"notNull": true
},
"sshCert": {
"type": "text",
"notNull": true
},
"privateKey": {
"type": "text",
"notNull": true
},
"createdAt": {
"type": "text",
"notNull": true,
"defaultNow": true
},
"expiresAt": {
"type": "varchar",
"length": 255,
"notNull": true
},
"lastUsed": {
"type": "text"
}
},
"indexes": [
{
"name": "idx_vault_tokens_user_profile",
"columns": ["userId", "profileId"],
"unique": true
}
],
"adopts": "vault_tokens"
}
}
}
@@ -0,0 +1,38 @@
-- vault 0001: adopt_vault_tables
-- Generated by termix-plugin migrations. Review before committing.
CREATE TABLE IF NOT EXISTS "vault_profiles" (
"id" integer PRIMARY KEY AUTOINCREMENT,
"user_id" text NOT NULL,
"name" text NOT NULL,
"description" text,
"folder" text,
"tags" text,
"vault_addr" text NOT NULL,
"vault_namespace" text,
"oidc_mount" text,
"oidc_role" text,
"ssh_mount" text,
"ssh_role" text NOT NULL,
"valid_principals" text,
"key_type" text,
"shared" integer NOT NULL DEFAULT 0,
"sync_id" text UNIQUE,
"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
);
ALTER TABLE "vault_profiles" RENAME TO "p_vault_profiles";
CREATE TABLE IF NOT EXISTS "vault_tokens" (
"id" integer PRIMARY KEY AUTOINCREMENT,
"user_id" text NOT NULL,
"profile_id" integer NOT NULL,
"ssh_cert" text NOT NULL,
"private_key" text NOT NULL,
"created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expires_at" text NOT NULL,
"last_used" text,
FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS "idx_vault_tokens_user_profile" ON "vault_tokens" ("user_id", "profile_id");
ALTER TABLE "vault_tokens" RENAME TO "p_vault_tokens";
+15
View File
@@ -0,0 +1,15 @@
{
"name": "@termix-plugin/vault",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "termix-plugin build",
"test": "termix-plugin test",
"typecheck": "tsc -p tsconfig.json",
"validate": "termix-plugin validate"
},
"devDependencies": {
"@termix/plugin-sdk": "*"
}
}
+206
View File
@@ -0,0 +1,206 @@
import type { PluginContext } from "@termix/plugin-sdk/backend";
import type { TokenStore } from "./token-store.js";
import {
completeVaultOidc,
generateEphemeralKeyPair,
signWithVault,
startVaultOidc,
type VaultProfileConfig,
} from "./vault-client.js";
/** The redirect URI 2.8 had Vault roles allow. */
export const LEGACY_CALLBACK_PATH = "/vault/oidc/callback";
export const CALLBACK_PATH = "/plugin-api/vault/oidc/callback";
const AUTH_TIMEOUT_MS = 5 * 60 * 1000;
/** The terminal socket, as far as a sign-in needs it. */
export interface SignInSocket {
send: (data: string) => void;
on?: (event: "close", listener: () => void) => void;
}
export interface StartRequest {
userId: string;
hostId: number;
profile: VaultProfileConfig;
socket: SignInSocket;
requestOrigin: string;
}
export interface CallbackResult {
ok: boolean;
message: string;
}
interface Session {
state: string;
userId: string;
hostId: number;
profile: VaultProfileConfig;
clientNonce: string;
privateKey: string;
publicKey: string;
socket: SignInSocket;
timeout: ReturnType<typeof setTimeout>;
}
function send(socket: SignInSocket, message: Record<string, unknown>): void {
try {
socket.send(JSON.stringify(message));
} catch {
// socket already gone
}
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
export type AuthSessions = ReturnType<typeof createAuthSessions>;
/**
* Vault OIDC sign-ins in flight, keyed by the state Vault put in its auth
* URL so the unauthenticated browser callback finds its way back. Created
* per activation; closeAll runs on deactivate.
*/
export function createAuthSessions(ctx: PluginContext, tokens: TokenStore) {
const sessions = new Map<string, Session>();
function end(session: Session): void {
clearTimeout(session.timeout);
sessions.delete(session.state);
}
async function callbackPath(): Promise<string> {
const legacy = await ctx.settings.get<boolean>("legacyCallback");
return legacy ? LEGACY_CALLBACK_PATH : CALLBACK_PATH;
}
return {
/** Asks Vault for an auth URL and sends it to the terminal. */
async start(request: StartRequest): Promise<void> {
try {
const keyPair = generateEphemeralKeyPair(request.profile.keyType);
const redirectUri = `${request.requestOrigin}${await callbackPath()}`;
const { authUrl, state, clientNonce } = await startVaultOidc(
ctx.fetch,
request.profile,
redirectUri,
);
const existing = sessions.get(state);
if (existing) end(existing);
const session: Session = {
state,
userId: request.userId,
hostId: request.hostId,
profile: request.profile,
clientNonce,
privateKey: keyPair.privateKey,
publicKey: keyPair.publicKey,
socket: request.socket,
timeout: setTimeout(() => {
const current = sessions.get(state);
if (current) end(current);
}, AUTH_TIMEOUT_MS),
};
sessions.set(state, session);
request.socket.on?.("close", () => {
const current = sessions.get(state);
if (current) end(current);
});
send(request.socket, {
type: "vault_auth_url",
hostId: request.hostId,
requestId: state,
url: authUrl,
});
} catch (error) {
const message = errorMessage(error);
ctx.log.error(`Failed to start Vault authentication: ${message}`);
send(request.socket, {
type: "vault_error",
hostId: request.hostId,
error: message,
});
}
},
/**
* The identity provider's redirect, via Vault: exchanges the code for a
* Vault token, has Vault sign the ephemeral key and caches the result.
*/
async complete(state: string, code: string): Promise<CallbackResult> {
const session = sessions.get(state);
if (!session) {
return {
ok: false,
message: "This sign-in request is no longer active.",
};
}
end(session);
try {
const token = await completeVaultOidc(ctx.fetch, session.profile, {
state,
code,
clientNonce: session.clientNonce,
});
const signed = await signWithVault(
ctx.fetch,
session.profile,
token,
session.publicKey,
);
const expiresAt = await tokens.save(
session.userId,
session.profile.id,
session.privateKey,
signed,
);
ctx.log.info(
`Vault signed a certificate for host ${session.hostId} with profile ${session.profile.id}`,
);
send(session.socket, {
type: "vault_completed",
hostId: session.hostId,
requestId: state,
expiresAt,
});
return {
ok: true,
message: "You can close this window and return to Termix.",
};
} catch (error) {
const message = errorMessage(error);
ctx.log.error(`Vault sign-in failed: ${message}`);
send(session.socket, {
type: "vault_error",
hostId: session.hostId,
requestId: state,
error: message,
});
return { ok: false, message };
}
},
/** Cancels by request id, or every sign-in the user has for a host. */
cancel(userId: string, hostId?: number, requestId?: string): void {
for (const session of [...sessions.values()]) {
if (session.userId !== userId) continue;
if (
requestId ? session.state === requestId : session.hostId === hostId
) {
end(session);
}
}
},
closeAll(): void {
for (const session of [...sessions.values()]) end(session);
},
};
}
+41
View File
@@ -0,0 +1,41 @@
import type { Router } from "express";
import type { PluginContext } from "@termix/plugin-sdk/backend";
import { profiles as profilesTable, tokens as tokensTable } from "./tables.js";
import { createProfileStore, SYNC_ENTITY } from "./profile-store.js";
import { createTokenStore } from "./token-store.js";
import { createAuthSessions } from "./auth-session.js";
import { createVaultProvider } from "./provider.js";
import { PUBLIC_PATHS, registerRoutes } from "./routes.js";
export async function activate(ctx: PluginContext) {
const profilesDb = await ctx.db.define(profilesTable);
const tokensDb = await ctx.db.define(tokensTable);
const profiles = createProfileStore(ctx, profilesDb, tokensDb);
const tokens = createTokenStore(ctx, tokensDb);
const sessions = createAuthSessions(ctx, tokens);
ctx.disposables.add(() => sessions.closeAll());
ctx.auth.registerSshAuthProvider(
createVaultProvider(ctx, profiles, tokens, sessions),
);
// Same wire name as 2.8, where core synced it. Hosts point at a profile
// through the profileId host setting, not a column, so nothing references it.
ctx.sync.registerEntity({
type: SYNC_ENTITY,
table: profilesDb,
order: 20,
});
registerRoutes(
ctx,
ctx.http.router<Router>({ public: PUBLIC_PATHS }),
profiles,
sessions,
);
}
export async function deactivate() {
// Everything above was registered through ctx and is disposed by core.
}
+139
View File
@@ -0,0 +1,139 @@
import { randomUUID } from "node:crypto";
import { desc, eq, or } from "drizzle-orm";
import type { PluginContext } from "@termix/plugin-sdk/backend";
import type { VaultProfileConfig } from "./vault-client.js";
/* eslint-disable @typescript-eslint/no-explicit-any */
// ctx.db.define hands the table back untyped and the drizzle handle is the
// server's own, so both are typed at this module's edge.
type Table = any;
type Drizzle = any;
/* eslint-enable @typescript-eslint/no-explicit-any */
export const SYNC_ENTITY = "vaultProfiles";
export interface ProfileRow {
id: number;
userId: string;
name: string;
description: string | null;
folder: string | null;
tags: string | null;
vaultAddr: string;
vaultNamespace: string | null;
oidcMount: string | null;
oidcRole: string | null;
sshMount: string | null;
sshRole: string;
validPrincipals: string | null;
keyType: string | null;
shared: boolean | number;
syncId: string | null;
createdAt: string;
updatedAt: string;
}
export type ProfileInput = Partial<
Omit<ProfileRow, "id" | "userId" | "syncId" | "createdAt" | "updatedAt">
>;
export function toConfig(row: ProfileRow): VaultProfileConfig {
return {
id: row.id,
vaultAddr: row.vaultAddr,
vaultNamespace: row.vaultNamespace ?? null,
oidcMount: row.oidcMount ?? null,
oidcRole: row.oidcRole ?? null,
sshMount: row.sshMount ?? null,
sshRole: row.sshRole,
validPrincipals: row.validPrincipals ?? null,
keyType: row.keyType ?? null,
};
}
export type ProfileStore = ReturnType<typeof createProfileStore>;
export function createProfileStore(
ctx: PluginContext,
table: Table,
tokensTable: Table,
) {
const client = () => ctx.db.client<Drizzle>();
async function findById(id: number): Promise<ProfileRow | null> {
const rows = await (
await client()
)
.select()
.from(table)
.where(eq(table.id, id))
.limit(1);
return rows[0] ?? null;
}
async function findBySyncId(syncId: string): Promise<ProfileRow | null> {
const rows = await (
await client()
)
.select()
.from(table)
.where(eq(table.syncId, syncId))
.limit(1);
return rows[0] ?? null;
}
return {
findById,
/** The user's own profiles plus every shared one. */
async listVisible(userId: string): Promise<ProfileRow[]> {
return (await client())
.select()
.from(table)
.where(or(eq(table.userId, userId), eq(table.shared, true)))
.orderBy(desc(table.updatedAt));
},
async create(
userId: string,
input: ProfileInput & {
name: string;
vaultAddr: string;
sshRole: string;
},
): Promise<ProfileRow> {
const syncId = randomUUID();
const now = new Date().toISOString();
await (await client()).insert(table).values({
...input,
userId,
shared: input.shared ? true : false,
syncId,
createdAt: now,
updatedAt: now,
});
await ctx.db.persist();
return (await findBySyncId(syncId)) as ProfileRow;
},
async update(id: number, input: ProfileInput): Promise<ProfileRow | null> {
await (
await client()
)
.update(table)
.set({ ...input, updatedAt: new Date().toISOString() })
.where(eq(table.id, id));
await ctx.db.persist();
return findById(id);
},
/** Deletes the profile, its cached certificates and records a tombstone. */
async remove(row: ProfileRow): Promise<void> {
const db = await client();
await db.delete(tokensTable).where(eq(tokensTable.profileId, row.id));
await db.delete(table).where(eq(table.id, row.id));
await ctx.db.persist();
await ctx.sync.recordTombstone(row.userId, SYNC_ENTITY, row.syncId);
},
};
}
+124
View File
@@ -0,0 +1,124 @@
import type {
PluginContext,
PluginSshAuthProvider,
PluginSshHost,
} from "@termix/plugin-sdk/backend";
import { applyCertificateAuth } from "@termix/plugin-sdk/ssh-certs";
import type { AuthSessions, SignInSocket } from "./auth-session.js";
import {
toConfig,
type ProfileRow,
type ProfileStore,
} from "./profile-store.js";
import type { TokenStore } from "./token-store.js";
export const AUTH_TYPE = "vault";
const REQUIRED_MESSAGE =
"Vault SSH signer authentication required. Please open a Terminal connection first.";
const AUTH_FAILED_PATTERN = /All configured authentication methods failed/i;
/** The profile a host points at, from its host settings. */
export async function profileForHost(
ctx: PluginContext,
profiles: ProfileStore,
hostId: number,
): Promise<ProfileRow | null> {
const raw = await ctx.settings.getHost<number | string>(hostId, "profileId");
const id = Number(raw);
if (!Number.isInteger(id) || id <= 0) return null;
return profiles.findById(id);
}
/**
* Hosts with auth type "vault" connect with a certificate Vault signed for
* the host's profile. Without one, the terminal is asked for a browser
* sign-in ("vault_auth_required"), which startInteraction runs.
*/
export function createVaultProvider(
ctx: PluginContext,
profiles: ProfileStore,
tokens: TokenStore,
sessions: AuthSessions,
): PluginSshAuthProvider {
return {
type: AUTH_TYPE,
labelKey: "hosts.filterAuthVault",
needsUserInteraction: true,
supportsBackground: false,
interaction: AUTH_TYPE,
prepare: async (config, host: PluginSshHost, env) => {
const profile = await profileForHost(ctx, profiles, host.id);
if (!profile) {
return {
status: "error",
code: "failed",
message: "Host has no Vault signer profile configured",
};
}
const cert = await tokens.get(env.userId, profile.id);
if (!cert) {
env.log(
"info",
"No valid Vault certificate found, requesting authentication",
);
return {
status: "interaction-required",
interaction: AUTH_TYPE,
message: REQUIRED_MESSAGE,
flag: "requiresVaultAuth",
};
}
try {
await applyCertificateAuth(
config as never,
env.client as never,
{ privateKey: cert.privateKey, certificate: cert.sshCert },
host.username,
);
} catch (error) {
return {
status: "error",
code: "failed",
message:
"Vault SSH signer authentication failed: " +
(error instanceof Error ? error.message : String(error)),
};
}
env.log("info", "Using cached Vault-signed certificate");
return { status: "ready" };
},
onAuthFailed: (host, env, context) => {
if (!AUTH_FAILED_PATTERN.test(context.error.message)) return undefined;
ctx.log.warn("Vault certificate authentication failed, forgetting it");
void profileForHost(ctx, profiles, host.id)
.then((profile) =>
profile ? tokens.remove(env.userId, profile.id) : undefined,
)
.catch(() => undefined);
return {
status: "interaction-required",
interaction: AUTH_TYPE,
message:
"Vault authentication failed or expired. Please authenticate again.",
};
},
startInteraction: async (request) => {
const profile = await profileForHost(ctx, profiles, request.hostId);
if (!profile) {
throw new Error("No Vault signer profile configured for this host");
}
await sessions.start({
userId: request.userId,
hostId: request.hostId,
profile: toConfig(profile),
socket: request.socket as SignInSocket,
requestOrigin: request.requestOrigin,
});
},
cancelInteraction: (request) => {
sessions.cancel(request.userId, request.hostId, request.requestId);
},
};
}
+337
View File
@@ -0,0 +1,337 @@
import type { Request, Response, Router } from "express";
import type { PluginContext } from "@termix/plugin-sdk/backend";
import type { AuthSessions } from "./auth-session.js";
import type {
ProfileInput,
ProfileRow,
ProfileStore,
} from "./profile-store.js";
/**
* Vault sends the browser back here without a Termix session: the sign-in is
* matched by the state Vault issued.
*/
export const PUBLIC_PATHS = ["/oidc/callback"];
export function escapeHtml(value: string): string {
return value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
export function resultPage(ok: boolean, message: string): string {
return `<!doctype html><html><head><meta charset="utf-8"><title>Termix</title>
<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
main{max-width:28rem;padding:2rem;border:1px solid #333;background:#181818}h1{font-size:1.1rem;margin:0 0 .5rem}p{margin:0;color:#aaa}</style></head>
<body><main><h1>${ok ? "Vault sign-in complete" : "Vault sign-in failed"}</h1><p>${escapeHtml(message)}</p></main>
${ok ? "<script>setTimeout(function(){window.close()},1500)</script>" : ""}</body></html>`;
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
function optional(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function formatProfile(row: ProfileRow, userId: string) {
return {
id: row.id,
name: row.name,
description: row.description,
folder: row.folder,
tags: row.tags ? row.tags.split(",").filter(Boolean) : [],
vaultAddr: row.vaultAddr,
vaultNamespace: row.vaultNamespace,
oidcMount: row.oidcMount,
oidcRole: row.oidcRole,
sshMount: row.sshMount,
sshRole: row.sshRole,
validPrincipals: row.validPrincipals,
keyType: row.keyType,
shared: !!row.shared,
owned: row.userId === userId,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
};
}
function parseId(req: Request): number | null {
const id = Number.parseInt(String(req.params.id), 10);
return Number.isFinite(id) ? id : null;
}
export function registerRoutes(
ctx: PluginContext,
router: Router,
profiles: ProfileStore,
sessions: AuthSessions,
): void {
const userId = () => ctx.currentActor() as string;
/**
* @openapi
* /plugin-api/vault/oidc/callback:
* get:
* summary: Vault OIDC callback
* description: Public. The redirect URI a Vault OIDC role allows. Matches the sign-in by the state Vault issued, has Vault sign the ephemeral key and tells the terminal to reconnect. The 2.8 URI /vault/oidc/callback redirects here.
* tags: [Vault]
* parameters:
* - in: query
* name: state
* schema:
* type: string
* - in: query
* name: code
* schema:
* type: string
* - in: query
* name: error
* schema:
* type: string
* responses:
* 200:
* description: Signed in; an HTML page that closes itself.
* 400:
* description: The sign-in failed or is no longer active.
*/
router.get("/oidc/callback", async (req: Request, res: Response) => {
const state = typeof req.query.state === "string" ? req.query.state : "";
const code = typeof req.query.code === "string" ? req.query.code : "";
const error = typeof req.query.error === "string" ? req.query.error : "";
const reply = (ok: boolean, message: string) =>
res
.status(ok ? 200 : 400)
.type("html")
.send(resultPage(ok, message));
if (error) return reply(false, `Vault returned: ${error}`);
if (!state || !code) return reply(false, "Missing state or code.");
const result = await sessions.complete(state, code);
return reply(result.ok, result.message);
});
router.use("/profiles", ctx.rbac.require("use") as never);
/**
* @openapi
* /plugin-api/vault/profiles:
* get:
* summary: List Vault profiles
* description: The caller's own Vault signer profiles and every shared one. Needs vault.use.
* tags: [Vault]
* responses:
* 200:
* description: Array of Vault profile objects.
* 403:
* description: Missing vault.use.
*/
router.get("/profiles", async (_req: Request, res: Response) => {
const rows = await profiles.listVisible(userId());
res.json(rows.map((row) => formatProfile(row, userId())));
});
/**
* @openapi
* /plugin-api/vault/profiles:
* post:
* summary: Create a Vault profile
* description: Creates a Vault signer profile owned by the caller. Needs vault.use, and vault.share to mark it shared.
* tags: [Vault]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [name, vaultAddr, sshRole]
* properties:
* name:
* type: string
* vaultAddr:
* type: string
* vaultNamespace:
* type: string
* oidcMount:
* type: string
* oidcRole:
* type: string
* sshMount:
* type: string
* sshRole:
* type: string
* validPrincipals:
* type: string
* keyType:
* type: string
* shared:
* type: boolean
* responses:
* 201:
* description: The created profile.
* 400:
* description: Missing required fields.
* 403:
* description: Missing vault.use, or vault.share for a shared profile.
*/
router.post("/profiles", async (req: Request, res: Response) => {
const body = (req.body ?? {}) as Record<string, unknown>;
if (
!isNonEmptyString(body.name) ||
!isNonEmptyString(body.vaultAddr) ||
!isNonEmptyString(body.sshRole)
) {
return res
.status(400)
.json({ error: "name, vaultAddr and sshRole are required" });
}
const shared = !!body.shared;
if (shared && !(await ctx.rbac.has("share"))) {
return res
.status(403)
.json({ error: "You are not allowed to share Vault profiles" });
}
const row = await profiles.create(userId(), {
name: body.name.trim(),
description: optional(body.description),
folder: optional(body.folder),
tags: Array.isArray(body.tags)
? body.tags.join(",")
: typeof body.tags === "string"
? body.tags
: "",
vaultAddr: body.vaultAddr.trim(),
vaultNamespace: optional(body.vaultNamespace),
oidcMount: optional(body.oidcMount),
oidcRole: optional(body.oidcRole),
sshMount: optional(body.sshMount),
sshRole: body.sshRole.trim(),
validPrincipals: optional(body.validPrincipals),
keyType: optional(body.keyType),
shared,
});
res.status(201).json(formatProfile(row, userId()));
});
/**
* @openapi
* /plugin-api/vault/profiles/{id}:
* put:
* summary: Update a Vault profile
* description: Only the owner may edit a profile. Needs vault.use, and vault.share to mark it shared.
* tags: [Vault]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* requestBody:
* content:
* application/json:
* schema:
* type: object
* responses:
* 200:
* description: The updated profile.
* 400:
* description: Invalid profile id.
* 403:
* description: Not the owner, or sharing without vault.share.
* 404:
* description: Profile not found.
*/
router.put("/profiles/:id", async (req: Request, res: Response) => {
const id = parseId(req);
if (id === null)
return res.status(400).json({ error: "Invalid profile id" });
const existing = await profiles.findById(id);
if (!existing) return res.status(404).json({ error: "Profile not found" });
if (existing.userId !== userId()) {
return res
.status(403)
.json({ error: "Only the owner can edit this profile" });
}
const body = (req.body ?? {}) as Record<string, unknown>;
const fields: ProfileInput = {};
if (isNonEmptyString(body.name)) fields.name = body.name.trim();
if (body.description !== undefined)
fields.description = optional(body.description);
if (body.folder !== undefined) fields.folder = optional(body.folder);
if (body.tags !== undefined) {
fields.tags = Array.isArray(body.tags)
? body.tags.join(",")
: typeof body.tags === "string"
? body.tags
: "";
}
if (isNonEmptyString(body.vaultAddr))
fields.vaultAddr = body.vaultAddr.trim();
if (body.vaultNamespace !== undefined)
fields.vaultNamespace = optional(body.vaultNamespace);
if (body.oidcMount !== undefined)
fields.oidcMount = optional(body.oidcMount);
if (body.oidcRole !== undefined) fields.oidcRole = optional(body.oidcRole);
if (body.sshMount !== undefined) fields.sshMount = optional(body.sshMount);
if (isNonEmptyString(body.sshRole)) fields.sshRole = body.sshRole.trim();
if (body.validPrincipals !== undefined)
fields.validPrincipals = optional(body.validPrincipals);
if (body.keyType !== undefined) fields.keyType = optional(body.keyType);
if (body.shared !== undefined) {
if (body.shared && !(await ctx.rbac.has("share"))) {
return res
.status(403)
.json({ error: "You are not allowed to share Vault profiles" });
}
fields.shared = !!body.shared;
}
const updated = await profiles.update(id, fields);
if (!updated) return res.status(404).json({ error: "Profile not found" });
res.json(formatProfile(updated, userId()));
});
/**
* @openapi
* /plugin-api/vault/profiles/{id}:
* delete:
* summary: Delete a Vault profile
* description: Deletes a profile and every certificate cached for it. Only the owner may delete it. Needs vault.use.
* tags: [Vault]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Deleted.
* 400:
* description: Invalid profile id.
* 403:
* description: Not the owner.
* 404:
* description: Profile not found.
*/
router.delete("/profiles/:id", async (req: Request, res: Response) => {
const id = parseId(req);
if (id === null)
return res.status(400).json({ error: "Invalid profile id" });
const existing = await profiles.findById(id);
if (!existing) return res.status(404).json({ error: "Profile not found" });
if (existing.userId !== userId()) {
return res
.status(403)
.json({ error: "Only the owner can delete this profile" });
}
await profiles.remove(existing);
res.json({ success: true });
});
}
Loaded 100 of 198 files, more files were not shown because too many files have changed in this diff. Show more