@elevasis/core 0.18.0 → 0.19.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.
Files changed (44) hide show
  1. package/dist/index.d.ts +82 -1
  2. package/dist/index.js +291 -171
  3. package/dist/knowledge/index.d.ts +43 -0
  4. package/dist/organization-model/index.d.ts +82 -1
  5. package/dist/organization-model/index.js +291 -171
  6. package/dist/test-utils/index.d.ts +41 -12
  7. package/dist/test-utils/index.js +291 -171
  8. package/package.json +2 -1
  9. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +78 -65
  10. package/src/auth/multi-tenancy/organizations/__tests__/api-schemas.test.ts +194 -0
  11. package/src/auth/multi-tenancy/organizations/api-schemas.ts +136 -128
  12. package/src/business/acquisition/api-schemas.test.ts +100 -2
  13. package/src/business/acquisition/api-schemas.ts +81 -43
  14. package/src/business/acquisition/build-templates.test.ts +212 -0
  15. package/src/business/acquisition/types.ts +21 -38
  16. package/src/execution/engine/index.ts +436 -434
  17. package/src/execution/engine/tools/integration/server/adapters/google-calendar/google-calendar-adapter.ts +428 -0
  18. package/src/execution/engine/tools/integration/server/adapters/google-calendar/index.ts +2 -0
  19. package/src/execution/engine/tools/lead-service-types.ts +51 -9
  20. package/src/execution/engine/tools/platform/acquisition/company-tools.ts +7 -6
  21. package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +6 -5
  22. package/src/execution/engine/tools/platform/acquisition/types.ts +20 -9
  23. package/src/execution/engine/tools/registry.ts +700 -698
  24. package/src/execution/engine/tools/tool-maps.ts +10 -0
  25. package/src/execution/external/__tests__/api-schemas.test.ts +127 -0
  26. package/src/integrations/oauth/__tests__/provider-registry.test.ts +7 -6
  27. package/src/integrations/oauth/provider-registry.ts +74 -61
  28. package/src/integrations/oauth/server/credentials.ts +43 -39
  29. package/src/knowledge/__tests__/queries.test.ts +89 -0
  30. package/src/organization-model/__tests__/icons.test.ts +61 -0
  31. package/src/organization-model/__tests__/knowledge.test.ts +118 -1
  32. package/src/organization-model/__tests__/prospecting-ssot.test.ts +94 -0
  33. package/src/organization-model/defaults.ts +8 -0
  34. package/src/organization-model/domains/knowledge.ts +9 -0
  35. package/src/organization-model/domains/prospecting.ts +272 -226
  36. package/src/organization-model/domains/sales.ts +32 -25
  37. package/src/organization-model/icons.ts +3 -0
  38. package/src/organization-model/types.ts +9 -1
  39. package/src/platform/constants/versions.ts +1 -1
  40. package/src/platform/utils/__tests__/validation.test.ts +1084 -1083
  41. package/src/platform/utils/validation.ts +425 -425
  42. package/src/reference/_generated/contracts.md +78 -65
  43. package/src/server.ts +6 -0
  44. 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
+ }