import { APICredentials } from '../utils/types.js';
import fs from 'fs-extra';
import path from 'path';
interface CachedCredentials {
credentials: APICredentials;
timestamp: number;
}
export class CredentialManager {
private cache: Map<string, CachedCredentials> = new Map();
private ttl: number = 300000; // 5 minutes
async getVaultCredentials(vaultPath: string): Promise<APICredentials | null> {
// Check cache first
const cached = this.getCached(vaultPath);
if (cached) {
return cached;
}
// Discover from plugin
const discovered = await this.discoverCredentials(vaultPath);
if (discovered) {
this.setCached(vaultPath, discovered);
}
return discovered;
}
private getCached(vaultPath: string): APICredentials | null {
const cached = this.cache.get(vaultPath);
if (!cached) return null;
// Check TTL
if (Date.now() - cached.timestamp > this.ttl) {
this.cache.delete(vaultPath);
return null;
}
return cached.credentials;
}
private setCached(vaultPath: string, credentials: APICredentials): void {
this.cache.set(vaultPath, {
credentials,
timestamp: Date.now(),
});
}
async hasRestAPI(vaultPath: string): Promise<boolean> {
const credentials = await this.getVaultCredentials(vaultPath);
return credentials !== null;
}
clearCache(vaultPath?: string): void {
if (vaultPath) {
this.cache.delete(vaultPath);
} else {
this.cache.clear();
}
}
private async discoverCredentials(vaultPath: string): Promise<APICredentials | null> {
const pluginDir = path.join(vaultPath, '.obsidian', 'plugins', 'obsidian-local-rest-api');
const manifestPath = path.join(pluginDir, 'manifest.json');
const dataPath = path.join(pluginDir, 'data.json');
// Check if plugin exists
if (!(await fs.pathExists(dataPath))) {
return null;
}
try {
// Detect plugin version
let pluginVersion = '1.0.0';
if (await fs.pathExists(manifestPath)) {
const manifest = await fs.readJson(manifestPath);
pluginVersion = manifest.version || '1.0.0';
}
// Read and parse data
const data = await fs.readJson(dataPath);
return this.parsePluginData(data, pluginVersion);
} catch (error) {
console.error(`Failed to read credentials from ${vaultPath}:`, error);
return null;
}
}
private parsePluginData(data: any, version: string): APICredentials | null {
// Parse v1.x schema
if (version.startsWith('1.')) {
if (!data.apiKey) return null;
return {
apiKey: data.apiKey,
port: data.port || 27124,
insecurePort: data.insecurePort,
useSecure: !data.enableInsecureServer,
host: 'localhost',
pluginVersion: version,
};
}
// Future: Parse v2.x schema
if (version.startsWith('2.')) {
// Handle different structure if needed
console.warn(`Plugin version ${version} not fully supported, trying v1 schema`);
return this.parsePluginData(data, '1.0.0');
}
// Unknown version - try v1 schema as fallback
console.warn(`Unknown plugin version ${version}, trying v1 schema`);
return this.parsePluginData(data, '1.0.0');
}
validateCredentials(creds: APICredentials): boolean {
// Validate host
const allowedHosts = ['localhost', '127.0.0.1'];
if (!allowedHosts.includes(creds.host)) {
return false;
}
// Validate port range
if (creds.port < 27000 || creds.port > 28000) {
return false;
}
// Validate API key format
if (!creds.apiKey || creds.apiKey.length < 10) {
return false;
}
return true;
}
async invalidateIfChanged(vaultPath: string): Promise<void> {
const dataPath = path.join(
vaultPath,
'.obsidian/plugins/obsidian-local-rest-api/data.json'
);
try {
const stats = await fs.stat(dataPath);
const cached = this.cache.get(vaultPath);
if (cached && stats.mtimeMs > cached.timestamp) {
this.clearCache(vaultPath);
}
} catch (error) {
// File doesn't exist or error reading - invalidate to be safe
this.clearCache(vaultPath);
}
}
}