From a608070808201bdd099b2a6227fe0655665e6462 Mon Sep 17 00:00:00 2001 From: Joseph Stanton Date: Sun, 5 Apr 2026 12:25:59 -0400 Subject: [PATCH 1/2] refactor: remove eslint suppressions and improve type safety across codebase - Replace `any` type casts with precise TypeScript types for Electron modules and Node.js APIs. - Remove obsolete `eslint-disable` comments for sentence case, type safety, and control character rules. - Enforce `obsidianmd/ui/sentence-case` rule with VaultCrypt-specific brand exceptions. - Shift promise-based methods to synchronous ones where applicable (`await` -> sync). - Improve descriptive error handling and modularize logic for Electron integration. --- eslint.config.mts | 12 ++++++++++++ src/attachment-chip.ts | 20 ++++++++++---------- src/chip-component.ts | 9 ++++----- src/clipboard-intercept.ts | 26 -------------------------- src/kdbx-service.ts | 2 +- src/keyring-service.ts | 6 +++--- src/main.ts | 23 ++++++++++++----------- src/modals/generate-password-modal.ts | 7 +++---- src/modals/keyring-modals.ts | 3 +-- src/modals/profile-modals.ts | 10 ++++------ src/modals/vault-dir-modals.ts | 7 +------ src/profile-service.ts | 6 +++--- src/settings.ts | 11 +++-------- 13 files changed, 57 insertions(+), 85 deletions(-) diff --git a/eslint.config.mts b/eslint.config.mts index 0d60279..ff52668 100644 --- a/eslint.config.mts +++ b/eslint.config.mts @@ -2,6 +2,7 @@ import tseslint from 'typescript-eslint'; import obsidianmd from "eslint-plugin-obsidianmd"; import globals from "globals"; import { globalIgnores } from "eslint/config"; +import { DEFAULT_BRANDS } from "eslint-plugin-obsidianmd/dist/lib/rules/ui/brands.js"; export default tseslint.config( { @@ -25,6 +26,17 @@ export default tseslint.config( { rules: { 'semi': 'error', + 'require-await': 'error' + } + }, + { + plugins: { obsidianmd }, + rules: { + // Extend the default brand list with VaultCrypt-specific proper nouns + 'obsidianmd/ui/sentence-case': ['error', { + enforceCamelCaseLower: true, + brands: [...DEFAULT_BRANDS, 'VaultCrypt', 'KeePassXC', 'KDBX'], + }], } }, globalIgnores([ diff --git a/src/attachment-chip.ts b/src/attachment-chip.ts index b1be8eb..ed708a4 100644 --- a/src/attachment-chip.ts +++ b/src/attachment-chip.ts @@ -15,7 +15,7 @@ const MAX_PREVIEW_BYTES = 10 * 1024; // 10 KB function sanitizeVaultSegment(value: string): string { if (!value) return '_'; return value - // eslint-disable-next-line no-control-regex + // eslint-disable-next-line no-control-regex -- control characters (0x00–0x1F) must be stripped from filesystem paths .replace(/[\\/:*?"<>|\x00-\x1F]/g, '_') .replace(/^\.+$/, '_'); } @@ -434,12 +434,13 @@ export function buildAttachmentChipElement(token: ParsedVcToken, plugin: VaultCr // Desktop: try Electron save dialog. if (Platform.isDesktop) { - let dialog: { - showSaveDialog(o: { defaultPath: string }): Promise<{ canceled: boolean; filePath?: string }> - } | undefined; + type ElectronDialog = { showSaveDialog(o: { defaultPath: string }): Promise<{ canceled: boolean; filePath?: string }> }; + type RemoteModule = { dialog?: ElectronDialog }; + type FsPromises = { writeFile(path: string, data: Uint8Array): Promise }; + type FsModule = { promises?: FsPromises }; + let dialog: ElectronDialog | undefined; try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call - dialog = (window as any).require?.('@electron/remote')?.dialog; + dialog = (window as unknown as { require?: (id: string) => RemoteModule }).require?.('@electron/remote')?.dialog; } catch { dialog = undefined; } @@ -453,10 +454,9 @@ export function buildAttachmentChipElement(token: ParsedVcToken, plugin: VaultCr } if (result.canceled || !result.filePath) return; try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call - const fs = (window as any).require?.('fs')?.promises; - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,no-undef - await fs.writeFile(result.filePath, Buffer.from(data)); + const fs = (window as unknown as { require?: (id: string) => FsModule }).require?.('fs')?.promises; + // eslint-disable-next-line no-undef -- Buffer is a Node.js global available in Electron + await fs?.writeFile(result.filePath, Buffer.from(data)); new Notice(`Saved to ${result.filePath}`); } catch (err) { new Notice(`Failed to save: ${err instanceof Error ? err.message : String(err)}`); diff --git a/src/chip-component.ts b/src/chip-component.ts index 36d5496..199db36 100644 --- a/src/chip-component.ts +++ b/src/chip-component.ts @@ -137,7 +137,6 @@ function buildSecretChipElement(token: ParsedVcToken, plugin: VaultCryptPlugin): if (Platform.isDesktop) { menu.addItem(item => item - // eslint-disable-next-line obsidianmd/ui/sentence-case .setTitle('Open in KeePassXC') .setIcon('external-link') .onClick(async () => { @@ -147,12 +146,12 @@ function buildSecretChipElement(token: ParsedVcToken, plugin: VaultCryptPlugin): return; } try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-call - const shell = (window as any).require?.('electron')?.shell; + type ElectronShell = { openPath: (path: string) => Promise }; + type ElectronModule = { shell?: ElectronShell }; + const shell = (window as unknown as { require?: (id: string) => ElectronModule }).require?.('electron')?.shell; const adapter = plugin.app.vault.adapter as { getFullPath?: (path: string) => string }; if (shell && adapter.getFullPath) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment - const errorMsg: string = await shell.openPath(adapter.getFullPath(config.path)); + const errorMsg = await shell.openPath(adapter.getFullPath(config.path)); if (errorMsg) { new Notice(`Failed to open file: ${errorMsg}`); } diff --git a/src/clipboard-intercept.ts b/src/clipboard-intercept.ts index ebfd307..098bac2 100644 --- a/src/clipboard-intercept.ts +++ b/src/clipboard-intercept.ts @@ -137,29 +137,3 @@ function findChipAtPos(view: EditorView, pos: number): HTMLElement | null { } } -/** - * Determines the appropriate clipboard text for a raw `{{vc:...}}` token - * that was found in a text node (e.g. when cursor is inside the token in - * CM6 and the decoration is suppressed). - */ -function resolveTokenCopyText( - plugin: VaultCryptPlugin, - profileId: string, - _entryPath: string, - _fieldName: string | null, -): string { - const pid = profileId.toLowerCase(); - const config = plugin.settings.profiles[pid]; - if (!config) return '[unknown]'; - - const isLocked = plugin.vaultCryptState$().profiles.find( - p => p.id === pid, - )?.isLocked ?? true; - - if (isLocked) return '[locked]'; - - // Profile is unlocked but the raw token is visible (cursor inside it), - // so the chip is not rendered and there is no revealed/masked state. - // Default to [encrypted] to avoid leaking plaintext without explicit reveal. - return '[encrypted]'; -} diff --git a/src/kdbx-service.ts b/src/kdbx-service.ts index 812a0a8..7fbadc1 100644 --- a/src/kdbx-service.ts +++ b/src/kdbx-service.ts @@ -147,7 +147,7 @@ export class KdbxService { return this.toEntryRecord(entryPath, entry); } - async setEntry(entryPath: string, fields: EntryFields): Promise { + setEntry(entryPath: string, fields: EntryFields): void { if (!this.db) throw new Error('No database is open'); const segments = entryPath.split('/'); const title = segments[segments.length - 1] ?? entryPath; diff --git a/src/keyring-service.ts b/src/keyring-service.ts index 574bc30..8138b11 100644 --- a/src/keyring-service.ts +++ b/src/keyring-service.ts @@ -16,7 +16,7 @@ export class KeyringService { constructor(private app: App) {} /** Checks whether the keyring file exists on disk. */ - async keyringExists(path: string): Promise { + keyringExists(path: string): boolean { return this.app.vault.getAbstractFileByPath(path) instanceof TFile; } @@ -62,7 +62,7 @@ export class KeyringService { const svc = new KdbxService(this.app); await svc.openDatabase(path, masterPassword); try { - await svc.setEntry(profileId, {Password: profilePassword}); + svc.setEntry(profileId, {Password: profilePassword}); await svc.saveDatabase(); } finally { svc.closeDatabase(); @@ -99,7 +99,7 @@ export class KeyringService { if (!entry?.fields.Password) { throw new Error(`No keyring entry found for profile "${oldId}"`); } - await svc.setEntry(newId, {Password: entry.fields.Password}); + svc.setEntry(newId, {Password: entry.fields.Password}); svc.deleteEntry(oldId); await svc.saveDatabase(); } finally { diff --git a/src/main.ts b/src/main.ts index e435c6a..baed10e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import {Editor, MarkdownView, Menu, Notice, Plugin, TFile} from 'obsidian'; +import {Editor, MarkdownView, Menu, Notice, Platform, Plugin, TFile} from 'obsidian'; import {DEFAULT_SETTINGS, ProfileConfig, VaultCryptSettings, VaultCryptSettingTab} from './settings'; import {KdbxService, KdbxVersion} from './kdbx-service'; import {ProfileService} from './profile-service'; @@ -42,8 +42,12 @@ interface ElectronClipboard { function getElectronClipboard(): ElectronClipboard | null { try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-member-access - return (window as any).require?.('electron')?.clipboard ?? null; + if(!Platform.isDesktop){ + return null; + } + type ElectronModule = { clipboard?: ElectronClipboard }; + const mod = (window as unknown as { require?: (id: string) => ElectronModule }).require?.('electron'); + return mod?.clipboard ?? null; } catch { return null; } @@ -148,7 +152,6 @@ export default class VaultCryptPlugin extends Plugin { }); // Ribbon icon → unlock modal (prefers keyring when available) - // eslint-disable-next-line obsidianmd/ui/sentence-case this.addRibbonIcon('lock', 'VaultCrypt', () => { if (this.shouldUseKeyringUnlock()) { new KeyringUnlockModal(this.app, this, () => { @@ -309,8 +312,7 @@ export default class VaultCryptPlugin extends Plugin { this.registerEvent( this.app.workspace.on('editor-menu', (menu: Menu, editor: Editor) => { menu.addItem(item => item - // eslint-disable-next-line obsidianmd/ui/sentence-case - .setTitle('VaultCrypt: Insert secret here') + .setTitle('VaultCrypt: insert secret here') .setIcon('key') .onClick(() => new InsertSecretModal(this.app, this, editor).open())); }) @@ -423,7 +425,6 @@ export default class VaultCryptPlugin extends Plugin { return; } if (profiles.length === 0) { - // eslint-disable-next-line obsidianmd/ui/sentence-case this.statusBarItem.setText('🔒 VaultCrypt'); return; } @@ -506,12 +507,12 @@ export default class VaultCryptPlugin extends Plugin { this.initRuntimeState(); } - async editProfile(name: string, updates: Partial>): Promise { - await this.profileService.editProfile(name, updates); + editProfile(name: string, updates: Partial>): void { + this.profileService.editProfile(name, updates); } - async renameProfile(oldName: string, newName: string): Promise { - await this.profileService.renameProfile(oldName, newName); + renameProfile(oldName: string, newName: string): void { + this.profileService.renameProfile(oldName, newName); this.initRuntimeState(); } diff --git a/src/modals/generate-password-modal.ts b/src/modals/generate-password-modal.ts index 0bf797c..381c08b 100644 --- a/src/modals/generate-password-modal.ts +++ b/src/modals/generate-password-modal.ts @@ -57,22 +57,21 @@ export class GeneratePasswordModal extends Modal { })); new Setting(contentEl) - // eslint-disable-next-line obsidianmd/ui/sentence-case - .setName('Uppercase letters (A–Z)') + .setName('Uppercase letters') .addToggle(t => t.setValue(this.opts.upper).onChange(v => { this.opts.upper = v; this.refreshPreview(); })); new Setting(contentEl) - .setName('Lowercase letters (a–z)') + .setName('Lowercase letters') .addToggle(t => t.setValue(this.opts.lower).onChange(v => { this.opts.lower = v; this.refreshPreview(); })); new Setting(contentEl) - .setName('Digits (0–9)') + .setName('Digits') .addToggle(t => t.setValue(this.opts.digits).onChange(v => { this.opts.digits = v; this.refreshPreview(); diff --git a/src/modals/keyring-modals.ts b/src/modals/keyring-modals.ts index cb342ef..6b3901d 100644 --- a/src/modals/keyring-modals.ts +++ b/src/modals/keyring-modals.ts @@ -281,8 +281,7 @@ export class AddToKeyringModal extends Modal { this.titleEl.setText("Add profile to keyring"); contentEl.createEl("p", { - // eslint-disable-next-line obsidianmd/ui/sentence-case - text: "⚠ Security tradeoff: adding a profile to the keyring means a compromised keyring or master password exposes this profile password alongside all others.", + text: "⚠ security tradeoff: adding a profile to the keyring means a compromised keyring or master password exposes this profile password alongside all others.", cls: "mod-warning", }); diff --git a/src/modals/profile-modals.ts b/src/modals/profile-modals.ts index 19c5976..9e3adb0 100644 --- a/src/modals/profile-modals.ts +++ b/src/modals/profile-modals.ts @@ -31,8 +31,7 @@ export class AddProfileModal extends Modal { .setName("Profile name") .setDesc("Alphanumeric and hyphens only (for example, my-profile)") .addText(text => text - // eslint-disable-next-line obsidianmd/ui/sentence-case - .setPlaceholder("my-profile") + .setPlaceholder("My-profile") .onChange(value => { this.name = value; })); @@ -217,7 +216,6 @@ export class EditProfileModal extends Modal { this.titleEl.setText("Edit profile"); new Setting(contentEl).setName("Profile name").setDesc(this.profileName); - // eslint-disable-next-line obsidianmd/ui/sentence-case new Setting(contentEl).setName("KDBX version").setDesc(String(this.config.kdbxVersion)); new Setting(contentEl).setName("Path").setDesc(this.config.path); @@ -253,13 +251,13 @@ export class EditProfileModal extends Modal { }); } - private async submit() { + private submit() { if (this.isSubmitting) return; this.isSubmitting = true; this.submitBtn.setDisabled(true); try { - await this.plugin.editProfile(this.profileName, { + this.plugin.editProfile(this.profileName, { autoLockMinutes: this.autoLockMinutes, defaultField: this.defaultField, }); @@ -379,7 +377,7 @@ export class RenameProfileModal extends Modal { ); } try { - await this.plugin.renameProfile(this.currentName, this.newName); + this.plugin.renameProfile(this.currentName, this.newName); } catch (e) { if (this.isManagedByKeyring) { try { diff --git a/src/modals/vault-dir-modals.ts b/src/modals/vault-dir-modals.ts index 884c667..dac9dea 100644 --- a/src/modals/vault-dir-modals.ts +++ b/src/modals/vault-dir-modals.ts @@ -17,7 +17,6 @@ export class SyncWarningModal extends Modal { const {contentEl} = this; const currentDir = this.plugin.settings.general.vaultCryptDir; - // eslint-disable-next-line obsidianmd/ui/sentence-case this.titleEl.setText('VaultCrypt sync notice'); contentEl.createEl('p', { @@ -27,20 +26,17 @@ export class SyncWarningModal extends Modal { }); contentEl.createEl('p', { - // eslint-disable-next-line obsidianmd/ui/sentence-case text: 'Would you like to move it to "VaultCrypt" (a visible folder that Obsidian Sync will include)?', }); new Setting(contentEl) .addButton(btn => btn - // eslint-disable-next-line obsidianmd/ui/sentence-case .setButtonText('Move to "VaultCrypt"') .setCta() .onClick(async () => { this.close(); try { await this.plugin.profileService.moveVaultDir('VaultCrypt'); - // eslint-disable-next-line obsidianmd/ui/sentence-case new Notice('VaultCrypt: directory moved to "VaultCrypt".'); } catch { // moveVaultDir already shows a Notice on failure @@ -91,7 +87,6 @@ export class MoveVaultDirModal extends Modal { const {contentEl} = this; const currentDir = this.plugin.settings.general.vaultCryptDir; - // eslint-disable-next-line obsidianmd/ui/sentence-case this.titleEl.setText('Change VaultCrypt directory'); new Setting(contentEl) @@ -106,7 +101,7 @@ export class MoveVaultDirModal extends Modal { new Setting(contentEl) .setName('Move existing files') - .setDesc('Move .kdbx databases and other files from the current directory to the new location.') + .setDesc('Move .KDBX databases and other files from the current directory to the new location.') .addToggle(toggle => toggle .setValue(this.moveFiles) .onChange(value => { diff --git a/src/profile-service.ts b/src/profile-service.ts index 3e066a4..042f287 100644 --- a/src/profile-service.ts +++ b/src/profile-service.ts @@ -180,10 +180,10 @@ export class ProfileService { } /** Updates mutable profile settings (auto-lock timeout, default field). */ - async editProfile( + editProfile( name: string, updates: Partial>, - ): Promise { + ): void { const key = name.toLowerCase(); const settings = peek(this.settings); if (!settings.profiles[key]) throw new Error(`Profile '${name}' not found.`); @@ -204,7 +204,7 @@ export class ProfileService { } /** Renames a profile key in settings and updates any runtime references. */ - async renameProfile(oldName: string, newName: string): Promise { + renameProfile(oldName: string, newName: string): void { const oldKey = oldName.toLowerCase(); const newKey = newName.toLowerCase(); const settings = peek(this.settings); diff --git a/src/settings.ts b/src/settings.ts index 49497ab..6e29877 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -188,8 +188,7 @@ export class VaultCryptSettingTab extends PluginSettingTab { new Setting(containerEl) .setName('Clipboard clear timer') - // eslint-disable-next-line obsidianmd/ui/sentence-case - .setDesc('How long after copying before the clipboard is cleared. Set to Disabled to turn off.') + .setDesc('How long after copying before the clipboard is cleared. Set to disabled to turn off.') .addDropdown(drop => { const allowed = new Set([0, 15, 30, 60, 120]); const current = this.plugin.settings.security.clipboardClearSeconds; @@ -212,8 +211,7 @@ export class VaultCryptSettingTab extends PluginSettingTab { new Setting(containerEl) .setName('Auto-unlock on file open') - // eslint-disable-next-line obsidianmd/ui/sentence-case - .setDesc('When opening a note, automatically unlock profiles using saved passwords. Requires "Remember password" to be enabled when unlocking.') + .setDesc('When opening a note, automatically unlock profiles using saved passwords. Requires "remember password" to be enabled when unlocking.') .addToggle(toggle => toggle .setValue(this.plugin.settings.security.autoUnlock) .onChange((value) => { @@ -222,12 +220,9 @@ export class VaultCryptSettingTab extends PluginSettingTab { }); })); - // General section - // eslint-disable-next-line obsidianmd/settings-tab/no-problematic-settings-headings - new Setting(containerEl).setName("General").setHeading(); + new Setting(containerEl).setName("Vault").setHeading(); new Setting(containerEl) - // eslint-disable-next-line obsidianmd/ui/sentence-case .setName('VaultCrypt directory path') .setDesc(`Current: ${this.plugin.settings.general.vaultCryptDir} — Folder where .kdbx databases are stored.`) .addButton(btn => btn From 17c96431c737c4f3e5e31f808283a6ff1aab83ac Mon Sep 17 00:00:00 2001 From: Joseph Stanton Date: Sun, 5 Apr 2026 13:10:28 -0400 Subject: [PATCH 2/2] Address CodeRabbit feedback on PR #77 - eslint.config.mts: remove import of internal DEFAULT_BRANDS symbol from eslint-plugin-obsidianmd; list the brands used in this plugin explicitly (VaultCrypt, KeePassXC, KDBX, Obsidian, Obsidian Sync) - src/attachment-chip.ts: validate fs API availability before writing; show a failure Notice and return early if unavailable, then use direct await fs.writeFile() rather than optional-chained fs?.writeFile() Co-Authored-By: Claude Sonnet 4.6 --- eslint.config.mts | 3 +-- src/attachment-chip.ts | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/eslint.config.mts b/eslint.config.mts index ff52668..a47567a 100644 --- a/eslint.config.mts +++ b/eslint.config.mts @@ -2,7 +2,6 @@ import tseslint from 'typescript-eslint'; import obsidianmd from "eslint-plugin-obsidianmd"; import globals from "globals"; import { globalIgnores } from "eslint/config"; -import { DEFAULT_BRANDS } from "eslint-plugin-obsidianmd/dist/lib/rules/ui/brands.js"; export default tseslint.config( { @@ -35,7 +34,7 @@ export default tseslint.config( // Extend the default brand list with VaultCrypt-specific proper nouns 'obsidianmd/ui/sentence-case': ['error', { enforceCamelCaseLower: true, - brands: [...DEFAULT_BRANDS, 'VaultCrypt', 'KeePassXC', 'KDBX'], + brands: ['VaultCrypt', 'KeePassXC', 'KDBX', 'Obsidian', 'Obsidian Sync'], }], } }, diff --git a/src/attachment-chip.ts b/src/attachment-chip.ts index ed708a4..b7a3d65 100644 --- a/src/attachment-chip.ts +++ b/src/attachment-chip.ts @@ -455,8 +455,12 @@ export function buildAttachmentChipElement(token: ParsedVcToken, plugin: VaultCr if (result.canceled || !result.filePath) return; try { const fs = (window as unknown as { require?: (id: string) => FsModule }).require?.('fs')?.promises; + if (!fs) { + new Notice('Failed to save: filesystem API unavailable'); + return; + } // eslint-disable-next-line no-undef -- Buffer is a Node.js global available in Electron - await fs?.writeFile(result.filePath, Buffer.from(data)); + await fs.writeFile(result.filePath, Buffer.from(data)); new Notice(`Saved to ${result.filePath}`); } catch (err) { new Notice(`Failed to save: ${err instanceof Error ? err.message : String(err)}`);