Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import { DataSource } from 'apollo-datasource';
2
3import { inject, type Dependencies } from '../../iocContainer/index.js';
4import '../../services/signalAuthService/index.js';
5import { Integration } from '../../services/signalsService/index.js';
6import {
7 CoopError,
8 ErrorType,
9 type ErrorInstanceData,
10} from '../../utils/errors.js';
11import { getIntegrationRegistry } from '../../services/integrationRegistry/index.js';
12import type {
13 IntegrationManifestEntry,
14 ModelCard,
15} from './integrationManifests.js';
16import { type GQLSetIntegrationConfigInput } from '../generated.js';
17
18export type TIntegrationConfigWithMetadata = Readonly<{
19 name: string;
20 apiCredential: Readonly<Record<string, unknown>>;
21 modelCard: ModelCard;
22 modelCardLearnMoreUrl?: string;
23 title: string;
24 docsUrl: string;
25 requiresConfig: boolean;
26 logoUrl?: string;
27 logoWithBackgroundUrl?: string;
28}>;
29
30function defaultCredentialForIntegrationId(
31 integrationId: string,
32): Record<string, unknown> {
33 switch (integrationId) {
34 case Integration.GOOGLE_CONTENT_SAFETY_API:
35 case Integration.OPEN_AI:
36 return { apiKey: '' };
37 case Integration.ZENTROPI:
38 return { apiKey: '', labelerVersions: [] };
39 default:
40 return {};
41 }
42}
43
44function mergeManifest(
45 integrationId: string,
46 apiCredential: Record<string, unknown>,
47 manifest: IntegrationManifestEntry,
48): TIntegrationConfigWithMetadata {
49 return {
50 name: integrationId,
51 apiCredential: { ...apiCredential, name: integrationId },
52 modelCard: manifest.modelCard,
53 modelCardLearnMoreUrl: manifest.modelCardLearnMoreUrl,
54 title: manifest.title,
55 docsUrl: manifest.docsUrl,
56 requiresConfig: manifest.requiresConfig,
57 logoUrl: manifest.logoUrl,
58 logoWithBackgroundUrl: manifest.logoWithBackgroundUrl,
59 };
60}
61
62/**
63 * TODO: this whole class should probably be merged into the signal auth service.
64 */
65class IntegrationAPI extends DataSource {
66 constructor(
67 private readonly signalAuthService: Dependencies['SignalAuthService'],
68 ) {
69 super();
70 }
71
72 async setConfig(
73 params: GQLSetIntegrationConfigInput,
74 orgId: string,
75 ): Promise<TIntegrationConfigWithMetadata> {
76 const { apiCredential } = params;
77
78 if (apiCredential.googleContentSafetyApi) {
79 return this.setConfigByIntegrationId(
80 'GOOGLE_CONTENT_SAFETY_API',
81 { apiKey: apiCredential.googleContentSafetyApi.apiKey },
82 orgId,
83 );
84 }
85 if (apiCredential.openAi) {
86 return this.setConfigByIntegrationId(
87 'OPEN_AI',
88 { apiKey: apiCredential.openAi.apiKey },
89 orgId,
90 );
91 }
92 if (apiCredential.zentropi) {
93 return this.setConfigByIntegrationId(
94 'ZENTROPI',
95 {
96 apiKey: apiCredential.zentropi.apiKey,
97 labelerVersions: [...(apiCredential.zentropi.labelerVersions ?? [])],
98 },
99 orgId,
100 );
101 }
102
103 throw new Error('No credentials provided');
104 }
105
106 async setConfigByIntegrationId(
107 integrationId: string,
108 credential: Record<string, unknown>,
109 orgId: string,
110 ): Promise<TIntegrationConfigWithMetadata> {
111 const registry = getIntegrationRegistry();
112 const manifest = registry.getManifest(integrationId);
113 if (manifest == null) {
114 throw new Error(`Unknown integration: ${integrationId}`);
115 }
116 const newCredential = await this.signalAuthService.setByIntegrationId(
117 integrationId,
118 orgId,
119 credential,
120 );
121 return mergeManifest(integrationId, newCredential, manifest);
122 }
123
124 async getConfig(
125 orgId: string,
126 integrationId: string,
127 ): Promise<TIntegrationConfigWithMetadata | undefined> {
128 const registry = getIntegrationRegistry();
129 const manifest = registry.getManifest(integrationId);
130 if (manifest == null) return undefined;
131 const credential = await this.signalAuthService.getByIntegrationId(
132 integrationId,
133 orgId,
134 );
135 if (credential == null) return undefined;
136 return mergeManifest(integrationId, credential, manifest);
137 }
138
139 async getConfigWithMetadata(
140 orgId: string,
141 integrationId: string,
142 ): Promise<TIntegrationConfigWithMetadata> {
143 const registry = getIntegrationRegistry();
144 const manifest = registry.getManifest(integrationId);
145 if (manifest == null) {
146 throw new Error(`Unknown integration: ${integrationId}`);
147 }
148 const credential = await this.signalAuthService.getByIntegrationId(
149 integrationId,
150 orgId,
151 );
152 const apiCredential =
153 credential ?? defaultCredentialForIntegrationId(integrationId);
154 return mergeManifest(integrationId, apiCredential, manifest);
155 }
156
157 getAvailableIntegrations() {
158 return getIntegrationRegistry().getAvailableIntegrations();
159 }
160
161 async getAllIntegrationConfigs(
162 orgId: string,
163 ): Promise<TIntegrationConfigWithMetadata[]> {
164 const ids = getIntegrationRegistry().getConfigurableIds();
165 return Promise.all(
166 ids.map(async (integrationId) =>
167 this.getConfigWithMetadata(orgId, integrationId),
168 ),
169 );
170 }
171}
172
173export default inject(['SignalAuthService'], IntegrationAPI);
174export type { IntegrationAPI };
175
176export type IntegrationErrorType =
177 | 'IntegrationConfigTooManyCredentialsError'
178 | 'IntegrationConfigUnsupportedIntegrationError'
179 | 'IntegrationNoInputCredentialsError'
180 | 'IntegrationEmptyInputCredentialsError';
181
182export const makeIntegrationConfigTooManyCredentialsError = (
183 data: ErrorInstanceData,
184) =>
185 new CoopError({
186 status: 400,
187 type: [ErrorType.InvalidUserInput],
188 title: 'This integration type expects a single api credential.',
189 name: 'IntegrationConfigTooManyCredentialsError',
190 ...data,
191 });
192
193export const makeIntegrationConfigUnsupportedIntegrationError = (
194 data: ErrorInstanceData,
195) =>
196 new CoopError({
197 status: 400,
198 type: [ErrorType.InvalidUserInput],
199 title: 'This integration type is not supported.',
200 name: 'IntegrationConfigUnsupportedIntegrationError',
201 ...data,
202 });
203
204export const makeIntegrationNoInputCredentialsError = (
205 data: ErrorInstanceData,
206) =>
207 new CoopError({
208 status: 400,
209 type: [ErrorType.InvalidUserInput],
210 title:
211 'This integration config creation expects at least one API credential.',
212 name: 'IntegrationNoInputCredentialsError',
213 ...data,
214 });
215
216export const makeIntegrationEmptyInputCredentialsError = (
217 data: ErrorInstanceData,
218) =>
219 new CoopError({
220 status: 400,
221 type: [ErrorType.InvalidUserInput],
222 title: 'This integration config creation expects no empty API credentials.',
223 name: 'IntegrationEmptyInputCredentialsError',
224 ...data,
225 });