Skip to content
Open
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 47 additions & 19 deletions src/main/core/linear/linear-connection-service.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { LinearClient } from '@linear/sdk';
import { AuthenticationLinearError, ForbiddenLinearError, LinearClient } from '@linear/sdk';
import { ISSUE_PROVIDER_CAPABILITIES, type ConnectionStatus } from '@shared/issue-providers';
import { encryptedAppSecretsStore } from '@main/core/secrets/encrypted-app-secrets-store';
import { log } from '@main/lib/logger';
import { telemetryService } from '@main/lib/telemetry';

function isAuthFailure(error: unknown): boolean {
return error instanceof AuthenticationLinearError || error instanceof ForbiddenLinearError;
}

export class LinearConnectionService {
private readonly LINEAR_TOKEN_SECRET_KEY = 'emdash-linear-token';

private cachedToken: string | null | undefined = undefined;
private client: LinearClient | null = null;
private clientToken: string | null = null;
private lastVerifiedDisplayName: string | undefined | null = null;

async saveToken(
token: string
Expand All @@ -20,16 +25,15 @@ export class LinearConnectionService {
return { success: false, error: 'Linear token cannot be empty.' };
}

const client = this.getClientForToken(clean);
const viewer = await client.viewer;
const org = await viewer.organization;
const displayName = await this.fetchViewerDisplayName(clean);

await this.storeToken(clean);
this.lastVerifiedDisplayName = displayName;
telemetryService.capture('integration_connected', { provider: 'linear' });

return {
success: true,
workspaceName: org?.name ?? viewer.displayName ?? undefined,
workspaceName: displayName,
};
} catch (error) {
const message =
Expand All @@ -46,6 +50,7 @@ export class LinearConnectionService {
this.cachedToken = null;
this.client = null;
this.clientToken = null;
this.lastVerifiedDisplayName = null;
telemetryService.capture('integration_disconnected', { provider: 'linear' });
return { success: true };
} catch (error) {
Expand All @@ -58,30 +63,46 @@ export class LinearConnectionService {
}

async checkConnection(): Promise<ConnectionStatus> {
const token = await this.getStoredToken();
if (!token) {
this.lastVerifiedDisplayName = null;
return {
connected: false,
capabilities: ISSUE_PROVIDER_CAPABILITIES.linear,
};
}

try {
const token = await this.getStoredToken();
if (!token) {
const displayName = await this.fetchViewerDisplayName(token);
this.lastVerifiedDisplayName = displayName;
return {
connected: true,
displayName,
capabilities: ISSUE_PROVIDER_CAPABILITIES.linear,
};
} catch (error) {
if (isAuthFailure(error)) {
this.lastVerifiedDisplayName = null;
const message = error instanceof Error ? error.message : 'Linear token rejected.';
return {
connected: false,
error: message,
capabilities: ISSUE_PROVIDER_CAPABILITIES.linear,
};
}

const client = this.getClientForToken(token);
const viewer = await client.viewer;
const org = await viewer.organization;
if (this.lastVerifiedDisplayName === null) {
return {
connected: false,
error: 'Unable to verify Linear connection. Please try again.',
capabilities: ISSUE_PROVIDER_CAPABILITIES.linear,
};
}

log.warn('Linear connection check failed transiently; keeping connected:', error);
return {
connected: true,
displayName: org?.name ?? viewer.displayName ?? undefined,
capabilities: ISSUE_PROVIDER_CAPABILITIES.linear,
};
} catch (error) {
const message =
error instanceof Error ? error.message : 'Failed to verify Linear connection.';
return {
connected: false,
error: message,
displayName: this.lastVerifiedDisplayName,
capabilities: ISSUE_PROVIDER_CAPABILITIES.linear,
};
}
Expand All @@ -96,6 +117,13 @@ export class LinearConnectionService {
return this.getClientForToken(token);
}

private async fetchViewerDisplayName(token: string): Promise<string | undefined> {
const client = this.getClientForToken(token);
const viewer = await client.viewer;
const organization = await viewer.organization;
return organization.name ?? viewer.displayName ?? undefined;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The original call sites used org?.name (optional chaining), but the extracted helper removed it. If viewer.organization ever resolves to null or undefined, organization.name will throw a TypeError. In saveToken this would cause a valid token to be rejected with an obscure error message; in checkConnection it would be treated as a transient failure. Adding ?. restores the original defensive behaviour.

Suggested change
return organization.name ?? viewer.displayName ?? undefined;
return organization?.name ?? viewer.displayName ?? undefined;
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/main/core/linear/linear-connection-service.ts
Line: 124

Comment:
The original call sites used `org?.name` (optional chaining), but the extracted helper removed it. If `viewer.organization` ever resolves to `null` or `undefined`, `organization.name` will throw a `TypeError`. In `saveToken` this would cause a valid token to be rejected with an obscure error message; in `checkConnection` it would be treated as a transient failure. Adding `?.` restores the original defensive behaviour.

```suggestion
    return organization?.name ?? viewer.displayName ?? undefined;
```

How can I resolve this? If you propose a fix, please make it concise.

}

private getClientForToken(token: string): LinearClient {
if (!this.client || this.clientToken !== token) {
this.client = new LinearClient({ apiKey: token });
Expand Down
Loading