import { env } from '../config/env'; import { logger } from '../utils/logger'; // --- Types --- export interface VaultwardenUser { Id: string; Email: string; Name: string; _Status: number; // 0 = enabled, 1 = invited, 2 = disabled CreatedAt: string; } // --- Client --- class VaultwardenClient { private sessionCookie: string | null = null; private sessionExpiresAt = 0; private get baseUrl(): string { return env.VAULTWARDEN_URL; } private get adminToken(): string { return env.VAULTWARDEN_ADMIN_TOKEN; } get hasCredentials(): boolean { return !!this.adminToken; } /** * Authenticate to the Vaultwarden admin panel and get a session cookie. * Session is cached and reused. */ private async ensureSession(): Promise { if (this.sessionCookie && Date.now() < this.sessionExpiresAt) return; const res = await fetch(`${this.baseUrl}/admin`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `token=${encodeURIComponent(this.adminToken)}`, redirect: 'manual', // Don't follow redirect — we need the Set-Cookie header }); // Vaultwarden returns 303 redirect on successful admin auth const setCookie = res.headers.get('set-cookie'); if (!setCookie) { throw new Error(`Vaultwarden admin auth failed (status ${res.status})`); } // Extract the session cookie value const match = setCookie.match(/VW_ADMIN=([^;]+)/); if (!match) { throw new Error('Vaultwarden admin auth: no session cookie found'); } this.sessionCookie = `VW_ADMIN=${match[1]}`; // Cache session for 20 minutes (Vaultwarden default is 20min) this.sessionExpiresAt = Date.now() + 19 * 60 * 1000; logger.debug('Vaultwarden admin session refreshed'); } /** * Make an authenticated request to the Vaultwarden admin API */ private async request( method: string, path: string, body?: unknown, ): Promise { await this.ensureSession(); const url = `${this.baseUrl}/admin${path}`; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10000); const headers: Record = { Cookie: this.sessionCookie!, }; let fetchBody: string | undefined; if (body) { headers['Content-Type'] = 'application/json'; fetchBody = JSON.stringify(body); } try { const res = await fetch(url, { method, headers, body: fetchBody, signal: controller.signal, }); if (!res.ok) { const text = await res.text().catch(() => ''); throw new Error(`Vaultwarden API ${method} ${path} returned ${res.status}: ${text}`); } const contentType = res.headers.get('content-type') || ''; if (contentType.includes('application/json')) { return (await res.json()) as T; } return {} as T; } finally { clearTimeout(timeout); } } // --- Health --- async healthCheck(): Promise { if (!this.hasCredentials) return false; try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); try { const res = await fetch(`${this.baseUrl}/alive`, { signal: controller.signal, }); return res.ok; } finally { clearTimeout(timeout); } } catch { return false; } } // --- User Management --- /** List all users */ async listUsers(): Promise { return this.request('GET', '/users'); } /** Find a user by email */ async findUserByEmail(email: string): Promise { try { const users = await this.listUsers(); return users.find(u => u.Email.toLowerCase() === email.toLowerCase()) || null; } catch (err) { logger.warn('Vaultwarden findUserByEmail failed:', err instanceof Error ? err.message : err); return null; } } /** Invite a user by email (user sets their own master password) */ async inviteUser(email: string): Promise { await this.request('POST', '/invite', { email }); } /** Deactivate a user (disable login without deleting data) */ async deactivateUser(userId: string): Promise { await this.request('POST', `/users/${userId}/disable`); } /** Re-enable a previously deactivated user */ async enableUser(userId: string): Promise { await this.request('POST', `/users/${userId}/enable`); } /** Delete a user and all their data (destructive) */ async deleteUser(userId: string): Promise { await this.request('DELETE', `/users/${userId}`); } } export const vaultwardenClient = new VaultwardenClient();