import type Database from 'better-sqlite3'; import type { SessionMetadata } from '@multi-simadmin/contracts'; import { CredentialResolverError } from './instance-credential-resolver.js'; import { UpstreamSessionClient } from './upstream-session-client.js'; export interface CredentialResolver { resolve(instanceId: string): Promise; } export class InstanceLoginServiceError extends Error { constructor(readonly code: 'NOT_FOUND' | 'CREDENTIAL_UNAVAILABLE') { super(code); } } export interface InstanceLoginServiceOptions { readonly db: Database.Database; readonly client: UpstreamSessionClient; readonly resolver: CredentialResolver; readonly now?: () => Date; } export class InstanceLoginService { private readonly now: () => Date; constructor(private readonly options: InstanceLoginServiceOptions) { this.now = options.now ?? (() => new Date()); } async login(instanceId: string, password?: string): Promise { const origin = this.origin(instanceId); let resolved: string; try { resolved = password ?? (await this.options.resolver.resolve(instanceId)); } catch (error) { if (error instanceof CredentialResolverError) throw new InstanceLoginServiceError(error.code); throw error; } const result = await this.options.client.login(instanceId, origin, resolved); return { instanceId, authenticated: result.authenticated, checkedAt: this.now().toISOString() }; } async logout(instanceId: string): Promise { await this.options.client.logout(instanceId, this.origin(instanceId)); return { instanceId, authenticated: false, checkedAt: this.now().toISOString() }; } private origin(instanceId: string): string { const row = this.options.db .prepare('SELECT base_url FROM instances WHERE id=?') .get(instanceId) as { base_url: string } | undefined; if (!row) throw new InstanceLoginServiceError('NOT_FOUND'); return row.base_url; } }