Mirror of https://github.com/roostorg/coop github.com/roostorg/coop
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 317 lines 12 kB view raw
1/** 2 * Integration plugin types for COOP. 3 * 4 * These types define the contract that third-party integration packages 5 * implement so adopters can install and configure them without adding 6 * every integration to the main COOP repo. 7 * 8 * Integration packages export a CoopIntegrationPlugin; adopters register 9 * them via an integrations config file (see CoopIntegrationsConfig). 10 */ 11 12/** Unique identifier for the integration (e.g. "GOOGLE_CONTENT_SAFETY_API"). */ 13export type IntegrationId = string; 14 15// --------------------------------------------------------------------------- 16// Model card (optional, per-integration metadata for display in the UI) 17// --------------------------------------------------------------------------- 18 19/** 20 * A single key-value row in a model card (e.g. "Release Date" -> "January 2026"). 21 * Values are plain strings; the UI can linkify URLs or format as needed. 22 */ 23export type ModelCardField = Readonly<{ 24 label: string; 25 value: string; 26}>; 27 28/** 29 * A named group of fields within a section (e.g. "Basic Information" with 30 * Model Name, Version, Release Date). Rendered as a bold subheading + key-value list. 31 */ 32export type ModelCardSubsection = Readonly<{ 33 title: string; 34 fields: readonly ModelCardField[]; 35}>; 36 37/** 38 * One collapsible section of a model card (e.g. "Model Details", "Training Data"). 39 * Either subsections (with bold sub-headings) or top-level fields, or both. 40 */ 41export type ModelCardSection = Readonly<{ 42 /** Stable id for the section (e.g. "trainingData", "biasAndLimitations"). */ 43 id: string; 44 /** Display title (e.g. "Model Details"). */ 45 title: string; 46 /** Optional grouped key-value blocks with their own titles. */ 47 subsections?: readonly ModelCardSubsection[]; 48 /** Optional flat key-value list when there are no subsections. */ 49 fields?: readonly ModelCardField[]; 50}>; 51 52/** 53 * Model card: structured, JSON-backed metadata for an integration, so the UI 54 * can display it in a consistent but integration-specific way. 55 * 56 * Required: modelName and version (always shown). All sections are optional; 57 * the UI renders only those present. Sections can have subsections (e.g. 58 * "Basic Information", "Model Architecture") or flat fields. 59 */ 60export type ModelCard = Readonly<{ 61 /** Required. Display name of the model (e.g. "GPT-4"). */ 62 modelName: string; 63 /** Required. Version string (e.g. "1.0.0" or "v0.0"). */ 64 version: string; 65 /** Optional. Release date or similar (e.g. "January 2026"). */ 66 releaseDate?: string; 67 /** Optional. Ordered list of sections; each can be collapsed/expanded in the UI. */ 68 sections?: readonly ModelCardSection[]; 69}>; 70 71/** 72 * Section ids that every integration's model card must include. 73 * Use assertModelCardHasRequiredSections() to validate at runtime. 74 */ 75export const REQUIRED_MODEL_CARD_SECTION_IDS = [ 76 'trainingData', 77 'policyAndTaxonomy', 78 'annotationMethodology', 79 'performanceBenchmarks', 80 'biasAndLimitations', 81 'implementationGuidance', 82 'relevantLinks', 83] as const; 84 85/** 86 * Asserts that a model card has at least the required sections. 87 * Call when registering integration manifests. 88 * @throws Error if any required section id is missing 89 */ 90export function assertModelCardHasRequiredSections(card: ModelCard): void { 91 const sectionIds = new Set((card.sections ?? []).map((s) => s.id)); 92 const missing = REQUIRED_MODEL_CARD_SECTION_IDS.filter( 93 (id) => !sectionIds.has(id), 94 ); 95 if (missing.length > 0) { 96 throw new Error( 97 `Model card is missing required section(s): ${missing.map((id) => `"${id}"`).join(', ')}.`, 98 ); 99 } 100} 101 102/** 103 * Describes a single configuration field for integrations that require 104 * user-supplied config (e.g. API keys or other settings). Used to generate or validate config forms. 105 */ 106export type IntegrationConfigField = Readonly<{ 107 /** Form field key (e.g. "apiKey", "truePercentage"). */ 108 key: string; 109 /** Human-readable label for the field. */ 110 label: string; 111 /** Whether the field is required. */ 112 required: boolean; 113 /** Input type for the UI. */ 114 inputType: 'text' | 'password' | 'json' | 'array'; 115 /** Optional placeholder or hint. */ 116 placeholder?: string; 117 /** Optional description for the field. */ 118 description?: string; 119}>; 120 121/** 122 * Metadata and capability description for an integration. 123 * This is the stable, structured information shown to users (name, docs, logos, etc.). 124 */ 125export type IntegrationManifest = Readonly<{ 126 /** Unique integration id. Must be UPPER_SNAKE_CASE to align with GraphQL enums when used in COOP. */ 127 id: IntegrationId; 128 /** Human-readable display name shown in the UI (e.g. signal modal, integration cards). Exposed as Signal.integrationTitle. */ 129 name: string; 130 /** Semantic version of the integration plugin (e.g. "1.0.0"). */ 131 version: string; 132 /** Short description for listings and tooltips. */ 133 description?: string; 134 /** Link to documentation or product page. */ 135 docsUrl?: string; 136 /** Whether this integration requires the user to supply config (e.g. API key). */ 137 requiresConfig: boolean; 138 /** 139 * Schema for configuration fields when requiresConfig is true. 140 * Enables UI generation and validation without hardcoding per-integration forms. 141 */ 142 configurationFields?: readonly IntegrationConfigField[]; 143 /** 144 * Optional list of signal type ids this integration provides (e.g. "ZENTROPI_LABELER"). 145 * Used by the platform to associate signals with this integration for display and gating. 146 */ 147 signalTypeIds?: readonly string[]; 148 /** 149 * Model card: structured metadata (model name, version, sections) for the UI. 150 * When present, the integration detail page renders it. Integrations must 151 * include all sections listed in REQUIRED_MODEL_CARD_SECTION_IDS; use 152 * assertModelCardHasRequiredSections() when registering. 153 */ 154 modelCard?: ModelCard; 155 /** 156 * ------------------------------------------------------------ 157 * LOGO/IMAGE SECTION: 158 * ------------------------------------------------------------ 159 * The following logo/image sections are optional. If none provided will use a fallback Coop logo. 160 * 161 * Provide either logoUrl and logoWithBackgroundUrl or logoPath and logoWithBackgroundPath. 162 * 163 * If you provide logoPath and logoWithBackgroundPath, the server will serve the files at 164 * GET /api/v1/integration-logos/:integrationId and GET /api/v1/integration-logos/:integrationId/with-background 165 * and set logoUrl and logoWithBackgroundUrl accordingly. 166 * Usage: logoUrl/logoPath = plain logo (no background), used on the integrations page; 167 * logoWithBackgroundUrl/logoWithBackgroundPath = logo with background, used in signal modals. 168 * If you provide logoUrl and logoWithBackgroundUrl, the server will use those URLs directly. 169 * Prefered size: ~180x180px for logoUrl and ~120x120px for logoWithBackgroundUrl. 170 * Prefer a square or horizontal logo that scales well. 171 */ 172 logoUrl?: string; 173 logoWithBackgroundUrl?: string; 174 logoPath?: string; 175 logoWithBackgroundPath?: string; 176}>; 177 178// --------------------------------------------------------------------------- 179// Plugin signals (for integrations that power routing/enforcement rules) 180// --------------------------------------------------------------------------- 181 182/** Context passed to plugin.createSignals() so the plugin can build signal instances with credential access. */ 183export type PluginSignalContext = Readonly<{ 184 /** Integration id (e.g. "ACME_API") from the plugin manifest. */ 185 integrationId: string; 186 /** Get stored credential/config for an org. Resolves to the JSON stored for this integration. */ 187 getCredential: (orgId: string) => Promise<Record<string, unknown>>; 188}>; 189 190/** Minimal signal descriptor returned by a plugin. The platform adapts this to its internal SignalBase. */ 191export type PluginSignalDescriptor = Readonly<{ 192 /** Stable signal type id (e.g. "ACME_MODERATION_SIGNAL"). Must match one of manifest.signalTypeIds. */ 193 id: Readonly<{ type: string }>; 194 displayName: string; 195 description: string; 196 docsUrl: string | null; 197 recommendedThresholds: Readonly<{ 198 highPrecisionThreshold: string | number; 199 highRecallThreshold: string | number; 200 }> | null; 201 supportedLanguages: readonly string[] | 'ALL'; 202 pricingStructure: Readonly<{ type: 'FREE' | 'SUBSCRIPTION' }>; 203 eligibleInputs: readonly string[]; 204 outputType: Readonly<{ scalarType: string }>; 205 getCost: () => number; 206 /** Run the signal. Input shape is platform-defined; result must have outputType and score. */ 207 run: (input: unknown) => Promise<unknown>; 208 getDisabledInfo: (orgId: string) => Promise< 209 | { disabled: false; disabledMessage?: string } 210 | { disabled: true; disabledMessage: string } 211 >; 212 needsMatchingValues: boolean; 213 eligibleSubcategories: ReadonlyArray<{ 214 id: string; 215 label: string; 216 description?: string; 217 childrenIds: readonly string[]; 218 }>; 219 needsActionPenalties: boolean; 220 /** Integration id (same as context.integrationId). */ 221 integration: string; 222 allowedInAutomatedRules: boolean; 223}>; 224 225/** 226 * Plugin contract that third-party integration packages must implement. 227 * Export this as the default export (or a named export) from the package. 228 * 229 * Example (in an integration package): 230 * 231 * const manifest: IntegrationManifest = { id: 'ACME_API', name: 'Acme API', ... }; 232 * const plugin: CoopIntegrationPlugin = { manifest }; 233 * export default plugin; 234 * 235 * To power routing/enforcement rules, also implement createSignals(context) and 236 * return one descriptor per manifest.signalTypeIds entry. 237 */ 238export type CoopIntegrationPlugin = Readonly<{ 239 manifest: IntegrationManifest; 240 /** 241 * Optional static config shape for this integration. 242 * If present, adopters can pass non-secret config in the integrations config file. 243 */ 244 configSchema?: unknown; 245 /** 246 * Optional. If this integration provides signals for use in rules, implement this. 247 * Return one descriptor per signal type id listed in manifest.signalTypeIds. 248 * The platform will register these so they appear in the rule builder and can be used in conditions. 249 */ 250 createSignals?: ( 251 context: PluginSignalContext, 252 ) => ReadonlyArray<Readonly<{ signalTypeId: string; signal: PluginSignalDescriptor }>>; 253}>; 254 255/** 256 * Single entry in the adopters' integrations config file. 257 * Enables or disables a plugin and optionally passes static config. 258 */ 259export type CoopIntegrationConfigEntry = Readonly<{ 260 /** NPM package name (e.g. "@acme/coop-integration-acme") or path to a local module. */ 261 package: string; 262 /** Whether this integration is enabled. Default true if omitted. */ 263 enabled?: boolean; 264 /** Optional static config passed to the integration (no secrets here; use org credentials in-app). */ 265 config?: Readonly<Record<string, unknown>>; 266}>; 267 268/** 269 * Root type for the integrations config file that adopters use to register 270 * plugin integrations. Can be JSON or a JS/TS module that exports this shape. 271 * 272 * Example integrations.config.json: 273 * 274 * { 275 * "integrations": [ 276 * { "package": "@acme/coop-integration-acme", "enabled": true }, 277 * { "package": "./local-integrations/foo", "config": { "endpoint": "https://..." } } 278 * ] 279 * } 280 */ 281export type CoopIntegrationsConfig = Readonly<{ 282 integrations: readonly CoopIntegrationConfigEntry[]; 283}>; 284 285/** 286 * Shape of the config stored in the database for each integration (per org). 287 * Stored in a generic table as JSON: one row per (org_id, integration_id) with 288 * config as a JSON-serializable object. Each integration defines its own required 289 * fields via IntegrationManifest.configurationFields; the app validates and 290 * serializes/deserializes to this type. 291 * 292 * Only JSON-serializable values (no functions, symbols, or BigInt) should be 293 * included so the payload can be stored in a JSONB or TEXT column. 294 */ 295export type StoredIntegrationConfigPayload = Readonly<Record<string, unknown>>; 296 297/** 298 * Type guard for CoopIntegrationPlugin. 299 */ 300export function isCoopIntegrationPlugin( 301 value: unknown, 302): value is CoopIntegrationPlugin { 303 if (value == null || typeof value !== 'object') { 304 return false; 305 } 306 const o = value as Record<string, unknown>; 307 if (o.manifest == null || typeof o.manifest !== 'object') { 308 return false; 309 } 310 const m = o.manifest as Record<string, unknown>; 311 return ( 312 typeof m.id === 'string' && 313 typeof m.name === 'string' && 314 typeof m.version === 'string' && 315 typeof m.requiresConfig === 'boolean' 316 ); 317}