-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathwebhook.zod.ts
More file actions
143 lines (125 loc) · 5.1 KB
/
webhook.zod.ts
File metadata and controls
143 lines (125 loc) · 5.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { z } from 'zod';
import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod';
/**
* Webhook Trigger Event
* When should this webhook fire?
*/
export const WebhookTriggerType = z.enum([
'create',
'update',
'delete',
'undelete',
'api' // Manually triggered
]);
/**
* CANONICAL WEBHOOK DEFINITION
*
* This is the single source of truth for webhook configuration across ObjectStack.
* All other protocols (workflow, connector, etc.) should import and reference this schema.
*
* Webhook Protocol - Outbound HTTP Integration
* Push data to external URLs when events occur in the system.
*
* **NAMING CONVENTION:**
* Webhook names are machine identifiers and must be lowercase snake_case.
*
* @example Good webhook names
* - 'stripe_payment_sync'
* - 'slack_notification'
* - 'crm_lead_export'
*
* @example Bad webhook names (will be rejected)
* - 'StripePaymentSync' (PascalCase)
* - 'slackNotification' (camelCase)
*
* @example Basic webhook configuration
* ```typescript
* const webhook: Webhook = {
* name: 'slack_notification',
* label: 'Slack Order Notification',
* object: 'order',
* triggers: ['create', 'update'],
* url: 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX',
* method: 'POST',
* headers: { 'Content-Type': 'application/json' },
* authentication: {
* type: 'bearer',
* credentials: { token: process.env.SLACK_TOKEN }
* },
* retryPolicy: {
* maxRetries: 3,
* backoffStrategy: 'exponential'
* }
* }
* ```
*/
export const WebhookSchema = z.object({
name: SnakeCaseIdentifierSchema.describe('Webhook unique name (lowercase snake_case)'),
label: z.string().optional().describe('Human-readable webhook label'),
/** Scope */
object: z.string().optional().describe('Object to listen to (optional for manual webhooks)'),
triggers: z.array(WebhookTriggerType).optional().describe('Events that trigger execution'),
/** Target */
url: z.string().url().describe('External webhook endpoint URL'),
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).default('POST').describe('HTTP method'),
/** Headers */
headers: z.record(z.string(), z.string()).optional().describe('Custom HTTP headers'),
/** Body/Payload */
body: z.unknown().optional().describe('Request body payload (if not using default record data)'),
/** Payload Configuration */
payloadFields: z.array(z.string()).optional().describe('Fields to include. Empty = All'),
includeSession: z.boolean().default(false).describe('Include user session info'),
/** Authentication */
authentication: z.object({
type: z.enum(['none', 'bearer', 'basic', 'api-key']).describe('Authentication type'),
credentials: z.record(z.string(), z.string()).optional().describe('Authentication credentials'),
}).optional().describe('Authentication configuration'),
/** Retry Policy */
retryPolicy: z.object({
maxRetries: z.number().int().min(0).max(10).default(3).describe('Maximum retry attempts'),
backoffStrategy: z.enum(['exponential', 'linear', 'fixed']).default('exponential').describe('Backoff strategy'),
initialDelayMs: z.number().int().min(100).default(1000).describe('Initial retry delay in milliseconds'),
maxDelayMs: z.number().int().min(1000).default(60000).describe('Maximum retry delay in milliseconds'),
}).optional().describe('Retry policy configuration'),
/** Timeout */
timeoutMs: z.number().int().min(1000).max(300000).default(30000).describe('Request timeout in milliseconds'),
/** Security */
secret: z.string().optional().describe('Signing secret for HMAC signature verification'),
/** Status */
isActive: z.boolean().default(true).describe('Whether webhook is active'),
/** Metadata */
description: z.string().optional().describe('Webhook description'),
tags: z.array(z.string()).optional().describe('Tags for organization'),
});
/**
* Webhook Receiver Schema (Inbound)
* Handling incoming HTTP hooks from Stripe, Slack, etc.
*
* **NAMING CONVENTION:**
* Webhook receiver names are machine identifiers and must be lowercase snake_case.
*
* @example Good names
* - 'stripe_webhook_handler'
* - 'github_events'
* - 'twilio_status_callback'
*
* @example Bad names (will be rejected)
* - 'StripeWebhookHandler' (PascalCase)
*/
export const WebhookReceiverSchema = z.object({
name: SnakeCaseIdentifierSchema.describe('Webhook receiver unique name (lowercase snake_case)'),
path: z.string().describe('URL Path (e.g. /webhooks/stripe)'),
/** Verification */
verificationType: z.enum(['none', 'header_token', 'hmac', 'ip_whitelist']).default('none'),
verificationParams: z.object({
header: z.string().optional(),
secret: z.string().optional(),
ips: z.array(z.string()).optional()
}).optional(),
/** Action */
action: z.enum(['trigger_flow', 'script', 'upsert_record']).default('trigger_flow'),
target: z.string().describe('Flow ID or Script name'),
});
export type Webhook = z.infer<typeof WebhookSchema>;
export type WebhookReceiver = z.infer<typeof WebhookReceiverSchema>;