import { Injectable } from "@nestjs/common"; import { z } from "zod"; import { router, protectedProcedure, workspaceProcedure, requireFeature, } from "../trpc/trpc.init"; import { SubscriptionFeature } from "../billing/subscription-features"; import { IntegrationsService, type IntegrationProviderName } from "./integrations.service"; import { IntegrationHealthService } from "./integration-health.service"; import { calComBookingSchema, type CalComBookingInput, documensoSendDocumentSchema, type DocumensoSendDocumentInput, documensoDocumentStatusSchema, type DocumensoDocumentStatusInput, } from "@micprotocol/shared-types"; const ALL_PROVIDERS = [ "google_calendar", "google_mail", "outlook", "riverside", "zoom", "cloudflare_r2", // Self-hosted open-source integration providers "calcom", "chatwoot", "documenso", "formbricks", "listmonk", "pretix", ] as const; const SELF_HOSTED_PROVIDERS = [ "calcom", "chatwoot", "documenso", "formbricks", "listmonk", "pretix", ] as const; const workspaceIdInput = z.object({ workspaceId: z.string().uuid() }); const providerInput = z.object({ workspaceId: z.string().uuid(), provider: z.enum(ALL_PROVIDERS), }); const selfHostedCredentialsSchema = z.object({ workspaceId: z.string().uuid(), provider: z.enum(["calcom", "chatwoot", "documenso", "formbricks", "listmonk", "pretix"]), apiKey: z.string().min(1, "API key is required"), baseUrl: z.string().url().optional(), webhookSecret: z.string().optional(), }); const r2CredentialsSchema = z.object({ workspaceId: z.string().uuid(), accessKeyId: z.string().min(1, "Access Key ID is required"), secretAccessKey: z.string().min(1, "Secret Access Key is required"), accountId: z.string().optional(), }); const syncCalendarSchema = z.object({ workspaceId: z.string().uuid(), provider: z.enum(["google_calendar", "outlook"]), }); const calendarEventSchema = z.object({ workspaceId: z.string().uuid(), provider: z.enum(["google_calendar", "outlook"]), eventData: z.object({ title: z.string().min(1).max(500), description: z.string().max(5000).optional(), startTime: z.string().datetime(), endTime: z.string().datetime(), timezone: z.string().optional(), location: z.string().max(500).optional(), meetLink: z.string().url().optional(), attendees: z .array( z.object({ email: z.string().email(), name: z.string().optional(), }), ) .optional(), }), }); const getEventsSchema = z.object({ workspaceId: z.string().uuid(), provider: z.enum(["google_calendar", "outlook"]), timeMin: z.string().datetime().optional(), timeMax: z.string().datetime().optional(), }); const chatwootConversationSchema = z.object({ workspaceId: z.string().uuid(), conversationId: z.number(), }); const chatwootMessageSchema = z.object({ workspaceId: z.string().uuid(), conversationId: z.number(), content: z.string().min(1).max(5000), }); const chatwootCreateConversationSchema = z.object({ workspaceId: z.string().uuid(), inboxId: z.number(), contactName: z.string().min(1), contactEmail: z.string().email(), contactPhone: z.string().optional(), message: z.string().min(1).max(5000), customAttributes: z.record(z.string(), z.any()).optional(), }); const webhookSecretSchema = z.object({ workspaceId: z.string().uuid(), provider: z.enum(["calcom", "chatwoot", "documenso", "formbricks", "listmonk", "pretix"]), secret: z.string().min(1, "Webhook secret is required"), }); const pretixEventsSchema = z.object({ workspaceId: z.string().uuid(), organizer: z.string().min(1, "Organizer slug is required"), }); const pretixItemsSchema = z.object({ workspaceId: z.string().uuid(), organizer: z.string().min(1, "Organizer slug is required"), event: z.string().min(1, "Event slug is required"), }); const pretixOrdersSchema = z.object({ workspaceId: z.string().uuid(), organizer: z.string().min(1, "Organizer slug is required"), event: z.string().min(1, "Event slug is required"), }); const pretixOrderPositionsSchema = z.object({ workspaceId: z.string().uuid(), organizer: z.string().min(1, "Organizer slug is required"), event: z.string().min(1, "Event slug is required"), order: z.string().min(1, "Order code is required"), }); @Injectable() export class IntegrationsRouter { constructor( private readonly integrationsService: IntegrationsService, private readonly healthService: IntegrationHealthService, ) {} get router() { return router({ // ═══ Provider Metadata ════════════════════════ getAllProviders: protectedProcedure.query(async () => { return this.integrationsService.getAllProviders(); }), // ═══ Integration CRUD ═══════════════════════ list: workspaceProcedure .input(workspaceIdInput) .query(async ({ ctx, input }) => { return this.integrationsService.listIntegrations( input.workspaceId, ctx.userId!, ); }), get: workspaceProcedure .input(providerInput) .query(async ({ ctx, input }) => { return this.integrationsService.getIntegration( input.workspaceId, input.provider as IntegrationProviderName, ctx.userId!, ); }), disconnect: workspaceProcedure .input(providerInput) .mutation(async ({ ctx, input }) => { return this.integrationsService.disconnectIntegration( input.workspaceId, input.provider as IntegrationProviderName, ctx.userId!, ); }), // ═══ OAuth Flow ═════════════════════════════ getAuthUrl: workspaceProcedure .input(providerInput) .query(async ({ ctx, input }) => { return this.integrationsService.getAuthUrl( input.workspaceId, input.provider as IntegrationProviderName, ctx.userId!, ); }), handleCallback: protectedProcedure .input( z.object({ code: z.string(), state: z.string(), }), ) .mutation(async ({ input }) => { return this.integrationsService.handleOAuthCallback( input.code, input.state, ); }), // ═══ API Key Integrations ═════════════════════ // R2 API key — Studio plan feature updateR2Integration: requireFeature(SubscriptionFeature.API_WEBHOOKS) .input(r2CredentialsSchema) .mutation(async ({ ctx, input }) => { return this.integrationsService.updateApiKeyIntegration( input.workspaceId, "cloudflare_r2", ctx.userId!, { accessKeyId: input.accessKeyId, secretAccessKey: input.secretAccessKey, accountId: input.accountId, }, ); }), // Self-hosted integrations (Cal.com, Chatwoot, Documenso, Formbricks, Listmonk) updateSelfHostedIntegration: requireFeature(SubscriptionFeature.SELF_HOSTED_INTEGRATIONS) .input(selfHostedCredentialsSchema) .mutation(async ({ ctx, input }) => { return this.integrationsService.updateApiKeyIntegration( input.workspaceId, input.provider, ctx.userId!, { apiKey: input.apiKey, baseUrl: input.baseUrl, webhookSecret: input.webhookSecret, }, ); }), // Check health of a self-hosted integration by hitting its health endpoint checkHealth: workspaceProcedure .input(providerInput) .query(async ({ ctx, input }) => { const integration = await this.integrationsService.getIntegration( input.workspaceId, input.provider as IntegrationProviderName, ctx.userId!, ); if (!integration) { return { alive: false, error: "Not connected" }; } const selfHosted = (integration as any).metadata?.baseUrl; if (!selfHosted) { return { alive: false, error: "No endpoint configured" }; } try { const res = await fetch(`${selfHosted}/health`, { signal: AbortSignal.timeout(5000), }); return { alive: res.ok, status: res.status }; } catch (e: any) { return { alive: false, error: e.message }; } }), // ═══ Calendar Sync ═══════════════════════════ syncCalendar: workspaceProcedure .input(syncCalendarSchema) .mutation(async ({ ctx, input }) => { return this.integrationsService.syncCalendarEvents( input.workspaceId, ctx.userId!, input.provider, ); }), getSyncedEvents: workspaceProcedure .input(getEventsSchema) .query(async ({ ctx, input }) => { return this.integrationsService.getSyncedEvents( input.workspaceId, input.provider, ctx.userId!, input.timeMin, input.timeMax, ); }), createCalendarEvent: workspaceProcedure .input(calendarEventSchema) .mutation(async ({ ctx, input }) => { return this.integrationsService.createCalendarEvent( input.workspaceId, ctx.userId!, input.provider, input.eventData, ); }), // ═══ Cal.com Scheduling ══════════════════════════════════════ listCalComEventTypes: workspaceProcedure .input(workspaceIdInput) .query(async ({ ctx, input }) => { return this.integrationsService.listCalComEventTypes( input.workspaceId, ctx.userId!, ); }), createCalComBooking: workspaceProcedure .input(calComBookingSchema) .mutation(async ({ ctx, input }) => { return this.integrationsService.createCalComBooking( input.workspaceId, ctx.userId!, { eventTypeId: input.eventTypeId, start: input.start, attendee: input.attendee, meetingUrl: input.meetingUrl, }, ); }), // ═══ Documenso E-Signatures ══════════════════════════════════════════════════ sendDocumensoDocument: workspaceProcedure .input(documensoSendDocumentSchema) .mutation(async ({ ctx, input }) => { return this.integrationsService.sendDocument( input.workspaceId, { title: input.document.title, recipients: (input.document.recipients ?? []).map((r: { email: string; name?: string }) => ({ email: r.email, name: r.name ?? "", })), }, ); }), getDocumensoDocumentStatus: workspaceProcedure .input(documensoDocumentStatusSchema) .query(async ({ ctx, input }) => { return this.integrationsService.getDocumensoDocumentStatus( input.workspaceId, Number(input.documentId), ); }), // ═══ Listmonk Newsletters ══════════════════════════════════════════════════════ listListmonkLists: workspaceProcedure .input(workspaceIdInput) .query(async ({ ctx, input }) => { return this.integrationsService.listListmonkLists(input.workspaceId); }), createListmonkList: workspaceProcedure .input( z.object({ workspaceId: z.string().uuid(), name: z.string().min(1), type: z.enum(["public", "private", "optin"]).optional(), }), ) .mutation(async ({ ctx, input }) => { return this.integrationsService.createListmonkList(input.workspaceId, { name: input.name, type: input.type ?? "public", }); }), subscribeListmonkSubscriber: workspaceProcedure .input( z.object({ workspaceId: z.string().uuid(), listId: z.number(), email: z.string().email(), name: z.string().optional(), attrs: z.record(z.string(), z.any()).optional(), }), ) .mutation(async ({ ctx, input }) => { return this.integrationsService.subscribeListmonkSubscriber(input.workspaceId, { listId: input.listId, email: input.email, name: input.name, attrs: input.attrs, }); }), sendListmonkTransactional: workspaceProcedure .input( z.object({ workspaceId: z.string().uuid(), to: z.string().email(), subject: z.string().min(1), body: z.string().min(1), type: z.enum(["html", "plain"]).optional(), }), ) .mutation(async ({ ctx, input }) => { return this.integrationsService.sendListmonkEmail(input.workspaceId, { to: input.to, subject: input.subject, body: input.body, type: input.type, }); }), // ═══ Chatwoot Customer Support ═════════════════════════════════════ getChatwootConversations: workspaceProcedure .input(workspaceIdInput) .query(async ({ ctx, input }) => { return this.integrationsService.getChatwootConversations( input.workspaceId, ctx.userId!, ); }), getChatwootMessages: workspaceProcedure .input(chatwootConversationSchema) .query(async ({ ctx, input }) => { return this.integrationsService.getChatwootMessages( input.workspaceId, ctx.userId!, input.conversationId, ); }), sendChatwootMessage: workspaceProcedure .input(chatwootMessageSchema) .mutation(async ({ ctx, input }) => { return this.integrationsService.sendChatwootMessage( input.workspaceId, ctx.userId!, { conversationId: input.conversationId, content: input.content, }, ); }), createChatwootConversation: workspaceProcedure .input(chatwootCreateConversationSchema) .mutation(async ({ ctx, input }) => { return this.integrationsService.createChatwootConversation( input.workspaceId, ctx.userId!, { inboxId: input.inboxId, contactName: input.contactName, contactEmail: input.contactEmail, contactPhone: input.contactPhone, message: input.message, customAttributes: input.customAttributes, }, ); }), // ═══ Pretix Ticketing ═══════════════════════════════════════════════════════════ listPretixEvents: workspaceProcedure .input(pretixEventsSchema) .query(async ({ ctx, input }) => { return this.integrationsService.listPretixEvents( input.workspaceId, ctx.userId!, input.organizer, ); }), listPretixItems: workspaceProcedure .input(pretixItemsSchema) .query(async ({ ctx, input }) => { return this.integrationsService.listPretixItems( input.workspaceId, ctx.userId!, input.organizer, input.event, ); }), listPretixOrders: workspaceProcedure .input(pretixOrdersSchema) .query(async ({ ctx, input }) => { return this.integrationsService.listPretixOrders( input.workspaceId, ctx.userId!, input.organizer, input.event, ); }), listPretixOrderPositions: workspaceProcedure .input(pretixOrderPositionsSchema) .query(async ({ ctx, input }) => { return this.integrationsService.listPretixOrderPositions( input.workspaceId, ctx.userId!, input.organizer, input.event, input.order, ); }), // ═══ Monitoring ═══════════════════════ getHealth: protectedProcedure.query(async () => { return this.healthService.getHealthResults(); }), getWebhookDeliveries: protectedProcedure .input(z.object({ limit: z.number().int().min(1).max(500).optional() })) .query(async ({ input }) => { return this.healthService.getWebhookDeliveries(input.limit ?? 50); }), // ═══ Self-Hosted Integration Management ═══ listSelfHosted: workspaceProcedure .input(workspaceIdInput) .query(async ({ ctx, input }) => { return this.integrationsService.listSelfHostedIntegrations( input.workspaceId, ctx.userId!, ); }), updateWebhookSecret: workspaceProcedure .input(webhookSecretSchema) .mutation(async ({ ctx, input }) => { return this.integrationsService.updateWebhookSecret( input.workspaceId, input.provider, input.secret, ctx.userId!, ); }), // ═══ Provider Metrics ═══════════════════ getMetrics: protectedProcedure.query(async () => { return this.integrationsService.getProviderMetrics(); }), // ═══ Stats ═══════════════════════════════════ getStats: workspaceProcedure .input(workspaceIdInput) .query(async ({ ctx, input }) => { return this.integrationsService.getIntegrationStats( input.workspaceId, ctx.userId!, ); }), }); } }