/
raiden
/
obsidian-git-encrypt
Обзор
Документация
Войти
/
raiden
/
obsidian-git-encrypt
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/session/sessionManager.ts
220 строк
7 KB
Robert Kuzhin
feat: add manual encrypted source control
09 авг 2026, 20:57
09 авг 2026, 20:57
3c8fa38
Код
Авторство
О чём код?
import { AuthenticationError, type KeyHandle } from "../crypto/index.js"; import type { DiffTarget, PullChangesResult, SourceControlStatus, SourceDiff, } from "../sourceControl/index.js"; import { SessionError, SessionStateError } from "./errors.js"; import type { SessionBackend, SessionRuntime, SessionState, TrackedFileInfo, } from "./sessionState.js"; export type SessionStateListener = (state: SessionState) => void; export class SessionManager { private runtime: SessionRuntime | undefined; private state: SessionState = { status: "locked", hasKey: false }; private readonly listeners = new Set<SessionStateListener>(); private operationActive = false; public constructor(private readonly backend: SessionBackend) {} public getState(): SessionState { return structuredClone(this.state); } public subscribe(listener: SessionStateListener): () => void { this.listeners.add(listener); listener(this.getState()); return () => this.listeners.delete(listener); } public createVault(password: Uint8Array): Promise<void> { return this.openSession("create", password); } public unlock(password: Uint8Array): Promise<void> { return this.openSession("unlock", password); } public listTrackedFiles(): Promise<readonly TrackedFileInfo[]> { const runtime = this.requireRuntime(); return this.runExclusive(() => runtime.listTrackedFiles()); } public getSourceControlStatus(): Promise<SourceControlStatus> { return this.runExclusive(() => this.requireRuntime().getSourceControlStatus()); } public stage(fileId: string): Promise<void> { return this.runLocalSourceOperation(() => this.requireRuntime().stage(fileId)); } public stageAll(): Promise<void> { return this.runLocalSourceOperation(() => this.requireRuntime().stageAll()); } public unstage(fileId: string): Promise<void> { return this.runLocalSourceOperation(() => this.requireRuntime().unstage(fileId)); } public unstageAll(): Promise<void> { return this.runLocalSourceOperation(() => this.requireRuntime().unstageAll()); } public commitSelected(message: string): Promise<string> { return this.runSourceOperation(() => this.requireRuntime().commitSelected(message)); } public fetchRemote(): Promise<SourceControlStatus> { return this.runSourceOperation(() => this.requireRuntime().fetchRemote()); } public pullRemote(): Promise<PullChangesResult> { return this.runSourceOperation(() => this.requireRuntime().pullRemote()); } public pushRemote(): Promise<void> { return this.runSourceOperation(() => this.requireRuntime().pushRemote()); } public getDiff(fileId: string, target: DiffTarget): Promise<SourceDiff> { return this.runExclusive(() => this.requireRuntime().getDiff(fileId, target)); } public async changePassword(newPassword: Uint8Array): Promise<void> { const runtime = this.requireRuntime(); await this.runExclusive(async () => { this.setState({ status: "syncing", hasKey: true }); try { await runtime.changePassword(newPassword); this.setState({ status: "unlocked", hasKey: true }); } catch (error) { this.setFailure(error, true); throw error; } }); } public async lock(options: { readonly force?: boolean } = {}): Promise<void> { const runtime = this.runtime; if (runtime === undefined) { this.setState({ status: "locked", hasKey: false }); return; } await this.runExclusive(async () => { this.setState({ status: "locking", hasKey: true }); if (options.force !== true) { try { await runtime.flush(); } catch (error) { this.setFailure(error, true); throw error; } } try { await runtime.stop(); } finally { this.backend.destroyKey(runtime.key); this.runtime = undefined; this.setState({ status: "locked", hasKey: false }); } }); } public requireKeyHandle(): KeyHandle { return this.requireRuntime().key; } private async openSession(operation: "create" | "unlock", password: Uint8Array): Promise<void> { if (this.runtime !== undefined) throw new SessionStateError("Session is already unlocked"); await this.runExclusive(async () => { this.setState({ status: "unlocking", hasKey: false }); let runtime: SessionRuntime | undefined; try { runtime = await this.backend[operation](password); await runtime.start(); this.runtime = runtime; this.setState({ status: "unlocked", hasKey: true }); } catch (error) { if (runtime !== undefined) { await runtime.stop().catch(() => undefined); this.backend.destroyKey(runtime.key); } this.setFailure(error, false); throw error; } finally { password.fill(0); } }); } private requireRuntime(): SessionRuntime { if (this.runtime === undefined) throw new SessionStateError("Vault is locked"); return this.runtime; } private async runExclusive<T>(operation: () => Promise<T>): Promise<T> { if (this.operationActive) throw new SessionStateError("Another session operation is active"); this.operationActive = true; try { return await operation(); } finally { this.operationActive = false; } } private async runSourceOperation<T>(operation: () => Promise<T>): Promise<T> { return this.runExclusive(async () => { this.setState({ status: "syncing", hasKey: true }); try { const result = await operation(); this.setState({ status: "unlocked", hasKey: true }); return result; } catch (error) { this.setFailure(error, true); throw error; } }); } private async runLocalSourceOperation<T>(operation: () => Promise<T>): Promise<T> { return this.runExclusive(async () => { try { const result = await operation(); this.setState({ status: "unlocked", hasKey: true }); return result; } catch (error) { this.setFailure(error, true); throw error; } }); } private setFailure(error: unknown, hasKey: boolean): void { this.setState({ status: "error", hasKey, message: safeErrorMessage(error) }); } private setState(state: SessionState): void { this.state = state; for (const listener of this.listeners) { try { listener(this.getState()); } catch { // State observers cannot alter session transitions. } } } } function safeErrorMessage(error: unknown): string { if (error instanceof AuthenticationError) { return "Wrong password or damaged encrypted configuration."; } if (error instanceof SessionError) return error.message; return "Encrypted vault operation failed. Check the plugin settings and retry."; }