import { existsSync } from 'node:fs' import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { getConfigDir } from '../../shared/utils/config-dir' import type { TeamsAccount, TeamsAccountType, TeamsAuthMethod, TeamsConfig, TeamsConfigLegacy, TeamsRegion, } from './types' export class TeamsCredentialManager { static accountOverride?: TeamsAccountType private configDir: string private credentialsPath: string constructor(configDir?: string) { this.configDir = configDir ?? getConfigDir() this.credentialsPath = join(this.configDir, 'teams-credentials.json') } async loadConfig(): Promise { if (!existsSync(this.credentialsPath)) { return null } try { const content = await readFile(this.credentialsPath, 'utf-8') const raw = JSON.parse(content) return this.migrateIfNeeded(raw) } catch { return null } } async saveConfig(config: TeamsConfig): Promise { await mkdir(this.configDir, { recursive: true }) const tmpPath = `${this.credentialsPath}.tmp` await writeFile(tmpPath, JSON.stringify(config, null, 2), { mode: 0o600 }) await rename(tmpPath, this.credentialsPath) } private migrateIfNeeded(raw: TeamsConfig | TeamsConfigLegacy): TeamsConfig { if ('accounts' in raw && raw.accounts) { return raw as TeamsConfig } const legacy = raw as TeamsConfigLegacy const account: TeamsAccount = { token: legacy.token, token_expires_at: legacy.token_expires_at, account_type: 'work', current_team: legacy.current_team, teams: legacy.teams, } return { current_account: 'work', accounts: { work: account }, } } private resolveAccountKey(config: TeamsConfig): string | null { return TeamsCredentialManager.accountOverride ?? config.current_account } async getCurrentAccount(): Promise { const config = await this.loadConfig() if (!config) return null const key = this.resolveAccountKey(config) if (!key) return null return config.accounts[key] ?? null } private resolveCurrentAccount(config: TeamsConfig): TeamsAccount | null { const key = this.resolveAccountKey(config) if (!key) return null return config.accounts[key] ?? null } async getToken(): Promise { const config = await this.loadConfig() if (!config) return null return this.resolveCurrentAccount(config)?.token ?? null } async getTokenWithExpiry(): Promise<{ token: string tokenExpiresAt?: string accountType?: TeamsAccountType region?: TeamsRegion } | null> { const config = await this.loadConfig() if (!config) return null const account = this.resolveCurrentAccount(config) if (!account?.token) return null return { token: account.token, tokenExpiresAt: account.token_expires_at, accountType: account.account_type, region: account.region, } } async setToken(token: string, accountType: TeamsAccountType, expiresAt?: string): Promise { let config = await this.loadConfig() if (!config) { config = { current_account: accountType, accounts: {} } } const existing = config.accounts[accountType] config.accounts[accountType] = { token, token_expires_at: expiresAt, account_type: accountType, user_name: existing?.user_name, current_team: existing?.current_team ?? null, teams: existing?.teams ?? {}, } if (!config.current_account) { config.current_account = accountType } await this.saveConfig(config) } async setDeviceCodeAccount(params: { accountType: TeamsAccountType token: string tokenExpiresAt: string aadRefreshToken?: string aadClientId?: string aadTenantId?: string region?: TeamsRegion userName?: string teams: Record currentTeam: string | null authMethod?: TeamsAuthMethod makeCurrent?: boolean }): Promise { let config = await this.loadConfig() if (!config) { config = { current_account: params.accountType, accounts: {} } } const existing = config.accounts[params.accountType] config.accounts[params.accountType] = { token: params.token, token_expires_at: params.tokenExpiresAt, region: params.region ?? existing?.region, account_type: params.accountType, user_name: params.userName ?? existing?.user_name, current_team: params.currentTeam, teams: params.teams, auth_method: params.authMethod ?? 'device-code', aad_refresh_token: params.aadRefreshToken, aad_client_id: params.aadClientId, aad_tenant_id: params.aadTenantId ?? existing?.aad_tenant_id, } if (params.makeCurrent ?? true) { config.current_account = params.accountType } await this.saveConfig(config) } async getAadRefresh(accountType: TeamsAccountType): Promise<{ refreshToken: string; clientId?: string } | null> { const config = await this.loadConfig() const account = config?.accounts[accountType] if (!account?.aad_refresh_token) return null return { refreshToken: account.aad_refresh_token, clientId: account.aad_client_id } } async updateAadRefreshToken(accountType: TeamsAccountType, refreshToken: string, clientId?: string): Promise { const config = await this.loadConfig() const account = config?.accounts[accountType] if (!config || !account) return account.aad_refresh_token = refreshToken if (clientId !== undefined) { account.aad_client_id = clientId } await this.saveConfig(config) } async getCurrentTeam(): Promise<{ team_id: string; team_name: string } | null> { const config = await this.loadConfig() if (!config) return null const account = this.resolveCurrentAccount(config) if (!account?.current_team) return null return account.teams[account.current_team] ?? null } async setCurrentTeam(teamId: string, teamName: string): Promise { const config = await this.loadConfig() if (!config) return const account = this.resolveCurrentAccount(config) if (!account) return account.current_team = teamId account.teams[teamId] = { team_id: teamId, team_name: teamName } await this.saveConfig(config) } async getCurrentAccountType(): Promise { const config = await this.loadConfig() if (!config) return null const key = this.resolveAccountKey(config) return (key as TeamsAccountType) ?? null } async setCurrentAccount(accountType: TeamsAccountType): Promise { const config = await this.loadConfig() if (!config) return if (!config.accounts[accountType]) return config.current_account = accountType await this.saveConfig(config) } async getAccounts(): Promise> { const config = await this.loadConfig() return config?.accounts ?? {} } async clearCredentials(): Promise { if (existsSync(this.credentialsPath)) { await rm(this.credentialsPath) } } async isTokenExpired(): Promise { const config = await this.loadConfig() if (!config) return true const account = this.resolveCurrentAccount(config) if (!account?.token_expires_at) return true return new Date(account.token_expires_at).getTime() <= Date.now() } async isAccountTokenExpired(accountType: TeamsAccountType): Promise { const config = await this.loadConfig() if (!config) return true const account = config.accounts[accountType] if (!account?.token_expires_at) return true return new Date(account.token_expires_at).getTime() <= Date.now() } }