Files
orca/src/main/linear/linear-token-store.ts
T

146 lines
4.1 KiB
TypeScript

import { getSecretStore } from '../../shared/secret-store'
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
import {
LEGACY_WORKSPACE_ID,
ensureOrcaDir,
ensureWorkspaceTokenDir,
getWorkspaceTokenPath
} from './linear-credential-paths'
import {
clearLegacyViewerOnDisk,
forgetLegacyViewer,
resetLegacyViewerCache
} from './linear-legacy-viewer-store'
import { emptyWorkspaceFile } from './linear-workspace-record'
import {
cacheToken,
clearCredentialError,
forgetCachedToken,
getCachedToken,
getWorkspaceFile,
getWorkspaceState,
recordCredentialError,
resetCredentialCaches,
resetWorkspaceFileCacheToEmpty,
resolveWorkspaceId,
upsertWorkspace,
writeWorkspaceFile
} from './linear-workspace-registry'
import {
CredentialDecryptionError,
readStoredCredentialToken
} from '../integration-credential-file'
import type { LinearWorkspace } from '../../shared/linear/workspace-types'
function writeEncryptedToken(path: string, apiKey: string): void {
if (getSecretStore().isEncryptionAvailable()) {
const encrypted = getSecretStore().encryptString(apiKey)
writeFileSync(path, encrypted, { mode: 0o600 })
return
}
console.warn('[linear] secret encryption unavailable — storing token in plaintext')
writeFileSync(path, apiKey, { encoding: 'utf-8', mode: 0o600 })
}
export function saveWorkspaceToken(workspaceId: string, apiKey: string): void {
ensureOrcaDir()
if (workspaceId !== LEGACY_WORKSPACE_ID) {
ensureWorkspaceTokenDir()
}
const tokenPath = getWorkspaceTokenPath(workspaceId)
writeEncryptedToken(tokenPath, apiKey)
cacheToken(workspaceId, apiKey)
clearCredentialError(workspaceId)
}
// Backward-compatible export for the legacy single-workspace storage path.
export function saveToken(apiKey: string): void {
saveWorkspaceToken(LEGACY_WORKSPACE_ID, apiKey)
}
export function loadToken(options: { force?: boolean; workspaceId?: string } = {}): string | null {
const workspaceId = options.workspaceId ?? resolveWorkspaceId()
if (!workspaceId) {
return null
}
const cached = getCachedToken(workspaceId)
if (cached !== undefined) {
return cached
}
if (!options.force) {
return null
}
const tokenPath = getWorkspaceTokenPath(workspaceId)
if (!existsSync(tokenPath)) {
return null
}
try {
const raw = readFileSync(tokenPath)
const token = readStoredCredentialToken('Linear', raw)
if (token) {
cacheToken(workspaceId, token)
}
clearCredentialError(workspaceId)
return token
} catch (error) {
if (error instanceof CredentialDecryptionError) {
recordCredentialError(workspaceId, error.message)
throw error
}
return null
}
}
export function clearTokenFile(workspaceId: string): void {
forgetCachedToken(workspaceId)
try {
unlinkSync(getWorkspaceTokenPath(workspaceId))
} catch {
// File may not exist — safe to ignore.
}
}
export function clearToken(workspaceId?: string): void {
if (!workspaceId) {
const state = getWorkspaceState()
for (const workspace of state.workspaces) {
clearTokenFile(workspace.id)
}
resetCredentialCaches()
resetLegacyViewerCache()
resetWorkspaceFileCacheToEmpty()
clearLegacyViewerOnDisk()
writeWorkspaceFile(emptyWorkspaceFile())
return
}
clearTokenFile(workspaceId)
if (workspaceId === LEGACY_WORKSPACE_ID) {
resetLegacyViewerCache()
clearLegacyViewerOnDisk()
return
}
const file = getWorkspaceFile()
const workspaces = file.workspaces.filter((workspace) => workspace.id !== workspaceId)
const activeWorkspaceId =
file.activeWorkspaceId === workspaceId ? (workspaces[0]?.id ?? null) : file.activeWorkspaceId
const selectedWorkspaceId =
file.selectedWorkspaceId === workspaceId ? activeWorkspaceId : file.selectedWorkspaceId
writeWorkspaceFile({
version: 1,
activeWorkspaceId,
selectedWorkspaceId,
workspaces
})
}
export function replaceLegacyWorkspace(workspace: LinearWorkspace, token: string): void {
saveWorkspaceToken(workspace.id, token)
clearTokenFile(LEGACY_WORKSPACE_ID)
clearLegacyViewerOnDisk()
forgetLegacyViewer()
upsertWorkspace(workspace, { select: true })
}