import { z } from "zod/v4"; import type { AppPermissionV2 } from "./permissions"; /** * Event-driven and scheduled triggers (plan ยง11.3). * * Event subscriptions (`webhooks`) are delivered through the platform's * durable app job queue to wherever the app's logic runs: `onEvent` in * Coline-hosted logic, or the external backend's `/coline/app` handler. * Deliveries retry with backoff and dead-letter after their final attempt. * Schedules dispatch into the app's logic without the developer running a * scheduler. */ /** * Workspace events the platform emits, and the read permission an install * needs before it may receive each one. Payloads carry identifiers and * metadata, never content; handlers fetch content through capabilities, * where the usual access checks apply. */ export const PLATFORM_EVENTS = { "task.created": "tasks.read", "task.updated": "tasks.read", "task.deleted": "tasks.read", "message.created": "messages.read", "note.created": "files.read", "note.updated": "files.read", "calendar.event_created": "calendar.read", "calendar.event_updated": "calendar.read", "calendar.event_deleted": "calendar.read", "app.installed": null, "app.updated": null, } as const satisfies Record; export type PlatformEventType = keyof typeof PLATFORM_EVENTS; export const PLATFORM_EVENT_TYPES = Object.keys(PLATFORM_EVENTS) as PlatformEventType[]; /** * An event pattern: an exact dotted name (`task.created`) or a namespace * wildcard (`task.*`, `crm.*`). App-emitted events are namespaced under * the emitting app's key. */ const eventPatternSchema = z .string() .trim() .regex( /^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_]*)*(\.\*|(\.[a-z][a-z0-9_]*))$/, "Event subscriptions are dotted lowercase names (`task.created`) or namespace wildcards (`task.*`).", ) .max(160); export function eventPatternMatches(pattern: string, eventType: string): boolean { if (pattern.endsWith(".*")) { const prefix = pattern.slice(0, -1); return eventType.startsWith(prefix) && eventType.length > prefix.length; } return pattern === eventType; } /** The read permission a platform event requires, or null when none applies. */ export function platformEventPermission(eventType: string): AppPermissionV2 | null { return Object.hasOwn(PLATFORM_EVENTS, eventType) ? PLATFORM_EVENTS[eventType as PlatformEventType] : null; } export function isPlatformEventNamespace(eventType: string): boolean { const namespace = eventType.split(".")[0] ?? ""; return PLATFORM_EVENT_TYPES.some((type) => type.startsWith(`${namespace}.`)); } const cronFieldPattern = "[0-9*,/-]+"; const cronExpressionSchema = z .string() .trim() .regex( new RegExp(`^${cronFieldPattern}( ${cronFieldPattern}){4}$`), "Schedules use 5-field cron expressions.", ) .max(120); export const appScheduleSchema = z.strictObject({ id: z .string() .trim() .regex(/^[a-z][a-z0-9-]*$/, "Schedule ids are lowercase kebab-case.") .max(120), cron: cronExpressionSchema, /** IANA time zone the cron expression is evaluated in. Defaults to UTC. */ timeZone: z.string().trim().min(1).max(64).optional(), description: z.string().trim().max(500).optional(), }); export type AppSchedule = z.infer; export const appEventSubscriptionSchema = z.strictObject({ event: eventPatternSchema, description: z.string().trim().max(500).optional(), }); export type AppEventSubscription = z.infer; export const appTriggersV2Schema = z .strictObject({ webhooks: z .array(appEventSubscriptionSchema) .max(50) .default([]) .refine( (subscriptions) => new Set(subscriptions.map((subscription) => subscription.event)).size === subscriptions.length, "Event subscriptions must be unique.", ), schedules: z .array(appScheduleSchema) .max(20) .default([]) .refine( (schedules) => new Set(schedules.map((schedule) => schedule.id)).size === schedules.length, "Schedule ids must be unique.", ), }) .default({ webhooks: [], schedules: [] }); export type AppTriggersV2 = z.infer;