@elevasis/core 0.18.0 → 0.20.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/index.d.ts +82 -1
- package/dist/index.js +353 -171
- package/dist/knowledge/index.d.ts +44 -1
- package/dist/organization-model/index.d.ts +82 -1
- package/dist/organization-model/index.js +353 -171
- package/dist/test-utils/index.d.ts +41 -12
- package/dist/test-utils/index.js +352 -171
- package/package.json +4 -3
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +89 -69
- package/src/auth/multi-tenancy/organizations/__tests__/api-schemas.test.ts +194 -0
- package/src/auth/multi-tenancy/organizations/api-schemas.ts +136 -128
- package/src/business/acquisition/api-schemas.test.ts +199 -15
- package/src/business/acquisition/api-schemas.ts +116 -51
- package/src/business/acquisition/build-templates.test.ts +212 -0
- package/src/business/acquisition/derive-actions.test.ts +1 -1
- package/src/business/acquisition/types.ts +21 -38
- package/src/business/deals/api-schemas.ts +2 -2
- package/src/execution/engine/index.ts +436 -434
- package/src/execution/engine/tools/integration/server/adapters/google-calendar/google-calendar-adapter.ts +428 -0
- package/src/execution/engine/tools/integration/server/adapters/google-calendar/index.ts +2 -0
- package/src/execution/engine/tools/lead-service-types.ts +51 -9
- package/src/execution/engine/tools/platform/acquisition/company-tools.ts +7 -6
- package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +6 -5
- package/src/execution/engine/tools/platform/acquisition/types.ts +20 -9
- package/src/execution/engine/tools/registry.ts +700 -698
- package/src/execution/engine/tools/tool-maps.ts +10 -0
- package/src/execution/external/__tests__/api-schemas.test.ts +127 -0
- package/src/integrations/oauth/__tests__/provider-registry.test.ts +7 -6
- package/src/integrations/oauth/provider-registry.ts +74 -61
- package/src/integrations/oauth/server/credentials.ts +43 -39
- package/src/knowledge/__tests__/queries.test.ts +89 -0
- package/src/organization-model/__tests__/graph.test.ts +108 -2
- package/src/organization-model/__tests__/icons.test.ts +61 -0
- package/src/organization-model/__tests__/knowledge.test.ts +118 -1
- package/src/organization-model/__tests__/prospecting-ssot.test.ts +91 -0
- package/src/organization-model/__tests__/schema.test.ts +122 -0
- package/src/organization-model/__tests__/surface-projection.test.ts +174 -0
- package/src/organization-model/defaults.ts +8 -0
- package/src/organization-model/domains/knowledge.ts +9 -0
- package/src/organization-model/domains/prospecting.ts +347 -226
- package/src/organization-model/domains/sales.ts +40 -30
- package/src/organization-model/graph/build.ts +74 -0
- package/src/organization-model/graph/schema.ts +1 -0
- package/src/organization-model/graph/types.ts +1 -0
- package/src/organization-model/icons.ts +3 -0
- package/src/organization-model/schema.ts +63 -0
- package/src/organization-model/surface-projection.ts +218 -0
- package/src/organization-model/types.ts +9 -1
- package/src/platform/constants/versions.ts +1 -1
- package/src/platform/utils/__tests__/validation.test.ts +1084 -1083
- package/src/platform/utils/validation.ts +425 -425
- package/src/reference/_generated/contracts.md +89 -69
- package/src/server.ts +6 -0
- package/src/supabase/database.types.ts +6 -12
|
@@ -1,425 +1,425 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Common validation utilities
|
|
3
|
-
*
|
|
4
|
-
* Reusable Zod schema primitives for input validation across all domains.
|
|
5
|
-
* These validators are composed by domain-specific api-schemas.ts files.
|
|
6
|
-
*
|
|
7
|
-
* Design Principles:
|
|
8
|
-
* - KISS: Pure functions, no classes, no state
|
|
9
|
-
* - DRY: Common patterns extracted once, reused everywhere
|
|
10
|
-
* - Type Safety: Full TypeScript inference, no any types
|
|
11
|
-
* - Composability: Small validators easily combined using Zod methods
|
|
12
|
-
*
|
|
13
|
-
* Security Benefits:
|
|
14
|
-
* - SQL Injection Prevention: Strict format validation (UUID, Email)
|
|
15
|
-
* - DoS Protection: Max length constraints on strings/arrays, pagination limits
|
|
16
|
-
* - Mass Assignment Prevention: Use .strict() mode on all request schemas
|
|
17
|
-
* - XSS Prevention: Input sanitization and format validation
|
|
18
|
-
*
|
|
19
|
-
* @module validation
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { z } from 'zod'
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* UUID validation schema
|
|
26
|
-
* Ensures string is a valid UUID (v4 format)
|
|
27
|
-
*/
|
|
28
|
-
export const UuidSchema = z.string().uuid()
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Non-empty string validation schema
|
|
32
|
-
* Trims whitespace and enforces minimum 1 character, maximum 1000 characters
|
|
33
|
-
*/
|
|
34
|
-
export const NonEmptyStringSchema = z.string().trim().min(1).max(1000)
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Resource type validation
|
|
38
|
-
* Used across execution systems for workflow and agent resources
|
|
39
|
-
*/
|
|
40
|
-
export const ResourceTypeSchema = z.enum(['agent', 'workflow'])
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Origin resource type validation
|
|
44
|
-
* Includes all possible execution origins for audit trails
|
|
45
|
-
*/
|
|
46
|
-
export const OriginResourceTypeSchema = z.enum(['agent', 'workflow', 'scheduler', 'api'])
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Creates a payload size validator with configurable max size
|
|
50
|
-
* Validates that JSON serialization of payload does not exceed size limit
|
|
51
|
-
*
|
|
52
|
-
* @param maxSizeBytes - Maximum allowed size in bytes (e.g., 500_000 for 500KB)
|
|
53
|
-
* @param options - Configuration options
|
|
54
|
-
* @param options.optional - If false, undefined values are rejected (default: false)
|
|
55
|
-
* @returns Zod schema that validates payload size
|
|
56
|
-
*
|
|
57
|
-
* @example
|
|
58
|
-
* ```typescript
|
|
59
|
-
* const PayloadSchema = createPayloadSizeValidator(500_000) // Required, 500KB limit
|
|
60
|
-
* const OptionalPayloadSchema = createPayloadSizeValidator(500_000, { optional: true })
|
|
61
|
-
* PayloadSchema.parse({ data: 'test' }) // OK
|
|
62
|
-
* PayloadSchema.parse({ data: 'x'.repeat(600_000) }) // Fails - too large
|
|
63
|
-
* PayloadSchema.parse(undefined) // Fails - required
|
|
64
|
-
* OptionalPayloadSchema.parse(undefined) // OK - optional
|
|
65
|
-
* ```
|
|
66
|
-
*/
|
|
67
|
-
export function createPayloadSizeValidator(maxSizeBytes: number, options?: { optional?: boolean }) {
|
|
68
|
-
const allowUndefined = options?.optional === true
|
|
69
|
-
|
|
70
|
-
return z.unknown().superRefine((val, ctx) => {
|
|
71
|
-
// Check if value is undefined
|
|
72
|
-
if (val === undefined) {
|
|
73
|
-
if (!allowUndefined) {
|
|
74
|
-
ctx.addIssue({
|
|
75
|
-
code: 'custom',
|
|
76
|
-
message: 'Payload is required'
|
|
77
|
-
})
|
|
78
|
-
}
|
|
79
|
-
return // Stop validation if undefined
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Null is treated as valid empty payload
|
|
83
|
-
if (val === null) {
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Check size
|
|
88
|
-
const size = JSON.stringify(val).length
|
|
89
|
-
if (size > maxSizeBytes) {
|
|
90
|
-
ctx.addIssue({
|
|
91
|
-
code: 'custom',
|
|
92
|
-
message: `Payload exceeds maximum size of ${Math.round(maxSizeBytes / 1000)}KB`
|
|
93
|
-
})
|
|
94
|
-
}
|
|
95
|
-
})
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Credential name validation
|
|
100
|
-
* - Lowercase letters, numbers, and hyphens only
|
|
101
|
-
* - Must contain at least one hyphen (enforces {service}-{env} convention)
|
|
102
|
-
* - No sequential hyphens, no leading/trailing hyphens
|
|
103
|
-
* - 1-100 characters
|
|
104
|
-
* - Input is auto-lowercased and trimmed
|
|
105
|
-
*
|
|
106
|
-
* SECURITY: Prevents path traversal attacks on /decrypt endpoint
|
|
107
|
-
* Rejects: '../admin-cred', 'test@prod', 'gmail prod', 'Gmail_Prod'
|
|
108
|
-
*
|
|
109
|
-
* Valid: 'gmail-prod', 'attio-dev', 'stripe-api-key'
|
|
110
|
-
* Invalid: 'GmailProd', 'gmail_prod', 'gmail--prod', 'gmail', '-gmail-prod'
|
|
111
|
-
*/
|
|
112
|
-
export const CredentialNameSchema = z
|
|
113
|
-
.string()
|
|
114
|
-
.trim()
|
|
115
|
-
.toLowerCase()
|
|
116
|
-
.min(1, 'Credential name required')
|
|
117
|
-
.max(100, 'Credential name too long (max 100 chars)')
|
|
118
|
-
.regex(
|
|
119
|
-
/^[a-z0-9]+(-[a-z0-9]+)+$/,
|
|
120
|
-
'Credential name must be lowercase letters, numbers, and hyphens in format: service-environment (e.g., gmail-prod, attio-dev)'
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Organization ID validation (UUID format)
|
|
125
|
-
*/
|
|
126
|
-
export const OrganizationIdSchema = UuidSchema
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* OAuth provider validation
|
|
130
|
-
* Must match providers in provider-registry.ts
|
|
131
|
-
*/
|
|
132
|
-
export const OAuthProviderSchema = z.enum(['google-sheets', 'dropbox'])
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* OAuth authorization code validation
|
|
136
|
-
* Typical OAuth codes are 20-500 characters
|
|
137
|
-
*/
|
|
138
|
-
export const OAuthCodeSchema = z
|
|
139
|
-
.string()
|
|
140
|
-
.min(10, 'Authorization code too short')
|
|
141
|
-
.max(1000, 'Authorization code too long')
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* OAuth state parameter validation
|
|
145
|
-
* Base64-encoded JSON state object, max 2KB
|
|
146
|
-
*/
|
|
147
|
-
export const OAuthStateParamSchema = z
|
|
148
|
-
.string()
|
|
149
|
-
.min(10, 'State parameter too short')
|
|
150
|
-
.max(2048, 'State parameter too long')
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Sanitized string (removes dangerous characters)
|
|
154
|
-
*/
|
|
155
|
-
export const SanitizedStringSchema = z
|
|
156
|
-
.string()
|
|
157
|
-
.trim()
|
|
158
|
-
.transform((str) => str.replace(/[<>'"]/g, ''))
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Validates email format (RFC 5322)
|
|
162
|
-
*
|
|
163
|
-
* Security: Prevents email header injection, validates format before sending
|
|
164
|
-
*
|
|
165
|
-
* @example
|
|
166
|
-
* EmailSchema.parse('user@example.com') // OK
|
|
167
|
-
* EmailSchema.parse('invalid') // Error: Invalid email
|
|
168
|
-
*/
|
|
169
|
-
export const EmailSchema = z.string().email()
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Validates URL format (HTTP/HTTPS)
|
|
173
|
-
*
|
|
174
|
-
* Security: Prevents open redirect attacks, validates callback/webhook URLs
|
|
175
|
-
*
|
|
176
|
-
* @example
|
|
177
|
-
* UrlSchema.parse('https://example.com') // OK
|
|
178
|
-
* UrlSchema.parse('not-a-url') // Error: Invalid URL
|
|
179
|
-
*
|
|
180
|
-
* @example
|
|
181
|
-
* // HTTPS only
|
|
182
|
-
* const SecureUrlSchema = UrlSchema.refine(
|
|
183
|
-
* (url) => url.startsWith('https://'),
|
|
184
|
-
* { message: 'HTTPS required' }
|
|
185
|
-
* )
|
|
186
|
-
*/
|
|
187
|
-
export const UrlSchema = z.string().url()
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Standard pagination parameters
|
|
191
|
-
*
|
|
192
|
-
* Constraints:
|
|
193
|
-
* - limit: 1-100 items (default: 20)
|
|
194
|
-
* - offset: 0+ items (default: 0)
|
|
195
|
-
*
|
|
196
|
-
* Uses z.coerce to handle string query parameters from URLs
|
|
197
|
-
*
|
|
198
|
-
* Security: DoS protection (max 100 items), prevents type coercion exploits
|
|
199
|
-
*
|
|
200
|
-
* @example
|
|
201
|
-
* // Query string: ?limit=50&offset=100
|
|
202
|
-
* PaginationSchema.parse({ limit: '50', offset: '100' })
|
|
203
|
-
* // Result: { limit: 50, offset: 100 }
|
|
204
|
-
*
|
|
205
|
-
* @example
|
|
206
|
-
* // Extend with filters
|
|
207
|
-
* const FilteredListSchema = PaginationSchema.extend({
|
|
208
|
-
* status: z.enum(['active', 'inactive']),
|
|
209
|
-
* search: z.string().optional()
|
|
210
|
-
* })
|
|
211
|
-
*/
|
|
212
|
-
export const PaginationSchema = z.object({
|
|
213
|
-
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
214
|
-
offset: z.coerce.number().int().min(0).default(0)
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Validates ISO 8601 datetime format
|
|
219
|
-
*
|
|
220
|
-
* Security: Prevents date parsing exploits, ensures consistent timezone handling
|
|
221
|
-
*
|
|
222
|
-
* @example
|
|
223
|
-
* TimestampSchema.parse('2025-11-13T10:30:00Z') // OK
|
|
224
|
-
* TimestampSchema.parse('invalid-date') // Error: Invalid datetime
|
|
225
|
-
*/
|
|
226
|
-
export const TimestampSchema = z.string().datetime()
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Date range with start and end timestamps
|
|
230
|
-
*
|
|
231
|
-
* Note: Does not validate that end > start by default.
|
|
232
|
-
* Use .refine() for logical validation.
|
|
233
|
-
*
|
|
234
|
-
* Security: Prevents time-based attacks, validates report ranges
|
|
235
|
-
*
|
|
236
|
-
* @example
|
|
237
|
-
* const schema = DateRangeSchema.refine(
|
|
238
|
-
* (data) => new Date(data.endDate) > new Date(data.startDate),
|
|
239
|
-
* { message: 'End date must be after start date' }
|
|
240
|
-
* )
|
|
241
|
-
*/
|
|
242
|
-
export const DateRangeSchema = z.object({
|
|
243
|
-
startDate: z.string().datetime(),
|
|
244
|
-
endDate: z.string().datetime()
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Creates an enum schema with custom error message
|
|
249
|
-
*
|
|
250
|
-
* @param values - Array of valid enum values
|
|
251
|
-
* @param errorMessage - Optional custom error message
|
|
252
|
-
*
|
|
253
|
-
* @example
|
|
254
|
-
* const StatusSchema = createEnumSchema(
|
|
255
|
-
* ['active', 'inactive', 'pending'],
|
|
256
|
-
* 'Status must be active, inactive, or pending'
|
|
257
|
-
* )
|
|
258
|
-
*
|
|
259
|
-
* const taskSchema = z.object({
|
|
260
|
-
* status: StatusSchema
|
|
261
|
-
* })
|
|
262
|
-
*/
|
|
263
|
-
export function createEnumSchema<T extends [string, ...string[]]>(values: T, errorMessage?: string) {
|
|
264
|
-
const schema = z.enum(values)
|
|
265
|
-
return errorMessage ? schema.describe(errorMessage) : schema
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Creates a string schema with custom length constraints
|
|
270
|
-
*
|
|
271
|
-
* @param minLength - Minimum string length
|
|
272
|
-
* @param maxLength - Maximum string length
|
|
273
|
-
* @param fieldName - Optional field name for error messages
|
|
274
|
-
*
|
|
275
|
-
* @example
|
|
276
|
-
* const UsernameSchema = createStringSchema(3, 20, 'Username')
|
|
277
|
-
* const BioSchema = createStringSchema(0, 500, 'Bio')
|
|
278
|
-
*
|
|
279
|
-
* const userSchema = z.object({
|
|
280
|
-
* username: UsernameSchema,
|
|
281
|
-
* bio: BioSchema.optional()
|
|
282
|
-
* })
|
|
283
|
-
*/
|
|
284
|
-
export function createStringSchema(minLength: number, maxLength: number, fieldName?: string): z.ZodString {
|
|
285
|
-
const schema = z.string().trim().min(minLength).max(maxLength)
|
|
286
|
-
return fieldName ? schema.describe(`${fieldName} (${minLength}-${maxLength} characters)`) : schema
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Creates an array schema with size constraints
|
|
291
|
-
*
|
|
292
|
-
* @param itemSchema - Zod schema for array items
|
|
293
|
-
* @param minItems - Minimum array size
|
|
294
|
-
* @param maxItems - Maximum array size
|
|
295
|
-
* @param fieldName - Optional field name for error messages
|
|
296
|
-
*
|
|
297
|
-
* @example
|
|
298
|
-
* const TagsSchema = createArraySchema(
|
|
299
|
-
* z.string(),
|
|
300
|
-
* 1,
|
|
301
|
-
* 10,
|
|
302
|
-
* 'Tags'
|
|
303
|
-
* )
|
|
304
|
-
*
|
|
305
|
-
* const EmailListSchema = createArraySchema(
|
|
306
|
-
* EmailSchema,
|
|
307
|
-
* 1,
|
|
308
|
-
* 5,
|
|
309
|
-
* 'Email addresses'
|
|
310
|
-
* )
|
|
311
|
-
*
|
|
312
|
-
* const articleSchema = z.object({
|
|
313
|
-
* tags: TagsSchema,
|
|
314
|
-
* notifyEmails: EmailListSchema
|
|
315
|
-
* })
|
|
316
|
-
*/
|
|
317
|
-
export function createArraySchema<T extends z.ZodTypeAny>(
|
|
318
|
-
itemSchema: T,
|
|
319
|
-
minItems: number,
|
|
320
|
-
maxItems: number,
|
|
321
|
-
fieldName?: string
|
|
322
|
-
): z.ZodArray<T> {
|
|
323
|
-
const schema = z.array(itemSchema).min(minItems).max(maxItems)
|
|
324
|
-
return fieldName ? schema.describe(`${fieldName} (${minItems}-${maxItems} items)`) : schema
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Formatted validation error response
|
|
329
|
-
* Used by API error handler to return structured validation errors
|
|
330
|
-
*/
|
|
331
|
-
export interface ValidationErrorResponse {
|
|
332
|
-
message: string
|
|
333
|
-
fields: Record<string, string[]>
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Format Zod validation errors into structured, user-friendly messages
|
|
338
|
-
*
|
|
339
|
-
* Transforms Zod's nested error structure into a flat field-to-errors mapping
|
|
340
|
-
* for better API consumer experience. Shows ALL validation errors at once.
|
|
341
|
-
*
|
|
342
|
-
* Security: Sanitizes internal schema details while preserving actionable feedback
|
|
343
|
-
*
|
|
344
|
-
* @param error - ZodError from failed validation
|
|
345
|
-
* @returns Structured error response with message and field-level errors
|
|
346
|
-
*
|
|
347
|
-
* @example
|
|
348
|
-
* ```typescript
|
|
349
|
-
* const schema = z.object({
|
|
350
|
-
* email: EmailSchema,
|
|
351
|
-
* age: z.number().min(18)
|
|
352
|
-
* })
|
|
353
|
-
*
|
|
354
|
-
* try {
|
|
355
|
-
* schema.parse({ email: 'invalid', age: 15 })
|
|
356
|
-
* } catch (error) {
|
|
357
|
-
* const formatted = formatZodValidationError(error as ZodError)
|
|
358
|
-
* // {
|
|
359
|
-
* // message: 'Validation failed',
|
|
360
|
-
* // fields: {
|
|
361
|
-
* // email: ['Invalid email'],
|
|
362
|
-
* // age: ['Number must be greater than or equal to 18']
|
|
363
|
-
* // }
|
|
364
|
-
* // }
|
|
365
|
-
* }
|
|
366
|
-
* ```
|
|
367
|
-
*
|
|
368
|
-
* @example
|
|
369
|
-
* ```typescript
|
|
370
|
-
* // Nested object validation
|
|
371
|
-
* const schema = z.object({
|
|
372
|
-
* user: z.object({
|
|
373
|
-
* profile: z.object({
|
|
374
|
-
* email: EmailSchema
|
|
375
|
-
* })
|
|
376
|
-
* })
|
|
377
|
-
* })
|
|
378
|
-
*
|
|
379
|
-
* // Error path: user.profile.email
|
|
380
|
-
* // Output: { 'user.profile.email': ['Invalid email'] }
|
|
381
|
-
* ```
|
|
382
|
-
*/
|
|
383
|
-
export function formatZodValidationError(error: z.ZodError): ValidationErrorResponse {
|
|
384
|
-
const fieldErrors: Record<string, string[]> = {}
|
|
385
|
-
|
|
386
|
-
for (const issue of error.issues) {
|
|
387
|
-
// Build field path (e.g., 'user.email' or 'items[0].name')
|
|
388
|
-
const path = issue.path.length > 0 ? issue.path.join('.') : '_root'
|
|
389
|
-
|
|
390
|
-
// Initialize field error array if not exists
|
|
391
|
-
if (!fieldErrors[path]) {
|
|
392
|
-
fieldErrors[path] = []
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Add error message (Zod provides good messages by default)
|
|
396
|
-
fieldErrors[path].push(issue.message)
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Count total errors for summary message
|
|
400
|
-
const errorCount = Object.keys(fieldErrors).length
|
|
401
|
-
const fieldWord = errorCount === 1 ? 'field' : 'fields'
|
|
402
|
-
|
|
403
|
-
return {
|
|
404
|
-
message: `Validation failed on ${errorCount} ${fieldWord}`,
|
|
405
|
-
fields: fieldErrors
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Export type helpers for domain schemas
|
|
411
|
-
*/
|
|
412
|
-
export type PaginationParams = z.infer<typeof PaginationSchema>
|
|
413
|
-
export type DateRange = z.infer<typeof DateRangeSchema>
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Standard paginated response envelope for collection endpoints.
|
|
417
|
-
* Use this for all new paginated API responses.
|
|
418
|
-
*/
|
|
419
|
-
export interface PaginatedResponse<T> {
|
|
420
|
-
data: T[]
|
|
421
|
-
total: number
|
|
422
|
-
page: number
|
|
423
|
-
limit: number
|
|
424
|
-
hasMore: boolean
|
|
425
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Common validation utilities
|
|
3
|
+
*
|
|
4
|
+
* Reusable Zod schema primitives for input validation across all domains.
|
|
5
|
+
* These validators are composed by domain-specific api-schemas.ts files.
|
|
6
|
+
*
|
|
7
|
+
* Design Principles:
|
|
8
|
+
* - KISS: Pure functions, no classes, no state
|
|
9
|
+
* - DRY: Common patterns extracted once, reused everywhere
|
|
10
|
+
* - Type Safety: Full TypeScript inference, no any types
|
|
11
|
+
* - Composability: Small validators easily combined using Zod methods
|
|
12
|
+
*
|
|
13
|
+
* Security Benefits:
|
|
14
|
+
* - SQL Injection Prevention: Strict format validation (UUID, Email)
|
|
15
|
+
* - DoS Protection: Max length constraints on strings/arrays, pagination limits
|
|
16
|
+
* - Mass Assignment Prevention: Use .strict() mode on all request schemas
|
|
17
|
+
* - XSS Prevention: Input sanitization and format validation
|
|
18
|
+
*
|
|
19
|
+
* @module validation
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { z } from 'zod'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* UUID validation schema
|
|
26
|
+
* Ensures string is a valid UUID (v4 format)
|
|
27
|
+
*/
|
|
28
|
+
export const UuidSchema = z.string().uuid()
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Non-empty string validation schema
|
|
32
|
+
* Trims whitespace and enforces minimum 1 character, maximum 1000 characters
|
|
33
|
+
*/
|
|
34
|
+
export const NonEmptyStringSchema = z.string().trim().min(1).max(1000)
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resource type validation
|
|
38
|
+
* Used across execution systems for workflow and agent resources
|
|
39
|
+
*/
|
|
40
|
+
export const ResourceTypeSchema = z.enum(['agent', 'workflow'])
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Origin resource type validation
|
|
44
|
+
* Includes all possible execution origins for audit trails
|
|
45
|
+
*/
|
|
46
|
+
export const OriginResourceTypeSchema = z.enum(['agent', 'workflow', 'scheduler', 'api'])
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates a payload size validator with configurable max size
|
|
50
|
+
* Validates that JSON serialization of payload does not exceed size limit
|
|
51
|
+
*
|
|
52
|
+
* @param maxSizeBytes - Maximum allowed size in bytes (e.g., 500_000 for 500KB)
|
|
53
|
+
* @param options - Configuration options
|
|
54
|
+
* @param options.optional - If false, undefined values are rejected (default: false)
|
|
55
|
+
* @returns Zod schema that validates payload size
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* const PayloadSchema = createPayloadSizeValidator(500_000) // Required, 500KB limit
|
|
60
|
+
* const OptionalPayloadSchema = createPayloadSizeValidator(500_000, { optional: true })
|
|
61
|
+
* PayloadSchema.parse({ data: 'test' }) // OK
|
|
62
|
+
* PayloadSchema.parse({ data: 'x'.repeat(600_000) }) // Fails - too large
|
|
63
|
+
* PayloadSchema.parse(undefined) // Fails - required
|
|
64
|
+
* OptionalPayloadSchema.parse(undefined) // OK - optional
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function createPayloadSizeValidator(maxSizeBytes: number, options?: { optional?: boolean }) {
|
|
68
|
+
const allowUndefined = options?.optional === true
|
|
69
|
+
|
|
70
|
+
return z.unknown().superRefine((val, ctx) => {
|
|
71
|
+
// Check if value is undefined
|
|
72
|
+
if (val === undefined) {
|
|
73
|
+
if (!allowUndefined) {
|
|
74
|
+
ctx.addIssue({
|
|
75
|
+
code: 'custom',
|
|
76
|
+
message: 'Payload is required'
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
return // Stop validation if undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Null is treated as valid empty payload
|
|
83
|
+
if (val === null) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check size
|
|
88
|
+
const size = JSON.stringify(val).length
|
|
89
|
+
if (size > maxSizeBytes) {
|
|
90
|
+
ctx.addIssue({
|
|
91
|
+
code: 'custom',
|
|
92
|
+
message: `Payload exceeds maximum size of ${Math.round(maxSizeBytes / 1000)}KB`
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Credential name validation
|
|
100
|
+
* - Lowercase letters, numbers, and hyphens only
|
|
101
|
+
* - Must contain at least one hyphen (enforces {service}-{env} convention)
|
|
102
|
+
* - No sequential hyphens, no leading/trailing hyphens
|
|
103
|
+
* - 1-100 characters
|
|
104
|
+
* - Input is auto-lowercased and trimmed
|
|
105
|
+
*
|
|
106
|
+
* SECURITY: Prevents path traversal attacks on /decrypt endpoint
|
|
107
|
+
* Rejects: '../admin-cred', 'test@prod', 'gmail prod', 'Gmail_Prod'
|
|
108
|
+
*
|
|
109
|
+
* Valid: 'gmail-prod', 'attio-dev', 'stripe-api-key'
|
|
110
|
+
* Invalid: 'GmailProd', 'gmail_prod', 'gmail--prod', 'gmail', '-gmail-prod'
|
|
111
|
+
*/
|
|
112
|
+
export const CredentialNameSchema = z
|
|
113
|
+
.string()
|
|
114
|
+
.trim()
|
|
115
|
+
.toLowerCase()
|
|
116
|
+
.min(1, 'Credential name required')
|
|
117
|
+
.max(100, 'Credential name too long (max 100 chars)')
|
|
118
|
+
.regex(
|
|
119
|
+
/^[a-z0-9]+(-[a-z0-9]+)+$/,
|
|
120
|
+
'Credential name must be lowercase letters, numbers, and hyphens in format: service-environment (e.g., gmail-prod, attio-dev)'
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Organization ID validation (UUID format)
|
|
125
|
+
*/
|
|
126
|
+
export const OrganizationIdSchema = UuidSchema
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* OAuth provider validation
|
|
130
|
+
* Must match providers in provider-registry.ts
|
|
131
|
+
*/
|
|
132
|
+
export const OAuthProviderSchema = z.enum(['google-sheets', 'google-calendar', 'dropbox'])
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* OAuth authorization code validation
|
|
136
|
+
* Typical OAuth codes are 20-500 characters
|
|
137
|
+
*/
|
|
138
|
+
export const OAuthCodeSchema = z
|
|
139
|
+
.string()
|
|
140
|
+
.min(10, 'Authorization code too short')
|
|
141
|
+
.max(1000, 'Authorization code too long')
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* OAuth state parameter validation
|
|
145
|
+
* Base64-encoded JSON state object, max 2KB
|
|
146
|
+
*/
|
|
147
|
+
export const OAuthStateParamSchema = z
|
|
148
|
+
.string()
|
|
149
|
+
.min(10, 'State parameter too short')
|
|
150
|
+
.max(2048, 'State parameter too long')
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Sanitized string (removes dangerous characters)
|
|
154
|
+
*/
|
|
155
|
+
export const SanitizedStringSchema = z
|
|
156
|
+
.string()
|
|
157
|
+
.trim()
|
|
158
|
+
.transform((str) => str.replace(/[<>'"]/g, ''))
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Validates email format (RFC 5322)
|
|
162
|
+
*
|
|
163
|
+
* Security: Prevents email header injection, validates format before sending
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* EmailSchema.parse('user@example.com') // OK
|
|
167
|
+
* EmailSchema.parse('invalid') // Error: Invalid email
|
|
168
|
+
*/
|
|
169
|
+
export const EmailSchema = z.string().email()
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Validates URL format (HTTP/HTTPS)
|
|
173
|
+
*
|
|
174
|
+
* Security: Prevents open redirect attacks, validates callback/webhook URLs
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* UrlSchema.parse('https://example.com') // OK
|
|
178
|
+
* UrlSchema.parse('not-a-url') // Error: Invalid URL
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* // HTTPS only
|
|
182
|
+
* const SecureUrlSchema = UrlSchema.refine(
|
|
183
|
+
* (url) => url.startsWith('https://'),
|
|
184
|
+
* { message: 'HTTPS required' }
|
|
185
|
+
* )
|
|
186
|
+
*/
|
|
187
|
+
export const UrlSchema = z.string().url()
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Standard pagination parameters
|
|
191
|
+
*
|
|
192
|
+
* Constraints:
|
|
193
|
+
* - limit: 1-100 items (default: 20)
|
|
194
|
+
* - offset: 0+ items (default: 0)
|
|
195
|
+
*
|
|
196
|
+
* Uses z.coerce to handle string query parameters from URLs
|
|
197
|
+
*
|
|
198
|
+
* Security: DoS protection (max 100 items), prevents type coercion exploits
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* // Query string: ?limit=50&offset=100
|
|
202
|
+
* PaginationSchema.parse({ limit: '50', offset: '100' })
|
|
203
|
+
* // Result: { limit: 50, offset: 100 }
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* // Extend with filters
|
|
207
|
+
* const FilteredListSchema = PaginationSchema.extend({
|
|
208
|
+
* status: z.enum(['active', 'inactive']),
|
|
209
|
+
* search: z.string().optional()
|
|
210
|
+
* })
|
|
211
|
+
*/
|
|
212
|
+
export const PaginationSchema = z.object({
|
|
213
|
+
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
214
|
+
offset: z.coerce.number().int().min(0).default(0)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Validates ISO 8601 datetime format
|
|
219
|
+
*
|
|
220
|
+
* Security: Prevents date parsing exploits, ensures consistent timezone handling
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* TimestampSchema.parse('2025-11-13T10:30:00Z') // OK
|
|
224
|
+
* TimestampSchema.parse('invalid-date') // Error: Invalid datetime
|
|
225
|
+
*/
|
|
226
|
+
export const TimestampSchema = z.string().datetime()
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Date range with start and end timestamps
|
|
230
|
+
*
|
|
231
|
+
* Note: Does not validate that end > start by default.
|
|
232
|
+
* Use .refine() for logical validation.
|
|
233
|
+
*
|
|
234
|
+
* Security: Prevents time-based attacks, validates report ranges
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* const schema = DateRangeSchema.refine(
|
|
238
|
+
* (data) => new Date(data.endDate) > new Date(data.startDate),
|
|
239
|
+
* { message: 'End date must be after start date' }
|
|
240
|
+
* )
|
|
241
|
+
*/
|
|
242
|
+
export const DateRangeSchema = z.object({
|
|
243
|
+
startDate: z.string().datetime(),
|
|
244
|
+
endDate: z.string().datetime()
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Creates an enum schema with custom error message
|
|
249
|
+
*
|
|
250
|
+
* @param values - Array of valid enum values
|
|
251
|
+
* @param errorMessage - Optional custom error message
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* const StatusSchema = createEnumSchema(
|
|
255
|
+
* ['active', 'inactive', 'pending'],
|
|
256
|
+
* 'Status must be active, inactive, or pending'
|
|
257
|
+
* )
|
|
258
|
+
*
|
|
259
|
+
* const taskSchema = z.object({
|
|
260
|
+
* status: StatusSchema
|
|
261
|
+
* })
|
|
262
|
+
*/
|
|
263
|
+
export function createEnumSchema<T extends [string, ...string[]]>(values: T, errorMessage?: string) {
|
|
264
|
+
const schema = z.enum(values)
|
|
265
|
+
return errorMessage ? schema.describe(errorMessage) : schema
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Creates a string schema with custom length constraints
|
|
270
|
+
*
|
|
271
|
+
* @param minLength - Minimum string length
|
|
272
|
+
* @param maxLength - Maximum string length
|
|
273
|
+
* @param fieldName - Optional field name for error messages
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* const UsernameSchema = createStringSchema(3, 20, 'Username')
|
|
277
|
+
* const BioSchema = createStringSchema(0, 500, 'Bio')
|
|
278
|
+
*
|
|
279
|
+
* const userSchema = z.object({
|
|
280
|
+
* username: UsernameSchema,
|
|
281
|
+
* bio: BioSchema.optional()
|
|
282
|
+
* })
|
|
283
|
+
*/
|
|
284
|
+
export function createStringSchema(minLength: number, maxLength: number, fieldName?: string): z.ZodString {
|
|
285
|
+
const schema = z.string().trim().min(minLength).max(maxLength)
|
|
286
|
+
return fieldName ? schema.describe(`${fieldName} (${minLength}-${maxLength} characters)`) : schema
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Creates an array schema with size constraints
|
|
291
|
+
*
|
|
292
|
+
* @param itemSchema - Zod schema for array items
|
|
293
|
+
* @param minItems - Minimum array size
|
|
294
|
+
* @param maxItems - Maximum array size
|
|
295
|
+
* @param fieldName - Optional field name for error messages
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* const TagsSchema = createArraySchema(
|
|
299
|
+
* z.string(),
|
|
300
|
+
* 1,
|
|
301
|
+
* 10,
|
|
302
|
+
* 'Tags'
|
|
303
|
+
* )
|
|
304
|
+
*
|
|
305
|
+
* const EmailListSchema = createArraySchema(
|
|
306
|
+
* EmailSchema,
|
|
307
|
+
* 1,
|
|
308
|
+
* 5,
|
|
309
|
+
* 'Email addresses'
|
|
310
|
+
* )
|
|
311
|
+
*
|
|
312
|
+
* const articleSchema = z.object({
|
|
313
|
+
* tags: TagsSchema,
|
|
314
|
+
* notifyEmails: EmailListSchema
|
|
315
|
+
* })
|
|
316
|
+
*/
|
|
317
|
+
export function createArraySchema<T extends z.ZodTypeAny>(
|
|
318
|
+
itemSchema: T,
|
|
319
|
+
minItems: number,
|
|
320
|
+
maxItems: number,
|
|
321
|
+
fieldName?: string
|
|
322
|
+
): z.ZodArray<T> {
|
|
323
|
+
const schema = z.array(itemSchema).min(minItems).max(maxItems)
|
|
324
|
+
return fieldName ? schema.describe(`${fieldName} (${minItems}-${maxItems} items)`) : schema
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Formatted validation error response
|
|
329
|
+
* Used by API error handler to return structured validation errors
|
|
330
|
+
*/
|
|
331
|
+
export interface ValidationErrorResponse {
|
|
332
|
+
message: string
|
|
333
|
+
fields: Record<string, string[]>
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Format Zod validation errors into structured, user-friendly messages
|
|
338
|
+
*
|
|
339
|
+
* Transforms Zod's nested error structure into a flat field-to-errors mapping
|
|
340
|
+
* for better API consumer experience. Shows ALL validation errors at once.
|
|
341
|
+
*
|
|
342
|
+
* Security: Sanitizes internal schema details while preserving actionable feedback
|
|
343
|
+
*
|
|
344
|
+
* @param error - ZodError from failed validation
|
|
345
|
+
* @returns Structured error response with message and field-level errors
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```typescript
|
|
349
|
+
* const schema = z.object({
|
|
350
|
+
* email: EmailSchema,
|
|
351
|
+
* age: z.number().min(18)
|
|
352
|
+
* })
|
|
353
|
+
*
|
|
354
|
+
* try {
|
|
355
|
+
* schema.parse({ email: 'invalid', age: 15 })
|
|
356
|
+
* } catch (error) {
|
|
357
|
+
* const formatted = formatZodValidationError(error as ZodError)
|
|
358
|
+
* // {
|
|
359
|
+
* // message: 'Validation failed',
|
|
360
|
+
* // fields: {
|
|
361
|
+
* // email: ['Invalid email'],
|
|
362
|
+
* // age: ['Number must be greater than or equal to 18']
|
|
363
|
+
* // }
|
|
364
|
+
* // }
|
|
365
|
+
* }
|
|
366
|
+
* ```
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
* ```typescript
|
|
370
|
+
* // Nested object validation
|
|
371
|
+
* const schema = z.object({
|
|
372
|
+
* user: z.object({
|
|
373
|
+
* profile: z.object({
|
|
374
|
+
* email: EmailSchema
|
|
375
|
+
* })
|
|
376
|
+
* })
|
|
377
|
+
* })
|
|
378
|
+
*
|
|
379
|
+
* // Error path: user.profile.email
|
|
380
|
+
* // Output: { 'user.profile.email': ['Invalid email'] }
|
|
381
|
+
* ```
|
|
382
|
+
*/
|
|
383
|
+
export function formatZodValidationError(error: z.ZodError): ValidationErrorResponse {
|
|
384
|
+
const fieldErrors: Record<string, string[]> = {}
|
|
385
|
+
|
|
386
|
+
for (const issue of error.issues) {
|
|
387
|
+
// Build field path (e.g., 'user.email' or 'items[0].name')
|
|
388
|
+
const path = issue.path.length > 0 ? issue.path.join('.') : '_root'
|
|
389
|
+
|
|
390
|
+
// Initialize field error array if not exists
|
|
391
|
+
if (!fieldErrors[path]) {
|
|
392
|
+
fieldErrors[path] = []
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Add error message (Zod provides good messages by default)
|
|
396
|
+
fieldErrors[path].push(issue.message)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Count total errors for summary message
|
|
400
|
+
const errorCount = Object.keys(fieldErrors).length
|
|
401
|
+
const fieldWord = errorCount === 1 ? 'field' : 'fields'
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
message: `Validation failed on ${errorCount} ${fieldWord}`,
|
|
405
|
+
fields: fieldErrors
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Export type helpers for domain schemas
|
|
411
|
+
*/
|
|
412
|
+
export type PaginationParams = z.infer<typeof PaginationSchema>
|
|
413
|
+
export type DateRange = z.infer<typeof DateRangeSchema>
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Standard paginated response envelope for collection endpoints.
|
|
417
|
+
* Use this for all new paginated API responses.
|
|
418
|
+
*/
|
|
419
|
+
export interface PaginatedResponse<T> {
|
|
420
|
+
data: T[]
|
|
421
|
+
total: number
|
|
422
|
+
page: number
|
|
423
|
+
limit: number
|
|
424
|
+
hasMore: boolean
|
|
425
|
+
}
|