@automate.ax/integration-contracts 0.84.0 → 0.86.1

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.
@@ -15,9 +15,10 @@ import type { stripeTriggerContracts } from "./stripe/index.js";
15
15
  import type { teamsTriggerContracts } from "./teams/index.js";
16
16
  import type { trelloTriggerContracts } from "./trello/index.js";
17
17
  import type { vercelTriggerContracts } from "./vercel/index.js";
18
+ import type { webflowTriggerContracts } from "./webflow/index.js";
18
19
  import type { whatsappTriggerContracts } from "./whatsapp/index.js";
19
20
  import type { z } from "zod";
20
- export type TriggerContractMap = typeof airtableTriggerContracts & typeof asanaTriggerContracts & typeof brevoTriggerContracts & typeof convexTriggerContracts & typeof githubTriggerContracts & typeof gmailTriggerContracts & typeof googleCalendarTriggerContracts & typeof googleFormsTriggerContracts & typeof googleMeetTriggerContracts & typeof linearTriggerContracts & typeof outlookTriggerContracts & typeof resendTriggerContracts & typeof slackTriggerContracts & typeof stripeTriggerContracts & typeof teamsTriggerContracts & typeof trelloTriggerContracts & typeof vercelTriggerContracts & typeof whatsappTriggerContracts;
21
+ export type TriggerContractMap = typeof airtableTriggerContracts & typeof asanaTriggerContracts & typeof brevoTriggerContracts & typeof convexTriggerContracts & typeof githubTriggerContracts & typeof gmailTriggerContracts & typeof googleCalendarTriggerContracts & typeof googleFormsTriggerContracts & typeof googleMeetTriggerContracts & typeof linearTriggerContracts & typeof outlookTriggerContracts & typeof resendTriggerContracts & typeof slackTriggerContracts & typeof stripeTriggerContracts & typeof teamsTriggerContracts & typeof trelloTriggerContracts & typeof vercelTriggerContracts & typeof webflowTriggerContracts & typeof whatsappTriggerContracts;
21
22
  export type IntegrationTriggerType = keyof TriggerContractMap;
22
23
  /** Canonical authoring configuration for one integration trigger type. */
23
24
  export type TriggerConfig<TType extends IntegrationTriggerType> = z.input<TriggerContractMap[TType]["configSchema"]> extends Record<string, never> ? object : z.input<TriggerContractMap[TType]["configSchema"]>;
@@ -0,0 +1,49 @@
1
+ import * as z from "zod";
2
+ import type { WebflowEvent, WebflowSemanticEvent } from "./schemas.js";
3
+ export * from "./schemas.js";
4
+ /** Site selected for one Webflow webhook subscription. */
5
+ export declare const WEBFLOW_TRIGGER_CONFIG_SCHEMA: z.ZodObject<{
6
+ siteId: z.ZodString;
7
+ }, z.core.$strip>;
8
+ type WebflowTriggerContract<TEvent> = {
9
+ readonly configSchema: typeof WEBFLOW_TRIGGER_CONFIG_SCHEMA;
10
+ readonly eventSchema: z.ZodType<TEvent>;
11
+ };
12
+ type WebflowSemanticTriggerTypes = {
13
+ readonly "webflow.collectionItem.changed": "collection_item_changed";
14
+ readonly "webflow.collectionItem.created": "collection_item_created";
15
+ readonly "webflow.collectionItem.deleted": "collection_item_deleted";
16
+ readonly "webflow.collectionItem.published": "collection_item_published";
17
+ readonly "webflow.collectionItem.unpublished": "collection_item_unpublished";
18
+ readonly "webflow.comment.created": "comment_created";
19
+ readonly "webflow.form.submitted": "form_submission";
20
+ readonly "webflow.inventory.changed": "ecomm_inventory_changed";
21
+ readonly "webflow.order.changed": "ecomm_order_changed";
22
+ readonly "webflow.order.created": "ecomm_new_order";
23
+ readonly "webflow.page.created": "page_created";
24
+ readonly "webflow.page.deleted": "page_deleted";
25
+ readonly "webflow.page.metadataUpdated": "page_metadata_updated";
26
+ readonly "webflow.site.published": "site_publish";
27
+ };
28
+ type WebflowTriggerContracts = {
29
+ readonly [TTrigger in keyof WebflowSemanticTriggerTypes]: WebflowTriggerContract<WebflowSemanticEvent<WebflowSemanticTriggerTypes[TTrigger]>>;
30
+ } & {
31
+ readonly "webflow.event": WebflowTriggerContract<WebflowEvent>;
32
+ };
33
+ export declare const webflowTriggerContracts: WebflowTriggerContracts;
34
+ export declare const webflowEventTypeMap: {
35
+ readonly collection_item_changed: "webflow.collectionItem.changed";
36
+ readonly collection_item_created: "webflow.collectionItem.created";
37
+ readonly collection_item_deleted: "webflow.collectionItem.deleted";
38
+ readonly collection_item_published: "webflow.collectionItem.published";
39
+ readonly collection_item_unpublished: "webflow.collectionItem.unpublished";
40
+ readonly comment_created: "webflow.comment.created";
41
+ readonly ecomm_inventory_changed: "webflow.inventory.changed";
42
+ readonly ecomm_new_order: "webflow.order.created";
43
+ readonly ecomm_order_changed: "webflow.order.changed";
44
+ readonly form_submission: "webflow.form.submitted";
45
+ readonly page_created: "webflow.page.created";
46
+ readonly page_deleted: "webflow.page.deleted";
47
+ readonly page_metadata_updated: "webflow.page.metadataUpdated";
48
+ readonly site_publish: "webflow.site.published";
49
+ };
@@ -0,0 +1,54 @@
1
+ import * as z from "zod";
2
+ import { WEBFLOW_EVENT_SCHEMA, webflowSemanticEventSchema } from "./schemas.js";
3
+ export * from "./schemas.js";
4
+ /** Site selected for one Webflow webhook subscription. */
5
+ export const WEBFLOW_TRIGGER_CONFIG_SCHEMA = z.object({
6
+ siteId: z.string().trim().min(1),
7
+ });
8
+ export const webflowTriggerContracts = {
9
+ "webflow.collectionItem.changed": semanticContract("collection_item_changed"),
10
+ "webflow.collectionItem.created": semanticContract("collection_item_created"),
11
+ "webflow.collectionItem.deleted": semanticContract("collection_item_deleted"),
12
+ "webflow.collectionItem.published": semanticContract("collection_item_published"),
13
+ "webflow.collectionItem.unpublished": semanticContract("collection_item_unpublished"),
14
+ "webflow.comment.created": semanticContract("comment_created"),
15
+ "webflow.event": {
16
+ configSchema: WEBFLOW_TRIGGER_CONFIG_SCHEMA,
17
+ eventSchema: WEBFLOW_EVENT_SCHEMA,
18
+ },
19
+ "webflow.form.submitted": semanticContract("form_submission"),
20
+ "webflow.inventory.changed": semanticContract("ecomm_inventory_changed"),
21
+ "webflow.order.changed": semanticContract("ecomm_order_changed"),
22
+ "webflow.order.created": semanticContract("ecomm_new_order"),
23
+ "webflow.page.created": semanticContract("page_created"),
24
+ "webflow.page.deleted": semanticContract("page_deleted"),
25
+ "webflow.page.metadataUpdated": semanticContract("page_metadata_updated"),
26
+ "webflow.site.published": semanticContract("site_publish"),
27
+ };
28
+ export const webflowEventTypeMap = {
29
+ collection_item_changed: "webflow.collectionItem.changed",
30
+ collection_item_created: "webflow.collectionItem.created",
31
+ collection_item_deleted: "webflow.collectionItem.deleted",
32
+ collection_item_published: "webflow.collectionItem.published",
33
+ collection_item_unpublished: "webflow.collectionItem.unpublished",
34
+ comment_created: "webflow.comment.created",
35
+ ecomm_inventory_changed: "webflow.inventory.changed",
36
+ ecomm_new_order: "webflow.order.created",
37
+ ecomm_order_changed: "webflow.order.changed",
38
+ form_submission: "webflow.form.submitted",
39
+ page_created: "webflow.page.created",
40
+ page_deleted: "webflow.page.deleted",
41
+ page_metadata_updated: "webflow.page.metadataUpdated",
42
+ site_publish: "webflow.site.published",
43
+ };
44
+ /**
45
+ * Builds a contract for one semantic Webflow webhook event.
46
+ *
47
+ * @param type - Webflow webhook event type exposed by the contract.
48
+ */
49
+ function semanticContract(type) {
50
+ return {
51
+ configSchema: WEBFLOW_TRIGGER_CONFIG_SCHEMA,
52
+ eventSchema: webflowSemanticEventSchema(type),
53
+ };
54
+ }
@@ -0,0 +1,119 @@
1
+ import { type Encodable } from "@automate.ax/codec";
2
+ import type { Webflow } from "webflow-api";
3
+ import * as z from "zod";
4
+ export declare const WEBFLOW_EVENT_TYPES: readonly ["form_submission", "site_publish", "page_created", "page_metadata_updated", "page_deleted", "ecomm_new_order", "ecomm_order_changed", "ecomm_inventory_changed", "collection_item_created", "collection_item_changed", "collection_item_deleted", "collection_item_published", "collection_item_unpublished", "comment_created"];
5
+ export type WebflowEventType = (typeof WEBFLOW_EVENT_TYPES)[number];
6
+ /** JSON representation of a Webflow ecommerce order webhook payload. */
7
+ export type WebflowOrderEvent = Omit<Webflow.Order, "acceptedOn" | "disputeUpdatedOn" | "disputedOn" | "fulfilledOn" | "refundedOn"> & {
8
+ acceptedOn?: string;
9
+ disputeUpdatedOn?: string | null;
10
+ disputedOn?: string | null;
11
+ fulfilledOn?: string | null;
12
+ refundedOn?: string | null;
13
+ };
14
+ /** JSON representation of a Webflow collection-item webhook payload. */
15
+ export interface WebflowCollectionItemEvent {
16
+ cmsLocaleId?: string;
17
+ collectionId: string;
18
+ createdOn?: string;
19
+ fieldData: Record<string, Encodable>;
20
+ id: string;
21
+ isArchived?: boolean;
22
+ isDraft?: boolean;
23
+ lastPublished?: string | null;
24
+ lastUpdated?: string;
25
+ siteId: string;
26
+ workspaceId: string;
27
+ }
28
+ /** Webflow form-submission webhook payload. */
29
+ export interface WebflowFormSubmissionEvent {
30
+ data: Record<string, Encodable>;
31
+ formElementId?: string;
32
+ formId: string;
33
+ id: string;
34
+ name: string;
35
+ schema: Array<{
36
+ fieldElementId: string;
37
+ fieldName: string;
38
+ fieldType: string;
39
+ }>;
40
+ siteId: string;
41
+ submittedAt: string;
42
+ }
43
+ /** Webflow page lifecycle webhook payload. */
44
+ export interface WebflowPageEvent {
45
+ archived: boolean;
46
+ createdOn?: string;
47
+ deletedOn?: string;
48
+ draft: boolean;
49
+ isBranch: boolean;
50
+ lastUpdated?: string;
51
+ pageId: string;
52
+ pageName: string | null;
53
+ pageTitle: string;
54
+ publishedPath: string;
55
+ siteId: string;
56
+ }
57
+ /** Webflow comment or reply webhook payload. */
58
+ export interface WebflowCommentEvent {
59
+ author: WebflowCommentUser;
60
+ breakpoint: string;
61
+ commentId: string;
62
+ content: string;
63
+ createdOn: string;
64
+ isResolved: boolean;
65
+ lastUpdated: string;
66
+ localeId: string | null;
67
+ mentionedUsers: WebflowCommentUser[];
68
+ pageId: string;
69
+ siteId: string;
70
+ threadId: string;
71
+ type: "new_comment" | "reply";
72
+ url: string;
73
+ }
74
+ /** User identity included in a Webflow comment webhook. */
75
+ export interface WebflowCommentUser {
76
+ email: string;
77
+ name: string;
78
+ userId: string;
79
+ }
80
+ /** One authenticated Webflow webhook envelope. */
81
+ export type WebflowEvent = (WebflowTypedEvent<"collection_item_changed", WebflowCollectionItemEvent> | WebflowTypedEvent<"collection_item_created", WebflowCollectionItemEvent> | WebflowTypedEvent<"collection_item_deleted", WebflowDeletedItemEvent> | WebflowTypedEvent<"collection_item_published", WebflowCollectionItemEvent> | WebflowTypedEvent<"collection_item_unpublished", WebflowDeletedItemEvent> | WebflowTypedEvent<"comment_created", WebflowCommentEvent> | WebflowTypedEvent<"ecomm_inventory_changed", WebflowInventoryEvent> | WebflowTypedEvent<"ecomm_new_order", WebflowOrderEvent> | WebflowTypedEvent<"ecomm_order_changed", WebflowOrderEvent> | WebflowTypedEvent<"form_submission", WebflowFormSubmissionEvent> | WebflowTypedEvent<"page_created", WebflowPageEvent> | WebflowTypedEvent<"page_deleted", WebflowPageEvent> | WebflowTypedEvent<"page_metadata_updated", WebflowPageEvent> | WebflowTypedEvent<"site_publish", WebflowSitePublishEvent>) & Encodable;
82
+ export type WebflowSemanticEvent<TType extends WebflowEventType> = Extract<WebflowEvent, {
83
+ triggerType: TType;
84
+ }>;
85
+ interface WebflowTypedEvent<TType extends WebflowEventType, TPayload> {
86
+ payload: TPayload;
87
+ triggerType: TType;
88
+ }
89
+ interface WebflowDeletedItemEvent {
90
+ cmsLocaleId?: string;
91
+ collectionId: string;
92
+ id: string;
93
+ siteId: string;
94
+ workspaceId: string;
95
+ }
96
+ interface WebflowInventoryEvent {
97
+ id: string;
98
+ inventoryType: "finite" | "infinite";
99
+ quantity?: number;
100
+ }
101
+ interface WebflowSitePublishEvent {
102
+ domains: string[];
103
+ publishScope?: "page" | "site";
104
+ publishTime?: string;
105
+ publishedBy: {
106
+ displayName: string;
107
+ };
108
+ publishedOn?: string;
109
+ site?: string;
110
+ siteId?: string;
111
+ }
112
+ export declare const WEBFLOW_EVENT_SCHEMA: z.ZodCustom<WebflowEvent, WebflowEvent>;
113
+ /**
114
+ * Narrows the Webflow event schema to one webhook event type.
115
+ *
116
+ * @param type - Webflow webhook event type to require.
117
+ */
118
+ export declare function webflowSemanticEventSchema<TType extends WebflowEventType>(type: TType): z.ZodType<WebflowSemanticEvent<TType>>;
119
+ export {};
@@ -0,0 +1,140 @@
1
+ import { isEncodable } from "@automate.ax/codec";
2
+ import * as serializers from "webflow-api/serialization";
3
+ import * as z from "zod";
4
+ export const WEBFLOW_EVENT_TYPES = [
5
+ "form_submission",
6
+ "site_publish",
7
+ "page_created",
8
+ "page_metadata_updated",
9
+ "page_deleted",
10
+ "ecomm_new_order",
11
+ "ecomm_order_changed",
12
+ "ecomm_inventory_changed",
13
+ "collection_item_created",
14
+ "collection_item_changed",
15
+ "collection_item_deleted",
16
+ "collection_item_published",
17
+ "collection_item_unpublished",
18
+ "comment_created",
19
+ ];
20
+ const ENCODABLE_RECORD_SCHEMA = z.record(z.string(), z.custom(isEncodable));
21
+ const COLLECTION_ITEM_SCHEMA = z.looseObject({
22
+ cmsLocaleId: z.string().optional(),
23
+ collectionId: z.string(),
24
+ createdOn: z.string().optional(),
25
+ fieldData: ENCODABLE_RECORD_SCHEMA,
26
+ id: z.string(),
27
+ isArchived: z.boolean().optional(),
28
+ isDraft: z.boolean().optional(),
29
+ lastPublished: z.string().nullable().optional(),
30
+ lastUpdated: z.string().optional(),
31
+ siteId: z.string(),
32
+ workspaceId: z.string(),
33
+ });
34
+ const DELETED_ITEM_SCHEMA = z.looseObject({
35
+ cmsLocaleId: z.string().optional(),
36
+ collectionId: z.string(),
37
+ id: z.string(),
38
+ siteId: z.string(),
39
+ workspaceId: z.string(),
40
+ });
41
+ const FORM_SUBMISSION_SCHEMA = z.looseObject({
42
+ data: ENCODABLE_RECORD_SCHEMA,
43
+ formElementId: z.string().optional(),
44
+ formId: z.string(),
45
+ id: z.string(),
46
+ name: z.string(),
47
+ schema: z.array(z.looseObject({
48
+ fieldElementId: z.string(),
49
+ fieldName: z.string(),
50
+ fieldType: z.string(),
51
+ })),
52
+ siteId: z.string(),
53
+ submittedAt: z.string(),
54
+ });
55
+ const PAGE_SCHEMA = z.looseObject({
56
+ archived: z.boolean(),
57
+ createdOn: z.string().optional(),
58
+ deletedOn: z.string().optional(),
59
+ draft: z.boolean(),
60
+ isBranch: z.boolean(),
61
+ lastUpdated: z.string().optional(),
62
+ pageId: z.string(),
63
+ pageName: z.string().nullable(),
64
+ pageTitle: z.string(),
65
+ publishedPath: z.string(),
66
+ siteId: z.string(),
67
+ });
68
+ const COMMENT_USER_SCHEMA = z.looseObject({
69
+ email: z.string(),
70
+ name: z.string(),
71
+ userId: z.string(),
72
+ });
73
+ const COMMENT_SCHEMA = z.looseObject({
74
+ author: COMMENT_USER_SCHEMA,
75
+ breakpoint: z.string(),
76
+ commentId: z.string(),
77
+ content: z.string(),
78
+ createdOn: z.string(),
79
+ isResolved: z.boolean(),
80
+ lastUpdated: z.string(),
81
+ localeId: z.string().nullable(),
82
+ mentionedUsers: z.array(COMMENT_USER_SCHEMA),
83
+ pageId: z.string(),
84
+ siteId: z.string(),
85
+ threadId: z.string(),
86
+ type: z.enum(["new_comment", "reply"]),
87
+ url: z.string(),
88
+ });
89
+ const INVENTORY_SCHEMA = z.looseObject({
90
+ id: z.string(),
91
+ inventoryType: z.enum(["finite", "infinite"]),
92
+ quantity: z.number().optional(),
93
+ });
94
+ const ORDER_SCHEMA = z.custom((value) => serializers.Order.parse(value, { unrecognizedObjectKeys: "fail" }).ok, { message: "Expected a Webflow ecommerce order." });
95
+ const SITE_PUBLISH_SCHEMA = z.looseObject({
96
+ domains: z.array(z.string()),
97
+ publishScope: z.enum(["page", "site"]).optional(),
98
+ publishTime: z.string().optional(),
99
+ publishedBy: z.looseObject({ displayName: z.string() }),
100
+ publishedOn: z.string().optional(),
101
+ site: z.string().optional(),
102
+ siteId: z.string().optional(),
103
+ });
104
+ const WEBFLOW_BASE_EVENT_SCHEMA = z.discriminatedUnion("triggerType", [
105
+ typedEventSchema("collection_item_changed", COLLECTION_ITEM_SCHEMA),
106
+ typedEventSchema("collection_item_created", COLLECTION_ITEM_SCHEMA),
107
+ typedEventSchema("collection_item_deleted", DELETED_ITEM_SCHEMA),
108
+ typedEventSchema("collection_item_published", COLLECTION_ITEM_SCHEMA),
109
+ typedEventSchema("collection_item_unpublished", DELETED_ITEM_SCHEMA),
110
+ typedEventSchema("comment_created", COMMENT_SCHEMA),
111
+ typedEventSchema("ecomm_inventory_changed", INVENTORY_SCHEMA),
112
+ typedEventSchema("ecomm_new_order", ORDER_SCHEMA),
113
+ typedEventSchema("ecomm_order_changed", ORDER_SCHEMA),
114
+ typedEventSchema("form_submission", FORM_SUBMISSION_SCHEMA),
115
+ typedEventSchema("page_created", PAGE_SCHEMA),
116
+ typedEventSchema("page_deleted", PAGE_SCHEMA),
117
+ typedEventSchema("page_metadata_updated", PAGE_SCHEMA),
118
+ typedEventSchema("site_publish", SITE_PUBLISH_SCHEMA),
119
+ ]);
120
+ export const WEBFLOW_EVENT_SCHEMA = z.custom((value) => WEBFLOW_BASE_EVENT_SCHEMA.safeParse(value).success, { message: "Expected a Webflow webhook event." });
121
+ /**
122
+ * Narrows the Webflow event schema to one webhook event type.
123
+ *
124
+ * @param type - Webflow webhook event type to require.
125
+ */
126
+ export function webflowSemanticEventSchema(type) {
127
+ return z.custom((value) => {
128
+ const event = WEBFLOW_EVENT_SCHEMA.safeParse(value);
129
+ return event.success && event.data.triggerType === type;
130
+ });
131
+ }
132
+ /**
133
+ * Builds one concrete Webflow event-envelope schema.
134
+ *
135
+ * @param type - Provider event discriminator.
136
+ * @param payloadSchema - Schema for that event's payload.
137
+ */
138
+ function typedEventSchema(type, payloadSchema) {
139
+ return z.looseObject({ payload: payloadSchema, triggerType: z.literal(type) });
140
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automate.ax/integration-contracts",
3
- "version": "0.84.0",
3
+ "version": "0.86.1",
4
4
  "description": "Shared integration payload contracts and provider primitives for Automate.ax.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,6 +36,7 @@
36
36
  "./trello": "./src/trello/index.ts",
37
37
  "./triggers": "./src/triggers.ts",
38
38
  "./vercel": "./src/vercel/index.ts",
39
+ "./webflow": "./src/webflow/index.ts",
39
40
  "./whatsapp": "./src/whatsapp/index.ts"
40
41
  },
41
42
  "cjs": false,
@@ -44,7 +45,7 @@
44
45
  }
45
46
  },
46
47
  "dependencies": {
47
- "@automate.ax/codec": "0.84.0",
48
+ "@automate.ax/codec": "0.86.1",
48
49
  "@googleapis/calendar": "^15.0.0",
49
50
  "@googleapis/forms": "^6.0.1",
50
51
  "@googleapis/gmail": "^12.0.0",
@@ -57,10 +58,11 @@
57
58
  "postal-mime": "^2.7.5",
58
59
  "stripe": "^22.1.0",
59
60
  "type-fest": "^5.6.0",
61
+ "webflow-api": "^3.3.4",
60
62
  "zod": "^4.3.6"
61
63
  },
62
64
  "devDependencies": {
63
- "@internal/config": "0.1.0",
65
+ "@internal/config": "0.2.0",
64
66
  "@types/bun": "latest",
65
67
  "@types/html-to-text": "^9.0.4",
66
68
  "@types/nodemailer": "^8.0.1",
@@ -180,6 +182,11 @@
180
182
  "types": "./dist/vercel/index.d.ts",
181
183
  "default": "./dist/vercel/index.js"
182
184
  },
185
+ "./webflow": {
186
+ "bun": "./src/webflow/index.ts",
187
+ "types": "./dist/webflow/index.d.ts",
188
+ "default": "./dist/webflow/index.js"
189
+ },
183
190
  "./whatsapp": {
184
191
  "bun": "./src/whatsapp/index.ts",
185
192
  "types": "./dist/whatsapp/index.d.ts",
package/src/triggers.ts CHANGED
@@ -15,6 +15,7 @@ import type { stripeTriggerContracts } from "./stripe"
15
15
  import type { teamsTriggerContracts } from "./teams"
16
16
  import type { trelloTriggerContracts } from "./trello"
17
17
  import type { vercelTriggerContracts } from "./vercel"
18
+ import type { webflowTriggerContracts } from "./webflow"
18
19
  import type { whatsappTriggerContracts } from "./whatsapp"
19
20
  import type { z } from "zod"
20
21
 
@@ -35,6 +36,7 @@ export type TriggerContractMap = typeof airtableTriggerContracts &
35
36
  typeof teamsTriggerContracts &
36
37
  typeof trelloTriggerContracts &
37
38
  typeof vercelTriggerContracts &
39
+ typeof webflowTriggerContracts &
38
40
  typeof whatsappTriggerContracts
39
41
  export type IntegrationTriggerType = keyof TriggerContractMap
40
42
 
@@ -0,0 +1,98 @@
1
+ import * as z from "zod"
2
+ import type {
3
+ WebflowEvent,
4
+ WebflowEventType,
5
+ WebflowSemanticEvent,
6
+ } from "./schemas"
7
+ import { WEBFLOW_EVENT_SCHEMA, webflowSemanticEventSchema } from "./schemas"
8
+
9
+ export * from "./schemas"
10
+
11
+ /** Site selected for one Webflow webhook subscription. */
12
+ export const WEBFLOW_TRIGGER_CONFIG_SCHEMA = z.object({
13
+ siteId: z.string().trim().min(1),
14
+ })
15
+
16
+ type WebflowTriggerContract<TEvent> = {
17
+ readonly configSchema: typeof WEBFLOW_TRIGGER_CONFIG_SCHEMA
18
+ readonly eventSchema: z.ZodType<TEvent>
19
+ }
20
+
21
+ type WebflowSemanticTriggerTypes = {
22
+ readonly "webflow.collectionItem.changed": "collection_item_changed"
23
+ readonly "webflow.collectionItem.created": "collection_item_created"
24
+ readonly "webflow.collectionItem.deleted": "collection_item_deleted"
25
+ readonly "webflow.collectionItem.published": "collection_item_published"
26
+ readonly "webflow.collectionItem.unpublished": "collection_item_unpublished"
27
+ readonly "webflow.comment.created": "comment_created"
28
+ readonly "webflow.form.submitted": "form_submission"
29
+ readonly "webflow.inventory.changed": "ecomm_inventory_changed"
30
+ readonly "webflow.order.changed": "ecomm_order_changed"
31
+ readonly "webflow.order.created": "ecomm_new_order"
32
+ readonly "webflow.page.created": "page_created"
33
+ readonly "webflow.page.deleted": "page_deleted"
34
+ readonly "webflow.page.metadataUpdated": "page_metadata_updated"
35
+ readonly "webflow.site.published": "site_publish"
36
+ }
37
+
38
+ type WebflowTriggerContracts = {
39
+ readonly [TTrigger in keyof WebflowSemanticTriggerTypes]: WebflowTriggerContract<
40
+ WebflowSemanticEvent<WebflowSemanticTriggerTypes[TTrigger]>
41
+ >
42
+ } & {
43
+ readonly "webflow.event": WebflowTriggerContract<WebflowEvent>
44
+ }
45
+
46
+ export const webflowTriggerContracts: WebflowTriggerContracts = {
47
+ "webflow.collectionItem.changed": semanticContract("collection_item_changed"),
48
+ "webflow.collectionItem.created": semanticContract("collection_item_created"),
49
+ "webflow.collectionItem.deleted": semanticContract("collection_item_deleted"),
50
+ "webflow.collectionItem.published": semanticContract(
51
+ "collection_item_published",
52
+ ),
53
+ "webflow.collectionItem.unpublished": semanticContract(
54
+ "collection_item_unpublished",
55
+ ),
56
+ "webflow.comment.created": semanticContract("comment_created"),
57
+ "webflow.event": {
58
+ configSchema: WEBFLOW_TRIGGER_CONFIG_SCHEMA,
59
+ eventSchema: WEBFLOW_EVENT_SCHEMA,
60
+ },
61
+ "webflow.form.submitted": semanticContract("form_submission"),
62
+ "webflow.inventory.changed": semanticContract("ecomm_inventory_changed"),
63
+ "webflow.order.changed": semanticContract("ecomm_order_changed"),
64
+ "webflow.order.created": semanticContract("ecomm_new_order"),
65
+ "webflow.page.created": semanticContract("page_created"),
66
+ "webflow.page.deleted": semanticContract("page_deleted"),
67
+ "webflow.page.metadataUpdated": semanticContract("page_metadata_updated"),
68
+ "webflow.site.published": semanticContract("site_publish"),
69
+ }
70
+
71
+ export const webflowEventTypeMap = {
72
+ collection_item_changed: "webflow.collectionItem.changed",
73
+ collection_item_created: "webflow.collectionItem.created",
74
+ collection_item_deleted: "webflow.collectionItem.deleted",
75
+ collection_item_published: "webflow.collectionItem.published",
76
+ collection_item_unpublished: "webflow.collectionItem.unpublished",
77
+ comment_created: "webflow.comment.created",
78
+ ecomm_inventory_changed: "webflow.inventory.changed",
79
+ ecomm_new_order: "webflow.order.created",
80
+ ecomm_order_changed: "webflow.order.changed",
81
+ form_submission: "webflow.form.submitted",
82
+ page_created: "webflow.page.created",
83
+ page_deleted: "webflow.page.deleted",
84
+ page_metadata_updated: "webflow.page.metadataUpdated",
85
+ site_publish: "webflow.site.published",
86
+ } as const satisfies Record<WebflowEventType, keyof WebflowTriggerContracts>
87
+
88
+ /**
89
+ * Builds a contract for one semantic Webflow webhook event.
90
+ *
91
+ * @param type - Webflow webhook event type exposed by the contract.
92
+ */
93
+ function semanticContract<TType extends WebflowEventType>(type: TType) {
94
+ return {
95
+ configSchema: WEBFLOW_TRIGGER_CONFIG_SCHEMA,
96
+ eventSchema: webflowSemanticEventSchema(type),
97
+ }
98
+ }
@@ -0,0 +1,306 @@
1
+ import { isEncodable, type Encodable } from "@automate.ax/codec"
2
+ import type { Webflow } from "webflow-api"
3
+ import * as serializers from "webflow-api/serialization"
4
+ import * as z from "zod"
5
+
6
+ export const WEBFLOW_EVENT_TYPES = [
7
+ "form_submission",
8
+ "site_publish",
9
+ "page_created",
10
+ "page_metadata_updated",
11
+ "page_deleted",
12
+ "ecomm_new_order",
13
+ "ecomm_order_changed",
14
+ "ecomm_inventory_changed",
15
+ "collection_item_created",
16
+ "collection_item_changed",
17
+ "collection_item_deleted",
18
+ "collection_item_published",
19
+ "collection_item_unpublished",
20
+ "comment_created",
21
+ ] as const satisfies readonly Webflow.TriggerType[]
22
+
23
+ export type WebflowEventType = (typeof WEBFLOW_EVENT_TYPES)[number]
24
+
25
+ /** JSON representation of a Webflow ecommerce order webhook payload. */
26
+ export type WebflowOrderEvent = Omit<
27
+ Webflow.Order,
28
+ | "acceptedOn"
29
+ | "disputeUpdatedOn"
30
+ | "disputedOn"
31
+ | "fulfilledOn"
32
+ | "refundedOn"
33
+ > & {
34
+ acceptedOn?: string
35
+ disputeUpdatedOn?: string | null
36
+ disputedOn?: string | null
37
+ fulfilledOn?: string | null
38
+ refundedOn?: string | null
39
+ }
40
+
41
+ /** JSON representation of a Webflow collection-item webhook payload. */
42
+ export interface WebflowCollectionItemEvent {
43
+ cmsLocaleId?: string
44
+ collectionId: string
45
+ createdOn?: string
46
+ fieldData: Record<string, Encodable>
47
+ id: string
48
+ isArchived?: boolean
49
+ isDraft?: boolean
50
+ lastPublished?: string | null
51
+ lastUpdated?: string
52
+ siteId: string
53
+ workspaceId: string
54
+ }
55
+
56
+ /** Webflow form-submission webhook payload. */
57
+ export interface WebflowFormSubmissionEvent {
58
+ data: Record<string, Encodable>
59
+ formElementId?: string
60
+ formId: string
61
+ id: string
62
+ name: string
63
+ schema: Array<{
64
+ fieldElementId: string
65
+ fieldName: string
66
+ fieldType: string
67
+ }>
68
+ siteId: string
69
+ submittedAt: string
70
+ }
71
+
72
+ /** Webflow page lifecycle webhook payload. */
73
+ export interface WebflowPageEvent {
74
+ archived: boolean
75
+ createdOn?: string
76
+ deletedOn?: string
77
+ draft: boolean
78
+ isBranch: boolean
79
+ lastUpdated?: string
80
+ pageId: string
81
+ pageName: string | null
82
+ pageTitle: string
83
+ publishedPath: string
84
+ siteId: string
85
+ }
86
+
87
+ /** Webflow comment or reply webhook payload. */
88
+ export interface WebflowCommentEvent {
89
+ author: WebflowCommentUser
90
+ breakpoint: string
91
+ commentId: string
92
+ content: string
93
+ createdOn: string
94
+ isResolved: boolean
95
+ lastUpdated: string
96
+ localeId: string | null
97
+ mentionedUsers: WebflowCommentUser[]
98
+ pageId: string
99
+ siteId: string
100
+ threadId: string
101
+ type: "new_comment" | "reply"
102
+ url: string
103
+ }
104
+
105
+ /** User identity included in a Webflow comment webhook. */
106
+ export interface WebflowCommentUser {
107
+ email: string
108
+ name: string
109
+ userId: string
110
+ }
111
+
112
+ /** One authenticated Webflow webhook envelope. */
113
+ export type WebflowEvent = (
114
+ | WebflowTypedEvent<"collection_item_changed", WebflowCollectionItemEvent>
115
+ | WebflowTypedEvent<"collection_item_created", WebflowCollectionItemEvent>
116
+ | WebflowTypedEvent<"collection_item_deleted", WebflowDeletedItemEvent>
117
+ | WebflowTypedEvent<"collection_item_published", WebflowCollectionItemEvent>
118
+ | WebflowTypedEvent<"collection_item_unpublished", WebflowDeletedItemEvent>
119
+ | WebflowTypedEvent<"comment_created", WebflowCommentEvent>
120
+ | WebflowTypedEvent<"ecomm_inventory_changed", WebflowInventoryEvent>
121
+ | WebflowTypedEvent<"ecomm_new_order", WebflowOrderEvent>
122
+ | WebflowTypedEvent<"ecomm_order_changed", WebflowOrderEvent>
123
+ | WebflowTypedEvent<"form_submission", WebflowFormSubmissionEvent>
124
+ | WebflowTypedEvent<"page_created", WebflowPageEvent>
125
+ | WebflowTypedEvent<"page_deleted", WebflowPageEvent>
126
+ | WebflowTypedEvent<"page_metadata_updated", WebflowPageEvent>
127
+ | WebflowTypedEvent<"site_publish", WebflowSitePublishEvent>
128
+ ) &
129
+ Encodable
130
+
131
+ export type WebflowSemanticEvent<TType extends WebflowEventType> = Extract<
132
+ WebflowEvent,
133
+ { triggerType: TType }
134
+ >
135
+
136
+ interface WebflowTypedEvent<TType extends WebflowEventType, TPayload> {
137
+ payload: TPayload
138
+ triggerType: TType
139
+ }
140
+
141
+ interface WebflowDeletedItemEvent {
142
+ cmsLocaleId?: string
143
+ collectionId: string
144
+ id: string
145
+ siteId: string
146
+ workspaceId: string
147
+ }
148
+
149
+ interface WebflowInventoryEvent {
150
+ id: string
151
+ inventoryType: "finite" | "infinite"
152
+ quantity?: number
153
+ }
154
+
155
+ interface WebflowSitePublishEvent {
156
+ domains: string[]
157
+ publishScope?: "page" | "site"
158
+ publishTime?: string
159
+ publishedBy: { displayName: string }
160
+ publishedOn?: string
161
+ site?: string
162
+ siteId?: string
163
+ }
164
+
165
+ const ENCODABLE_RECORD_SCHEMA = z.record(
166
+ z.string(),
167
+ z.custom<Encodable>(isEncodable),
168
+ )
169
+ const COLLECTION_ITEM_SCHEMA = z.looseObject({
170
+ cmsLocaleId: z.string().optional(),
171
+ collectionId: z.string(),
172
+ createdOn: z.string().optional(),
173
+ fieldData: ENCODABLE_RECORD_SCHEMA,
174
+ id: z.string(),
175
+ isArchived: z.boolean().optional(),
176
+ isDraft: z.boolean().optional(),
177
+ lastPublished: z.string().nullable().optional(),
178
+ lastUpdated: z.string().optional(),
179
+ siteId: z.string(),
180
+ workspaceId: z.string(),
181
+ })
182
+ const DELETED_ITEM_SCHEMA = z.looseObject({
183
+ cmsLocaleId: z.string().optional(),
184
+ collectionId: z.string(),
185
+ id: z.string(),
186
+ siteId: z.string(),
187
+ workspaceId: z.string(),
188
+ })
189
+ const FORM_SUBMISSION_SCHEMA = z.looseObject({
190
+ data: ENCODABLE_RECORD_SCHEMA,
191
+ formElementId: z.string().optional(),
192
+ formId: z.string(),
193
+ id: z.string(),
194
+ name: z.string(),
195
+ schema: z.array(
196
+ z.looseObject({
197
+ fieldElementId: z.string(),
198
+ fieldName: z.string(),
199
+ fieldType: z.string(),
200
+ }),
201
+ ),
202
+ siteId: z.string(),
203
+ submittedAt: z.string(),
204
+ })
205
+ const PAGE_SCHEMA = z.looseObject({
206
+ archived: z.boolean(),
207
+ createdOn: z.string().optional(),
208
+ deletedOn: z.string().optional(),
209
+ draft: z.boolean(),
210
+ isBranch: z.boolean(),
211
+ lastUpdated: z.string().optional(),
212
+ pageId: z.string(),
213
+ pageName: z.string().nullable(),
214
+ pageTitle: z.string(),
215
+ publishedPath: z.string(),
216
+ siteId: z.string(),
217
+ })
218
+ const COMMENT_USER_SCHEMA = z.looseObject({
219
+ email: z.string(),
220
+ name: z.string(),
221
+ userId: z.string(),
222
+ })
223
+ const COMMENT_SCHEMA = z.looseObject({
224
+ author: COMMENT_USER_SCHEMA,
225
+ breakpoint: z.string(),
226
+ commentId: z.string(),
227
+ content: z.string(),
228
+ createdOn: z.string(),
229
+ isResolved: z.boolean(),
230
+ lastUpdated: z.string(),
231
+ localeId: z.string().nullable(),
232
+ mentionedUsers: z.array(COMMENT_USER_SCHEMA),
233
+ pageId: z.string(),
234
+ siteId: z.string(),
235
+ threadId: z.string(),
236
+ type: z.enum(["new_comment", "reply"]),
237
+ url: z.string(),
238
+ })
239
+ const INVENTORY_SCHEMA = z.looseObject({
240
+ id: z.string(),
241
+ inventoryType: z.enum(["finite", "infinite"]),
242
+ quantity: z.number().optional(),
243
+ })
244
+ const ORDER_SCHEMA = z.custom<WebflowOrderEvent>(
245
+ (value) =>
246
+ serializers.Order.parse(value, { unrecognizedObjectKeys: "fail" }).ok,
247
+ { message: "Expected a Webflow ecommerce order." },
248
+ )
249
+ const SITE_PUBLISH_SCHEMA = z.looseObject({
250
+ domains: z.array(z.string()),
251
+ publishScope: z.enum(["page", "site"]).optional(),
252
+ publishTime: z.string().optional(),
253
+ publishedBy: z.looseObject({ displayName: z.string() }),
254
+ publishedOn: z.string().optional(),
255
+ site: z.string().optional(),
256
+ siteId: z.string().optional(),
257
+ })
258
+
259
+ const WEBFLOW_BASE_EVENT_SCHEMA = z.discriminatedUnion("triggerType", [
260
+ typedEventSchema("collection_item_changed", COLLECTION_ITEM_SCHEMA),
261
+ typedEventSchema("collection_item_created", COLLECTION_ITEM_SCHEMA),
262
+ typedEventSchema("collection_item_deleted", DELETED_ITEM_SCHEMA),
263
+ typedEventSchema("collection_item_published", COLLECTION_ITEM_SCHEMA),
264
+ typedEventSchema("collection_item_unpublished", DELETED_ITEM_SCHEMA),
265
+ typedEventSchema("comment_created", COMMENT_SCHEMA),
266
+ typedEventSchema("ecomm_inventory_changed", INVENTORY_SCHEMA),
267
+ typedEventSchema("ecomm_new_order", ORDER_SCHEMA),
268
+ typedEventSchema("ecomm_order_changed", ORDER_SCHEMA),
269
+ typedEventSchema("form_submission", FORM_SUBMISSION_SCHEMA),
270
+ typedEventSchema("page_created", PAGE_SCHEMA),
271
+ typedEventSchema("page_deleted", PAGE_SCHEMA),
272
+ typedEventSchema("page_metadata_updated", PAGE_SCHEMA),
273
+ typedEventSchema("site_publish", SITE_PUBLISH_SCHEMA),
274
+ ])
275
+
276
+ export const WEBFLOW_EVENT_SCHEMA = z.custom<WebflowEvent>(
277
+ (value) => WEBFLOW_BASE_EVENT_SCHEMA.safeParse(value).success,
278
+ { message: "Expected a Webflow webhook event." },
279
+ )
280
+
281
+ /**
282
+ * Narrows the Webflow event schema to one webhook event type.
283
+ *
284
+ * @param type - Webflow webhook event type to require.
285
+ */
286
+ export function webflowSemanticEventSchema<TType extends WebflowEventType>(
287
+ type: TType,
288
+ ): z.ZodType<WebflowSemanticEvent<TType>> {
289
+ return z.custom<WebflowSemanticEvent<TType>>((value) => {
290
+ const event = WEBFLOW_EVENT_SCHEMA.safeParse(value)
291
+ return event.success && event.data.triggerType === type
292
+ })
293
+ }
294
+
295
+ /**
296
+ * Builds one concrete Webflow event-envelope schema.
297
+ *
298
+ * @param type - Provider event discriminator.
299
+ * @param payloadSchema - Schema for that event's payload.
300
+ */
301
+ function typedEventSchema<TType extends WebflowEventType>(
302
+ type: TType,
303
+ payloadSchema: z.ZodType,
304
+ ) {
305
+ return z.looseObject({ payload: payloadSchema, triggerType: z.literal(type) })
306
+ }