From 95fd26656be4d82c9b6ee8aeee70617ac85c834f Mon Sep 17 00:00:00 2001 From: jarek Date: Mon, 17 Aug 2026 09:50:34 +0200 Subject: [PATCH] Bitwarden Secrets Manager as a secret provider via an operator-installed bws client --- .../provider-icons/Bitwarden.svelte | 7 ++++++ src/lib/components/provider-icons/index.ts | 6 +++++ src/lib/server/secretproviders/bitwarden.ts | 9 ++++---- src/lib/server/secretproviders/index.ts | 4 +++- src/lib/server/secretproviders/shared.ts | 22 ++++++++++++++++--- .../settings/secrets/ProviderModal.svelte | 21 +++++++++++++++++- 6 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 src/lib/components/provider-icons/Bitwarden.svelte diff --git a/src/lib/components/provider-icons/Bitwarden.svelte b/src/lib/components/provider-icons/Bitwarden.svelte new file mode 100644 index 0000000..71e5d55 --- /dev/null +++ b/src/lib/components/provider-icons/Bitwarden.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/src/lib/components/provider-icons/index.ts b/src/lib/components/provider-icons/index.ts index 86c27f2..55ab428 100644 --- a/src/lib/components/provider-icons/index.ts +++ b/src/lib/components/provider-icons/index.ts @@ -4,6 +4,8 @@ import OnePassword from './OnePassword.svelte'; import Infisical from './Infisical.svelte'; import HashiCorpVault from './HashiCorpVault.svelte'; import Doppler from './Doppler.svelte'; +import Bitwarden from './Bitwarden.svelte'; +import ProtonPass from './ProtonPass.svelte'; /** Brand icon for a secret-provider type. Falls back to a generic key icon. */ export function getProviderIcon(type: string): Component { @@ -17,6 +19,10 @@ export function getProviderIcon(type: string): Component { return HashiCorpVault; case 'doppler': return Doppler; + case 'bitwarden': + return Bitwarden; + case 'proton': + return ProtonPass; default: return KeyRound; // any future provider until it has a brand icon } diff --git a/src/lib/server/secretproviders/bitwarden.ts b/src/lib/server/secretproviders/bitwarden.ts index 7b35b2a..91f84ca 100644 --- a/src/lib/server/secretproviders/bitwarden.ts +++ b/src/lib/server/secretproviders/bitwarden.ts @@ -202,11 +202,10 @@ async function runBws(args: string[], limits: CommandLimits): Promise { } return await executeBws(stateDir, args, limits); } finally { - try { - await rm(stateDir, { recursive: true, force: true }); - } catch { - throw new BwsAdapterError('Bitwarden bws temporary state could not be removed'); - } + // Swallow a cleanup failure: throwing here would mask the real error on the + // failure path, and turn a successful pull into a thrown error on the happy + // path. rm --force rarely fails; a stray temp dir is harmless residue. + await rm(stateDir, { recursive: true, force: true }).catch(() => undefined); } } diff --git a/src/lib/server/secretproviders/index.ts b/src/lib/server/secretproviders/index.ts index b8ce170..53d8591 100644 --- a/src/lib/server/secretproviders/index.ts +++ b/src/lib/server/secretproviders/index.ts @@ -19,6 +19,7 @@ import { infisicalProvider } from './infisical'; import { vaultProvider } from './vault'; import { dopplerProvider } from './doppler'; import { bitwardenProvider } from './bitwarden'; +import { protonProvider } from './proton'; // Registered providers. Adding a new backend means dropping a file in this // directory and registering it here; each implements the SecretProvider @@ -29,7 +30,8 @@ const providers: Record = { [infisicalProvider.type]: infisicalProvider as SecretProvider, [vaultProvider.type]: vaultProvider as SecretProvider, [dopplerProvider.type]: dopplerProvider as SecretProvider, - [bitwardenProvider.type]: bitwardenProvider as SecretProvider + [bitwardenProvider.type]: bitwardenProvider as SecretProvider, + [protonProvider.type]: protonProvider as SecretProvider }; /** Returns the provider for a stored type, or undefined if unknown. */ diff --git a/src/lib/server/secretproviders/shared.ts b/src/lib/server/secretproviders/shared.ts index ca48e11..d938fc2 100644 --- a/src/lib/server/secretproviders/shared.ts +++ b/src/lib/server/secretproviders/shared.ts @@ -32,6 +32,7 @@ export type SecretProviderType = | 'vault' | 'doppler' | 'bitwarden' + | 'proton' // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | (string & {}); @@ -137,7 +138,12 @@ export interface ConnectConfig { */ export interface InfisicalConfig { host: string; - token: string; + /** Static service/access token. Supply this OR clientId+clientSecret (Universal Auth). */ + token?: string; + /** Universal Auth (Machine Identity) client ID. Paired with clientSecret. */ + clientId?: string; + /** Universal Auth (Machine Identity) client secret. Paired with clientId. */ + clientSecret?: string; projectId: string; environment?: string; path?: string; @@ -177,6 +183,15 @@ export interface BitwardenConfig { serverUrl?: string; } +/** + * Proton Pass: a Personal Access Token (`pst_...::...`) authenticates an + * operator-installed pass-cli client. Bulk pulls one vault (selected by name); + * inline `pass://SHARE_ID/ITEM_ID[/FIELD]` references resolve a single field. + */ +export interface ProtonConfig { + token: string; +} + /** Persisted (encrypted) config, discriminated by the provider `type`. */ export type SecretProviderConfig = | ServiceAccountConfig @@ -184,7 +199,8 @@ export type SecretProviderConfig = | InfisicalConfig | VaultConfig | DopplerConfig - | BitwardenConfig; + | BitwardenConfig + | ProtonConfig; /** * Config keys that hold a SECRET across every provider type. Only these are stripped @@ -193,7 +209,7 @@ export type SecretProviderConfig = * a non-secret coordinate the user needs to see and edit. Keep in sync with the * `type: 'password'` fields in ProviderModal.svelte's PROVIDER_FIELDS. */ -export const SECRET_CONFIG_KEYS = new Set(['token']); +export const SECRET_CONFIG_KEYS = new Set(['token', 'clientSecret']); /** * Merges an incoming (edit-form) config OVER the stored one for a write/test: the incoming diff --git a/src/routes/settings/secrets/ProviderModal.svelte b/src/routes/settings/secrets/ProviderModal.svelte index 74d5d8a..46b07e0 100644 --- a/src/routes/settings/secrets/ProviderModal.svelte +++ b/src/routes/settings/secrets/ProviderModal.svelte @@ -25,6 +25,7 @@ { value: 'vault', label: 'HashiCorp Vault' }, { value: 'doppler', label: 'Doppler' }, { value: 'bitwarden', label: 'Bitwarden Secrets Manager' }, + { value: 'proton', label: 'Proton Pass' }, ]; // Config fields per provider type, matching the config shapes in @@ -39,7 +40,9 @@ ], infisical: [ { key: 'host', label: 'API host', type: 'text', required: true, placeholder: 'https://app.infisical.com', hint: 'Infisical Cloud or your self-hosted URL.' }, - { key: 'token', label: 'Access token', type: 'password', required: true, placeholder: 'st...', hint: 'A service token or machine-identity access token.' }, + { key: 'token', label: 'Access token', type: 'password', required: false, placeholder: 'st...', hint: 'A static service/access token. Leave blank to use Universal Auth (client ID + secret) below instead.' }, + { key: 'clientId', label: 'Universal Auth client ID', type: 'text', required: false, placeholder: 'machine identity client id', hint: 'A Machine Identity client ID. Pair with the client secret; leave blank if using a static token.' }, + { key: 'clientSecret', label: 'Universal Auth client secret', type: 'password', required: false, placeholder: 'machine identity client secret', hint: 'The Machine Identity client secret. Exchanged for a short-lived token via Universal Auth.' }, { key: 'projectId', label: 'Project ID', type: 'text', required: true, placeholder: 'workspace / project id', hint: 'The workspace/project the secrets live in.' }, { key: 'environment', label: 'Environment', type: 'text', required: true, placeholder: 'prod', hint: 'Environment slug, e.g. prod / staging.' }, { key: 'path', label: 'Secret path', type: 'text', required: false, placeholder: '/', hint: 'Folder path within the project. Defaults to /.' }, @@ -59,6 +62,9 @@ { key: 'token', label: 'Machine Account access token', type: 'password', required: true, placeholder: 'Machine Account access token', hint: 'A Bitwarden Secrets Manager Machine Account token with read access to the Project.' }, { key: 'serverUrl', label: 'Server URL', type: 'text', required: false, placeholder: 'https://vault.bitwarden.com', hint: 'Optional for EU or self-hosted Bitwarden. Leave blank for Bitwarden US cloud.' }, ], + proton: [ + { key: 'token', label: 'Personal access token', type: 'password', required: true, placeholder: 'pst_...::...', hint: 'A Proton Pass personal access token (pst_...) used by the operator-installed pass-cli.' }, + ], }; export function providerTypeLabel(type: string): string { @@ -90,6 +96,11 @@ label: 'Project', placeholder: 'Bitwarden Project UUID', hint: 'Bulk-load every secret from this Bitwarden Secrets Manager Project.' + }, + 'proton': { + label: 'Vault', + placeholder: 'Proton Pass vault name', + hint: 'Bulk-load every item from this Proton Pass vault. Leave blank to inject only inline pass:// references.' } }; @@ -415,6 +426,14 @@ DOCKHAND_BWS_PATH process override).

{/if} + {#if formType === 'proton'} +

+ Proton Pass requires an externally installed or mounted official + pass-cli client at /usr/local/bin/pass-cli (or an absolute + DOCKHAND_PASS_CLI_PATH process override). Supports both a bulk vault pull + and inline pass:// references. +

+ {/if}

Configuration is stored encrypted.{#if isEditing} Leave secret fields blank to keep the existing values.{/if}