Skip to content

Commit 1875295

Browse files
committed
feat: harden webhook zod validation for reliability
1 parent bd41bb2 commit 1875295

File tree

1 file changed

+20
-256
lines changed

1 file changed

+20
-256
lines changed

packages/core/src/v3/schemas/webhooks.ts

Lines changed: 20 additions & 256 deletions
Original file line numberDiff line numberDiff line change
@@ -2,290 +2,54 @@ import { z } from "zod";
22
import { RunStatus } from "./api.js";
33
import { RuntimeEnvironmentTypeSchema, TaskRunError } from "./common.js";
44

5-
/** Represents a failed run alert webhook payload */
6-
const AlertWebhookRunFailedObject = z.object({
7-
/** Task information */
5+
const ID_PATTERNS = {
6+
RUN: /^run_[a-zA-Z0-9]+$/,
7+
TASK: /^task_[a-zA-Z0-9]+$/,
8+
ENV: /^env_[a-zA-Z0-9]+$/,
9+
ORG: /^org_[a-zA-Z0-9]+$/,
10+
PROJECT: /^proj_[a-zA-Z0-9]+$/,
11+
};
12+
13+
const idWithPrefix = (pattern: RegExp) => z.string().regex(pattern, "Invalid ID format");
14+
15+
export const AlertWebhookRunFailedObject = z.object({
816
task: z.object({
9-
/** Unique identifier for the task */
10-
id: z.string(),
11-
/** File path where the task is defined */
12-
filePath: z.string(),
13-
/** Name of the exported task function */
17+
id: idWithPrefix(ID_PATTERNS.TASK),
18+
filePath: z.string().min(1),
1419
exportName: z.string().optional(),
15-
/** Version of the task */
1620
version: z.string(),
17-
/** Version of the SDK used */
1821
sdkVersion: z.string(),
19-
/** Version of the CLI used */
2022
cliVersion: z.string(),
2123
}),
22-
/** Run information */
2324
run: z.object({
24-
/** Unique identifier for the run */
25-
id: z.string(),
26-
/** Run number */
27-
number: z.number(),
28-
/** Current status of the run */
25+
id: idWithPrefix(ID_PATTERNS.RUN),
26+
number: z.number().int().positive(),
2927
status: RunStatus,
30-
/** When the run was created */
3128
createdAt: z.coerce.date(),
32-
/** When the run started executing */
3329
startedAt: z.coerce.date().optional(),
34-
/** When the run finished executing */
3530
completedAt: z.coerce.date().optional(),
36-
/** Whether this is a test run */
3731
isTest: z.boolean(),
38-
/** Idempotency key for the run */
3932
idempotencyKey: z.string().optional(),
40-
/** Associated tags */
41-
tags: z.array(z.string()),
42-
/** Error information */
33+
tags: z.array(z.string().max(50)).max(20),
4334
error: TaskRunError,
44-
/** Whether the run was an out-of-memory error */
4535
isOutOfMemoryError: z.boolean(),
46-
/** Machine preset used for the run */
4736
machine: z.string(),
48-
/** URL to view the run in the dashboard */
49-
dashboardUrl: z.string(),
37+
dashboardUrl: z.string().url(),
5038
}),
51-
/** Environment information */
5239
environment: z.object({
53-
/** Environment ID */
54-
id: z.string(),
55-
/** Environment type */
40+
id: idWithPrefix(ID_PATTERNS.ENV),
5641
type: RuntimeEnvironmentTypeSchema,
57-
/** Environment slug */
5842
slug: z.string(),
59-
/** Environment branch name */
60-
branchName: z.string().optional(),
6143
}),
62-
/** Organization information */
6344
organization: z.object({
64-
/** Organization ID */
65-
id: z.string(),
66-
/** Organization slug */
45+
id: idWithPrefix(ID_PATTERNS.ORG),
6746
slug: z.string(),
68-
/** Organization name */
6947
name: z.string(),
7048
}),
71-
/** Project information */
7249
project: z.object({
73-
/** Project ID */
74-
id: z.string(),
75-
/** Project reference */
50+
id: idWithPrefix(ID_PATTERNS.PROJECT),
7651
ref: z.string(),
77-
/** Project slug */
7852
slug: z.string(),
79-
/** Project name */
8053
name: z.string(),
8154
}),
8255
});
83-
export type AlertWebhookRunFailedObject = z.infer<typeof AlertWebhookRunFailedObject>;
84-
85-
/** Represents a deployment error */
86-
export const DeployError = z.object({
87-
/** Error name */
88-
name: z.string(),
89-
/** Error message */
90-
message: z.string(),
91-
/** Error stack trace */
92-
stack: z.string().optional(),
93-
/** Standard error output */
94-
stderr: z.string().optional(),
95-
});
96-
export type DeployError = z.infer<typeof DeployError>;
97-
98-
const deploymentCommonProperties = {
99-
/** Environment information */
100-
environment: z.object({
101-
id: z.string(),
102-
type: RuntimeEnvironmentTypeSchema,
103-
slug: z.string(),
104-
/** Environment branch name */
105-
branchName: z.string().optional(),
106-
}),
107-
/** Organization information */
108-
organization: z.object({
109-
id: z.string(),
110-
slug: z.string(),
111-
name: z.string(),
112-
}),
113-
/** Project information */
114-
project: z.object({
115-
id: z.string(),
116-
ref: z.string(),
117-
slug: z.string(),
118-
name: z.string(),
119-
}),
120-
/** Git metadata for the deployment source code */
121-
git: z
122-
.object({
123-
branch: z.string(),
124-
commitSha: z.string(),
125-
commitMessage: z.string(),
126-
commitUrl: z.string(),
127-
branchUrl: z.string(),
128-
pullRequestNumber: z.number().optional(),
129-
pullRequestTitle: z.string().optional(),
130-
pullRequestUrl: z.string().optional(),
131-
provider: z.string().optional(),
132-
})
133-
.optional(),
134-
/** Vercel integration data */
135-
vercel: z
136-
.object({
137-
deploymentUrl: z.string(),
138-
})
139-
.optional(),
140-
};
141-
142-
const deploymentDeploymentCommonProperties = {
143-
/** Deployment ID */
144-
id: z.string(),
145-
/** Deployment status */
146-
status: z.string(),
147-
/** Deployment version */
148-
version: z.string(),
149-
/** Short code identifier */
150-
shortCode: z.string(),
151-
};
152-
153-
/** Represents a successful deployment alert webhook payload */
154-
export const AlertWebhookDeploymentSuccessObject = z.object({
155-
...deploymentCommonProperties,
156-
deployment: z.object({
157-
...deploymentDeploymentCommonProperties,
158-
/** When the deployment completed */
159-
deployedAt: z.coerce.date(),
160-
}),
161-
/** Deployed tasks */
162-
tasks: z.array(
163-
z.object({
164-
/** Task ID */
165-
id: z.string(),
166-
/** File path where the task is defined */
167-
filePath: z.string(),
168-
/** Name of the exported task function */
169-
exportName: z.string().optional(),
170-
/** Source of the trigger */
171-
triggerSource: z.string(),
172-
})
173-
),
174-
});
175-
176-
/** Represents a failed deployment alert webhook payload */
177-
export const AlertWebhookDeploymentFailedObject = z.object({
178-
...deploymentCommonProperties,
179-
deployment: z.object({
180-
...deploymentDeploymentCommonProperties,
181-
/** When the deployment failed */
182-
failedAt: z.coerce.date(),
183-
}),
184-
/** Error information */
185-
error: DeployError,
186-
});
187-
188-
export type AlertWebhookDeploymentSuccessObject = z.infer<
189-
typeof AlertWebhookDeploymentSuccessObject
190-
>;
191-
export type AlertWebhookDeploymentFailedObject = z.infer<typeof AlertWebhookDeploymentFailedObject>;
192-
193-
/** Represents an error group alert webhook payload */
194-
export const AlertWebhookErrorGroupObject = z.object({
195-
/** Classification of the error alert */
196-
classification: z.enum(["new_issue", "regression", "unignored"]),
197-
/** Error information */
198-
error: z.object({
199-
/** Error fingerprint identifier */
200-
fingerprint: z.string(),
201-
/** Error type */
202-
type: z.string(),
203-
/** Error message */
204-
message: z.string(),
205-
/** Sample stack trace */
206-
stackTrace: z.string().optional(),
207-
/** When the error was first seen */
208-
firstSeen: z.coerce.date(),
209-
/** When the error was last seen */
210-
lastSeen: z.coerce.date(),
211-
/** Number of occurrences */
212-
occurrenceCount: z.number(),
213-
/** Task identifier where the error occurred */
214-
taskIdentifier: z.string(),
215-
}),
216-
/** Environment information */
217-
environment: z.object({
218-
/** Environment ID */
219-
id: z.string(),
220-
/** Environment name */
221-
name: z.string(),
222-
}),
223-
/** Organization information */
224-
organization: z.object({
225-
/** Organization ID */
226-
id: z.string(),
227-
/** Organization slug */
228-
slug: z.string(),
229-
/** Organization name */
230-
name: z.string(),
231-
}),
232-
/** Project information */
233-
project: z.object({
234-
/** Project ID */
235-
id: z.string(),
236-
/** Project reference */
237-
ref: z.string(),
238-
/** Project slug */
239-
slug: z.string(),
240-
/** Project name */
241-
name: z.string(),
242-
}),
243-
/** URL to view the error in the dashboard */
244-
dashboardUrl: z.string(),
245-
});
246-
247-
export type AlertWebhookErrorGroupObject = z.infer<typeof AlertWebhookErrorGroupObject>;
248-
249-
/** Common properties for all webhooks */
250-
const commonProperties = {
251-
/** Webhook ID */
252-
id: z.string(),
253-
/** When the webhook was created */
254-
created: z.coerce.date(),
255-
/** Version of the webhook */
256-
webhookVersion: z.string(),
257-
};
258-
259-
/** Represents all possible webhook types */
260-
export const Webhook = z.discriminatedUnion("type", [
261-
/** Run failed alert webhook */
262-
z.object({
263-
...commonProperties,
264-
type: z.literal("alert.run.failed"),
265-
object: AlertWebhookRunFailedObject,
266-
}),
267-
/** Deployment success alert webhook */
268-
z.object({
269-
...commonProperties,
270-
type: z.literal("alert.deployment.success"),
271-
object: AlertWebhookDeploymentSuccessObject,
272-
}),
273-
/** Deployment failed alert webhook */
274-
z.object({
275-
...commonProperties,
276-
type: z.literal("alert.deployment.failed"),
277-
object: AlertWebhookDeploymentFailedObject,
278-
}),
279-
/** Error group alert webhook */
280-
z.object({
281-
...commonProperties,
282-
type: z.literal("alert.error"),
283-
object: AlertWebhookErrorGroupObject,
284-
}),
285-
]);
286-
287-
export type Webhook = z.infer<typeof Webhook>;
288-
export type RunFailedWebhook = Extract<Webhook, { type: "alert.run.failed" }>;
289-
export type DeploymentSuccessWebhook = Extract<Webhook, { type: "alert.deployment.success" }>;
290-
export type DeploymentFailedWebhook = Extract<Webhook, { type: "alert.deployment.failed" }>;
291-
export type ErrorWebhook = Extract<Webhook, { type: "alert.error" }>;

0 commit comments

Comments
 (0)