@automate.ax/integration-contracts 0.145.1 → 0.146.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,294 @@
1
+ import { encodableSchema, type Encodable } from "@automate.ax/codec"
2
+ import * as z from "zod"
3
+ import type { HighLevelOperationKey } from "./operation-manifest"
4
+ import { highLevelOperation, highLevelOperationOutputSchema } from "./schemas"
5
+ import type { HighLevelOperationInput, HighLevelOperationOutput } from "./types"
6
+
7
+ const HIGH_LEVEL_API_BASE_URL = "https://services.leadconnectorhq.com/"
8
+ const HIGH_LEVEL_API_ORIGIN = new URL(HIGH_LEVEL_API_BASE_URL).origin
9
+ const HIGH_LEVEL_SECRET_SCHEMA = z.object({
10
+ accessToken: z.string().min(1),
11
+ locationId: z.string().min(1),
12
+ })
13
+
14
+ /** Resolved account accepted by the shared HighLevel API client. */
15
+ export interface HighLevelResolvedAccount {
16
+ connectionMethodId: string
17
+ secret: Record<string, unknown>
18
+ serviceId: "highlevel"
19
+ }
20
+
21
+ /** Structured error returned by a rejected HighLevel request. */
22
+ export class HighLevelApiError extends Error {
23
+ readonly body?: Encodable
24
+ readonly retryAfter?: string
25
+ readonly status: number
26
+
27
+ /**
28
+ * Creates an error from one rejected HighLevel response.
29
+ *
30
+ * @param options - Rejected response details.
31
+ * @param options.body - Encodable provider error body.
32
+ * @param options.retryAfter - Provider retry timing.
33
+ * @param options.status - HTTP status.
34
+ */
35
+ constructor(options: {
36
+ body?: Encodable
37
+ retryAfter?: string
38
+ status: number
39
+ }) {
40
+ super(
41
+ getErrorMessage(options.body) ??
42
+ `HighLevel API request failed with status ${options.status}.`,
43
+ )
44
+ this.name = "HighLevelApiError"
45
+ this.body = options.body
46
+ this.retryAfter = options.retryAfter
47
+ this.status = options.status
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Creates an authenticated client for the frozen HighLevel action surface.
53
+ *
54
+ * @param account - Resolved HighLevel account.
55
+ * @throws When the connection method or secret is invalid.
56
+ */
57
+ export function getHighLevelApi(account: HighLevelResolvedAccount) {
58
+ const credentials = HIGH_LEVEL_SECRET_SCHEMA.parse(account.secret)
59
+ if (
60
+ account.connectionMethodId !== "oauth" &&
61
+ account.connectionMethodId !== "private-integration-token"
62
+ ) {
63
+ throw new Error(
64
+ `Unsupported HighLevel connection method: ${account.connectionMethodId}`,
65
+ )
66
+ }
67
+
68
+ return {
69
+ /**
70
+ * Executes and validates one named HighLevel OpenAPI operation.
71
+ *
72
+ * @param key - Frozen operation key.
73
+ * @param input - Flattened operation input.
74
+ */
75
+ async operation<TKey extends HighLevelOperationKey>(
76
+ key: TKey,
77
+ input: HighLevelOperationInput<TKey>,
78
+ ): Promise<HighLevelOperationOutput<TKey>> {
79
+ const definition = highLevelOperation(key)
80
+ const pathValues = new Map<string, unknown>()
81
+ const queryValues = new Map<string, unknown>()
82
+ const bodyValues: Record<string, unknown> = {}
83
+ const inputRecord = input as Record<string, unknown>
84
+
85
+ for (const name of definition.pathParameters) {
86
+ pathValues.set(name, inputRecord[name])
87
+ }
88
+ for (const { name } of definition.queryParameters) {
89
+ queryValues.set(name, inputRecord[name])
90
+ }
91
+ for (const name of definition.bodyParameters) {
92
+ if (inputRecord[name] !== undefined)
93
+ bodyValues[name] = inputRecord[name]
94
+ }
95
+ for (const placement of definition.locationPlacements) {
96
+ if (placement === "path")
97
+ pathValues.set("locationId", credentials.locationId)
98
+ if (placement === "query")
99
+ queryValues.set("locationId", credentials.locationId)
100
+ if (placement === "body") bodyValues.locationId = credentials.locationId
101
+ }
102
+
103
+ // Resolve path placeholders before applying the fixed-origin guard.
104
+ const path = definition.path.replace(
105
+ /\{([^}]+)\}/g,
106
+ (_match, name: string) => encodePathSegment(pathValues.get(name), name),
107
+ )
108
+ const normalizedPath = path.replace(/^\/+/, "")
109
+ const url = new URL(normalizedPath, HIGH_LEVEL_API_BASE_URL)
110
+ if (
111
+ !normalizedPath ||
112
+ normalizedPath.includes("://") ||
113
+ normalizedPath.includes("\\") ||
114
+ normalizedPath.includes("?") ||
115
+ normalizedPath.includes("#") ||
116
+ url.origin !== HIGH_LEVEL_API_ORIGIN
117
+ ) {
118
+ throw new TypeError(
119
+ "HighLevel API paths must remain provider-relative.",
120
+ )
121
+ }
122
+ for (const parameter of definition.queryParameters) {
123
+ appendQuery(
124
+ url,
125
+ { ...parameter, name: parameter.providerName },
126
+ queryValues.get(parameter.name),
127
+ )
128
+ }
129
+ if (definition.locationPlacements.includes("query")) {
130
+ url.searchParams.set(
131
+ definition.locationParameterNames.query ?? "locationId",
132
+ credentials.locationId,
133
+ )
134
+ }
135
+
136
+ const hasBody =
137
+ definition.bodyParameters.length > 0 ||
138
+ definition.locationPlacements.includes("body")
139
+ const response = await fetch(url, {
140
+ body: hasBody ? JSON.stringify(bodyValues) : undefined,
141
+ headers: {
142
+ Accept: "application/json",
143
+ Authorization: `Bearer ${credentials.accessToken}`,
144
+ Version: definition.apiVersion,
145
+ ...(hasBody ? { "Content-Type": "application/json" } : {}),
146
+ },
147
+ method: definition.method,
148
+ redirect: "error",
149
+ })
150
+ const parsed =
151
+ response.ok && definition.responseMode === "binary"
152
+ ? {
153
+ contentType:
154
+ response.headers.get("Content-Type") ??
155
+ "application/octet-stream",
156
+ data: Buffer.from(await response.arrayBuffer()).toString(
157
+ "base64",
158
+ ),
159
+ }
160
+ : await parseResponseText(response)
161
+ if (!response.ok) {
162
+ const body = encodableSchema.safeParse(parsed)
163
+ throw new HighLevelApiError({
164
+ ...(body.success && { body: body.data }),
165
+ retryAfter: getRetryAfter(response.headers),
166
+ status: response.status,
167
+ })
168
+ }
169
+ return highLevelOperationOutputSchema(key).parse(parsed)
170
+ },
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Encodes one required path parameter.
176
+ *
177
+ * @param value - Parameter value.
178
+ * @param name - Parameter name.
179
+ * @throws When the value is not scalar.
180
+ */
181
+ function encodePathSegment(value: unknown, name: string) {
182
+ if (typeof value !== "number" && typeof value !== "string") {
183
+ throw new TypeError(`HighLevel path parameter ${name} is required.`)
184
+ }
185
+ return encodeURIComponent(String(value))
186
+ }
187
+
188
+ /**
189
+ * Appends one OpenAPI query parameter.
190
+ *
191
+ * @param url - Provider request URL.
192
+ * @param parameter - Query serialization metadata.
193
+ * @param parameter.explode - Whether array items repeat the query key.
194
+ * @param parameter.name - Query parameter name.
195
+ * @param parameter.style - OpenAPI serialization style.
196
+ * @param value - Parameter value.
197
+ * @throws When the value is not scalar or an array of scalars.
198
+ */
199
+ function appendQuery(
200
+ url: URL,
201
+ parameter: { explode: boolean; name: string; style: string },
202
+ value: unknown,
203
+ ) {
204
+ if (value == null) return
205
+ if (Array.isArray(value)) {
206
+ if (parameter.style === "form" && parameter.explode) {
207
+ for (const item of value)
208
+ url.searchParams.append(parameter.name, stringifyQueryValue(item))
209
+ } else {
210
+ url.searchParams.set(
211
+ parameter.name,
212
+ value.map(stringifyQueryValue).join(","),
213
+ )
214
+ }
215
+ return
216
+ }
217
+ url.searchParams.set(parameter.name, stringifyQueryValue(value))
218
+ }
219
+
220
+ /**
221
+ * Converts one supported query value to text.
222
+ *
223
+ * @param value - Query value.
224
+ * @throws When the value is not scalar.
225
+ */
226
+ function stringifyQueryValue(value: unknown) {
227
+ if (
228
+ typeof value !== "boolean" &&
229
+ typeof value !== "number" &&
230
+ typeof value !== "string"
231
+ ) {
232
+ throw new TypeError("HighLevel query parameters must be scalar.")
233
+ }
234
+ return String(value)
235
+ }
236
+
237
+ /**
238
+ * Reads provider-directed retry timing.
239
+ *
240
+ * @param headers - Rejected response headers.
241
+ */
242
+ function getRetryAfter(headers: Headers) {
243
+ const retryAfter = headers.get("Retry-After")
244
+ if (retryAfter) return retryAfter
245
+ const interval = headers.get("X-RateLimit-Interval-Milliseconds")
246
+ if (!interval || !/^\d+$/.test(interval)) return undefined
247
+ return String(Math.ceil(Number(interval) / 1_000))
248
+ }
249
+
250
+ /**
251
+ * Extracts a provider error message.
252
+ *
253
+ * @param value - Encodable provider error body.
254
+ */
255
+ function getErrorMessage(value: Encodable | undefined) {
256
+ if (!isPlainObject(value)) return undefined
257
+ for (const key of ["message", "error", "error_description"]) {
258
+ const candidate = value[key]
259
+ if (typeof candidate === "string" && candidate.trim()) return candidate
260
+ }
261
+ return undefined
262
+ }
263
+
264
+ /**
265
+ * Narrows a value to a plain object.
266
+ *
267
+ * @param value - Candidate value.
268
+ */
269
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
270
+ return typeof value === "object" && value !== null && !Array.isArray(value)
271
+ }
272
+
273
+ /**
274
+ * Parses JSON while retaining non-JSON provider bodies.
275
+ *
276
+ * @param value - Response body text.
277
+ */
278
+ function parseJson(value: string): unknown {
279
+ try {
280
+ return JSON.parse(value)
281
+ } catch {
282
+ return { response: value }
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Reads one text or JSON provider response.
288
+ *
289
+ * @param response - Provider response to decode.
290
+ */
291
+ async function parseResponseText(response: Response) {
292
+ const text = await response.text()
293
+ return text ? parseJson(text) : null
294
+ }
@@ -0,0 +1,355 @@
1
+ /* oxlint-disable typescript/no-unsafe-type-assertion -- Object.keys and Object.fromEntries erase the exact generated webhook key correlation. */
2
+ import { encodableSchema } from "@automate.ax/codec"
3
+ import * as z from "zod"
4
+
5
+ export const HIGH_LEVEL_TRIGGER_CONFIG_SCHEMA = z.object({})
6
+
7
+ const BASE_EVENT_FIELDS = {
8
+ locationId: z.string().min(1),
9
+ timestamp: z.iso.datetime({ offset: true }),
10
+ webhookId: z.string().min(1),
11
+ } as const
12
+ const CONTACT_FIELDS = {
13
+ ...BASE_EVENT_FIELDS,
14
+ address1: z.string().optional(),
15
+ assignedTo: z.string().optional(),
16
+ attachments: encodableSchema.array().optional(),
17
+ city: z.string().optional(),
18
+ companyName: z.string().optional(),
19
+ country: z.string().optional(),
20
+ customFields: encodableSchema.array().optional(),
21
+ dateAdded: z.string().optional(),
22
+ dateOfBirth: z.string().optional(),
23
+ dnd: z.boolean().optional(),
24
+ dndSettings: encodableSchema.optional(),
25
+ email: z.string().optional(),
26
+ firstName: z.string().optional(),
27
+ id: z.string().min(1),
28
+ lastName: z.string().optional(),
29
+ name: z.string().optional(),
30
+ phone: z.string().optional(),
31
+ postalCode: z.string().optional(),
32
+ source: z.string().optional(),
33
+ state: z.string().optional(),
34
+ tags: z.string().array().optional(),
35
+ website: z.string().optional(),
36
+ } as const
37
+ const OPPORTUNITY_FIELDS = {
38
+ ...BASE_EVENT_FIELDS,
39
+ assignedTo: z.string().optional(),
40
+ contactId: z.string().optional(),
41
+ dateAdded: z.string().optional(),
42
+ id: z.string().min(1),
43
+ monetaryValue: z.number().optional(),
44
+ name: z.string().optional(),
45
+ pipelineId: z.string().optional(),
46
+ pipelineStageId: z.string().optional(),
47
+ source: z.string().optional(),
48
+ status: z.string().optional(),
49
+ } as const
50
+ const TASK_FIELDS = {
51
+ ...BASE_EVENT_FIELDS,
52
+ assignedTo: z.string().optional(),
53
+ body: z.string().optional(),
54
+ contactId: z.string().optional(),
55
+ dateAdded: z.string().optional(),
56
+ dueDate: z.string().optional(),
57
+ id: z.string().min(1),
58
+ title: z.string().optional(),
59
+ } as const
60
+ const NOTE_FIELDS = {
61
+ ...BASE_EVENT_FIELDS,
62
+ body: z.string().optional(),
63
+ contactId: z.string().optional(),
64
+ dateAdded: z.string().optional(),
65
+ id: z.string().min(1),
66
+ } as const
67
+ const MESSAGE_FIELDS = {
68
+ ...BASE_EVENT_FIELDS,
69
+ attachments: encodableSchema.array().optional(),
70
+ bcc: z.union([z.string(), z.string().array()]).optional(),
71
+ bccList: z.string().array().optional(),
72
+ body: z.string().optional(),
73
+ callDuration: z.number().optional(),
74
+ callStatus: z.string().optional(),
75
+ cc: z.union([z.string(), z.string().array()]).optional(),
76
+ ccList: z.string().array().optional(),
77
+ chatWidgetId: z.string().optional(),
78
+ contactId: z.string().optional(),
79
+ contentType: z.string().optional(),
80
+ conversationId: z.string().optional(),
81
+ conversationProviderId: z.string().optional(),
82
+ dateAdded: z.string().optional(),
83
+ direction: z.string().optional(),
84
+ emailMessageId: z.string().optional(),
85
+ from: z.string().optional(),
86
+ messageId: z.string().optional(),
87
+ messageType: z.string().optional(),
88
+ messageTypeId: z.string().optional(),
89
+ messageTypeString: z.string().optional(),
90
+ provider: z.string().optional(),
91
+ source: z.string().optional(),
92
+ status: z.string().optional(),
93
+ subject: z.string().optional(),
94
+ threadId: z.string().optional(),
95
+ to: z.union([z.string(), z.string().array()]).optional(),
96
+ userId: z.string().optional(),
97
+ } as const
98
+ const USER_FIELDS = {
99
+ ...BASE_EVENT_FIELDS,
100
+ companyId: z.string().optional(),
101
+ email: z.string().optional(),
102
+ extension: z.string().optional(),
103
+ firstName: z.string().optional(),
104
+ id: z.string().min(1),
105
+ lastName: z.string().optional(),
106
+ locations: z.string().array().optional(),
107
+ permissions: encodableSchema.optional(),
108
+ phone: z.string().optional(),
109
+ role: z.string().optional(),
110
+ scopes: z.string().array().optional(),
111
+ } as const
112
+ const APPOINTMENT_FIELDS = {
113
+ ...BASE_EVENT_FIELDS,
114
+ appointment: z
115
+ .object({
116
+ address: z.string().optional(),
117
+ appointmentStatus: z.string().optional(),
118
+ assignedUserId: z.string().optional(),
119
+ calendarId: z.string().optional(),
120
+ contactId: z.string().optional(),
121
+ dateAdded: z.string().optional(),
122
+ dateUpdated: z.string().optional(),
123
+ endTime: z.string().optional(),
124
+ groupId: z.string().optional(),
125
+ id: z.string().min(1),
126
+ notes: z.string().optional(),
127
+ source: z.string().optional(),
128
+ startTime: z.string().optional(),
129
+ title: z.string().optional(),
130
+ users: z.string().array().optional(),
131
+ })
132
+ .catchall(encodableSchema),
133
+ } as const
134
+
135
+ export const HIGH_LEVEL_EVENT_SCHEMAS = {
136
+ AppointmentCreate: eventSchema("AppointmentCreate", APPOINTMENT_FIELDS),
137
+ AppointmentDelete: eventSchema("AppointmentDelete", APPOINTMENT_FIELDS),
138
+ AppointmentUpdate: eventSchema("AppointmentUpdate", APPOINTMENT_FIELDS),
139
+ CampaignStatusUpdate: eventSchema("CampaignStatusUpdate", {
140
+ ...BASE_EVENT_FIELDS,
141
+ contactId: z.string().optional(),
142
+ dateAdded: z.string().optional(),
143
+ id: z.string().min(1),
144
+ replied: z.string().optional(),
145
+ status: z.string().optional(),
146
+ templateId: z.string().optional(),
147
+ }),
148
+ ContactCreate: eventSchema("ContactCreate", CONTACT_FIELDS),
149
+ ContactDelete: eventSchema("ContactDelete", CONTACT_FIELDS),
150
+ ContactDndUpdate: eventSchema("ContactDndUpdate", CONTACT_FIELDS),
151
+ ContactTagUpdate: eventSchema("ContactTagUpdate", CONTACT_FIELDS),
152
+ ContactUpdate: eventSchema("ContactUpdate", CONTACT_FIELDS),
153
+ ConversationUnreadUpdate: eventSchema("ConversationUnreadUpdate", {
154
+ ...BASE_EVENT_FIELDS,
155
+ contactId: z.string().optional(),
156
+ deleted: z.boolean().optional(),
157
+ id: z.string().min(1),
158
+ inbox: z.boolean().optional(),
159
+ starred: z.boolean().optional(),
160
+ unreadCount: z.number().int().nonnegative().optional(),
161
+ }),
162
+ ConversationUpdate: eventSchema("ConversationUpdate", {
163
+ ...BASE_EVENT_FIELDS,
164
+ companyId: z.string().optional(),
165
+ contactId: z.string().optional(),
166
+ newConversationId: z.string().min(1),
167
+ oldConversationId: z.string().min(1),
168
+ }),
169
+ InboundMessage: eventSchema("InboundMessage", MESSAGE_FIELDS),
170
+ LocationUpdate: eventSchema("LocationUpdate", {
171
+ companyId: z.string().optional(),
172
+ email: z.string().optional(),
173
+ id: z.string().min(1),
174
+ locationId: z.string().min(1).optional(),
175
+ name: z.string().optional(),
176
+ stripeProductId: z.string().optional(),
177
+ timestamp: BASE_EVENT_FIELDS.timestamp,
178
+ webhookId: BASE_EVENT_FIELDS.webhookId,
179
+ }),
180
+ NoteCreate: eventSchema("NoteCreate", NOTE_FIELDS),
181
+ NoteDelete: eventSchema("NoteDelete", NOTE_FIELDS),
182
+ NoteUpdate: eventSchema("NoteUpdate", NOTE_FIELDS),
183
+ OpportunityAssignedToUpdate: eventSchema(
184
+ "OpportunityAssignedToUpdate",
185
+ OPPORTUNITY_FIELDS,
186
+ ),
187
+ OpportunityCreate: eventSchema("OpportunityCreate", OPPORTUNITY_FIELDS),
188
+ OpportunityDelete: eventSchema("OpportunityDelete", OPPORTUNITY_FIELDS),
189
+ OpportunityMonetaryValueUpdate: eventSchema(
190
+ "OpportunityMonetaryValueUpdate",
191
+ OPPORTUNITY_FIELDS,
192
+ ),
193
+ OpportunityStageUpdate: eventSchema(
194
+ "OpportunityStageUpdate",
195
+ OPPORTUNITY_FIELDS,
196
+ ),
197
+ OpportunityStatusUpdate: eventSchema(
198
+ "OpportunityStatusUpdate",
199
+ OPPORTUNITY_FIELDS,
200
+ ),
201
+ OpportunityUpdate: eventSchema("OpportunityUpdate", OPPORTUNITY_FIELDS),
202
+ OutboundMessage: eventSchema("OutboundMessage", MESSAGE_FIELDS),
203
+ TaskComplete: eventSchema("TaskComplete", TASK_FIELDS),
204
+ TaskCreate: eventSchema("TaskCreate", TASK_FIELDS),
205
+ TaskDelete: eventSchema("TaskDelete", TASK_FIELDS),
206
+ UserCreate: eventSchema("UserCreate", USER_FIELDS),
207
+ UserDelete: eventSchema("UserDelete", USER_FIELDS),
208
+ UserUpdate: eventSchema("UserUpdate", USER_FIELDS),
209
+ } as const
210
+
211
+ export const HIGH_LEVEL_WEBHOOK_EVENT_TYPES = Object.keys(
212
+ HIGH_LEVEL_EVENT_SCHEMAS,
213
+ ) as HighLevelWebhookEventType[]
214
+
215
+ export type HighLevelWebhookEventType = keyof typeof HIGH_LEVEL_EVENT_SCHEMAS
216
+ export type HighLevelWebhookEvent = z.output<
217
+ (typeof HIGH_LEVEL_EVENT_SCHEMAS)[HighLevelWebhookEventType]
218
+ >
219
+
220
+ export const HIGH_LEVEL_EVENT_SCHEMA = z.discriminatedUnion("type", [
221
+ HIGH_LEVEL_EVENT_SCHEMAS.AppointmentCreate,
222
+ HIGH_LEVEL_EVENT_SCHEMAS.AppointmentDelete,
223
+ HIGH_LEVEL_EVENT_SCHEMAS.AppointmentUpdate,
224
+ HIGH_LEVEL_EVENT_SCHEMAS.CampaignStatusUpdate,
225
+ HIGH_LEVEL_EVENT_SCHEMAS.ContactCreate,
226
+ HIGH_LEVEL_EVENT_SCHEMAS.ContactDelete,
227
+ HIGH_LEVEL_EVENT_SCHEMAS.ContactDndUpdate,
228
+ HIGH_LEVEL_EVENT_SCHEMAS.ContactTagUpdate,
229
+ HIGH_LEVEL_EVENT_SCHEMAS.ContactUpdate,
230
+ HIGH_LEVEL_EVENT_SCHEMAS.ConversationUnreadUpdate,
231
+ HIGH_LEVEL_EVENT_SCHEMAS.ConversationUpdate,
232
+ HIGH_LEVEL_EVENT_SCHEMAS.InboundMessage,
233
+ HIGH_LEVEL_EVENT_SCHEMAS.LocationUpdate,
234
+ HIGH_LEVEL_EVENT_SCHEMAS.NoteCreate,
235
+ HIGH_LEVEL_EVENT_SCHEMAS.NoteDelete,
236
+ HIGH_LEVEL_EVENT_SCHEMAS.NoteUpdate,
237
+ HIGH_LEVEL_EVENT_SCHEMAS.OpportunityAssignedToUpdate,
238
+ HIGH_LEVEL_EVENT_SCHEMAS.OpportunityCreate,
239
+ HIGH_LEVEL_EVENT_SCHEMAS.OpportunityDelete,
240
+ HIGH_LEVEL_EVENT_SCHEMAS.OpportunityMonetaryValueUpdate,
241
+ HIGH_LEVEL_EVENT_SCHEMAS.OpportunityStageUpdate,
242
+ HIGH_LEVEL_EVENT_SCHEMAS.OpportunityStatusUpdate,
243
+ HIGH_LEVEL_EVENT_SCHEMAS.OpportunityUpdate,
244
+ HIGH_LEVEL_EVENT_SCHEMAS.OutboundMessage,
245
+ HIGH_LEVEL_EVENT_SCHEMAS.TaskComplete,
246
+ HIGH_LEVEL_EVENT_SCHEMAS.TaskCreate,
247
+ HIGH_LEVEL_EVENT_SCHEMAS.TaskDelete,
248
+ HIGH_LEVEL_EVENT_SCHEMAS.UserCreate,
249
+ HIGH_LEVEL_EVENT_SCHEMAS.UserDelete,
250
+ HIGH_LEVEL_EVENT_SCHEMAS.UserUpdate,
251
+ ])
252
+
253
+ export const HIGH_LEVEL_APP_LIFECYCLE_EVENT_SCHEMA = z
254
+ .discriminatedUnion("type", [
255
+ eventSchema("INSTALL", {
256
+ appId: z.string().min(1),
257
+ companyId: z.string().min(1).optional(),
258
+ companyName: z.string().optional(),
259
+ isWhitelabelCompany: z.boolean().optional(),
260
+ locationId: z.string().min(1).optional(),
261
+ planId: z.string().optional(),
262
+ trial: encodableSchema.optional(),
263
+ userId: z.string().optional(),
264
+ whitelabelDetails: encodableSchema.optional(),
265
+ }),
266
+ eventSchema("UNINSTALL", {
267
+ appId: z.string().min(1),
268
+ companyId: z.string().min(1).optional(),
269
+ locationId: z.string().min(1).optional(),
270
+ }),
271
+ ])
272
+ .refine((event) => event.locationId || event.companyId, {
273
+ message: "HighLevel app lifecycle events require a location or company ID.",
274
+ })
275
+
276
+ export type HighLevelAppLifecycleEvent = z.output<
277
+ typeof HIGH_LEVEL_APP_LIFECYCLE_EVENT_SCHEMA
278
+ >
279
+
280
+ export const highLevelEventTypeMap = {
281
+ AppointmentCreate: "highlevel.appointment.created",
282
+ AppointmentDelete: "highlevel.appointment.deleted",
283
+ AppointmentUpdate: "highlevel.appointment.updated",
284
+ CampaignStatusUpdate: "highlevel.campaign.statusUpdated",
285
+ ContactCreate: "highlevel.contact.created",
286
+ ContactDelete: "highlevel.contact.deleted",
287
+ ContactDndUpdate: "highlevel.contact.dndUpdated",
288
+ ContactTagUpdate: "highlevel.contact.tagsUpdated",
289
+ ContactUpdate: "highlevel.contact.updated",
290
+ ConversationUnreadUpdate: "highlevel.conversation.unreadUpdated",
291
+ ConversationUpdate: "highlevel.conversation.updated",
292
+ InboundMessage: "highlevel.message.received",
293
+ LocationUpdate: "highlevel.location.updated",
294
+ NoteCreate: "highlevel.note.created",
295
+ NoteDelete: "highlevel.note.deleted",
296
+ NoteUpdate: "highlevel.note.updated",
297
+ OpportunityAssignedToUpdate: "highlevel.opportunity.assigneeUpdated",
298
+ OpportunityCreate: "highlevel.opportunity.created",
299
+ OpportunityDelete: "highlevel.opportunity.deleted",
300
+ OpportunityMonetaryValueUpdate: "highlevel.opportunity.valueUpdated",
301
+ OpportunityStageUpdate: "highlevel.opportunity.stageUpdated",
302
+ OpportunityStatusUpdate: "highlevel.opportunity.statusUpdated",
303
+ OpportunityUpdate: "highlevel.opportunity.updated",
304
+ OutboundMessage: "highlevel.message.sent",
305
+ TaskComplete: "highlevel.task.completed",
306
+ TaskCreate: "highlevel.task.created",
307
+ TaskDelete: "highlevel.task.deleted",
308
+ UserCreate: "highlevel.user.created",
309
+ UserDelete: "highlevel.user.deleted",
310
+ UserUpdate: "highlevel.user.updated",
311
+ } as const satisfies Record<HighLevelWebhookEventType, `highlevel.${string}`>
312
+
313
+ type HighLevelTriggerContract<TEvent> = {
314
+ readonly configSchema: typeof HIGH_LEVEL_TRIGGER_CONFIG_SCHEMA
315
+ readonly eventSchema: z.ZodType<TEvent>
316
+ }
317
+
318
+ /** Typed semantic contracts derived from provider webhook discriminators. */
319
+ type HighLevelSemanticTriggerContracts = {
320
+ readonly [TType in HighLevelWebhookEventType as (typeof highLevelEventTypeMap)[TType]]: HighLevelTriggerContract<
321
+ z.output<(typeof HIGH_LEVEL_EVENT_SCHEMAS)[TType]>
322
+ >
323
+ }
324
+
325
+ export const highLevelTriggerContracts = {
326
+ "highlevel.event": {
327
+ configSchema: HIGH_LEVEL_TRIGGER_CONFIG_SCHEMA,
328
+ eventSchema: HIGH_LEVEL_EVENT_SCHEMA,
329
+ },
330
+ ...Object.fromEntries(
331
+ Object.entries(highLevelEventTypeMap).map(([providerType, eventType]) => [
332
+ eventType,
333
+ {
334
+ configSchema: HIGH_LEVEL_TRIGGER_CONFIG_SCHEMA,
335
+ eventSchema:
336
+ HIGH_LEVEL_EVENT_SCHEMAS[providerType as HighLevelWebhookEventType],
337
+ },
338
+ ]),
339
+ ),
340
+ } as unknown as HighLevelSemanticTriggerContracts & {
341
+ readonly "highlevel.event": HighLevelTriggerContract<HighLevelWebhookEvent>
342
+ }
343
+
344
+ /**
345
+ * Creates an extensible schema for one HighLevel webhook discriminator.
346
+ *
347
+ * @param type - Provider event discriminator.
348
+ * @param shape - Known fields for the event.
349
+ */
350
+ function eventSchema<
351
+ const TType extends string,
352
+ TShape extends Record<string, z.ZodType>,
353
+ >(type: TType, shape: TShape) {
354
+ return z.object({ ...shape, type: z.literal(type) }).catchall(encodableSchema)
355
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./api"
2
+ export * from "./events"
3
+ export * from "./operation-manifest"
4
+ export * from "./schemas"
5
+ export type { HighLevelOperationMap } from "./openapi-types.generated"
6
+ export type * from "./types"