diff --git a/eslint.config.mts b/eslint.config.mts index 0d60279..a47567a 100644 --- a/eslint.config.mts +++ b/eslint.config.mts @@ -25,6 +25,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: ['VaultCrypt', 'KeePassXC', 'KDBX', 'Obsidian', 'Obsidian Sync'], + }], } }, globalIgnores([ diff --git a/src/attachment-chip.ts b/src/attachment-chip.ts index b1be8eb..b7a3d65 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,9 +454,12 @@ 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 + 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)); new Notice(`Saved to ${result.filePath}`); } catch (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