Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
152 changes: 30 additions & 122 deletions src/api/routers/integrationsDiscovery.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { TRPCError } from '@trpc/server';
import { and, eq } from 'drizzle-orm';
import { eq } from 'drizzle-orm';
import { ImapFlow } from 'imapflow';
import twilio from 'twilio';
import { z } from 'zod';
import { getDb } from '../../db/client.js';
import { decryptCredential, encryptCredential } from '../../db/crypto.js';
import { credentials, integrationCredentials, projectIntegrations } from '../../db/schema/index.js';
import { decryptCredential } from '../../db/crypto.js';
import {
upsertCredentialByEnvVarKey,
upsertGmailIntegrationWithCredentials,
} from '../../db/repositories/credentialsRepository.js';
import { credentials } from '../../db/schema/index.js';
import { exchangeGmailCode, getGmailAuthUrl, getGmailUserInfo } from '../../email/gmail/oauth.js';
import { jiraClient, withJiraCredentials } from '../../jira/client.js';
import { trelloClient, withTrelloCredentials } from '../../trello/client.js';
Expand Down Expand Up @@ -289,127 +293,31 @@ export const integrationsDiscoveryRouter = router({
// Get user email
const userInfo = await getGmailUserInfo(tokens.access_token);

const db = getDb();

// Ensure Gmail integration exists for the project
const [existingIntegration] = await db
.select({ id: projectIntegrations.id })
.from(projectIntegrations)
.where(
and(
eq(projectIntegrations.projectId, projectId),
eq(projectIntegrations.category, 'email'),
),
);

let integrationId: number;
if (existingIntegration) {
// Update to gmail provider
await db
.update(projectIntegrations)
.set({ provider: 'gmail', config: {}, updatedAt: new Date() })
.where(eq(projectIntegrations.id, existingIntegration.id));
integrationId = existingIntegration.id;
} else {
// Create new gmail integration
const [newIntegration] = await db
.insert(projectIntegrations)
.values({
projectId,
category: 'email',
provider: 'gmail',
config: {},
})
.returning({ id: projectIntegrations.id });
integrationId = newIntegration.id;
}

// Create or update gmail_email credential
const emailCredName = `Gmail: ${userInfo.email}`;
const [existingEmailCred] = await db
.select({ id: credentials.id })
.from(credentials)
.where(
and(
eq(credentials.orgId, ctx.effectiveOrgId),
eq(credentials.envVarKey, 'EMAIL_GMAIL_ADDRESS'),
eq(credentials.name, emailCredName),
),
);

let emailCredId: number;
if (existingEmailCred) {
await db
.update(credentials)
.set({
value: encryptCredential(userInfo.email, ctx.effectiveOrgId),
updatedAt: new Date(),
})
.where(eq(credentials.id, existingEmailCred.id));
emailCredId = existingEmailCred.id;
} else {
const [newCred] = await db
.insert(credentials)
.values({
orgId: ctx.effectiveOrgId,
name: emailCredName,
envVarKey: 'EMAIL_GMAIL_ADDRESS',
value: encryptCredential(userInfo.email, ctx.effectiveOrgId),
isDefault: false,
})
.returning({ id: credentials.id });
emailCredId = newCred.id;
}

// Create or update gmail_refresh_token credential
const refreshCredName = `Gmail Refresh Token: ${userInfo.email}`;
const [existingRefreshCred] = await db
.select({ id: credentials.id })
.from(credentials)
.where(
and(
eq(credentials.orgId, ctx.effectiveOrgId),
eq(credentials.envVarKey, 'EMAIL_GMAIL_REFRESH_TOKEN'),
eq(credentials.name, refreshCredName),
),
);

let refreshCredId: number;
if (existingRefreshCred) {
await db
.update(credentials)
.set({
value: encryptCredential(tokens.refresh_token, ctx.effectiveOrgId),
updatedAt: new Date(),
})
.where(eq(credentials.id, existingRefreshCred.id));
refreshCredId = existingRefreshCred.id;
} else {
const [newCred] = await db
.insert(credentials)
.values({
orgId: ctx.effectiveOrgId,
name: refreshCredName,
envVarKey: 'EMAIL_GMAIL_REFRESH_TOKEN',
value: encryptCredential(tokens.refresh_token, ctx.effectiveOrgId),
isDefault: false,
})
.returning({ id: credentials.id });
refreshCredId = newCred.id;
}

// Link credentials to integration
// Delete any existing credential links for this integration
await db
.delete(integrationCredentials)
.where(eq(integrationCredentials.integrationId, integrationId));

// Insert new credential links
await db.insert(integrationCredentials).values([
{ integrationId, role: 'gmail_email', credentialId: emailCredId },
{ integrationId, role: 'gmail_refresh_token', credentialId: refreshCredId },
// Upsert gmail_email and gmail_refresh_token credentials
const [emailCredId, refreshCredId] = await Promise.all([
upsertCredentialByEnvVarKey({
orgId: ctx.effectiveOrgId,
envVarKey: 'EMAIL_GMAIL_ADDRESS',
name: `Gmail: ${userInfo.email}`,
value: userInfo.email,
}),
upsertCredentialByEnvVarKey({
orgId: ctx.effectiveOrgId,
envVarKey: 'EMAIL_GMAIL_REFRESH_TOKEN',
name: `Gmail Refresh Token: ${userInfo.email}`,
value: tokens.refresh_token,
}),
]);

// Upsert the Gmail integration and link credentials
await upsertGmailIntegrationWithCredentials({
projectId,
credentialLinks: [
{ role: 'gmail_email', credentialId: emailCredId },
{ role: 'gmail_refresh_token', credentialId: refreshCredId },
],
});

logger.info('Gmail OAuth credentials stored successfully', {
projectId,
email: userInfo.email,
Expand Down
4 changes: 2 additions & 2 deletions src/backends/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { ModelSpec } from 'llmist';

import { hasFinishValidation } from '../agents/definitions/index.js';
import { getAgentProfile } from '../agents/definitions/profiles.js';
import { getToolManifests } from '../agents/definitions/toolManifests.js';
import type { PromptContext } from '../agents/prompts/index.js';
import {
type LogWriter,
Expand All @@ -17,11 +19,9 @@ import { loadPartials } from '../db/repositories/partialsRepository.js';
import { recordInitialComment } from '../gadgets/sessionState.js';
import { withGitHubToken } from '../github/client.js';
import type { AgentInput, AgentResult, CascadeConfig, ProjectConfig } from '../types/index.js';
import { getAgentProfile } from './agent-profiles.js';
import { postProcessResult } from './postProcess.js';
import { createProgressMonitor } from './progress.js';
import { augmentProjectSecrets, resolveGitHubToken } from './secretBuilder.js';
import { getToolManifests } from './toolManifests.js';
import type { AgentBackend, AgentBackendInput } from './types.js';

/**
Expand Down
7 changes: 0 additions & 7 deletions src/backends/agent-profiles.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/backends/llmist/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import os from 'node:os';
import { LLMist, type ModelSpec, createLogger } from 'llmist';

import { createIntegrationChecker } from '../../agents/capabilities/index.js';
import { getAgentProfile } from '../../agents/definitions/profiles.js';
import { type BuilderType, createConfiguredBuilder } from '../../agents/shared/builderFactory.js';
import { injectSyntheticCall } from '../../agents/shared/syntheticCalls.js';
import { runAgentLoop } from '../../agents/utils/agentLoop.js';
Expand All @@ -13,7 +14,6 @@ import { createTrackingContext } from '../../agents/utils/tracking.js';
import { CUSTOM_MODELS } from '../../config/customModels.js';
import { createLLMCallLogger } from '../../utils/llmLogging.js';
import { extractPRUrl } from '../../utils/prUrl.js';
import { getAgentProfile } from '../agent-profiles.js';
import type { AgentBackend, AgentBackendInput, AgentBackendResult } from '../types.js';

/**
Expand Down
2 changes: 1 addition & 1 deletion src/backends/secretBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { AgentProfile } from '../agents/definitions/profiles.js';
import { getAllProjectCredentials } from '../config/provider.js';
import { getPersonaToken } from '../github/personas.js';
import { getJiraConfig } from '../pm/config.js';
import type { AgentInput, ProjectConfig } from '../types/index.js';
import { parseRepoFullName } from '../utils/repo.js';
import type { AgentProfile } from './agent-profiles.js';

/**
* Resolve the GitHub token for profiles that need GitHub client access.
Expand Down
6 changes: 0 additions & 6 deletions src/backends/toolManifests.ts

This file was deleted.

19 changes: 0 additions & 19 deletions src/config/triggerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,6 @@ export const TrelloTriggerConfigSchema = z.object({
commentMention: z.boolean().default(true),
});

/**
* @deprecated Use `StatusChangedSchema` instead.
*/
export const IssueTransitionedSchema = StatusChangedSchema;
export type IssueTransitionedConfig = StatusChangedConfig;

/**
* Trigger configuration for JIRA integrations.
* All triggers default to `true` for backward compatibility.
Expand Down Expand Up @@ -434,16 +428,3 @@ export function resolveTrelloStatusChangedEnabled(
// No config present — default enabled for backward compatibility
return true;
}

/**
* @deprecated Use `resolveStatusChangedEnabled` instead.
* Resolve whether the issue-transitioned trigger is enabled for a specific agent type.
* Supports both the new nested object format and the legacy boolean format.
* Returns `true` when no config is present (backward compatible).
*/
export function resolveIssueTransitionedEnabled(
config: Partial<JiraTriggerConfig> | undefined,
agentType: string,
): boolean {
return resolveStatusChangedEnabled(config, agentType);
}
104 changes: 104 additions & 0 deletions src/db/repositories/credentialsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,110 @@ import { getDb } from '../client.js';
import { decryptCredential, encryptCredential } from '../crypto.js';
import { credentials, integrationCredentials, projectIntegrations } from '../schema/index.js';

// ============================================================================
// Gmail-specific repository helpers
// ============================================================================

/**
* Find or create a credential by (orgId, envVarKey, name), then update its value.
* Returns the credential ID.
*
* Used in the Gmail OAuth callback to upsert gmail_email and gmail_refresh_token
* credentials without duplicating the find-or-create + update pattern inline.
*/
export async function upsertCredentialByEnvVarKey(params: {
orgId: string;
envVarKey: string;
name: string;
value: string;
}): Promise<number> {
const db = getDb();
const { orgId, envVarKey, name, value } = params;
const encryptedValue = encryptCredential(value, orgId);

const [existing] = await db
.select({ id: credentials.id })
.from(credentials)
.where(
and(
eq(credentials.orgId, orgId),
eq(credentials.envVarKey, envVarKey),
eq(credentials.name, name),
),
);

if (existing) {
await db
.update(credentials)
.set({ value: encryptedValue, updatedAt: new Date() })
.where(eq(credentials.id, existing.id));
return existing.id;
}

const [created] = await db
.insert(credentials)
.values({
orgId,
name,
envVarKey,
value: encryptedValue,
isDefault: false,
})
.returning({ id: credentials.id });
return created.id;
}

/**
* Upsert a Gmail integration for a project (find/create the integration row),
* then replace all credential links with the provided ones.
*
* @param projectId - The project to upsert the integration for.
* @param credentialLinks - Array of { role, credentialId } pairs to link.
* @returns The integration ID.
*/
export async function upsertGmailIntegrationWithCredentials(params: {
projectId: string;
credentialLinks: Array<{ role: string; credentialId: number }>;
}): Promise<number> {
const db = getDb();
const { projectId, credentialLinks } = params;

// Find or create the email integration row
const [existing] = await db
.select({ id: projectIntegrations.id })
.from(projectIntegrations)
.where(
and(eq(projectIntegrations.projectId, projectId), eq(projectIntegrations.category, 'email')),
);

let integrationId: number;
if (existing) {
await db
.update(projectIntegrations)
.set({ provider: 'gmail', config: {}, updatedAt: new Date() })
.where(eq(projectIntegrations.id, existing.id));
integrationId = existing.id;
} else {
const [created] = await db
.insert(projectIntegrations)
.values({ projectId, category: 'email', provider: 'gmail', config: {} })
.returning({ id: projectIntegrations.id });
integrationId = created.id;
}

// Replace credential links
await db
.delete(integrationCredentials)
.where(eq(integrationCredentials.integrationId, integrationId));
await db
.insert(integrationCredentials)
.values(
credentialLinks.map(({ role, credentialId }) => ({ integrationId, role, credentialId })),
);

return integrationId;
}

// ============================================================================
// Integration credential resolution
// ============================================================================
Expand Down
6 changes: 4 additions & 2 deletions tests/unit/backends/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,10 @@ vi.mock('../../../src/github/client.js', () => ({
withGitHubToken: vi.fn((_token: string, fn: () => Promise<unknown>) => fn()),
}));

vi.mock('../../../src/backends/agent-profiles.js', () => ({
vi.mock('../../../src/agents/definitions/profiles.js', () => ({
getAgentProfile: vi.fn(),
hasFinishValidation: vi.fn(() => false),
getAgentCapabilities: vi.fn(),
}));

const mockCaptureException = vi.fn();
Expand Down Expand Up @@ -113,11 +115,11 @@ vi.mock('../../../src/db/repositories/runsRepository.js', () => ({
storeRunLogs: (...args: unknown[]) => mockStoreRunLogs(...args),
}));

import { type AgentProfile, getAgentProfile } from '../../../src/agents/definitions/profiles.js';
import { resolveModelConfig } from '../../../src/agents/shared/modelResolution.js';
import { setupRepository } from '../../../src/agents/shared/repository.js';
import { createAgentLogger } from '../../../src/agents/utils/logging.js';
import { executeWithBackend } from '../../../src/backends/adapter.js';
import { type AgentProfile, getAgentProfile } from '../../../src/backends/agent-profiles.js';
import { createProgressMonitor } from '../../../src/backends/progress.js';
import type { AgentBackend } from '../../../src/backends/types.js';
import { getAllProjectCredentials } from '../../../src/config/provider.js';
Expand Down
Loading