The Coline App SDK: apps, tools, file types and surfaces that run inside Coline workspaces, and public websites with Coline Sites. TypeScript, with an in-memory test workspace. MIT.
| /** |
| 5 | * Event-driven and scheduled triggers (plan §11.3). |
| 6 | * |
| 7 | * Event subscriptions (`webhooks`) are delivered through the platform's |
| 8 | * durable app job queue to wherever the app's logic runs: `onEvent` in |
| 9 | * Coline-hosted logic, or the external backend's `/coline/app` handler. |
| 10 | * Deliveries retry with backoff and dead-letter after their final attempt. |
| 11 | * Schedules dispatch into the app's logic without the developer running a |
| 12 | * scheduler. |
| 13 | */ |
| 14 |
| 15 | /** |
| 16 | * Workspace events the platform emits, and the read permission an install |
| 17 | * needs before it may receive each one. Payloads carry identifiers and |
| 18 | * metadata, never content; handlers fetch content through capabilities, |
| 19 | * where the usual access checks apply. |
| 20 | */ |
| 21 | export const PLATFORM_EVENTS = { |
| 22 | "task.created": "tasks.read", |
| 23 | "task.updated": "tasks.read", |
| 24 | "task.deleted": "tasks.read", |
| 25 | "message.created": "messages.read", |
| 26 | "note.created": "files.read", |
| 27 | "note.updated": "files.read", |
| 28 | "calendar.event_created": "calendar.read", |
| 29 | "calendar.event_updated": "calendar.read", |
| 30 | "calendar.event_deleted": "calendar.read", |
| 31 | "app.installed": null, |
| 32 | "app.updated": null, |
| 33 | } as const satisfies Record<string, AppPermissionV2 | null>; |
| 34 |
| 35 | export type PlatformEventType = keyof typeof PLATFORM_EVENTS; |
| 36 |
| 37 | export const PLATFORM_EVENT_TYPES = Object.keys(PLATFORM_EVENTS) as PlatformEventType[]; |
| 38 |
| 39 | /** |
| 40 | * An event pattern: an exact dotted name (`task.created`) or a namespace |
| 41 | * wildcard (`task.*`, `crm.*`). App-emitted events are namespaced under |
| 42 | * the emitting app's key. |
| 43 | */ |
| 44 | const eventPatternSchema = z |
| 45 | .string() |
| 46 | .trim() |
| 47 | .regex( |
| 48 | /^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_]*)*(\.\*|(\.[a-z][a-z0-9_]*))$/, |
| 49 | "Event subscriptions are dotted lowercase names (`task.created`) or namespace wildcards (`task.*`).", |
| 50 | ) |
| 51 | .max(160); |
| 52 |
| 53 | export function eventPatternMatches(pattern: string, eventType: string): boolean { |
| 54 | if (pattern.endsWith(".*")) { |
| 55 | const prefix = pattern.slice(0, -1); |
| 56 | return eventType.startsWith(prefix) && eventType.length > prefix.length; |
| 57 | } |
| 58 | return pattern === eventType; |
| 59 | } |
| 60 |
| 61 | /** The read permission a platform event requires, or null when none applies. */ |
| 62 | export function platformEventPermission(eventType: string): AppPermissionV2 | null { |
| 63 | return Object.hasOwn(PLATFORM_EVENTS, eventType) |
| 64 | ? PLATFORM_EVENTS[eventType as PlatformEventType] |
| 65 | : null; |
| 66 | } |
| 67 |
| 68 | export function isPlatformEventNamespace(eventType: string): boolean { |
| 69 | const namespace = eventType.split(".")[0] ?? ""; |
| 70 | return PLATFORM_EVENT_TYPES.some((type) => type.startsWith(`${namespace}.`)); |
| 71 | } |
| 72 |
| 73 | const cronFieldPattern = "[0-9*,/-]+"; |
| 74 | const cronExpressionSchema = z |
| 75 | .string() |
| 76 | .trim() |
| 77 | .regex( |
| 78 | new RegExp(`^${cronFieldPattern}( ${cronFieldPattern}){4}$`), |
| 79 | "Schedules use 5-field cron expressions.", |
| 80 | ) |
| 81 | .max(120); |
| 82 |
| 83 | export const appScheduleSchema = z.strictObject({ |
| 84 | id: z |
| 85 | .string() |
| 86 | .trim() |
| 87 | .regex(/^[a-z][a-z0-9-]*$/, "Schedule ids are lowercase kebab-case.") |
| 88 | .max(120), |
| 89 | cron: cronExpressionSchema, |
| 90 | /** IANA time zone the cron expression is evaluated in. Defaults to UTC. */ |
| 91 | timeZone: z.string().trim().min(1).max(64).optional(), |
| 92 | description: z.string().trim().max(500).optional(), |
| 93 | }); |
| 94 | export type AppSchedule = z.infer<typeof appScheduleSchema>; |
| 95 |
| 96 | export const appEventSubscriptionSchema = z.strictObject({ |
| 97 | event: eventPatternSchema, |
| 98 | description: z.string().trim().max(500).optional(), |
| 99 | }); |
| 100 | export type AppEventSubscription = z.infer<typeof appEventSubscriptionSchema>; |
| 101 |
| 102 | export const appTriggersV2Schema = z |
| 103 | .strictObject({ |
| 104 | webhooks: z |
| 105 | .array(appEventSubscriptionSchema) |
| 106 | .max(50) |
| 107 | .default([]) |
| 108 | .refine( |
| 109 | (subscriptions) => |
| 110 | new Set(subscriptions.map((subscription) => subscription.event)).size === |
| 111 | subscriptions.length, |
| 112 | "Event subscriptions must be unique.", |
| 113 | ), |
| 114 | schedules: z |
| 115 | .array(appScheduleSchema) |
| 116 | .max(20) |
| 117 | .default([]) |
| 118 | .refine( |
| 119 | (schedules) => new Set(schedules.map((schedule) => schedule.id)).size === schedules.length, |
| 120 | "Schedule ids must be unique.", |
| 121 | ), |
| 122 | }) |
| 123 | .default({ webhooks: [], schedules: [] }); |
| 124 | export type AppTriggersV2 = z.infer<typeof appTriggersV2Schema>; |
| 125 |