@automate.ax/integration-contracts 0.55.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.
- package/dist/airtable/api.d.ts +19 -0
- package/dist/airtable/api.js +44 -0
- package/dist/airtable/index.d.ts +353 -0
- package/dist/airtable/index.js +32 -0
- package/dist/airtable/schemas.d.ts +716 -0
- package/dist/airtable/schemas.js +184 -0
- package/dist/asana/api.d.ts +70 -0
- package/dist/asana/api.js +119 -0
- package/dist/asana/index.d.ts +384 -0
- package/dist/asana/index.js +38 -0
- package/dist/asana/schemas.d.ts +1060 -0
- package/dist/asana/schemas.js +512 -0
- package/dist/brevo/api.d.ts +31 -0
- package/dist/brevo/api.js +64 -0
- package/dist/brevo/index.d.ts +822 -0
- package/dist/brevo/index.js +51 -0
- package/dist/brevo/schemas.d.ts +1217 -0
- package/dist/brevo/schemas.js +332 -0
- package/dist/github/index.d.ts +37 -0
- package/dist/github/index.js +106 -0
- package/dist/github/schemas.d.ts +8014 -0
- package/dist/github/schemas.js +113 -0
- package/dist/gmail/gmail.d.ts +172 -0
- package/dist/gmail/gmail.js +405 -0
- package/dist/gmail/index.d.ts +262 -0
- package/dist/gmail/index.js +23 -0
- package/dist/gmail/schemas.d.ts +444 -0
- package/dist/gmail/schemas.js +242 -0
- package/dist/google-calendar/event-schemas.d.ts +117 -0
- package/dist/google-calendar/event-schemas.js +13 -0
- package/dist/google-calendar/google-calendar.d.ts +147 -0
- package/dist/google-calendar/google-calendar.js +583 -0
- package/dist/google-calendar/index.d.ts +365 -0
- package/dist/google-calendar/index.js +24 -0
- package/dist/google-calendar/schemas.d.ts +592 -0
- package/dist/google-calendar/schemas.js +379 -0
- package/dist/google-forms/event-schemas.d.ts +105 -0
- package/dist/google-forms/event-schemas.js +20 -0
- package/dist/google-forms/google-forms.d.ts +243 -0
- package/dist/google-forms/google-forms.js +581 -0
- package/dist/google-forms/index.d.ts +141 -0
- package/dist/google-forms/index.js +20 -0
- package/dist/google-forms/schemas.d.ts +618 -0
- package/dist/google-forms/schemas.js +383 -0
- package/dist/linear/api.d.ts +63 -0
- package/dist/linear/api.js +88 -0
- package/dist/linear/index.d.ts +1338 -0
- package/dist/linear/index.js +55 -0
- package/dist/linear/schemas.d.ts +1795 -0
- package/dist/linear/schemas.js +760 -0
- package/dist/outlook/graph.d.ts +34 -0
- package/dist/outlook/graph.js +98 -0
- package/dist/outlook/index.d.ts +115 -0
- package/dist/outlook/index.js +11 -0
- package/dist/outlook/schemas.d.ts +529 -0
- package/dist/outlook/schemas.js +312 -0
- package/dist/resend/index.d.ts +707 -0
- package/dist/resend/index.js +54 -0
- package/dist/resend/schemas.d.ts +1033 -0
- package/dist/resend/schemas.js +377 -0
- package/dist/slack/index.d.ts +151 -0
- package/dist/slack/index.js +22 -0
- package/dist/slack/schemas.d.ts +641 -0
- package/dist/slack/schemas.js +581 -0
- package/dist/teams/graph.d.ts +33 -0
- package/dist/teams/graph.js +95 -0
- package/dist/teams/index.d.ts +164 -0
- package/dist/teams/index.js +21 -0
- package/dist/teams/schemas.d.ts +675 -0
- package/dist/teams/schemas.js +365 -0
- package/dist/trello/api.d.ts +69 -0
- package/dist/trello/api.js +140 -0
- package/dist/trello/event-schemas.d.ts +1101 -0
- package/dist/trello/event-schemas.js +233 -0
- package/dist/trello/index.d.ts +721 -0
- package/dist/trello/index.js +35 -0
- package/dist/trello/schemas.d.ts +283 -0
- package/dist/trello/schemas.js +401 -0
- package/dist/triggers.d.ts +26 -0
- package/dist/triggers.js +33 -0
- package/dist/vercel/index.d.ts +1065 -0
- package/dist/vercel/index.js +114 -0
- package/dist/vercel/schemas.d.ts +1518 -0
- package/dist/vercel/schemas.js +335 -0
- package/dist/whatsapp/api.d.ts +62 -0
- package/dist/whatsapp/api.js +121 -0
- package/dist/whatsapp/index.d.ts +614 -0
- package/dist/whatsapp/index.js +35 -0
- package/dist/whatsapp/schemas.d.ts +926 -0
- package/dist/whatsapp/schemas.js +326 -0
- package/package.json +169 -0
- package/src/airtable/api.ts +66 -0
- package/src/airtable/index.ts +41 -0
- package/src/airtable/schemas.ts +231 -0
- package/src/asana/api.ts +171 -0
- package/src/asana/index.ts +49 -0
- package/src/asana/schemas.ts +655 -0
- package/src/brevo/api.ts +89 -0
- package/src/brevo/index.ts +66 -0
- package/src/brevo/schemas.ts +449 -0
- package/src/github/index.ts +217 -0
- package/src/github/schemas.ts +435 -0
- package/src/gmail/gmail.ts +577 -0
- package/src/gmail/index.ts +26 -0
- package/src/gmail/schemas.ts +327 -0
- package/src/google-calendar/event-schemas.ts +16 -0
- package/src/google-calendar/google-calendar.ts +727 -0
- package/src/google-calendar/index.ts +27 -0
- package/src/google-calendar/schemas.ts +529 -0
- package/src/google-forms/event-schemas.ts +26 -0
- package/src/google-forms/google-forms.ts +683 -0
- package/src/google-forms/index.ts +23 -0
- package/src/google-forms/schemas.ts +499 -0
- package/src/linear/api.ts +141 -0
- package/src/linear/index.ts +71 -0
- package/src/linear/schemas.ts +1060 -0
- package/src/outlook/graph.ts +132 -0
- package/src/outlook/index.ts +14 -0
- package/src/outlook/schemas.ts +388 -0
- package/src/resend/index.ts +70 -0
- package/src/resend/schemas.ts +462 -0
- package/src/slack/index.ts +29 -0
- package/src/slack/schemas.ts +737 -0
- package/src/teams/graph.ts +130 -0
- package/src/teams/index.ts +27 -0
- package/src/teams/schemas.ts +463 -0
- package/src/trello/api.ts +190 -0
- package/src/trello/event-schemas.ts +290 -0
- package/src/trello/index.ts +45 -0
- package/src/trello/schemas.ts +507 -0
- package/src/triggers.ts +68 -0
- package/src/vercel/index.ts +147 -0
- package/src/vercel/schemas.ts +594 -0
- package/src/whatsapp/api.ts +171 -0
- package/src/whatsapp/index.ts +46 -0
- package/src/whatsapp/schemas.ts +341 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { JsonObject } from "type-fest"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
|
|
4
|
+
export const WHATSAPP_GRAPH_API_VERSION = "v26.0"
|
|
5
|
+
const WHATSAPP_GRAPH_API_ORIGIN = "https://graph.facebook.com"
|
|
6
|
+
const WHATSAPP_SECRET_SCHEMA = z.object({
|
|
7
|
+
accessToken: z.string().min(1),
|
|
8
|
+
businessId: z.string().min(1),
|
|
9
|
+
phoneNumberId: z.string().min(1),
|
|
10
|
+
wabaId: z.string().min(1),
|
|
11
|
+
})
|
|
12
|
+
const WHATSAPP_ERROR_SCHEMA = z.looseObject({
|
|
13
|
+
code: z.number().int().optional(),
|
|
14
|
+
error_data: z
|
|
15
|
+
.looseObject({
|
|
16
|
+
details: z.string().optional(),
|
|
17
|
+
messaging_product: z.string().optional(),
|
|
18
|
+
})
|
|
19
|
+
.optional(),
|
|
20
|
+
error_subcode: z.number().int().optional(),
|
|
21
|
+
fbtrace_id: z.string().optional(),
|
|
22
|
+
message: z.string().optional(),
|
|
23
|
+
type: z.string().optional(),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
/** Options for one authenticated WhatsApp Graph API request. */
|
|
27
|
+
export interface WhatsAppApiRequestOptions<TSchema extends z.ZodType> {
|
|
28
|
+
/** JSON request body. */
|
|
29
|
+
body?: JsonObject
|
|
30
|
+
|
|
31
|
+
/** HTTP verb. */
|
|
32
|
+
method?: "DELETE" | "GET" | "POST"
|
|
33
|
+
|
|
34
|
+
/** Schema that validates and types the successful response. */
|
|
35
|
+
responseSchema: TSchema
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Authenticated WhatsApp Business Platform helper for custom actions. */
|
|
39
|
+
export interface WhatsAppApi {
|
|
40
|
+
/** Meta Business Portfolio ID that owns the connected assets. */
|
|
41
|
+
businessId: string
|
|
42
|
+
|
|
43
|
+
/** Connected WhatsApp business phone-number ID. */
|
|
44
|
+
phoneNumberId: string
|
|
45
|
+
|
|
46
|
+
/** Connected WhatsApp Business Account ID. */
|
|
47
|
+
wabaId: string
|
|
48
|
+
|
|
49
|
+
/** Calls a Graph API path under the pinned platform version. */
|
|
50
|
+
request<TSchema extends z.ZodType>(
|
|
51
|
+
path: string,
|
|
52
|
+
options: WhatsAppApiRequestOptions<TSchema>,
|
|
53
|
+
): Promise<z.output<TSchema>>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Structured error returned by Meta's Graph API. */
|
|
57
|
+
export class WhatsAppApiError extends Error {
|
|
58
|
+
readonly code?: number
|
|
59
|
+
readonly details?: string
|
|
60
|
+
readonly fbtraceId?: string
|
|
61
|
+
readonly retryAfter?: number
|
|
62
|
+
readonly status: number
|
|
63
|
+
readonly subcode?: number
|
|
64
|
+
readonly type?: string
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Creates a structured Graph API error.
|
|
68
|
+
*
|
|
69
|
+
* @param options - Provider and transport error details.
|
|
70
|
+
* @param options.code - Meta Graph API error code.
|
|
71
|
+
* @param options.details - WhatsApp-specific failure details.
|
|
72
|
+
* @param options.fbtraceId - Meta trace identifier.
|
|
73
|
+
* @param options.message - Human-readable provider message.
|
|
74
|
+
* @param options.retryAfter - Retry delay in seconds.
|
|
75
|
+
* @param options.status - HTTP response status.
|
|
76
|
+
* @param options.subcode - Meta Graph API error subcode.
|
|
77
|
+
* @param options.type - Meta error type.
|
|
78
|
+
*/
|
|
79
|
+
constructor(options: {
|
|
80
|
+
code?: number
|
|
81
|
+
details?: string
|
|
82
|
+
fbtraceId?: string
|
|
83
|
+
message: string
|
|
84
|
+
retryAfter?: number
|
|
85
|
+
status: number
|
|
86
|
+
subcode?: number
|
|
87
|
+
type?: string
|
|
88
|
+
}) {
|
|
89
|
+
super(options.message)
|
|
90
|
+
this.name = "WhatsAppApiError"
|
|
91
|
+
this.code = options.code
|
|
92
|
+
this.details = options.details
|
|
93
|
+
this.fbtraceId = options.fbtraceId
|
|
94
|
+
this.retryAfter = options.retryAfter
|
|
95
|
+
this.status = options.status
|
|
96
|
+
this.subcode = options.subcode
|
|
97
|
+
this.type = options.type
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Creates an authenticated WhatsApp Business Platform client.
|
|
103
|
+
*
|
|
104
|
+
* @param secret - Resolved WhatsApp integration secret.
|
|
105
|
+
*/
|
|
106
|
+
export function getWhatsAppApi(secret: Record<string, unknown>): WhatsAppApi {
|
|
107
|
+
const { accessToken, businessId, phoneNumberId, wabaId } =
|
|
108
|
+
WHATSAPP_SECRET_SCHEMA.parse(secret)
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
businessId,
|
|
112
|
+
phoneNumberId,
|
|
113
|
+
wabaId,
|
|
114
|
+
request: async (path, options) => {
|
|
115
|
+
if (!/^\/[A-Za-z0-9_.,?=&/-]+$/.test(path)) {
|
|
116
|
+
throw new Error("WhatsApp Graph API paths must be relative.")
|
|
117
|
+
}
|
|
118
|
+
const response = await fetch(
|
|
119
|
+
`${WHATSAPP_GRAPH_API_ORIGIN}/${WHATSAPP_GRAPH_API_VERSION}${path}`,
|
|
120
|
+
{
|
|
121
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
122
|
+
headers: {
|
|
123
|
+
Accept: "application/json",
|
|
124
|
+
Authorization: `Bearer ${accessToken}`,
|
|
125
|
+
...(options.body && { "Content-Type": "application/json" }),
|
|
126
|
+
},
|
|
127
|
+
method: options.method ?? (options.body ? "POST" : "GET"),
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
const payload: unknown = await response.json()
|
|
131
|
+
if (!response.ok) throw toWhatsAppApiError(response, payload)
|
|
132
|
+
return options.responseSchema.parse(payload)
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Converts an unsuccessful Graph response into a structured error.
|
|
139
|
+
*
|
|
140
|
+
* @param response - Unsuccessful HTTP response.
|
|
141
|
+
* @param payload - Parsed Graph API payload.
|
|
142
|
+
*/
|
|
143
|
+
function toWhatsAppApiError(response: Response, payload: unknown) {
|
|
144
|
+
const error = z
|
|
145
|
+
.looseObject({ error: WHATSAPP_ERROR_SCHEMA.optional() })
|
|
146
|
+
.safeParse(payload)
|
|
147
|
+
const providerError = error.success ? error.data.error : undefined
|
|
148
|
+
return new WhatsAppApiError({
|
|
149
|
+
code: providerError?.code,
|
|
150
|
+
details: providerError?.error_data?.details,
|
|
151
|
+
fbtraceId: providerError?.fbtrace_id,
|
|
152
|
+
message:
|
|
153
|
+
providerError?.message ??
|
|
154
|
+
`WhatsApp Graph API request failed (${response.status}).`,
|
|
155
|
+
retryAfter: parseRetryAfter(response.headers.get("Retry-After")),
|
|
156
|
+
status: response.status,
|
|
157
|
+
subcode: providerError?.error_subcode,
|
|
158
|
+
type: providerError?.type,
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Parses Meta's integer retry delay header.
|
|
164
|
+
*
|
|
165
|
+
* @param value - Raw `Retry-After` value.
|
|
166
|
+
*/
|
|
167
|
+
function parseRetryAfter(value: string | null) {
|
|
168
|
+
if (value === null) return undefined
|
|
169
|
+
const seconds = Number(value)
|
|
170
|
+
return Number.isFinite(seconds) ? seconds : undefined
|
|
171
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import {
|
|
3
|
+
WHATSAPP_MESSAGE_DELIVERED_EVENT_SCHEMA,
|
|
4
|
+
WHATSAPP_MESSAGE_FAILED_EVENT_SCHEMA,
|
|
5
|
+
WHATSAPP_MESSAGE_PLAYED_EVENT_SCHEMA,
|
|
6
|
+
WHATSAPP_MESSAGE_READ_EVENT_SCHEMA,
|
|
7
|
+
WHATSAPP_MESSAGE_RECEIVED_EVENT_SCHEMA,
|
|
8
|
+
WHATSAPP_MESSAGE_SENT_EVENT_SCHEMA,
|
|
9
|
+
WHATSAPP_MESSAGE_STATUS_EVENT_SCHEMA,
|
|
10
|
+
} from "./schemas"
|
|
11
|
+
|
|
12
|
+
export * from "./api"
|
|
13
|
+
export * from "./schemas"
|
|
14
|
+
|
|
15
|
+
export const WHATSAPP_TRIGGER_CONFIG_SCHEMA = z.object({})
|
|
16
|
+
|
|
17
|
+
export const whatsappTriggerContracts = {
|
|
18
|
+
"whatsapp.message.delivered": {
|
|
19
|
+
configSchema: WHATSAPP_TRIGGER_CONFIG_SCHEMA,
|
|
20
|
+
eventSchema: WHATSAPP_MESSAGE_DELIVERED_EVENT_SCHEMA,
|
|
21
|
+
},
|
|
22
|
+
"whatsapp.message.failed": {
|
|
23
|
+
configSchema: WHATSAPP_TRIGGER_CONFIG_SCHEMA,
|
|
24
|
+
eventSchema: WHATSAPP_MESSAGE_FAILED_EVENT_SCHEMA,
|
|
25
|
+
},
|
|
26
|
+
"whatsapp.message.played": {
|
|
27
|
+
configSchema: WHATSAPP_TRIGGER_CONFIG_SCHEMA,
|
|
28
|
+
eventSchema: WHATSAPP_MESSAGE_PLAYED_EVENT_SCHEMA,
|
|
29
|
+
},
|
|
30
|
+
"whatsapp.message.read": {
|
|
31
|
+
configSchema: WHATSAPP_TRIGGER_CONFIG_SCHEMA,
|
|
32
|
+
eventSchema: WHATSAPP_MESSAGE_READ_EVENT_SCHEMA,
|
|
33
|
+
},
|
|
34
|
+
"whatsapp.message.received": {
|
|
35
|
+
configSchema: WHATSAPP_TRIGGER_CONFIG_SCHEMA,
|
|
36
|
+
eventSchema: WHATSAPP_MESSAGE_RECEIVED_EVENT_SCHEMA,
|
|
37
|
+
},
|
|
38
|
+
"whatsapp.message.sent": {
|
|
39
|
+
configSchema: WHATSAPP_TRIGGER_CONFIG_SCHEMA,
|
|
40
|
+
eventSchema: WHATSAPP_MESSAGE_SENT_EVENT_SCHEMA,
|
|
41
|
+
},
|
|
42
|
+
"whatsapp.message.statusEvent": {
|
|
43
|
+
configSchema: WHATSAPP_TRIGGER_CONFIG_SCHEMA,
|
|
44
|
+
eventSchema: WHATSAPP_MESSAGE_STATUS_EVENT_SCHEMA,
|
|
45
|
+
},
|
|
46
|
+
} as const
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
|
|
3
|
+
const WHATSAPP_ERROR_SCHEMA = z.object({
|
|
4
|
+
code: z.number().int(),
|
|
5
|
+
error_data: z
|
|
6
|
+
.object({
|
|
7
|
+
details: z.string().optional(),
|
|
8
|
+
messaging_product: z.string().optional(),
|
|
9
|
+
})
|
|
10
|
+
.optional(),
|
|
11
|
+
error_subcode: z.number().int().optional(),
|
|
12
|
+
fbtrace_id: z.string().optional(),
|
|
13
|
+
message: z.string(),
|
|
14
|
+
title: z.string().optional(),
|
|
15
|
+
})
|
|
16
|
+
const WHATSAPP_MESSAGE_SCHEMA = z.object({
|
|
17
|
+
audio: z
|
|
18
|
+
.object({ id: z.string(), mime_type: z.string().optional() })
|
|
19
|
+
.optional(),
|
|
20
|
+
button: z.object({ payload: z.string(), text: z.string() }).optional(),
|
|
21
|
+
contacts: z.json().array().optional(),
|
|
22
|
+
context: z
|
|
23
|
+
.object({
|
|
24
|
+
forwarded: z.boolean().optional(),
|
|
25
|
+
from: z.string().optional(),
|
|
26
|
+
id: z.string(),
|
|
27
|
+
})
|
|
28
|
+
.optional(),
|
|
29
|
+
document: z
|
|
30
|
+
.object({
|
|
31
|
+
caption: z.string().optional(),
|
|
32
|
+
filename: z.string().optional(),
|
|
33
|
+
id: z.string(),
|
|
34
|
+
mime_type: z.string().optional(),
|
|
35
|
+
})
|
|
36
|
+
.optional(),
|
|
37
|
+
errors: WHATSAPP_ERROR_SCHEMA.array().optional(),
|
|
38
|
+
from: z.string(),
|
|
39
|
+
id: z.string(),
|
|
40
|
+
image: z
|
|
41
|
+
.object({
|
|
42
|
+
caption: z.string().optional(),
|
|
43
|
+
id: z.string(),
|
|
44
|
+
mime_type: z.string().optional(),
|
|
45
|
+
})
|
|
46
|
+
.optional(),
|
|
47
|
+
interactive: z
|
|
48
|
+
.object({
|
|
49
|
+
button_reply: z.object({ id: z.string(), title: z.string() }).optional(),
|
|
50
|
+
list_reply: z
|
|
51
|
+
.object({
|
|
52
|
+
description: z.string().optional(),
|
|
53
|
+
id: z.string(),
|
|
54
|
+
title: z.string(),
|
|
55
|
+
})
|
|
56
|
+
.optional(),
|
|
57
|
+
type: z.enum(["button_reply", "list_reply"]),
|
|
58
|
+
})
|
|
59
|
+
.optional(),
|
|
60
|
+
location: z
|
|
61
|
+
.object({
|
|
62
|
+
address: z.string().optional(),
|
|
63
|
+
latitude: z.number(),
|
|
64
|
+
longitude: z.number(),
|
|
65
|
+
name: z.string().optional(),
|
|
66
|
+
url: z.string().optional(),
|
|
67
|
+
})
|
|
68
|
+
.optional(),
|
|
69
|
+
order: z.json().optional(),
|
|
70
|
+
reaction: z.object({ emoji: z.string(), message_id: z.string() }).optional(),
|
|
71
|
+
referral: z.json().optional(),
|
|
72
|
+
sticker: z
|
|
73
|
+
.object({ id: z.string(), mime_type: z.string().optional() })
|
|
74
|
+
.optional(),
|
|
75
|
+
system: z.json().optional(),
|
|
76
|
+
text: z.object({ body: z.string() }).optional(),
|
|
77
|
+
timestamp: z.string(),
|
|
78
|
+
type: z.enum([
|
|
79
|
+
"audio",
|
|
80
|
+
"button",
|
|
81
|
+
"contacts",
|
|
82
|
+
"document",
|
|
83
|
+
"image",
|
|
84
|
+
"interactive",
|
|
85
|
+
"location",
|
|
86
|
+
"order",
|
|
87
|
+
"reaction",
|
|
88
|
+
"sticker",
|
|
89
|
+
"system",
|
|
90
|
+
"text",
|
|
91
|
+
"unknown",
|
|
92
|
+
"unsupported",
|
|
93
|
+
"video",
|
|
94
|
+
]),
|
|
95
|
+
unknown: z.json().optional(),
|
|
96
|
+
unsupported: z.json().optional(),
|
|
97
|
+
video: z
|
|
98
|
+
.object({
|
|
99
|
+
caption: z.string().optional(),
|
|
100
|
+
id: z.string(),
|
|
101
|
+
mime_type: z.string().optional(),
|
|
102
|
+
})
|
|
103
|
+
.optional(),
|
|
104
|
+
})
|
|
105
|
+
const WHATSAPP_STATUS_SCHEMA = z.object({
|
|
106
|
+
biz_opaque_callback_data: z.string().optional(),
|
|
107
|
+
conversation: z
|
|
108
|
+
.object({
|
|
109
|
+
expiration_timestamp: z.string().optional(),
|
|
110
|
+
id: z.string(),
|
|
111
|
+
origin: z.object({ type: z.string() }),
|
|
112
|
+
})
|
|
113
|
+
.optional(),
|
|
114
|
+
errors: WHATSAPP_ERROR_SCHEMA.array().optional(),
|
|
115
|
+
id: z.string(),
|
|
116
|
+
pricing: z
|
|
117
|
+
.object({
|
|
118
|
+
billable: z.boolean().optional(),
|
|
119
|
+
category: z.string(),
|
|
120
|
+
pricing_model: z.enum(["CBP", "PMP"]),
|
|
121
|
+
type: z
|
|
122
|
+
.enum(["regular", "free_customer_service", "free_entry_point"])
|
|
123
|
+
.optional(),
|
|
124
|
+
})
|
|
125
|
+
.optional(),
|
|
126
|
+
recipient_id: z.string(),
|
|
127
|
+
status: z.enum(["sent", "delivered", "read", "failed", "played"]),
|
|
128
|
+
timestamp: z.string(),
|
|
129
|
+
})
|
|
130
|
+
const WHATSAPP_NORMALIZED_ERROR_SCHEMA = z.object({
|
|
131
|
+
code: z.number().int(),
|
|
132
|
+
details: z.string().optional(),
|
|
133
|
+
message: z.string(),
|
|
134
|
+
subcode: z.number().int().optional(),
|
|
135
|
+
title: z.string().optional(),
|
|
136
|
+
traceId: z.string().optional(),
|
|
137
|
+
})
|
|
138
|
+
const WHATSAPP_CHANGE_VALUE_SCHEMA = z.object({
|
|
139
|
+
contacts: z
|
|
140
|
+
.object({
|
|
141
|
+
profile: z.object({ name: z.string().optional() }),
|
|
142
|
+
wa_id: z.string(),
|
|
143
|
+
})
|
|
144
|
+
.array()
|
|
145
|
+
.optional(),
|
|
146
|
+
messages: WHATSAPP_MESSAGE_SCHEMA.array().optional(),
|
|
147
|
+
messaging_product: z.literal("whatsapp"),
|
|
148
|
+
metadata: z.object({
|
|
149
|
+
display_phone_number: z.string(),
|
|
150
|
+
phone_number_id: z.string(),
|
|
151
|
+
}),
|
|
152
|
+
statuses: WHATSAPP_STATUS_SCHEMA.array().optional(),
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
/** Provider-native WhatsApp webhook envelope. */
|
|
156
|
+
export const WHATSAPP_WEBHOOK_SCHEMA = z.object({
|
|
157
|
+
entry: z
|
|
158
|
+
.object({
|
|
159
|
+
changes: z
|
|
160
|
+
.object({
|
|
161
|
+
field: z.literal("messages"),
|
|
162
|
+
value: WHATSAPP_CHANGE_VALUE_SCHEMA,
|
|
163
|
+
})
|
|
164
|
+
.array(),
|
|
165
|
+
id: z.string(),
|
|
166
|
+
})
|
|
167
|
+
.array(),
|
|
168
|
+
object: z.literal("whatsapp_business_account"),
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
/** Normalized incoming WhatsApp message event. */
|
|
172
|
+
export const WHATSAPP_MESSAGE_RECEIVED_EVENT_SCHEMA = z.object({
|
|
173
|
+
businessAccountId: z.string(),
|
|
174
|
+
displayPhoneNumber: z.string(),
|
|
175
|
+
message: z.object({
|
|
176
|
+
content: z.json().optional(),
|
|
177
|
+
context: z
|
|
178
|
+
.object({
|
|
179
|
+
forwarded: z.boolean().optional(),
|
|
180
|
+
from: z.string().optional(),
|
|
181
|
+
messageId: z.string(),
|
|
182
|
+
})
|
|
183
|
+
.optional(),
|
|
184
|
+
id: z.string(),
|
|
185
|
+
timestamp: z.string(),
|
|
186
|
+
type: WHATSAPP_MESSAGE_SCHEMA.shape.type,
|
|
187
|
+
}),
|
|
188
|
+
phoneNumberId: z.string(),
|
|
189
|
+
providerEvent: WHATSAPP_MESSAGE_SCHEMA,
|
|
190
|
+
sender: z.object({ id: z.string(), name: z.string().optional() }),
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
/** Normalized WhatsApp message status event. */
|
|
194
|
+
export const WHATSAPP_MESSAGE_STATUS_EVENT_SCHEMA = z.object({
|
|
195
|
+
businessAccountId: z.string(),
|
|
196
|
+
callbackData: z.string().optional(),
|
|
197
|
+
conversation: z
|
|
198
|
+
.object({
|
|
199
|
+
expirationTimestamp: z.string().optional(),
|
|
200
|
+
id: z.string(),
|
|
201
|
+
originType: z.string(),
|
|
202
|
+
})
|
|
203
|
+
.optional(),
|
|
204
|
+
errors: WHATSAPP_NORMALIZED_ERROR_SCHEMA.array().optional(),
|
|
205
|
+
messageId: z.string(),
|
|
206
|
+
phoneNumberId: z.string(),
|
|
207
|
+
pricing: z
|
|
208
|
+
.object({
|
|
209
|
+
billable: z.boolean().optional(),
|
|
210
|
+
category: z.string(),
|
|
211
|
+
model: z.enum(["CBP", "PMP"]),
|
|
212
|
+
type: z
|
|
213
|
+
.enum(["regular", "free_customer_service", "free_entry_point"])
|
|
214
|
+
.optional(),
|
|
215
|
+
})
|
|
216
|
+
.optional(),
|
|
217
|
+
providerEvent: WHATSAPP_STATUS_SCHEMA,
|
|
218
|
+
recipientId: z.string(),
|
|
219
|
+
status: WHATSAPP_STATUS_SCHEMA.shape.status,
|
|
220
|
+
timestamp: z.string(),
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
export const WHATSAPP_MESSAGE_SENT_EVENT_SCHEMA =
|
|
224
|
+
WHATSAPP_MESSAGE_STATUS_EVENT_SCHEMA.extend({
|
|
225
|
+
providerEvent: WHATSAPP_STATUS_SCHEMA.extend({ status: z.literal("sent") }),
|
|
226
|
+
status: z.literal("sent"),
|
|
227
|
+
})
|
|
228
|
+
export const WHATSAPP_MESSAGE_DELIVERED_EVENT_SCHEMA =
|
|
229
|
+
WHATSAPP_MESSAGE_STATUS_EVENT_SCHEMA.extend({
|
|
230
|
+
providerEvent: WHATSAPP_STATUS_SCHEMA.extend({
|
|
231
|
+
status: z.literal("delivered"),
|
|
232
|
+
}),
|
|
233
|
+
status: z.literal("delivered"),
|
|
234
|
+
})
|
|
235
|
+
export const WHATSAPP_MESSAGE_READ_EVENT_SCHEMA =
|
|
236
|
+
WHATSAPP_MESSAGE_STATUS_EVENT_SCHEMA.extend({
|
|
237
|
+
providerEvent: WHATSAPP_STATUS_SCHEMA.extend({ status: z.literal("read") }),
|
|
238
|
+
status: z.literal("read"),
|
|
239
|
+
})
|
|
240
|
+
export const WHATSAPP_MESSAGE_FAILED_EVENT_SCHEMA =
|
|
241
|
+
WHATSAPP_MESSAGE_STATUS_EVENT_SCHEMA.extend({
|
|
242
|
+
providerEvent: WHATSAPP_STATUS_SCHEMA.extend({
|
|
243
|
+
status: z.literal("failed"),
|
|
244
|
+
}),
|
|
245
|
+
status: z.literal("failed"),
|
|
246
|
+
})
|
|
247
|
+
export const WHATSAPP_MESSAGE_PLAYED_EVENT_SCHEMA =
|
|
248
|
+
WHATSAPP_MESSAGE_STATUS_EVENT_SCHEMA.extend({
|
|
249
|
+
providerEvent: WHATSAPP_STATUS_SCHEMA.extend({
|
|
250
|
+
status: z.literal("played"),
|
|
251
|
+
}),
|
|
252
|
+
status: z.literal("played"),
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Flattens one verified provider callback into independently ingestible events.
|
|
257
|
+
*
|
|
258
|
+
* @param input - Untrusted WhatsApp webhook envelope.
|
|
259
|
+
*/
|
|
260
|
+
export function normalizeWhatsAppWebhook(input: unknown) {
|
|
261
|
+
// Parse once before flattening every entry and change in the delivery batch.
|
|
262
|
+
const webhook = WHATSAPP_WEBHOOK_SCHEMA.parse(input)
|
|
263
|
+
const messages: z.output<typeof WHATSAPP_MESSAGE_RECEIVED_EVENT_SCHEMA>[] = []
|
|
264
|
+
const statuses: z.output<typeof WHATSAPP_MESSAGE_STATUS_EVENT_SCHEMA>[] = []
|
|
265
|
+
|
|
266
|
+
for (const entry of webhook.entry) {
|
|
267
|
+
for (const { value } of entry.changes) {
|
|
268
|
+
for (const message of value.messages ?? []) {
|
|
269
|
+
// Meta supplies contacts separately, so match the sender for its profile.
|
|
270
|
+
const sender = value.contacts?.find(
|
|
271
|
+
({ wa_id }) => wa_id === message.from,
|
|
272
|
+
)
|
|
273
|
+
messages.push({
|
|
274
|
+
businessAccountId: entry.id,
|
|
275
|
+
displayPhoneNumber: value.metadata.display_phone_number,
|
|
276
|
+
message: {
|
|
277
|
+
content: message[message.type],
|
|
278
|
+
...(message.context && {
|
|
279
|
+
context: {
|
|
280
|
+
forwarded: message.context.forwarded,
|
|
281
|
+
from: message.context.from,
|
|
282
|
+
messageId: message.context.id,
|
|
283
|
+
},
|
|
284
|
+
}),
|
|
285
|
+
id: message.id,
|
|
286
|
+
timestamp: message.timestamp,
|
|
287
|
+
type: message.type,
|
|
288
|
+
},
|
|
289
|
+
phoneNumberId: value.metadata.phone_number_id,
|
|
290
|
+
providerEvent: message,
|
|
291
|
+
sender: { id: message.from, name: sender?.profile.name },
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
for (const status of value.statuses ?? []) {
|
|
295
|
+
statuses.push({
|
|
296
|
+
businessAccountId: entry.id,
|
|
297
|
+
callbackData: status.biz_opaque_callback_data,
|
|
298
|
+
...(status.conversation && {
|
|
299
|
+
conversation: {
|
|
300
|
+
expirationTimestamp: status.conversation.expiration_timestamp,
|
|
301
|
+
id: status.conversation.id,
|
|
302
|
+
originType: status.conversation.origin.type,
|
|
303
|
+
},
|
|
304
|
+
}),
|
|
305
|
+
errors: status.errors?.map(normalizeWhatsAppError),
|
|
306
|
+
messageId: status.id,
|
|
307
|
+
phoneNumberId: value.metadata.phone_number_id,
|
|
308
|
+
...(status.pricing && {
|
|
309
|
+
pricing: {
|
|
310
|
+
billable: status.pricing.billable,
|
|
311
|
+
category: status.pricing.category,
|
|
312
|
+
model: status.pricing.pricing_model,
|
|
313
|
+
type: status.pricing.type,
|
|
314
|
+
},
|
|
315
|
+
}),
|
|
316
|
+
providerEvent: status,
|
|
317
|
+
recipientId: status.recipient_id,
|
|
318
|
+
status: status.status,
|
|
319
|
+
timestamp: status.timestamp,
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return { messages, statuses }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Converts one provider-native Meta error to the public event shape.
|
|
329
|
+
*
|
|
330
|
+
* @param error - Provider-native webhook error.
|
|
331
|
+
*/
|
|
332
|
+
function normalizeWhatsAppError(error: z.output<typeof WHATSAPP_ERROR_SCHEMA>) {
|
|
333
|
+
return {
|
|
334
|
+
code: error.code,
|
|
335
|
+
details: error.error_data?.details,
|
|
336
|
+
message: error.message,
|
|
337
|
+
subcode: error.error_subcode,
|
|
338
|
+
title: error.title,
|
|
339
|
+
traceId: error.fbtrace_id,
|
|
340
|
+
}
|
|
341
|
+
}
|