···9494}
95959696/**
9797- * Describes a single credential field for integrations that require
9898- * API keys or other secrets. Used to generate or validate credential forms.
9797+ * Describes a single configuration field for integrations that require
9898+ * user-supplied config (e.g. API keys or other settings). Used to generate or validate config forms.
9999 */
100100-export type IntegrationCredentialField = Readonly<{
101101- /** Form field key (e.g. "apiKey", "labelerVersions"). */
100100+export type IntegrationConfigField = Readonly<{
101101+ /** Form field key (e.g. "apiKey", "truePercentage"). */
102102 key: string;
103103 /** Human-readable label for the field. */
104104 label: string;
···119119export type IntegrationManifest = Readonly<{
120120 /** Unique integration id. Must be UPPER_SNAKE_CASE to align with GraphQL enums when used in COOP. */
121121 id: IntegrationId;
122122- /** Human-readable display name (e.g. "Google Content Safety API"). */
122122+ /** Human-readable display name shown in the UI (e.g. signal modal, integration cards). Exposed as Signal.integrationTitle. */
123123 name: string;
124124 /** Semantic version of the integration plugin (e.g. "1.0.0"). */
125125 version: string;
···127127 description?: string;
128128 /** Link to documentation or product page. */
129129 docsUrl?: string;
130130- /** Optional URL to a logo image (or asset key if using a bundler). */
131131- logoUrl?: string;
132132- /** Optional URL to a logo variant (e.g. with background) for cards. */
133133- logoWithBackgroundUrl?: string;
134134- /** Whether this integration requires the user to supply credentials (e.g. API key). */
135135- requiresCredentials: boolean;
130130+ /** Whether this integration requires the user to supply config (e.g. API key). */
131131+ requiresConfig: boolean;
136132 /**
137137- * Schema for credential fields when requiresCredentials is true.
133133+ * Schema for configuration fields when requiresConfig is true.
138134 * Enables UI generation and validation without hardcoding per-integration forms.
139135 */
140140- credentialFields?: readonly IntegrationCredentialField[];
136136+ configurationFields?: readonly IntegrationConfigField[];
141137 /**
142138 * Optional list of signal type ids this integration provides (e.g. "ZENTROPI_LABELER").
143139 * Used by the platform to associate signals with this integration for display and gating.
···151147 * registering.
152148 */
153149 modelCard?: ModelCard;
150150+ /**
151151+ * ------------------------------------------------------------
152152+ * LOGO/IMAGE SECTION:
153153+ * ------------------------------------------------------------
154154+ * The following logo/image sections are optional. If none provided will use a fallback Coop logo.
155155+ *
156156+ * Provide either logoUrl and logoWithBackgroundUrl or logoPath and logoWithBackgroundPath.
157157+ *
158158+ * If you provide logoPath and logoWithBackgroundPath, the server will serve the files at
159159+ * GET /api/v1/integration-logos/:integrationId and GET /api/v1/integration-logos/:integrationId/with-background
160160+ * and set logoUrl and logoWithBackgroundUrl accordingly.
161161+ * If you provide logoUrl and logoWithBackgroundUrl, the server will use those URLs directly.
162162+ * Prefered size: ~180x180px for logoUrl and ~120x120px for logoWithBackgroundUrl.
163163+ * Prefer a square or horizontal logo that scales well.
164164+ */
165165+ logoUrl?: string;
166166+ logoWithBackgroundUrl?: string;
167167+ logoPath?: string;
168168+ logoWithBackgroundPath?: string;
169169+}>;
170170+171171+// ---------------------------------------------------------------------------
172172+// Plugin signals (for integrations that power routing/enforcement rules)
173173+// ---------------------------------------------------------------------------
174174+175175+/** Context passed to plugin.createSignals() so the plugin can build signal instances with credential access. */
176176+export type PluginSignalContext = Readonly<{
177177+ /** Integration id (e.g. "ACME_API") from the plugin manifest. */
178178+ integrationId: string;
179179+ /** Get stored credential/config for an org. Resolves to the JSON stored for this integration. */
180180+ getCredential: (orgId: string) => Promise<Record<string, unknown>>;
181181+}>;
182182+183183+/** Minimal signal descriptor returned by a plugin. The platform adapts this to its internal SignalBase. */
184184+export type PluginSignalDescriptor = Readonly<{
185185+ /** Stable signal type id (e.g. "ACME_MODERATION_SIGNAL"). Must match one of manifest.signalTypeIds. */
186186+ id: Readonly<{ type: string }>;
187187+ displayName: string;
188188+ description: string;
189189+ docsUrl: string | null;
190190+ recommendedThresholds: Readonly<{
191191+ highPrecisionThreshold: string | number;
192192+ highRecallThreshold: string | number;
193193+ }> | null;
194194+ supportedLanguages: readonly string[] | 'ALL';
195195+ pricingStructure: Readonly<{ type: 'FREE' | 'SUBSCRIPTION' }>;
196196+ eligibleInputs: readonly string[];
197197+ outputType: Readonly<{ scalarType: string }>;
198198+ getCost: () => number;
199199+ /** Run the signal. Input shape is platform-defined; result must have outputType and score. */
200200+ run: (input: unknown) => Promise<unknown>;
201201+ getDisabledInfo: (orgId: string) => Promise<
202202+ | { disabled: false; disabledMessage?: string }
203203+ | { disabled: true; disabledMessage: string }
204204+ >;
205205+ needsMatchingValues: boolean;
206206+ eligibleSubcategories: ReadonlyArray<{
207207+ id: string;
208208+ label: string;
209209+ description?: string;
210210+ childrenIds: readonly string[];
211211+ }>;
212212+ needsActionPenalties: boolean;
213213+ /** Integration id (same as context.integrationId). */
214214+ integration: string;
215215+ allowedInAutomatedRules: boolean;
154216}>;
155217156218/**
···162224 * const manifest: IntegrationManifest = { id: 'ACME_API', name: 'Acme API', ... };
163225 * const plugin: CoopIntegrationPlugin = { manifest };
164226 * export default plugin;
227227+ *
228228+ * To power routing/enforcement rules, also implement createSignals(context) and
229229+ * return one descriptor per manifest.signalTypeIds entry.
165230 */
166231export type CoopIntegrationPlugin = Readonly<{
167232 manifest: IntegrationManifest;
···170235 * If present, adopters can pass non-secret config in the integrations config file.
171236 */
172237 configSchema?: unknown;
238238+ /**
239239+ * Optional. If this integration provides signals for use in rules, implement this.
240240+ * Return one descriptor per signal type id listed in manifest.signalTypeIds.
241241+ * The platform will register these so they appear in the rule builder and can be used in conditions.
242242+ */
243243+ createSignals?: (
244244+ context: PluginSignalContext,
245245+ ) => ReadonlyArray<Readonly<{ signalTypeId: string; signal: PluginSignalDescriptor }>>;
173246}>;
174247175248/**
···206279 * Shape of the config stored in the database for each integration (per org).
207280 * Stored in a generic table as JSON: one row per (org_id, integration_id) with
208281 * config as a JSON-serializable object. Each integration defines its own required
209209- * fields via IntegrationManifest.credentialFields; the app validates and
282282+ * fields via IntegrationManifest.configurationFields; the app validates and
210283 * serializes/deserializes to this type.
211284 *
212285 * Only JSON-serializable values (no functions, symbols, or BigInt) should be
···232305 typeof m.id === 'string' &&
233306 typeof m.name === 'string' &&
234307 typeof m.version === 'string' &&
235235- typeof m.requiresCredentials === 'boolean'
308308+ typeof m.requiresConfig === 'boolean'
236309 );
237310}