Skip to content

Commit 4a5d164

Browse files
committed
Merge branch 'main' of https://github.com/trycompai/comp into chas/remove-hosts-when-removing-member
2 parents 4134dc2 + 83e70f0 commit 4a5d164

47 files changed

Lines changed: 2490 additions & 832 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/api/src/integration-platform/controllers/checks.controller.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { ConnectionRepository } from '../repositories/connection.repository';
1717
import { CredentialVaultService } from '../services/credential-vault.service';
1818
import { ProviderRepository } from '../repositories/provider.repository';
1919
import { CheckRunRepository } from '../repositories/check-run.repository';
20+
import { getStringValue, toStringCredentials } from '../utils/credential-utils';
2021

2122
interface RunChecksDto {
2223
checkId?: string;
@@ -199,10 +200,12 @@ export class ChecksController {
199200

200201
try {
201202
// Run checks
203+
const accessToken = getStringValue(credentials.access_token);
204+
const stringCredentials = toStringCredentials(credentials);
202205
const result = await runAllChecks({
203206
manifest,
204-
accessToken: credentials.access_token ?? undefined,
205-
credentials: credentials,
207+
accessToken,
208+
credentials: stringCredentials,
206209
variables,
207210
connectionId,
208211
organizationId: connection.organizationId,

apps/api/src/integration-platform/controllers/connections.controller.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,26 @@ import {
2323
TASK_TEMPLATE_INFO,
2424
type OAuthConfig,
2525
type TaskTemplateId,
26+
type IntegrationCredentials,
2627
} from '@comp/integration-platform';
2728

2829
interface CreateConnectionDto {
2930
providerSlug: string;
3031
organizationId: string;
31-
credentials?: Record<string, string>;
32+
credentials?: Record<string, string | string[]>;
3233
}
3334

3435
interface ListConnectionsQuery {
3536
organizationId: string;
3637
}
3738

39+
const hasCredentialValue = (value?: string | string[]): boolean => {
40+
if (Array.isArray(value)) {
41+
return value.length > 0;
42+
}
43+
return typeof value === 'string' && value.trim().length > 0;
44+
};
45+
3846
@Controller({ path: 'integrations/connections', version: '1' })
3947
export class ConnectionsController {
4048
private readonly logger = new Logger(ConnectionsController.name);
@@ -402,7 +410,9 @@ export class ConnectionsController {
402410
}
403411

404412
try {
405-
const isValid = await manifest.handler.testConnection(credentials);
413+
const isValid = await manifest.handler.testConnection(
414+
credentials as IntegrationCredentials,
415+
);
406416

407417
if (isValid) {
408418
await this.connectionService.activateConnection(connection.id);
@@ -575,7 +585,10 @@ export class ConnectionsController {
575585
}
576586

577587
// For OAuth, validate access_token exists
578-
if (manifest.auth.type === 'oauth2' && !credentials.access_token) {
588+
const accessToken =
589+
typeof credentials.access_token === 'string' ? credentials.access_token : undefined;
590+
591+
if (manifest.auth.type === 'oauth2' && !accessToken) {
579592
throw new HttpException(
580593
'No valid OAuth credentials found. Please reconnect.',
581594
HttpStatus.BAD_REQUEST,
@@ -585,7 +598,7 @@ export class ConnectionsController {
585598
// For API key auth, validate key exists
586599
if (manifest.auth.type === 'api_key') {
587600
const apiKeyField = manifest.auth.config.name;
588-
if (!credentials[apiKeyField] && !credentials.api_key) {
601+
if (!hasCredentialValue(credentials[apiKeyField]) && !hasCredentialValue(credentials.api_key)) {
589602
throw new HttpException('API key not found', HttpStatus.BAD_REQUEST);
590603
}
591604
}
@@ -594,7 +607,7 @@ export class ConnectionsController {
594607
if (manifest.auth.type === 'basic') {
595608
const usernameField = manifest.auth.config.usernameField || 'username';
596609
const passwordField = manifest.auth.config.passwordField || 'password';
597-
if (!credentials[usernameField] || !credentials[passwordField]) {
610+
if (!hasCredentialValue(credentials[usernameField]) || !hasCredentialValue(credentials[passwordField])) {
598611
throw new HttpException(
599612
'Username and password required',
600613
HttpStatus.BAD_REQUEST,
@@ -615,7 +628,7 @@ export class ConnectionsController {
615628

616629
return {
617630
success: true,
618-
accessToken: credentials.access_token ?? undefined,
631+
accessToken,
619632
credentials,
620633
};
621634
}
@@ -627,7 +640,7 @@ export class ConnectionsController {
627640
async updateCredentials(
628641
@Param('id') id: string,
629642
@Query('organizationId') organizationId: string,
630-
@Body() body: { credentials: Record<string, string> },
643+
@Body() body: { credentials: Record<string, string | string[]> },
631644
) {
632645
if (!organizationId) {
633646
throw new HttpException(

apps/api/src/integration-platform/controllers/sync.controller.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1318,7 +1318,12 @@ export class SyncController {
13181318
type: 'system';
13191319
}
13201320

1321-
const apiKey = credentials.api_key;
1321+
const apiKey = Array.isArray(credentials.api_key)
1322+
? credentials.api_key[0]
1323+
: credentials.api_key;
1324+
if (!apiKey) {
1325+
throw new HttpException('API key not found', HttpStatus.BAD_REQUEST);
1326+
}
13221327
const users: JumpCloudUser[] = [];
13231328

13241329
try {

apps/api/src/integration-platform/controllers/task-integrations.controller.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { ProviderRepository } from '../repositories/provider.repository';
2121
import { CheckRunRepository } from '../repositories/check-run.repository';
2222
import { CredentialVaultService } from '../services/credential-vault.service';
2323
import { OAuthCredentialsService } from '../services/oauth-credentials.service';
24+
import { getStringValue, toStringCredentials } from '../utils/credential-utils';
2425
import { db } from '@db';
2526
import type { Prisma } from '@prisma/client';
2627

@@ -346,10 +347,12 @@ export class TaskIntegrationsController {
346347

347348
try {
348349
// Run the specific check
350+
const accessToken = getStringValue(credentials.access_token);
351+
const stringCredentials = toStringCredentials(credentials);
349352
const result = await runAllChecks({
350353
manifest,
351-
accessToken: credentials.access_token ?? undefined,
352-
credentials: credentials,
354+
accessToken,
355+
credentials: stringCredentials,
353356
variables,
354357
connectionId,
355358
organizationId,

apps/api/src/integration-platform/controllers/variables.controller.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,9 @@ export class VariablesController {
227227
const credentials =
228228
await this.credentialVaultService.getDecryptedCredentials(connectionId);
229229

230-
if (!credentials?.access_token) {
230+
const accessToken =
231+
typeof credentials?.access_token === 'string' ? credentials.access_token : undefined;
232+
if (!accessToken) {
231233
throw new HttpException(
232234
'No valid credentials found',
233235
HttpStatus.BAD_REQUEST,
@@ -239,12 +241,12 @@ export class VariablesController {
239241

240242
const buildHeaders = () => ({
241243
...defaultHeaders,
242-
Authorization: `Bearer ${credentials.access_token}`,
244+
Authorization: `Bearer ${accessToken}`,
243245
});
244246

245247
// Create minimal context for fetching options
246248
const fetchContext = {
247-
accessToken: credentials.access_token,
249+
accessToken,
248250

249251
fetch: async <T = unknown>(path: string): Promise<T> => {
250252
const url = new URL(path, baseUrl);

apps/api/src/integration-platform/repositories/connection.repository.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,9 @@ export class ConnectionRepository {
3838
providerId: string,
3939
organizationId: string,
4040
): Promise<IntegrationConnection | null> {
41-
return db.integrationConnection.findUnique({
42-
where: {
43-
providerId_organizationId: {
44-
providerId,
45-
organizationId,
46-
},
47-
},
41+
return db.integrationConnection.findFirst({
42+
where: { providerId, organizationId },
43+
orderBy: { createdAt: 'desc' },
4844
include: {
4945
provider: true,
5046
},
@@ -146,13 +142,8 @@ export class ConnectionRepository {
146142
providerId: string,
147143
organizationId: string,
148144
): Promise<void> {
149-
await db.integrationConnection.delete({
150-
where: {
151-
providerId_organizationId: {
152-
providerId,
153-
organizationId,
154-
},
155-
},
145+
await db.integrationConnection.deleteMany({
146+
where: { providerId, organizationId },
156147
});
157148
}
158149
}

apps/api/src/integration-platform/services/connection-auth-teardown.service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ export class ConnectionAuthTeardownService {
3030

3131
const credentials =
3232
await this.credentialVaultService.getDecryptedCredentials(connectionId);
33-
const accessToken = credentials?.access_token;
33+
const accessToken =
34+
typeof credentials?.access_token === 'string' ? credentials.access_token : undefined;
3435

3536
if (providerSlug && accessToken) {
3637
try {

apps/api/src/integration-platform/services/credential-vault.service.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -159,16 +159,25 @@ export class CredentialVaultService {
159159
*/
160160
async storeApiKeyCredentials(
161161
connectionId: string,
162-
credentials: Record<string, string>,
162+
credentials: Record<string, string | string[]>,
163163
): Promise<void> {
164164
const encryptedPayload: Record<string, unknown> = {};
165165

166166
for (const [key, value] of Object.entries(credentials)) {
167167
if (typeof value === 'string') {
168168
encryptedPayload[key] = await this.encrypt(value);
169-
} else {
170-
encryptedPayload[key] = value;
169+
continue;
170+
}
171+
172+
if (Array.isArray(value)) {
173+
const encryptedItems = await Promise.all(
174+
value.map((item) => (typeof item === 'string' ? this.encrypt(item) : item)),
175+
);
176+
encryptedPayload[key] = encryptedItems;
177+
continue;
171178
}
179+
180+
encryptedPayload[key] = value;
172181
}
173182

174183
const credentialVersion = await this.credentialRepository.create({
@@ -190,7 +199,7 @@ export class CredentialVaultService {
190199
*/
191200
async getDecryptedCredentials(
192201
connectionId: string,
193-
): Promise<Record<string, string> | null> {
202+
): Promise<Record<string, string | string[]> | null> {
194203
const latestVersion =
195204
await this.credentialRepository.findLatestByConnection(connectionId);
196205
if (!latestVersion) return null;
@@ -199,13 +208,23 @@ export class CredentialVaultService {
199208
string,
200209
unknown
201210
>;
202-
const decrypted: Record<string, string> = {};
211+
const decrypted: Record<string, string | string[]> = {};
203212

204213
for (const [key, value] of Object.entries(encryptedPayload)) {
205214
if (this.isEncryptedData(value)) {
206215
decrypted[key] = await this.decrypt(value);
207216
} else if (typeof value === 'string') {
208217
decrypted[key] = value;
218+
} else if (Array.isArray(value)) {
219+
const decryptedItems = await Promise.all(
220+
value.map(async (item) => {
221+
if (this.isEncryptedData(item)) {
222+
return this.decrypt(item);
223+
}
224+
return typeof item === 'string' ? item : '';
225+
}),
226+
);
227+
decrypted[key] = decryptedItems.filter((item) => item.length > 0);
209228
}
210229
}
211230

@@ -228,7 +247,7 @@ export class CredentialVaultService {
228247
*/
229248
async rotateCredentials(
230249
connectionId: string,
231-
newCredentials: Record<string, string>,
250+
newCredentials: Record<string, string | string[]>,
232251
): Promise<void> {
233252
const latestVersion =
234253
await this.credentialRepository.findLatestByConnection(connectionId);
@@ -270,7 +289,7 @@ export class CredentialVaultService {
270289
*/
271290
async getRefreshToken(connectionId: string): Promise<string | null> {
272291
const credentials = await this.getDecryptedCredentials(connectionId);
273-
return credentials?.refresh_token || null;
292+
return typeof credentials?.refresh_token === 'string' ? credentials.refresh_token : null;
274293
}
275294

276295
/**
@@ -395,6 +414,6 @@ export class CredentialVaultService {
395414

396415
// Get current credentials
397416
const credentials = await this.getDecryptedCredentials(connectionId);
398-
return credentials?.access_token || null;
417+
return typeof credentials?.access_token === 'string' ? credentials.access_token : null;
399418
}
400419
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Shared credential utility functions for normalizing credential values
3+
* across controllers that handle integration credentials.
4+
*/
5+
6+
/**
7+
* Extracts a single string value from a credential that may be string or string[]
8+
* @param value - The credential value which may be string or string[]
9+
* @returns The first string value, or undefined if no value exists
10+
*/
11+
export function getStringValue(value?: string | string[]): string | undefined {
12+
if (Array.isArray(value)) {
13+
return value[0];
14+
}
15+
return value;
16+
}
17+
18+
/**
19+
* Normalizes credentials from Record<string, string | string[]> to Record<string, string>
20+
* by extracting the first value from arrays
21+
* @param credentials - The credentials object with potential array values
22+
* @returns A normalized credentials object with only string values
23+
*/
24+
export function toStringCredentials(
25+
credentials: Record<string, string | string[]>,
26+
): Record<string, string> {
27+
const normalized: Record<string, string> = {};
28+
for (const [key, value] of Object.entries(credentials)) {
29+
const stringValue = getStringValue(value);
30+
if (typeof stringValue === 'string' && stringValue.length > 0) {
31+
normalized[key] = stringValue;
32+
}
33+
}
34+
return normalized;
35+
}

apps/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@
111111
"sonner": "^2.0.5",
112112
"stripe": "^20.0.0",
113113
"swr": "^2.3.4",
114-
"three": "^0.177.0",
114+
"three": "^0.182.0",
115115
"ts-pattern": "^5.7.0",
116116
"use-debounce": "^10.0.4",
117117
"use-long-press": "^3.3.0",

0 commit comments

Comments
 (0)