/** * Integration plugin types for COOP. * * These types define the contract that third-party integration packages * implement so adopters can install and configure them without adding * every integration to the main COOP repo. * * Integration packages export a CoopIntegrationPlugin; adopters register * them via an integrations config file (see CoopIntegrationsConfig). */ /** Unique identifier for the integration (e.g. "GOOGLE_CONTENT_SAFETY_API"). */ export type IntegrationId = string; // --------------------------------------------------------------------------- // Model card (optional, per-integration metadata for display in the UI) // --------------------------------------------------------------------------- /** * A single key-value row in a model card (e.g. "Release Date" -> "January 2026"). * Values are plain strings; the UI can linkify URLs or format as needed. */ export type ModelCardField = Readonly<{ label: string; value: string; }>; /** * A named group of fields within a section (e.g. "Basic Information" with * Model Name, Version, Release Date). Rendered as a bold subheading + key-value list. */ export type ModelCardSubsection = Readonly<{ title: string; fields: readonly ModelCardField[]; }>; /** * One collapsible section of a model card (e.g. "Model Details", "Training Data"). * Either subsections (with bold sub-headings) or top-level fields, or both. */ export type ModelCardSection = Readonly<{ /** Stable id for the section (e.g. "trainingData", "biasAndLimitations"). */ id: string; /** Display title (e.g. "Model Details"). */ title: string; /** Optional grouped key-value blocks with their own titles. */ subsections?: readonly ModelCardSubsection[]; /** Optional flat key-value list when there are no subsections. */ fields?: readonly ModelCardField[]; }>; /** * Model card: structured, JSON-backed metadata for an integration, so the UI * can display it in a consistent but integration-specific way. * * Required: modelName and version (always shown). All sections are optional; * the UI renders only those present. Sections can have subsections (e.g. * "Basic Information", "Model Architecture") or flat fields. */ export type ModelCard = Readonly<{ /** Required. Display name of the model (e.g. "GPT-4"). */ modelName: string; /** Required. Version string (e.g. "1.0.0" or "v0.0"). */ version: string; /** Optional. Release date or similar (e.g. "January 2026"). */ releaseDate?: string; /** Optional. Ordered list of sections; each can be collapsed/expanded in the UI. */ sections?: readonly ModelCardSection[]; }>; /** * Section ids that every integration's model card must include. * Use assertModelCardHasRequiredSections() to validate at runtime. */ export const REQUIRED_MODEL_CARD_SECTION_IDS = [ 'trainingData', 'policyAndTaxonomy', 'annotationMethodology', 'performanceBenchmarks', 'biasAndLimitations', 'implementationGuidance', 'relevantLinks', ] as const; /** * Asserts that a model card has at least the required sections. * Call when registering integration manifests. * @throws Error if any required section id is missing */ export function assertModelCardHasRequiredSections(card: ModelCard): void { const sectionIds = new Set((card.sections ?? []).map((s) => s.id)); const missing = REQUIRED_MODEL_CARD_SECTION_IDS.filter( (id) => !sectionIds.has(id), ); if (missing.length > 0) { throw new Error( `Model card is missing required section(s): ${missing.map((id) => `"${id}"`).join(', ')}.`, ); } } /** * Describes a single configuration field for integrations that require * user-supplied config (e.g. API keys or other settings). Used to generate or validate config forms. */ export type IntegrationConfigField = Readonly<{ /** Form field key (e.g. "apiKey", "truePercentage"). */ key: string; /** Human-readable label for the field. */ label: string; /** Whether the field is required. */ required: boolean; /** Input type for the UI. */ inputType: 'text' | 'password' | 'json' | 'array'; /** Optional placeholder or hint. */ placeholder?: string; /** Optional description for the field. */ description?: string; }>; /** * Metadata and capability description for an integration. * This is the stable, structured information shown to users (name, docs, logos, etc.). */ export type IntegrationManifest = Readonly<{ /** Unique integration id. Must be UPPER_SNAKE_CASE to align with GraphQL enums when used in COOP. */ id: IntegrationId; /** Human-readable display name shown in the UI (e.g. signal modal, integration cards). Exposed as Signal.integrationTitle. */ name: string; /** Semantic version of the integration plugin (e.g. "1.0.0"). */ version: string; /** Short description for listings and tooltips. */ description?: string; /** Link to documentation or product page. */ docsUrl?: string; /** Whether this integration requires the user to supply config (e.g. API key). */ requiresConfig: boolean; /** * Schema for configuration fields when requiresConfig is true. * Enables UI generation and validation without hardcoding per-integration forms. */ configurationFields?: readonly IntegrationConfigField[]; /** * Optional list of signal type ids this integration provides (e.g. "ZENTROPI_LABELER"). * Used by the platform to associate signals with this integration for display and gating. */ signalTypeIds?: readonly string[]; /** * Model card: structured metadata (model name, version, sections) for the UI. * When present, the integration detail page renders it. Integrations must * include all sections listed in REQUIRED_MODEL_CARD_SECTION_IDS; use * assertModelCardHasRequiredSections() when registering. */ modelCard?: ModelCard; /** * ------------------------------------------------------------ * LOGO/IMAGE SECTION: * ------------------------------------------------------------ * The following logo/image sections are optional. If none provided will use a fallback Coop logo. * * Provide either logoUrl and logoWithBackgroundUrl or logoPath and logoWithBackgroundPath. * * If you provide logoPath and logoWithBackgroundPath, the server will serve the files at * GET /api/v1/integration-logos/:integrationId and GET /api/v1/integration-logos/:integrationId/with-background * and set logoUrl and logoWithBackgroundUrl accordingly. * Usage: logoUrl/logoPath = plain logo (no background), used on the integrations page; * logoWithBackgroundUrl/logoWithBackgroundPath = logo with background, used in signal modals. * If you provide logoUrl and logoWithBackgroundUrl, the server will use those URLs directly. * Prefered size: ~180x180px for logoUrl and ~120x120px for logoWithBackgroundUrl. * Prefer a square or horizontal logo that scales well. */ logoUrl?: string; logoWithBackgroundUrl?: string; logoPath?: string; logoWithBackgroundPath?: string; }>; // --------------------------------------------------------------------------- // Plugin signals (for integrations that power routing/enforcement rules) // --------------------------------------------------------------------------- /** Context passed to plugin.createSignals() so the plugin can build signal instances with credential access. */ export type PluginSignalContext = Readonly<{ /** Integration id (e.g. "ACME_API") from the plugin manifest. */ integrationId: string; /** Get stored credential/config for an org. Resolves to the JSON stored for this integration. */ getCredential: (orgId: string) => Promise>; }>; /** Minimal signal descriptor returned by a plugin. The platform adapts this to its internal SignalBase. */ export type PluginSignalDescriptor = Readonly<{ /** Stable signal type id (e.g. "ACME_MODERATION_SIGNAL"). Must match one of manifest.signalTypeIds. */ id: Readonly<{ type: string }>; displayName: string; description: string; docsUrl: string | null; recommendedThresholds: Readonly<{ highPrecisionThreshold: string | number; highRecallThreshold: string | number; }> | null; supportedLanguages: readonly string[] | 'ALL'; pricingStructure: Readonly<{ type: 'FREE' | 'SUBSCRIPTION' }>; eligibleInputs: readonly string[]; outputType: Readonly<{ scalarType: string }>; getCost: () => number; /** Run the signal. Input shape is platform-defined; result must have outputType and score. */ run: (input: unknown) => Promise; getDisabledInfo: (orgId: string) => Promise< | { disabled: false; disabledMessage?: string } | { disabled: true; disabledMessage: string } >; needsMatchingValues: boolean; eligibleSubcategories: ReadonlyArray<{ id: string; label: string; description?: string; childrenIds: readonly string[]; }>; needsActionPenalties: boolean; /** Integration id (same as context.integrationId). */ integration: string; allowedInAutomatedRules: boolean; }>; /** * Plugin contract that third-party integration packages must implement. * Export this as the default export (or a named export) from the package. * * Example (in an integration package): * * const manifest: IntegrationManifest = { id: 'ACME_API', name: 'Acme API', ... }; * const plugin: CoopIntegrationPlugin = { manifest }; * export default plugin; * * To power routing/enforcement rules, also implement createSignals(context) and * return one descriptor per manifest.signalTypeIds entry. */ export type CoopIntegrationPlugin = Readonly<{ manifest: IntegrationManifest; /** * Optional static config shape for this integration. * If present, adopters can pass non-secret config in the integrations config file. */ configSchema?: unknown; /** * Optional. If this integration provides signals for use in rules, implement this. * Return one descriptor per signal type id listed in manifest.signalTypeIds. * The platform will register these so they appear in the rule builder and can be used in conditions. */ createSignals?: ( context: PluginSignalContext, ) => ReadonlyArray>; }>; /** * Single entry in the adopters' integrations config file. * Enables or disables a plugin and optionally passes static config. */ export type CoopIntegrationConfigEntry = Readonly<{ /** NPM package name (e.g. "@acme/coop-integration-acme") or path to a local module. */ package: string; /** Whether this integration is enabled. Default true if omitted. */ enabled?: boolean; /** Optional static config passed to the integration (no secrets here; use org credentials in-app). */ config?: Readonly>; }>; /** * Root type for the integrations config file that adopters use to register * plugin integrations. Can be JSON or a JS/TS module that exports this shape. * * Example integrations.config.json: * * { * "integrations": [ * { "package": "@acme/coop-integration-acme", "enabled": true }, * { "package": "./local-integrations/foo", "config": { "endpoint": "https://..." } } * ] * } */ export type CoopIntegrationsConfig = Readonly<{ integrations: readonly CoopIntegrationConfigEntry[]; }>; /** * Shape of the config stored in the database for each integration (per org). * Stored in a generic table as JSON: one row per (org_id, integration_id) with * config as a JSON-serializable object. Each integration defines its own required * fields via IntegrationManifest.configurationFields; the app validates and * serializes/deserializes to this type. * * Only JSON-serializable values (no functions, symbols, or BigInt) should be * included so the payload can be stored in a JSONB or TEXT column. */ export type StoredIntegrationConfigPayload = Readonly>; /** * Type guard for CoopIntegrationPlugin. */ export function isCoopIntegrationPlugin( value: unknown, ): value is CoopIntegrationPlugin { if (value == null || typeof value !== 'object') { return false; } const o = value as Record; if (o.manifest == null || typeof o.manifest !== 'object') { return false; } const m = o.manifest as Record; return ( typeof m.id === 'string' && typeof m.name === 'string' && typeof m.version === 'string' && typeof m.requiresConfig === 'boolean' ); }