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.
| 6 | MessageDeletedEvent, |
| 7 | ReactionAddedEvent, |
| 8 | NoteCreatedEvent, |
| 9 | NoteUpdatedEvent, |
| 10 | NoteDeletedEvent, |
| 11 | TaskCreatedEvent, |
| 12 | TaskUpdatedEvent, |
| 13 | TaskDeletedEvent, |
| 14 | CalendarEventCreatedEvent, |
| 15 | MemberJoinedEvent, |
| 16 | MemberLeftEvent, |
| 17 | } from "./webhooks"; |
| 18 |
| 19 | // --------------------------------------------------------------------------- |
| 20 | // Event type → event interface mapping |
| 21 | // --------------------------------------------------------------------------- |
| 22 |
| 23 | interface WebhookEventMap { |
| 24 | "message.created": MessageCreatedEvent; |
| 25 | "message.updated": MessageUpdatedEvent; |
| 26 | "message.deleted": MessageDeletedEvent; |
| 27 | "reaction.added": ReactionAddedEvent; |
| 28 | "note.created": NoteCreatedEvent; |
| 29 | "note.updated": NoteUpdatedEvent; |
| 30 | "note.deleted": NoteDeletedEvent; |
| 31 | "task.created": TaskCreatedEvent; |
| 32 | "task.updated": TaskUpdatedEvent; |
| 33 | "task.deleted": TaskDeletedEvent; |
| 34 | "calendar_event.created": CalendarEventCreatedEvent; |
| 35 | "member.joined": MemberJoinedEvent; |
| 36 | "member.left": MemberLeftEvent; |
| 37 | } |
| 38 |
| 39 | type KnownEventType = keyof WebhookEventMap; |
| 40 |
| 41 | type EventHandler<T extends WebhookEvent> = (event: T) => void | Promise<void>; |
| 42 |
| 43 | /** |
| 44 | * Typed webhook event router — register handlers for specific event types and |
| 45 | * let the router dispatch incoming webhook events to the right handler. |
| 46 | * |
| 47 | * @example |
| 48 | * ```ts |
| 49 | * import { WebhookRouter, webhooks } from "@colineapp/sdk"; |
| 50 | * |
| 51 | * const router = new WebhookRouter() |
| 52 | * .on("message.created", async (event) => { |
| 53 | * console.log("New message:", event.data.plaintext); |
| 54 | * }) |
| 55 | * .on("task.created", async (event) => { |
| 56 | * console.log("New task:", event.data.title); |
| 57 | * }) |
| 58 | * .onUnhandled((event) => { |
| 59 | * console.log("Unhandled event:", event.type); |
| 60 | * }); |
| 61 | * |
| 62 | * // In your server: |
| 63 | * const event = await webhooks.constructEvent(body, headers, secret); |
| 64 | * await router.handle(event); |
| 65 | * |
| 66 | * // Or use with createHandler: |
| 67 | * const handler = createHandler({ |
| 68 | * app, |
| 69 | * secret, |
| 70 | * onWebhook: (event) => router.handle(event), |
| 71 | * }); |
| 72 | * ``` |
| 73 | */ |
| 74 | export class WebhookRouter { |
| 75 | private readonly handlers = new Map<string, EventHandler<WebhookEvent>[]>(); |
| 76 | private unhandledHandler: EventHandler<WebhookEvent> | undefined; |
| 77 |
| 78 | /** |
| 79 | * Register a handler for a specific webhook event type. Type-safe: the |
| 80 | * handler receives the correctly narrowed event type. |
| 81 | * |
| 82 | * Multiple handlers can be registered for the same event type; they run |
| 83 | * sequentially in registration order. |
| 84 | */ |
| 85 | on<T extends KnownEventType>( |
| 86 | type: T, |
| 87 | handler: EventHandler<WebhookEventMap[T]>, |
| 88 | ): this; |
| 89 | on(type: string, handler: EventHandler<WebhookEvent>): this; |
| 90 | on(type: string, handler: EventHandler<WebhookEvent>): this { |
| 91 | const existing = this.handlers.get(type); |
| 92 | if (existing) { |
| 93 | existing.push(handler); |
| 94 | } else { |
| 95 | this.handlers.set(type, [handler]); |
| 96 | } |
| 97 | return this; |
| 98 | } |
| 99 |
| 100 | /** |
| 101 | * Register a catch-all handler for events that don't have a specific |
| 102 | * handler registered. |
| 103 | */ |
| 104 | onUnhandled(handler: EventHandler<WebhookEvent>): this { |
| 105 | this.unhandledHandler = handler; |
| 106 | return this; |
| 107 | } |
| 108 |
| 109 | /** |
| 110 | * Remove all handlers for a specific event type, or all handlers if no |
| 111 | * type is provided. |
| 112 | */ |
| 113 | off(type?: WebhookEventType | string): this { |
| 114 | if (type) { |
| 115 | this.handlers.delete(type); |
| 116 | } else { |
| 117 | this.handlers.clear(); |
| 118 | this.unhandledHandler = undefined; |
| 119 | } |
| 120 | return this; |
| 121 | } |
| 122 |
| 123 | /** |
| 124 | * Dispatch a webhook event to registered handlers. |
| 125 | */ |
| 126 | async handle(event: WebhookEvent): Promise<void> { |
| 127 | const eventHandlers = this.handlers.get(event.type); |
| 128 | if (eventHandlers && eventHandlers.length > 0) { |
| 129 | for (const handler of eventHandlers) { |
| 130 | await handler(event); |
| 131 | } |
| 132 | } else if (this.unhandledHandler) { |
| 133 | await this.unhandledHandler(event); |
| 134 | } |
| 135 | } |
| 136 |
| 137 | /** |
| 138 | * Returns the set of event types that have at least one handler registered. |
| 139 | */ |
| 140 | registeredTypes(): Set<string> { |
| 141 | return new Set(this.handlers.keys()); |
| 142 | } |
| 143 | } |
| 144 |