export interface IntegrationFormFieldBase { description?: string label: string name: string required: boolean } export type IntegrationFormField = | (IntegrationFormFieldBase & { default?: string input: "email" | "text" | "url" placeholder?: string }) | (IntegrationFormFieldBase & { input: "password" placeholder?: string }) | (IntegrationFormFieldBase & { default?: number input: "number" placeholder?: string }) | (IntegrationFormFieldBase & { default?: boolean input: "boolean" }) interface IntegrationConnectionDefinitionBase { description?: string id: string name: string } export type IntegrationConnectionDefinition = | (IntegrationConnectionDefinitionBase & { /** Exact provider scopes this redirect flow may request. */ allowedScopes?: readonly string[] type: "redirect" }) | (IntegrationConnectionDefinitionBase & { credentialUrl?: string fields: readonly IntegrationFormField[] type: "form" }) /** Context shown before a user starts a provider connection. */ export interface IntegrationConnectionNotice { action: { href: string label: string } description: string title: string } export interface IntegrationServiceDefinition { connectionMethods: readonly IntegrationConnectionDefinition[] connectionNotice?: IntegrationConnectionNotice description?: string id: string /** * Overrides the icon asset key inferred from `id`. * * Omit this unless the service intentionally reuses another icon asset. */ icon?: string name: string preferredConnectionMethodId?: string } export type IntegrationScopeRequirement = ScopeRequirementGroup | string /** A recursive group of integration authorization scope requirements. */ export interface ScopeRequirementGroup { requirements: readonly IntegrationScopeRequirement[] type: "and" | "or" } /** Combinators for declaring recursive integration scope requirements. */ export const integrationScope = { /** * Requires every scope or nested group. * * @param requirements - Required scopes and nested groups. */ and(...requirements: IntegrationScopeRequirement[]): ScopeRequirementGroup { return { requirements: [...new Set(requirements)], type: "and" } }, /** * Requires any scope or nested group. * * Alternatives must be ordered from most permissive to least permissive. * * @param requirements - Alternative scopes and nested groups. */ any(...requirements: IntegrationScopeRequirement[]): ScopeRequirementGroup { return { requirements: [...new Set(requirements)], type: "or" } }, } /** One connection method accepted by an integration account requirement. */ export interface IntegrationAccountConnectionOption { connectionMethodId?: string requiredScopes: readonly IntegrationScopeRequirement[] } /** One normalized integration account requirement. */ export interface IntegrationAccountRequirement< TServiceId extends string = string, > { connectionOptions: readonly IntegrationAccountConnectionOption[] serviceId: TServiceId } /** Account-selection behavior for one public trigger type. */ export interface TriggerAccountRequirement extends IntegrationAccountRequirement { /** Whether omission plans the project's default binding. Defaults to true. */ planDefaultBinding?: boolean } /** Runtime-neutral metadata for one public trigger type. */ export interface TriggerDefinition< TType extends string = string, TServiceId extends string = string, > { account?: TriggerAccountRequirement description?: string /** Adds labeled secondary details to this trigger's presentation. */ getDetails?(context: TriggerPresentationContext): TriggerPresentationDetail[] /** Adds an unlabeled primary value to this trigger's presentation. */ getPrimary?(context: TriggerPresentationContext): TriggerPresentationValue /** * Overrides the connected service icon. * * Omit this from account-backed triggers to inherit their service icon. */ icon?: string /** * Overrides the display name inferred from `type`. * * Omit this unless custom copy adds value beyond the inferred name. * `triggerCatalog` and `getTriggerDefinition` always expose a resolved name. */ name?: string type: TType } /** Runtime-neutral context used to describe one configured trigger. */ export interface TriggerPresentationContext { accountLabel?: string appOrigin: string automationId: string config: unknown hookSlot: number projectId: string scopePath?: readonly number[] } /** Presentation metadata for one nested automation scope. */ export interface TriggerScopePresentation { path: readonly number[] presentation: "collapsed" | "expanded" | "hidden" } /** One user-facing value in a configured trigger's presentation. */ export interface TriggerPresentationValue { copyable?: boolean value: string } /** One labeled secondary property of a configured trigger. */ export interface TriggerPresentationDetail extends TriggerPresentationValue { label: string } /** Client-safe presentation of one configured trigger. */ export interface TriggerPresentation { details: TriggerPresentationDetail[] name: string primary?: TriggerPresentationValue type: string } /** * Preserves the literal IDs and connection shapes in one shared definition. * * @param service - Complete runtime-neutral service definition. * @throws When the service or a form connection is empty or IDs repeat. */ export function defineIntegrationService< const TService extends IntegrationServiceDefinition, >(service: TService) { if (service.connectionMethods.length === 0) { throw new Error( `Integration service has no connection methods: ${service.id}`, ) } const emptyFormConnection = service.connectionMethods.find( (connection) => connection.type === "form" && connection.fields.length === 0, ) if (emptyFormConnection) { throw new Error( `Integration service ${service.id} has no fields for form connection: ${emptyFormConnection.id}`, ) } const duplicateConnectionMethodId = service.connectionMethods.find( (connection, index) => service.connectionMethods.findIndex(({ id }) => id === connection.id) !== index, )?.id if (duplicateConnectionMethodId) { throw new Error( `Integration service ${service.id} has duplicate connection method ID: ${duplicateConnectionMethodId}`, ) } if ( service.preferredConnectionMethodId && !service.connectionMethods.some( ({ id }) => id === service.preferredConnectionMethodId, ) ) { throw new Error( `Integration service ${service.id} has an unknown preferred connection method: ${service.preferredConnectionMethodId}`, ) } return service } /** * Validates and preserves one complete compile-time service catalog. * * @param services - Shared integration service definitions. * @throws When multiple services use the same ID. */ export function defineIntegrationCatalog< const TCatalog extends readonly IntegrationServiceDefinition[], >(services: TCatalog) { const duplicateServiceId = services.find( (service, index) => services.findIndex(({ id }) => id === service.id) !== index, )?.id if (duplicateServiceId) { throw new Error(`Duplicate integration service ID: ${duplicateServiceId}`) } return services }