@orchestrator-claude/definitions 3.5.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/agents/api-extractor.md +687 -0
- package/agents/business-rule-miner.md +754 -0
- package/agents/code-archaeologist.md +720 -0
- package/agents/docs-guardian.md +524 -0
- package/agents/implementer.md +512 -0
- package/agents/legacy-discoverer.md +583 -0
- package/agents/legacy-synthesizer.md +1101 -0
- package/agents/orchestrator.md +165 -0
- package/agents/planner.md +365 -0
- package/agents/researcher.md +447 -0
- package/agents/reviewer.md +514 -0
- package/agents/schema-extractor.md +781 -0
- package/agents/specifier.md +360 -0
- package/agents/task-generator.md +390 -0
- package/bin/orch-defs.js +2 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +172 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/DiffCommand.d.ts +13 -0
- package/dist/commands/DiffCommand.d.ts.map +1 -0
- package/dist/commands/DiffCommand.js +74 -0
- package/dist/commands/DiffCommand.js.map +1 -0
- package/dist/commands/SeedCommand.d.ts +19 -0
- package/dist/commands/SeedCommand.d.ts.map +1 -0
- package/dist/commands/SeedCommand.js +56 -0
- package/dist/commands/SeedCommand.js.map +1 -0
- package/dist/http/ApiClient.d.ts +50 -0
- package/dist/http/ApiClient.d.ts.map +1 -0
- package/dist/http/ApiClient.js +58 -0
- package/dist/http/ApiClient.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest/ManifestLoader.d.ts +34 -0
- package/dist/manifest/ManifestLoader.d.ts.map +1 -0
- package/dist/manifest/ManifestLoader.js +110 -0
- package/dist/manifest/ManifestLoader.js.map +1 -0
- package/dist/manifest/types.d.ts +59 -0
- package/dist/manifest/types.d.ts.map +1 -0
- package/dist/manifest/types.js +5 -0
- package/dist/manifest/types.js.map +1 -0
- package/dist/scripts/generate-manifest.d.ts +10 -0
- package/dist/scripts/generate-manifest.d.ts.map +1 -0
- package/dist/scripts/generate-manifest.js +114 -0
- package/dist/scripts/generate-manifest.js.map +1 -0
- package/hooks/post-agent-artifact-relay.sh +157 -0
- package/hooks/post-artifact-generate.sh +39 -0
- package/hooks/post-implement-validate.sh +139 -0
- package/hooks/post-phase-checkpoint.sh +322 -0
- package/hooks/pre-agent-invoke.sh +34 -0
- package/hooks/pre-phase-advance.sh +40 -0
- package/hooks/track-agent-invocation.sh +241 -0
- package/kb/auth-strategies.md +742 -0
- package/kb/docs-constitution.md +310 -0
- package/kb/error-handling.md +555 -0
- package/kb/rest-conventions.md +458 -0
- package/kb/validation-patterns.md +589 -0
- package/manifest.json +314 -0
- package/package.json +65 -0
- package/skills/artifact-validator/SKILL.md +226 -0
- package/skills/docs-guardian/SKILL.md +230 -0
- package/skills/kb-lookup/SKILL.md +257 -0
- package/skills/phase-gate-evaluator/SKILL.md +274 -0
- package/skills/release/SKILL.md +239 -0
- package/skills/release/release.sh +491 -0
- package/skills/smoke-test/SKILL.md +195 -0
- package/skills/workflow-status/SKILL.md +322 -0
- package/workflows/bug-fix.json +74 -0
- package/workflows/feature-development.json +88 -0
- package/workflows/legacy-analysis.json +304 -0
- package/workflows/refactoring.json +74 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Validation Patterns"
|
|
3
|
+
category: "patterns"
|
|
4
|
+
tier: "foundational"
|
|
5
|
+
tags: ["api", "validation", "zod", "security"]
|
|
6
|
+
template: "api"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Validation Patterns
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
This document defines validation patterns and best practices for ensuring data integrity and security in REST APIs using Zod for runtime type validation.
|
|
14
|
+
|
|
15
|
+
**Project**: my-project
|
|
16
|
+
**Template**: api
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. Why Validation Matters
|
|
21
|
+
|
|
22
|
+
### Security
|
|
23
|
+
|
|
24
|
+
- **Prevent injection attacks**: SQL, NoSQL, XSS
|
|
25
|
+
- **Prevent buffer overflows**: Limit string/array sizes
|
|
26
|
+
- **Type safety at runtime**: TypeScript only validates at compile time
|
|
27
|
+
|
|
28
|
+
### Data Integrity
|
|
29
|
+
|
|
30
|
+
- **Consistent data**: Enforce business rules at entry point
|
|
31
|
+
- **Clear error messages**: Help clients fix invalid requests
|
|
32
|
+
- **Fail fast**: Catch errors early in request lifecycle
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 2. Zod Schema Basics
|
|
37
|
+
|
|
38
|
+
### Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install zod
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Basic Types
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { z } from 'zod';
|
|
48
|
+
|
|
49
|
+
// Primitives
|
|
50
|
+
const stringSchema = z.string();
|
|
51
|
+
const numberSchema = z.number();
|
|
52
|
+
const booleanSchema = z.boolean();
|
|
53
|
+
const dateSchema = z.date();
|
|
54
|
+
|
|
55
|
+
// String validations
|
|
56
|
+
const emailSchema = z.string().email();
|
|
57
|
+
const urlSchema = z.string().url();
|
|
58
|
+
const uuidSchema = z.string().uuid();
|
|
59
|
+
const minMaxSchema = z.string().min(1).max(100);
|
|
60
|
+
|
|
61
|
+
// Number validations
|
|
62
|
+
const positiveSchema = z.number().positive();
|
|
63
|
+
const intSchema = z.number().int();
|
|
64
|
+
const rangeSchema = z.number().min(0).max(100);
|
|
65
|
+
|
|
66
|
+
// Optional and nullable
|
|
67
|
+
const optionalSchema = z.string().optional();
|
|
68
|
+
const nullableSchema = z.string().nullable();
|
|
69
|
+
|
|
70
|
+
// Arrays
|
|
71
|
+
const arraySchema = z.array(z.string());
|
|
72
|
+
const nonEmptyArraySchema = z.array(z.string()).nonempty();
|
|
73
|
+
const minLengthArraySchema = z.array(z.string()).min(1).max(10);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 3. Object Schemas
|
|
79
|
+
|
|
80
|
+
### Basic Object
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
const UserSchema = z.object({
|
|
84
|
+
id: z.string().uuid(),
|
|
85
|
+
name: z.string().min(1).max(100),
|
|
86
|
+
email: z.string().email(),
|
|
87
|
+
age: z.number().int().positive().optional(),
|
|
88
|
+
role: z.enum(['user', 'admin', 'moderator']),
|
|
89
|
+
createdAt: z.date(),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Infer TypeScript type from schema
|
|
93
|
+
type User = z.infer<typeof UserSchema>;
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Nested Objects
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const AddressSchema = z.object({
|
|
100
|
+
street: z.string(),
|
|
101
|
+
city: z.string(),
|
|
102
|
+
state: z.string().length(2), // US state code
|
|
103
|
+
zipCode: z.string().regex(/^\d{5}$/),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const UserWithAddressSchema = z.object({
|
|
107
|
+
name: z.string(),
|
|
108
|
+
email: z.string().email(),
|
|
109
|
+
address: AddressSchema,
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Partial and Pick
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// Make all fields optional
|
|
117
|
+
const PartialUserSchema = UserSchema.partial();
|
|
118
|
+
|
|
119
|
+
// Pick specific fields
|
|
120
|
+
const UserLoginSchema = UserSchema.pick({
|
|
121
|
+
email: true,
|
|
122
|
+
password: true,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Omit specific fields
|
|
126
|
+
const UserWithoutPasswordSchema = UserSchema.omit({
|
|
127
|
+
password: true,
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 4. Request Validation Schemas
|
|
134
|
+
|
|
135
|
+
### Create Schemas
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// POST /api/v1/users
|
|
139
|
+
const CreateUserSchema = z.object({
|
|
140
|
+
name: z.string().min(1, 'Name is required').max(100),
|
|
141
|
+
email: z.string().email('Invalid email format'),
|
|
142
|
+
password: z
|
|
143
|
+
.string()
|
|
144
|
+
.min(8, 'Password must be at least 8 characters')
|
|
145
|
+
.regex(
|
|
146
|
+
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
|
|
147
|
+
'Password must contain uppercase, lowercase, and number'
|
|
148
|
+
),
|
|
149
|
+
role: z.enum(['user', 'admin']).default('user'),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
type CreateUserInput = z.infer<typeof CreateUserSchema>;
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Update Schemas
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// PATCH /api/v1/users/:id
|
|
159
|
+
const UpdateUserSchema = z.object({
|
|
160
|
+
name: z.string().min(1).max(100).optional(),
|
|
161
|
+
email: z.string().email().optional(),
|
|
162
|
+
age: z.number().int().positive().optional(),
|
|
163
|
+
}).strict(); // Disallow unknown fields
|
|
164
|
+
|
|
165
|
+
type UpdateUserInput = z.infer<typeof UpdateUserSchema>;
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Query Parameter Schemas
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// GET /api/v1/users?page=1&limit=20&role=admin
|
|
172
|
+
const ListUsersQuerySchema = z.object({
|
|
173
|
+
page: z.coerce.number().int().positive().default(1),
|
|
174
|
+
limit: z.coerce.number().int().positive().max(100).default(20),
|
|
175
|
+
role: z.enum(['user', 'admin', 'moderator']).optional(),
|
|
176
|
+
sort: z.enum(['name', 'email', 'createdAt']).default('createdAt'),
|
|
177
|
+
order: z.enum(['asc', 'desc']).default('desc'),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
type ListUsersQuery = z.infer<typeof ListUsersQuerySchema>;
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Path Parameter Schemas
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
// GET /api/v1/users/:id
|
|
187
|
+
const UserIdParamSchema = z.object({
|
|
188
|
+
id: z.string().uuid('Invalid user ID format'),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
type UserIdParam = z.infer<typeof UserIdParamSchema>;
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## 5. Validation Middleware
|
|
197
|
+
|
|
198
|
+
### Generic Validation Middleware
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// src/presentation/middleware/validate.ts
|
|
202
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
203
|
+
import type { z } from 'zod';
|
|
204
|
+
|
|
205
|
+
export type ValidateTarget = 'body' | 'query' | 'params';
|
|
206
|
+
|
|
207
|
+
export function validate<T extends z.ZodSchema>(
|
|
208
|
+
schema: T,
|
|
209
|
+
target: ValidateTarget = 'body'
|
|
210
|
+
) {
|
|
211
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
212
|
+
const data = req[target];
|
|
213
|
+
const result = schema.safeParse(data);
|
|
214
|
+
|
|
215
|
+
if (!result.success) {
|
|
216
|
+
return res.status(400).json({
|
|
217
|
+
error: 'ValidationError',
|
|
218
|
+
message: 'Request validation failed',
|
|
219
|
+
details: result.error.issues.map(issue => ({
|
|
220
|
+
path: issue.path.join('.'),
|
|
221
|
+
message: issue.message,
|
|
222
|
+
code: issue.code,
|
|
223
|
+
})),
|
|
224
|
+
timestamp: new Date().toISOString(),
|
|
225
|
+
path: req.path,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Replace with validated data (with defaults applied)
|
|
230
|
+
req[target] = result.data;
|
|
231
|
+
next();
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Usage in Routes
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import { validate } from '../middleware/validate';
|
|
240
|
+
import { CreateUserSchema, UpdateUserSchema, ListUsersQuerySchema } from '../schemas/user';
|
|
241
|
+
|
|
242
|
+
// Create user
|
|
243
|
+
router.post(
|
|
244
|
+
'/api/v1/users',
|
|
245
|
+
validate(CreateUserSchema, 'body'),
|
|
246
|
+
async (req, res, next) => {
|
|
247
|
+
// req.body is now validated and typed
|
|
248
|
+
const result = await createUserUseCase.execute(req.body);
|
|
249
|
+
res.status(201).json(result);
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Update user
|
|
254
|
+
router.patch(
|
|
255
|
+
'/api/v1/users/:id',
|
|
256
|
+
validate(UserIdParamSchema, 'params'),
|
|
257
|
+
validate(UpdateUserSchema, 'body'),
|
|
258
|
+
async (req, res, next) => {
|
|
259
|
+
// req.params.id and req.body are validated
|
|
260
|
+
const result = await updateUserUseCase.execute({
|
|
261
|
+
userId: req.params.id,
|
|
262
|
+
...req.body,
|
|
263
|
+
});
|
|
264
|
+
res.json(result);
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// List users with query params
|
|
269
|
+
router.get(
|
|
270
|
+
'/api/v1/users',
|
|
271
|
+
validate(ListUsersQuerySchema, 'query'),
|
|
272
|
+
async (req, res, next) => {
|
|
273
|
+
// req.query is validated with defaults
|
|
274
|
+
const result = await listUsersUseCase.execute(req.query);
|
|
275
|
+
res.json(result);
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 6. Custom Validations
|
|
283
|
+
|
|
284
|
+
### Refine for Complex Rules
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
const PasswordChangeSchema = z
|
|
288
|
+
.object({
|
|
289
|
+
oldPassword: z.string(),
|
|
290
|
+
newPassword: z.string().min(8),
|
|
291
|
+
confirmPassword: z.string(),
|
|
292
|
+
})
|
|
293
|
+
.refine((data) => data.newPassword === data.confirmPassword, {
|
|
294
|
+
message: "Passwords don't match",
|
|
295
|
+
path: ['confirmPassword'], // Error will be on confirmPassword field
|
|
296
|
+
})
|
|
297
|
+
.refine((data) => data.oldPassword !== data.newPassword, {
|
|
298
|
+
message: 'New password must be different from old password',
|
|
299
|
+
path: ['newPassword'],
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Transform Values
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
const TrimmedStringSchema = z.string().transform((val) => val.trim());
|
|
307
|
+
|
|
308
|
+
const LowercaseEmailSchema = z
|
|
309
|
+
.string()
|
|
310
|
+
.email()
|
|
311
|
+
.transform((val) => val.toLowerCase());
|
|
312
|
+
|
|
313
|
+
const CreateUserSchema = z.object({
|
|
314
|
+
name: TrimmedStringSchema,
|
|
315
|
+
email: LowercaseEmailSchema,
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Custom Validators
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
function isValidCreditCard(value: string): boolean {
|
|
323
|
+
// Luhn algorithm implementation
|
|
324
|
+
return /^\d{16}$/.test(value); // Simplified
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const PaymentSchema = z.object({
|
|
328
|
+
cardNumber: z.string().refine(isValidCreditCard, {
|
|
329
|
+
message: 'Invalid credit card number',
|
|
330
|
+
}),
|
|
331
|
+
cvv: z.string().regex(/^\d{3,4}$/),
|
|
332
|
+
expiry: z
|
|
333
|
+
.string()
|
|
334
|
+
.regex(/^\d{2}\/\d{2}$/)
|
|
335
|
+
.refine((val) => {
|
|
336
|
+
const [month, year] = val.split('/').map(Number);
|
|
337
|
+
const now = new Date();
|
|
338
|
+
const expiry = new Date(2000 + year, month - 1);
|
|
339
|
+
return expiry > now;
|
|
340
|
+
}, 'Card has expired'),
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## 7. Sanitization
|
|
347
|
+
|
|
348
|
+
### Input Sanitization
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
import validator from 'validator';
|
|
352
|
+
|
|
353
|
+
const SanitizedStringSchema = z
|
|
354
|
+
.string()
|
|
355
|
+
.transform((val) => validator.escape(val)); // Escape HTML entities
|
|
356
|
+
|
|
357
|
+
const SafeUserInputSchema = z.object({
|
|
358
|
+
comment: z
|
|
359
|
+
.string()
|
|
360
|
+
.max(1000)
|
|
361
|
+
.transform((val) => validator.escape(val)),
|
|
362
|
+
username: z
|
|
363
|
+
.string()
|
|
364
|
+
.regex(/^[a-zA-Z0-9_-]+$/, 'Username can only contain alphanumeric characters, underscores, and hyphens'),
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## 8. File Upload Validation
|
|
371
|
+
|
|
372
|
+
### Multer + Zod
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import multer from 'multer';
|
|
376
|
+
|
|
377
|
+
const upload = multer({
|
|
378
|
+
limits: {
|
|
379
|
+
fileSize: 5 * 1024 * 1024, // 5MB
|
|
380
|
+
},
|
|
381
|
+
fileFilter: (req, file, cb) => {
|
|
382
|
+
const allowedMimes = ['image/jpeg', 'image/png', 'image/gif'];
|
|
383
|
+
|
|
384
|
+
if (!allowedMimes.includes(file.mimetype)) {
|
|
385
|
+
return cb(new Error('Invalid file type'));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
cb(null, true);
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const FileUploadSchema = z.object({
|
|
393
|
+
title: z.string().min(1).max(100),
|
|
394
|
+
description: z.string().max(500).optional(),
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
router.post(
|
|
398
|
+
'/api/v1/uploads',
|
|
399
|
+
upload.single('file'),
|
|
400
|
+
validate(FileUploadSchema, 'body'),
|
|
401
|
+
async (req, res) => {
|
|
402
|
+
if (!req.file) {
|
|
403
|
+
return res.status(400).json({ error: 'File is required' });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Process file and validated body
|
|
407
|
+
const result = await uploadFileUseCase.execute({
|
|
408
|
+
file: req.file,
|
|
409
|
+
...req.body,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
res.status(201).json(result);
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## 9. Testing Validation
|
|
420
|
+
|
|
421
|
+
### Test Valid Inputs
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
import request from 'supertest';
|
|
425
|
+
import { app } from '../src/server';
|
|
426
|
+
|
|
427
|
+
describe('POST /api/v1/users - Validation', () => {
|
|
428
|
+
it('should accept valid user data', async () => {
|
|
429
|
+
const validUser = {
|
|
430
|
+
name: 'John Doe',
|
|
431
|
+
email: 'john@example.com',
|
|
432
|
+
password: 'SecureP@ss123',
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
await request(app)
|
|
436
|
+
.post('/api/v1/users')
|
|
437
|
+
.send(validUser)
|
|
438
|
+
.expect(201);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('should reject invalid email', async () => {
|
|
442
|
+
const invalidUser = {
|
|
443
|
+
name: 'John Doe',
|
|
444
|
+
email: 'not-an-email',
|
|
445
|
+
password: 'SecureP@ss123',
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const response = await request(app)
|
|
449
|
+
.post('/api/v1/users')
|
|
450
|
+
.send(invalidUser)
|
|
451
|
+
.expect(400);
|
|
452
|
+
|
|
453
|
+
expect(response.body.error).toBe('ValidationError');
|
|
454
|
+
expect(response.body.details).toEqual(
|
|
455
|
+
expect.arrayContaining([
|
|
456
|
+
expect.objectContaining({
|
|
457
|
+
path: 'email',
|
|
458
|
+
message: expect.stringContaining('Invalid email'),
|
|
459
|
+
}),
|
|
460
|
+
])
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should reject weak password', async () => {
|
|
465
|
+
const invalidUser = {
|
|
466
|
+
name: 'John Doe',
|
|
467
|
+
email: 'john@example.com',
|
|
468
|
+
password: 'weak',
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const response = await request(app)
|
|
472
|
+
.post('/api/v1/users')
|
|
473
|
+
.send(invalidUser)
|
|
474
|
+
.expect(400);
|
|
475
|
+
|
|
476
|
+
expect(response.body.details).toEqual(
|
|
477
|
+
expect.arrayContaining([
|
|
478
|
+
expect.objectContaining({
|
|
479
|
+
path: 'password',
|
|
480
|
+
message: expect.stringContaining('at least 8 characters'),
|
|
481
|
+
}),
|
|
482
|
+
])
|
|
483
|
+
);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should reject unknown fields when strict', async () => {
|
|
487
|
+
const userWithExtra = {
|
|
488
|
+
name: 'John Doe',
|
|
489
|
+
email: 'john@example.com',
|
|
490
|
+
password: 'SecureP@ss123',
|
|
491
|
+
extraField: 'should-not-be-here',
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
await request(app)
|
|
495
|
+
.post('/api/v1/users')
|
|
496
|
+
.send(userWithExtra)
|
|
497
|
+
.expect(400);
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## 10. Best Practices
|
|
505
|
+
|
|
506
|
+
### DO
|
|
507
|
+
|
|
508
|
+
1. **Validate all inputs**: Body, query, params, headers
|
|
509
|
+
2. **Use strict mode**: `.strict()` to reject unknown fields
|
|
510
|
+
3. **Transform and sanitize**: Trim, lowercase, escape HTML
|
|
511
|
+
4. **Provide clear error messages**: Help clients fix issues
|
|
512
|
+
5. **Test edge cases**: Empty strings, negative numbers, large files
|
|
513
|
+
6. **Set limits**: Max string length, array size, file size
|
|
514
|
+
7. **Use enums**: For fixed sets of values
|
|
515
|
+
|
|
516
|
+
### DON'T
|
|
517
|
+
|
|
518
|
+
1. **Don't trust client input**: Always validate on server
|
|
519
|
+
2. **Don't expose internal errors**: Sanitize error messages
|
|
520
|
+
3. **Don't skip validation**: Even for "trusted" clients
|
|
521
|
+
4. **Don't validate in use cases**: Validate at API boundary
|
|
522
|
+
5. **Don't use regex for complex logic**: Use `.refine()`
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## 11. Common Schemas
|
|
527
|
+
|
|
528
|
+
### Email
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
const EmailSchema = z
|
|
532
|
+
.string()
|
|
533
|
+
.email()
|
|
534
|
+
.transform((val) => val.toLowerCase());
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### Password
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
const PasswordSchema = z
|
|
541
|
+
.string()
|
|
542
|
+
.min(8, 'Password must be at least 8 characters')
|
|
543
|
+
.regex(
|
|
544
|
+
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/,
|
|
545
|
+
'Password must contain uppercase, lowercase, number, and special character'
|
|
546
|
+
);
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Phone Number
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
const PhoneSchema = z
|
|
553
|
+
.string()
|
|
554
|
+
.regex(/^\+?[1-9]\d{1,14}$/, 'Invalid phone number format (E.164)');
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### URL
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
const URLSchema = z.string().url();
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Date Range
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
const DateRangeSchema = z
|
|
567
|
+
.object({
|
|
568
|
+
startDate: z.coerce.date(),
|
|
569
|
+
endDate: z.coerce.date(),
|
|
570
|
+
})
|
|
571
|
+
.refine((data) => data.endDate > data.startDate, {
|
|
572
|
+
message: 'End date must be after start date',
|
|
573
|
+
path: ['endDate'],
|
|
574
|
+
});
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
## References
|
|
580
|
+
|
|
581
|
+
- [Zod Documentation](https://zod.dev/)
|
|
582
|
+
- [OWASP Input Validation](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html)
|
|
583
|
+
- [validator.js](https://github.com/validatorjs/validator.js)
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
**Generated for**: my-project
|
|
588
|
+
**Template**: api
|
|
589
|
+
**Constitution Preset**: orchestrator
|