···11+/**
22+ * Integration plugin types for COOP.
33+ *
44+ * These types define the contract that third-party integration packages
55+ * implement so adopters can install and configure them without adding
66+ * every integration to the main COOP repo.
77+ *
88+ * Integration packages export a CoopIntegrationPlugin; adopters register
99+ * them via an integrations config file (see CoopIntegrationsConfig).
1010+ */
1111+1212+/** Unique identifier for the integration (e.g. "GOOGLE_CONTENT_SAFETY_API"). */
1313+export type IntegrationId = string;
1414+1515+// ---------------------------------------------------------------------------
1616+// Model card (optional, per-integration metadata for display in the UI)
1717+// ---------------------------------------------------------------------------
1818+1919+/**
2020+ * A single key-value row in a model card (e.g. "Release Date" -> "January 2026").
2121+ * Values are plain strings; the UI can linkify URLs or format as needed.
2222+ */
2323+export type ModelCardField = Readonly<{
2424+ label: string;
2525+ value: string;
2626+}>;
2727+2828+/**
2929+ * A named group of fields within a section (e.g. "Basic Information" with
3030+ * Model Name, Version, Release Date). Rendered as a bold subheading + key-value list.
3131+ */
3232+export type ModelCardSubsection = Readonly<{
3333+ title: string;
3434+ fields: readonly ModelCardField[];
3535+}>;
3636+3737+/**
3838+ * One collapsible section of a model card (e.g. "Model Details", "Training Data").
3939+ * Either subsections (with bold sub-headings) or top-level fields, or both.
4040+ */
4141+export type ModelCardSection = Readonly<{
4242+ /** Stable id for the section (e.g. "modelDetails", "trainingData"). */
4343+ id: string;
4444+ /** Display title (e.g. "Model Details"). */
4545+ title: string;
4646+ /** Optional grouped key-value blocks with their own titles. */
4747+ subsections?: readonly ModelCardSubsection[];
4848+ /** Optional flat key-value list when there are no subsections. */
4949+ fields?: readonly ModelCardField[];
5050+}>;
5151+5252+/**
5353+ * Model card: structured, JSON-backed metadata for an integration, so the UI
5454+ * can display it in a consistent but integration-specific way.
5555+ *
5656+ * Required: modelName and version (always shown). All sections are optional;
5757+ * the UI renders only those present. Sections can have subsections (e.g.
5858+ * "Basic Information", "Model Architecture") or flat fields.
5959+ */
6060+export type ModelCard = Readonly<{
6161+ /** Required. Display name of the model (e.g. "GPT-4"). */
6262+ modelName: string;
6363+ /** Required. Version string (e.g. "1.0.0" or "v0.0"). */
6464+ version: string;
6565+ /** Optional. Release date or similar (e.g. "January 2026"). */
6666+ releaseDate?: string;
6767+ /** Optional. Ordered list of sections; each can be collapsed/expanded in the UI. */
6868+ sections?: readonly ModelCardSection[];
6969+}>;
7070+7171+/**
7272+ * Section ids that every integration's model card must include.
7373+ * Use assertModelCardHasRequiredSections() to validate at runtime.
7474+ */
7575+export const REQUIRED_MODEL_CARD_SECTION_IDS = [
7676+ 'modelDetails',
7777+ 'technicalIntegration',
7878+] as const;
7979+8080+/**
8181+ * Asserts that a model card has at least the required sections (basic information
8282+ * and technical integration). Call when registering integration manifests.
8383+ * @throws Error if any required section id is missing
8484+ */
8585+export function assertModelCardHasRequiredSections(card: ModelCard): void {
8686+ const sectionIds = new Set((card.sections ?? []).map((s) => s.id));
8787+ for (const requiredId of REQUIRED_MODEL_CARD_SECTION_IDS) {
8888+ if (!sectionIds.has(requiredId)) {
8989+ throw new Error(
9090+ `Model card must include a section with id "${requiredId}" (e.g. Basic Information / Model Details and Technical Integration).`,
9191+ );
9292+ }
9393+ }
9494+}
9595+9696+/**
9797+ * Describes a single credential field for integrations that require
9898+ * API keys or other secrets. Used to generate or validate credential forms.
9999+ */
100100+export type IntegrationCredentialField = Readonly<{
101101+ /** Form field key (e.g. "apiKey", "labelerVersions"). */
102102+ key: string;
103103+ /** Human-readable label for the field. */
104104+ label: string;
105105+ /** Whether the field is required. */
106106+ required: boolean;
107107+ /** Input type for the UI. */
108108+ inputType: 'text' | 'password' | 'json' | 'array';
109109+ /** Optional placeholder or hint. */
110110+ placeholder?: string;
111111+ /** Optional description for the field. */
112112+ description?: string;
113113+}>;
114114+115115+/**
116116+ * Metadata and capability description for an integration.
117117+ * This is the stable, structured information shown to users (name, docs, logos, etc.).
118118+ */
119119+export 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"). */
123123+ name: string;
124124+ /** Semantic version of the integration plugin (e.g. "1.0.0"). */
125125+ version: string;
126126+ /** Short description for listings and tooltips. */
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;
136136+ /**
137137+ * Schema for credential fields when requiresCredentials is true.
138138+ * Enables UI generation and validation without hardcoding per-integration forms.
139139+ */
140140+ credentialFields?: readonly IntegrationCredentialField[];
141141+ /**
142142+ * Optional list of signal type ids this integration provides (e.g. "ZENTROPI_LABELER").
143143+ * Used by the platform to associate signals with this integration for display and gating.
144144+ */
145145+ signalTypeIds?: readonly string[];
146146+ /**
147147+ * Model card: structured metadata (model name, version, sections) for the UI.
148148+ * When present, the integration detail page renders it. Built-in integrations
149149+ * should always provide a model card with at least sections "modelDetails" and
150150+ * "technicalIntegration"; use assertModelCardHasRequiredSections() when
151151+ * registering.
152152+ */
153153+ modelCard?: ModelCard;
154154+}>;
155155+156156+/**
157157+ * Plugin contract that third-party integration packages must implement.
158158+ * Export this as the default export (or a named export) from the package.
159159+ *
160160+ * Example (in an integration package):
161161+ *
162162+ * const manifest: IntegrationManifest = { id: 'ACME_API', name: 'Acme API', ... };
163163+ * const plugin: CoopIntegrationPlugin = { manifest };
164164+ * export default plugin;
165165+ */
166166+export type CoopIntegrationPlugin = Readonly<{
167167+ manifest: IntegrationManifest;
168168+ /**
169169+ * Optional static config shape for this integration.
170170+ * If present, adopters can pass non-secret config in the integrations config file.
171171+ */
172172+ configSchema?: unknown;
173173+}>;
174174+175175+/**
176176+ * Single entry in the adopters' integrations config file.
177177+ * Enables or disables a plugin and optionally passes static config.
178178+ */
179179+export type CoopIntegrationConfigEntry = Readonly<{
180180+ /** NPM package name (e.g. "@acme/coop-integration-acme") or path to a local module. */
181181+ package: string;
182182+ /** Whether this integration is enabled. Default true if omitted. */
183183+ enabled?: boolean;
184184+ /** Optional static config passed to the integration (no secrets here; use org credentials in-app). */
185185+ config?: Readonly<Record<string, unknown>>;
186186+}>;
187187+188188+/**
189189+ * Root type for the integrations config file that adopters use to register
190190+ * plugin integrations. Can be JSON or a JS/TS module that exports this shape.
191191+ *
192192+ * Example integrations.config.json:
193193+ *
194194+ * {
195195+ * "integrations": [
196196+ * { "package": "@acme/coop-integration-acme", "enabled": true },
197197+ * { "package": "./local-integrations/foo", "config": { "endpoint": "https://..." } }
198198+ * ]
199199+ * }
200200+ */
201201+export type CoopIntegrationsConfig = Readonly<{
202202+ integrations: readonly CoopIntegrationConfigEntry[];
203203+}>;
204204+205205+/**
206206+ * Shape of the config stored in the database for each integration (per org).
207207+ * Stored in a generic table as JSON: one row per (org_id, integration_id) with
208208+ * config as a JSON-serializable object. Each integration defines its own required
209209+ * fields via IntegrationManifest.credentialFields; the app validates and
210210+ * serializes/deserializes to this type.
211211+ *
212212+ * Only JSON-serializable values (no functions, symbols, or BigInt) should be
213213+ * included so the payload can be stored in a JSONB or TEXT column.
214214+ */
215215+export type StoredIntegrationConfigPayload = Readonly<Record<string, unknown>>;
216216+217217+/**
218218+ * Type guard for CoopIntegrationPlugin.
219219+ */
220220+export function isCoopIntegrationPlugin(
221221+ value: unknown,
222222+): value is CoopIntegrationPlugin {
223223+ if (value == null || typeof value !== 'object') {
224224+ return false;
225225+ }
226226+ const o = value as Record<string, unknown>;
227227+ if (o.manifest == null || typeof o.manifest !== 'object') {
228228+ return false;
229229+ }
230230+ const m = o.manifest as Record<string, unknown>;
231231+ return (
232232+ typeof m.id === 'string' &&
233233+ typeof m.name === 'string' &&
234234+ typeof m.version === 'string' &&
235235+ typeof m.requiresCredentials === 'boolean'
236236+ );
237237+}