@scallywag/validation 1.0.0 → 1.0.1
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/README.md.bak +1372 -0
- package/dist/env.d.ts +56 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +262 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.d.ts +85 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +403 -0
- package/dist/middleware.js.map +1 -0
- package/dist/sanitization.d.ts +41 -0
- package/dist/sanitization.d.ts.map +1 -0
- package/dist/sanitization.js +111 -0
- package/dist/sanitization.js.map +1 -0
- package/dist/schemas.d.ts +231 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +245 -0
- package/dist/schemas.js.map +1 -0
- package/dist/types.d.ts +136 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/validators.d.ts +111 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/validators.js +324 -0
- package/dist/validators.js.map +1 -0
- package/dist/wrappers.d.ts +117 -0
- package/dist/wrappers.d.ts.map +1 -0
- package/dist/wrappers.js +184 -0
- package/dist/wrappers.js.map +1 -0
- package/dist/zod-schema-converter.d.ts +80 -0
- package/dist/zod-schema-converter.d.ts.map +1 -0
- package/dist/zod-schema-converter.js +97 -0
- package/dist/zod-schema-converter.js.map +1 -0
- package/package.json +36 -1
- package/index.js +0 -1
package/README.md.bak
ADDED
|
@@ -0,0 +1,1372 @@
|
|
|
1
|
+
# Validation Module
|
|
2
|
+
|
|
3
|
+
Production-grade validation framework built on Zod. Provides schema validation, input sanitization (XSS/SQL injection prevention), Next.js API middleware, environment variable validation, security middleware, rate limiting, and Zod-to-JSON-Schema conversion.
|
|
4
|
+
|
|
5
|
+
## Directory Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
packages/kernel/validation/
|
|
9
|
+
├── index.ts # Central exports (re-exports middleware, schemas, types, validators, zod-schema-converter)
|
|
10
|
+
├── types.ts # Type definitions and enums
|
|
11
|
+
├── validators.ts # Core validation functions and utilities
|
|
12
|
+
├── schemas.ts # Pre-built Zod schema collections
|
|
13
|
+
├── sanitization.ts # XSS/SQL injection prevention and string sanitization
|
|
14
|
+
├── middleware.ts # Next.js API route validation and security middleware
|
|
15
|
+
├── env.ts # Environment variable validation and security scanning
|
|
16
|
+
└── zod-schema-converter.ts # Zod-to-JSON-Schema conversion utilities
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Architecture
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
┌──────────────────────────────────────────────────────────────────────┐
|
|
23
|
+
│ VALIDATION SYSTEM │
|
|
24
|
+
├──────────────────────────────────────────────────────────────────────┤
|
|
25
|
+
│ │
|
|
26
|
+
│ ┌────────────────────────────────────────────────────────────────┐ │
|
|
27
|
+
│ │ Validators (Core) │ │
|
|
28
|
+
│ │ validateData() - Zod schema validation │ │
|
|
29
|
+
│ │ validateRequest() - Request validation + sanitization │ │
|
|
30
|
+
│ │ validateFileUpload() - File type/size validation │ │
|
|
31
|
+
│ │ batchValidate() - Parallel validation │ │
|
|
32
|
+
│ │ validateEnv() - Environment variable validation │ │
|
|
33
|
+
│ │ parseAndValidateJSON() - JSON parse + Zod validate │ │
|
|
34
|
+
│ │ createValidator() - Type guard factory │ │
|
|
35
|
+
│ │ createAsyncValidator() - Async type guard factory │ │
|
|
36
|
+
│ │ createValidationDecorator() - Method decorator factory │ │
|
|
37
|
+
│ │ formatValidationErrors() - Error formatter for API responses │ │
|
|
38
|
+
│ │ hasErrorCode() - Error code checker │ │
|
|
39
|
+
│ │ getFieldErrors() - Field-specific error extraction │ │
|
|
40
|
+
│ └────────────────────────────────────────────────────────────────┘ │
|
|
41
|
+
│ │
|
|
42
|
+
│ ┌────────────────────────────────────────────────────────────────┐ │
|
|
43
|
+
│ │ Sanitization │ │
|
|
44
|
+
│ │ containsXssPatterns() - XSS detection (no modification) │ │
|
|
45
|
+
│ │ sanitizeString() - String sanitization with options │ │
|
|
46
|
+
│ │ sanitizeObjectStrings() - Recursive deep object sanitization │ │
|
|
47
|
+
│ │ XSS_PATTERNS - Regex patterns for XSS vectors │ │
|
|
48
|
+
│ │ SQL_PATTERNS - Regex patterns for SQL injection │ │
|
|
49
|
+
│ │ HTML_ENTITIES - Entity encoding map │ │
|
|
50
|
+
│ └────────────────────────────────────────────────────────────────┘ │
|
|
51
|
+
│ │
|
|
52
|
+
│ ┌────────────────────────────────────────────────────────────────┐ │
|
|
53
|
+
│ │ Pre-Built Schemas │ │
|
|
54
|
+
│ │ primitiveSchemas - email, url, uuid, dates, numbers │ │
|
|
55
|
+
│ │ securitySchemas - safeString, safeFilename, strongPassword │ │
|
|
56
|
+
│ │ apiSchemas - pagination, apiResponse, errorResponse │ │
|
|
57
|
+
│ │ authSchemas - userRegistration, userLogin, jwtPayload │ │
|
|
58
|
+
│ │ fractalSchemas - exportConfig, testConfig, cacheConfig │ │
|
|
59
|
+
│ │ envSchemas - development, production │ │
|
|
60
|
+
│ │ fileSchemas - imageUpload, documentUpload │ │
|
|
61
|
+
│ └────────────────────────────────────────────────────────────────┘ │
|
|
62
|
+
│ │
|
|
63
|
+
│ ┌────────────────────────────────────────────────────────────────┐ │
|
|
64
|
+
│ │ Middleware │ │
|
|
65
|
+
│ │ createValidationMiddleware() - Body/query/header/input │ │
|
|
66
|
+
│ │ createSecurityMiddleware() - Bot/origin/auth checks │ │
|
|
67
|
+
│ │ createRateLimitMiddleware() - Per-IP rate limiting │ │
|
|
68
|
+
│ │ createApiMiddleware() - Comprehensive middleware stack │ │
|
|
69
|
+
│ │ withValidation() - HOF for route handlers │ │
|
|
70
|
+
│ │ validateRoute() - Method decorator for routes │ │
|
|
71
|
+
│ │ combineMiddleware() - Middleware combinator │ │
|
|
72
|
+
│ └────────────────────────────────────────────────────────────────┘ │
|
|
73
|
+
│ │
|
|
74
|
+
│ ┌────────────────────────────────────────────────────────────────┐ │
|
|
75
|
+
│ │ Environment Validation │ │
|
|
76
|
+
│ │ validateEnvironment() - NODE_ENV-aware validation │ │
|
|
77
|
+
│ │ scanEnvironmentSecurity() - Security issue scanning │ │
|
|
78
|
+
│ │ logEnvironmentValidation() - Structured result logging │ │
|
|
79
|
+
│ │ initializeEnvironment() - Startup validation (exits on │ │
|
|
80
|
+
│ │ failure) │ │
|
|
81
|
+
│ │ processValidationResult() - Result processing helper │ │
|
|
82
|
+
│ │ processValidationError() - Error processing helper │ │
|
|
83
|
+
│ └────────────────────────────────────────────────────────────────┘ │
|
|
84
|
+
│ │
|
|
85
|
+
│ ┌────────────────────────────────────────────────────────────────┐ │
|
|
86
|
+
│ │ Zod Schema Converter │ │
|
|
87
|
+
│ │ zodToJsonSchema() - Zod -> JSON Schema │ │
|
|
88
|
+
│ │ safeZodToJsonSchema() - Safe conversion (returns undef) │ │
|
|
89
|
+
│ │ extractMethodSchemas() - Input/output schema extraction │ │
|
|
90
|
+
│ │ isZodSchemaWithJsonSupport() - Runtime Zod 4 detection │ │
|
|
91
|
+
│ └────────────────────────────────────────────────────────────────┘ │
|
|
92
|
+
│ │
|
|
93
|
+
└──────────────────────────────────────────────────────────────────────┘
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Type Definitions
|
|
97
|
+
|
|
98
|
+
All types are defined in `types.ts`.
|
|
99
|
+
|
|
100
|
+
### ValidationResult
|
|
101
|
+
|
|
102
|
+
Discriminated union representing success or failure:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
type ValidationResult<T = unknown> =
|
|
106
|
+
| { success: true; data: T }
|
|
107
|
+
| { success: false; errors: ValidationError[] };
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### ValidationError
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
interface ValidationError {
|
|
114
|
+
field: string; // Dot-notation path (e.g. "address.city")
|
|
115
|
+
message: string; // Human-readable error message
|
|
116
|
+
code: string; // Zod error code or custom code
|
|
117
|
+
value?: unknown; // The received value (when available)
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### ValidationOptions
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
interface ValidationOptions {
|
|
125
|
+
strict?: boolean; // Fail on unknown fields
|
|
126
|
+
stripUnknown?: boolean; // Remove unknown fields
|
|
127
|
+
errorMap?: z.ZodErrorMap; // Custom Zod error messages
|
|
128
|
+
async?: boolean; // Use parseAsync instead of parse
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### SanitizationOptions
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
interface SanitizationOptions {
|
|
136
|
+
html?: boolean; // HTML entity encoding (default: false)
|
|
137
|
+
sql?: boolean; // SQL injection protection (default: true in sanitizeString)
|
|
138
|
+
xss?: boolean; // XSS pattern removal (default: true in sanitizeString)
|
|
139
|
+
trim?: boolean; // Trim whitespace (default: true in sanitizeString)
|
|
140
|
+
lowercase?: boolean; // Convert to lowercase
|
|
141
|
+
uppercase?: boolean; // Convert to uppercase
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### SecurityLevel
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
enum SecurityLevel {
|
|
149
|
+
LOW = 'low', // No checks (pass-through)
|
|
150
|
+
MEDIUM = 'medium', // Bot detection
|
|
151
|
+
HIGH = 'high', // Bot + automated tool detection + origin required
|
|
152
|
+
CRITICAL = 'critical', // Bot + automated + scripting detection + origin + auth required
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### EndpointValidation
|
|
157
|
+
|
|
158
|
+
Configuration for validation middleware. Supports two modes: unified input or legacy separate schemas.
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
interface EndpointValidation {
|
|
162
|
+
input?: z.ZodSchema; // Unified: merges body + query + params into one validated object
|
|
163
|
+
output?: z.ZodSchema; // Response validation schema
|
|
164
|
+
body?: z.ZodSchema; // Legacy: request body schema
|
|
165
|
+
query?: z.ZodSchema; // Legacy: query parameter schema
|
|
166
|
+
params?: z.ZodSchema; // Legacy: URL parameter schema
|
|
167
|
+
headers?: z.ZodSchema; // Header validation schema (works with both modes)
|
|
168
|
+
response?: z.ZodSchema; // Response schema (alias for output)
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### FractalValidationContext
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
interface FractalValidationContext {
|
|
176
|
+
interface: string; // e.g. 'api', 'cli', 'mcp', 'sdk'
|
|
177
|
+
method?: string; // HTTP method
|
|
178
|
+
path?: string; // Route path
|
|
179
|
+
securityLevel: SecurityLevel; // Required security level
|
|
180
|
+
userRole?: string; // Current user role
|
|
181
|
+
permissions?: string[]; // Current user permissions
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### FieldMetadata
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
interface FieldMetadata {
|
|
189
|
+
required: boolean;
|
|
190
|
+
type: string;
|
|
191
|
+
description?: string;
|
|
192
|
+
examples?: unknown[];
|
|
193
|
+
constraints?: Record<string, unknown>;
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### SchemaMetadata
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
interface SchemaMetadata {
|
|
201
|
+
name: string;
|
|
202
|
+
description: string;
|
|
203
|
+
version: string;
|
|
204
|
+
fields: Record<string, FieldMetadata>;
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### ValidationRule
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
type ValidationRuleType =
|
|
212
|
+
| 'string'
|
|
213
|
+
| 'number'
|
|
214
|
+
| 'boolean'
|
|
215
|
+
| 'array'
|
|
216
|
+
| 'object'
|
|
217
|
+
| 'email'
|
|
218
|
+
| 'url'
|
|
219
|
+
| 'uuid'
|
|
220
|
+
| 'date'
|
|
221
|
+
| 'custom';
|
|
222
|
+
|
|
223
|
+
interface ValidationRule {
|
|
224
|
+
field: string;
|
|
225
|
+
type: ValidationRuleType;
|
|
226
|
+
required: boolean;
|
|
227
|
+
constraints?: {
|
|
228
|
+
min?: number;
|
|
229
|
+
max?: number;
|
|
230
|
+
pattern?: string;
|
|
231
|
+
enum?: unknown[];
|
|
232
|
+
custom?: (value: unknown) => boolean | string;
|
|
233
|
+
};
|
|
234
|
+
message?: string;
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Full API Surface
|
|
239
|
+
|
|
240
|
+
### validators.ts
|
|
241
|
+
|
|
242
|
+
#### `validateData<T>(schema, data, options?): Promise<ValidationResult<T>>`
|
|
243
|
+
|
|
244
|
+
Core validation function. Parses data against a Zod schema, returning a typed discriminated union.
|
|
245
|
+
|
|
246
|
+
- Uses `schema.parseAsync()` when `options.async` is true, otherwise `schema.parse()`
|
|
247
|
+
- Maps `ZodError` issues to `ValidationError[]` with dot-notation field paths
|
|
248
|
+
- Non-Zod errors produce a single error with field `'unknown'` and code `'unknown_error'`
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
import { validateData } from '@scallywag/kernel/validation';
|
|
252
|
+
import { z } from 'zod';
|
|
253
|
+
|
|
254
|
+
const schema = z.object({
|
|
255
|
+
email: z.string().email(),
|
|
256
|
+
age: z.number().min(18),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const result = await validateData(schema, { email: 'a@b.com', age: 25 });
|
|
260
|
+
if (result.success) {
|
|
261
|
+
// result.data: { email: string; age: number }
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
#### `validateRequest<T>(schema, data, context?): Promise<ValidationResult<T>>`
|
|
266
|
+
|
|
267
|
+
Validates data with automatic pre-sanitization. When data is an object, runs `sanitizeObjectStrings()` before validation. Sets `strict: true` when `context.securityLevel === 'critical'`.
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import { validateRequest, SecurityLevel } from '@scallywag/kernel/validation';
|
|
271
|
+
|
|
272
|
+
const result = await validateRequest(userSchema, requestBody, {
|
|
273
|
+
interface: 'api',
|
|
274
|
+
securityLevel: SecurityLevel.HIGH,
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
#### `validateFileUpload(file, allowedTypes, maxSize): ValidationResult<File>`
|
|
279
|
+
|
|
280
|
+
Synchronous file validation. Checks:
|
|
281
|
+
|
|
282
|
+
1. File is not null/undefined
|
|
283
|
+
2. `file.type` is in `allowedTypes` array
|
|
284
|
+
3. `file.size` does not exceed `maxSize` (bytes)
|
|
285
|
+
|
|
286
|
+
Returns all applicable errors (not just the first).
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
import { validateFileUpload } from '@scallywag/kernel/validation';
|
|
290
|
+
|
|
291
|
+
const result = validateFileUpload(
|
|
292
|
+
file,
|
|
293
|
+
['image/jpeg', 'image/png', 'image/webp'],
|
|
294
|
+
5 * 1024 * 1024
|
|
295
|
+
);
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
#### `batchValidate<T>(schema, dataArray, options?): Promise<Array<ValidationResult<T>>>`
|
|
299
|
+
|
|
300
|
+
Validates an array of values in parallel using `Promise.all`. Each element is independently validated against the same schema.
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
const results = await batchValidate(userSchema, [user1, user2, user3]);
|
|
304
|
+
const valid = results.filter((r) => r.success);
|
|
305
|
+
const invalid = results.filter((r) => !r.success);
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
#### `validateEnv<T>(schema): T`
|
|
309
|
+
|
|
310
|
+
Parses `process.env` against a Zod schema. Throws on failure with formatted error messages showing `path: message` per issue.
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
const env = validateEnv(
|
|
314
|
+
z.object({
|
|
315
|
+
DATABASE_URL: z.string().url(),
|
|
316
|
+
PORT: z.coerce.number().default(3000),
|
|
317
|
+
})
|
|
318
|
+
);
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
#### `createValidator<T>(schema): (data: unknown) => data is T`
|
|
322
|
+
|
|
323
|
+
Creates a synchronous type guard function from a Zod schema.
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
const isUser = createValidator(userSchema);
|
|
327
|
+
|
|
328
|
+
if (isUser(unknownData)) {
|
|
329
|
+
// unknownData is narrowed to User type
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
#### `createAsyncValidator<T>(schema): (data: unknown) => Promise<boolean>`
|
|
334
|
+
|
|
335
|
+
Creates an async validator function. Returns `true`/`false` without throwing.
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
const isValidAsync = createAsyncValidator(asyncSchema);
|
|
339
|
+
const valid = await isValidAsync(data);
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
#### `createValidationDecorator<T>(schema, options?): MethodDecorator`
|
|
343
|
+
|
|
344
|
+
Creates a method decorator that validates the first argument before calling the method. Replaces the first argument with the validated/parsed data. Throws on validation failure.
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
class UserService {
|
|
348
|
+
@createValidationDecorator(createUserSchema)
|
|
349
|
+
async createUser(data: CreateUserInput) {
|
|
350
|
+
// data is guaranteed valid
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
#### `parseAndValidateJSON<T>(schema, jsonString): T`
|
|
356
|
+
|
|
357
|
+
Combines `JSON.parse()` with Zod validation. Throws `SyntaxError` for invalid JSON, `ZodError` for invalid data.
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
const user = parseAndValidateJSON(
|
|
361
|
+
userSchema,
|
|
362
|
+
'{"email":"a@b.com","name":"Jo"}'
|
|
363
|
+
);
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
#### `safeParseAndValidateJSON<T>(schema, jsonString): ValidationResult<T>`
|
|
367
|
+
|
|
368
|
+
Safe version of `parseAndValidateJSON` that returns a `ValidationResult` instead of throwing. Distinguishes JSON parse errors (code `'invalid_json'`) from Zod validation errors.
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
const result = safeParseAndValidateJSON(userSchema, rawJson);
|
|
372
|
+
if (result.success) {
|
|
373
|
+
console.log(result.data);
|
|
374
|
+
} else {
|
|
375
|
+
// result.errors[0].code might be 'invalid_json' or a Zod error code
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
#### `formatValidationErrors(errors): Record<string, string[]>`
|
|
380
|
+
|
|
381
|
+
Groups validation errors by field name into a field-to-messages map. Suitable for API error responses.
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// Input: [{ field: 'email', message: 'Invalid', code: 'invalid_string' }]
|
|
385
|
+
// Output: { email: ['Invalid'] }
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
#### `hasErrorCode(result, code): boolean`
|
|
389
|
+
|
|
390
|
+
Checks if a failed `ValidationResult` contains a specific error code.
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
if (hasErrorCode(result, 'too_small')) {
|
|
394
|
+
// Handle minimum length violation
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
#### `getFieldErrors(result, field): ValidationError[]`
|
|
399
|
+
|
|
400
|
+
Extracts all errors for a specific field from a `ValidationResult`.
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
const emailErrors = getFieldErrors(result, 'email');
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### sanitization.ts
|
|
407
|
+
|
|
408
|
+
#### Constants
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
// XSS detection patterns
|
|
412
|
+
const XSS_PATTERNS = {
|
|
413
|
+
scriptTags: /<script[^>]*>.*?<\/script>/gi,
|
|
414
|
+
iframeTags: /<iframe[^>]*>.*?<\/iframe>/gi,
|
|
415
|
+
javascriptProtocol: /javascript:/gi,
|
|
416
|
+
vbscriptProtocol: /vbscript:/gi,
|
|
417
|
+
eventHandlers: /on\w+=/gi,
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// SQL injection patterns
|
|
421
|
+
const SQL_PATTERNS = {
|
|
422
|
+
specialChars: /['";\\]/g,
|
|
423
|
+
keywords: /\b(union|select|insert|update|delete|drop|exec|execute)\b/gi,
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// HTML entity encoding map
|
|
427
|
+
const HTML_ENTITIES: Record<string, string> = {
|
|
428
|
+
'&': '&',
|
|
429
|
+
'<': '<',
|
|
430
|
+
'>': '>',
|
|
431
|
+
'"': '"',
|
|
432
|
+
"'": ''',
|
|
433
|
+
};
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
#### `containsXssPatterns(value): boolean`
|
|
437
|
+
|
|
438
|
+
Detection-only check. Returns `true` if string contains `<script` tags, event handlers (`on*=`), or `javascript:` protocol. Does not modify the string.
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
if (containsXssPatterns(userInput)) {
|
|
442
|
+
// Reject input
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
#### `sanitizeString(input, options?): string`
|
|
447
|
+
|
|
448
|
+
Applies sanitization transforms in order:
|
|
449
|
+
|
|
450
|
+
1. **Trim** (default on, disable with `trim: false`)
|
|
451
|
+
2. **XSS removal** (default on, disable with `xss: false`) - strips script/iframe tags, javascript:/vbscript: protocols, event handlers
|
|
452
|
+
3. **SQL removal** (default on, disable with `sql: false`) - strips quotes, semicolons, backslashes, SQL keywords
|
|
453
|
+
4. **HTML encoding** (default off, enable with `html: true`) - encodes `& < > " '` to entities
|
|
454
|
+
5. **Case transform** - `lowercase: true` or `uppercase: true` (mutually exclusive, lowercase checked first)
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
const clean = sanitizeString('<script>alert("xss")</script>Hello', {
|
|
458
|
+
xss: true,
|
|
459
|
+
sql: true,
|
|
460
|
+
html: false,
|
|
461
|
+
trim: true,
|
|
462
|
+
});
|
|
463
|
+
// Result: "Hello"
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
#### `sanitizeObjectStrings(obj): unknown`
|
|
467
|
+
|
|
468
|
+
Recursively walks an object/array structure and applies `sanitizeString()` with `{ xss: true, sql: true, trim: true }` to every string value. Non-string/non-object values pass through unchanged.
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
const safe = sanitizeObjectStrings({
|
|
472
|
+
name: ' <script>xss</script>John ',
|
|
473
|
+
tags: ['hello', '<iframe>bad</iframe>'],
|
|
474
|
+
count: 42,
|
|
475
|
+
});
|
|
476
|
+
// Result: { name: 'John', tags: ['hello', ''], count: 42 }
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### schemas.ts
|
|
480
|
+
|
|
481
|
+
All schemas are exported as named object collections.
|
|
482
|
+
|
|
483
|
+
#### `primitiveSchemas`
|
|
484
|
+
|
|
485
|
+
| Key | Schema | Description |
|
|
486
|
+
| ---------------- | -------------------------------------- | ------------------------ |
|
|
487
|
+
| `nonEmptyString` | `z.string().min(1)` | Non-empty string |
|
|
488
|
+
| `email` | `z.string().email()` | Email format |
|
|
489
|
+
| `url` | `z.string().url()` | URL format |
|
|
490
|
+
| `uuid` | `z.string().uuid()` | UUID format |
|
|
491
|
+
| `positiveInt` | `z.number().int().positive()` | Positive integer |
|
|
492
|
+
| `nonNegativeInt` | `z.number().int().min(0)` | Non-negative integer |
|
|
493
|
+
| `percentage` | `z.number().min(0).max(100)` | 0-100 range |
|
|
494
|
+
| `isoDate` | `z.string().datetime()` | ISO 8601 datetime string |
|
|
495
|
+
| `futureDate` | `z.date().refine(d => d > new Date())` | Date in the future |
|
|
496
|
+
| `booleanString` | `z.enum(['true','false']).transform()` | String to boolean |
|
|
497
|
+
|
|
498
|
+
#### `securitySchemas`
|
|
499
|
+
|
|
500
|
+
| Key | Description |
|
|
501
|
+
| ---------------- | --------------------------------------------------------------------------------------------- |
|
|
502
|
+
| `safeString` | Rejects strings with `<script`, `javascript:`, `data:`, `vbscript:`, or `on*=` event handlers |
|
|
503
|
+
| `safeFilename` | Rejects `<>:"/\|?*` characters and `..` path traversal |
|
|
504
|
+
| `sqlSafeString` | Rejects SQL keywords (union, select, insert, update, delete, drop, exec, execute) |
|
|
505
|
+
| `strongPassword` | Min 8 chars, requires uppercase + lowercase + digit + special character |
|
|
506
|
+
|
|
507
|
+
#### `apiSchemas`
|
|
508
|
+
|
|
509
|
+
| Key | Description |
|
|
510
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
511
|
+
| `pagination` | `{ page, limit, sortBy?, sortOrder }` with coercion and defaults (page=1, limit=20, sortOrder='desc') |
|
|
512
|
+
| `apiResponse(dataSchema)` | Generic wrapper: `{ success, data?, error?, timestamp, requestId }` |
|
|
513
|
+
| `errorResponse` | `{ success: false, error, details?: [{ field, message, code }], timestamp, requestId }` |
|
|
514
|
+
|
|
515
|
+
#### `authSchemas`
|
|
516
|
+
|
|
517
|
+
| Key | Description |
|
|
518
|
+
| ------------------ | ----------------------------------------------------------------------------- |
|
|
519
|
+
| `userRegistration` | `{ email, password(strong), firstName, lastName, acceptTerms(must be true) }` |
|
|
520
|
+
| `userLogin` | `{ email, password, rememberMe? }` |
|
|
521
|
+
| `jwtPayload` | `{ sub(uuid), email, roles[], permissions[], iat, exp }` |
|
|
522
|
+
|
|
523
|
+
#### `fractalSchemas`
|
|
524
|
+
|
|
525
|
+
| Key | Description |
|
|
526
|
+
| ------------------ | --------------------------------------------------------------------------------------------------------- |
|
|
527
|
+
| `exportConfig` | `{ interfaces(api/cli/sdk/webhook), method?, path?, description, version?, authentication?, rateLimit? }` |
|
|
528
|
+
| `testConfig` | `{ categories[], priority, timeout?, retries, parallel }` |
|
|
529
|
+
| `validationConfig` | `{ rules[], securityLevel, sanitize }` |
|
|
530
|
+
| `cacheConfig` | `{ ttl, key, strategy, invalidateOn?, compress }` |
|
|
531
|
+
| `authConfig` | `{ roles[], permissions?, requireAll, allowAnonymous, sessionRequired }` |
|
|
532
|
+
|
|
533
|
+
#### `envSchemas`
|
|
534
|
+
|
|
535
|
+
| Key | Description |
|
|
536
|
+
| ------------- | -------------------------------------------------------------------------------------------------------- |
|
|
537
|
+
| `development` | `NODE_ENV='development'`, optional DATABASE_URL/API_KEY, DEBUG defaults true |
|
|
538
|
+
| `production` | `NODE_ENV='production'`, required DATABASE_URL, API_KEY(min 32), JWT_SECRET(min 32), DEBUG must be false |
|
|
539
|
+
|
|
540
|
+
#### `fileSchemas`
|
|
541
|
+
|
|
542
|
+
| Key | Description |
|
|
543
|
+
| ---------------- | ------------------------------------------------------------------------ |
|
|
544
|
+
| `imageUpload` | `{ filename(safe), mimetype(jpeg/png/gif/webp), size(max 5MB), buffer }` |
|
|
545
|
+
| `documentUpload` | `{ filename(safe), mimetype(pdf/text/json), size(max 10MB), buffer }` |
|
|
546
|
+
|
|
547
|
+
### middleware.ts
|
|
548
|
+
|
|
549
|
+
#### `createValidationMiddleware(config: EndpointValidation)`
|
|
550
|
+
|
|
551
|
+
Returns an async function `(request, context?, params?) => NextResponse | { validatedData }`.
|
|
552
|
+
|
|
553
|
+
Two validation modes:
|
|
554
|
+
|
|
555
|
+
1. **Unified input** (`config.input`): Merges query params + URL params + body into one object, validates against `config.input`. Still validates `config.headers` separately.
|
|
556
|
+
2. **Legacy** (`config.body`/`config.query`/`config.headers`): Validates each part independently.
|
|
557
|
+
|
|
558
|
+
On failure: returns `NextResponse` with status 400, `{ success: false, error, details, timestamp }`.
|
|
559
|
+
On success: returns `{ validatedData: { body?, query?, headers?, input? } }`.
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
const middleware = createValidationMiddleware({
|
|
563
|
+
input: z.object({ title: z.string(), category: z.string() }),
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const result = await middleware(request, undefined, { id: '123' });
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
#### `validateRoute(config: EndpointValidation)`
|
|
570
|
+
|
|
571
|
+
Method decorator. Wraps a route handler method to run validation before execution. Extracts route params from the Next.js context argument automatically.
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
class EventsController {
|
|
575
|
+
@validateRoute({ input: CreateEventSchema })
|
|
576
|
+
async POST(request: NextRequest, validatedData: Record<string, unknown>) {
|
|
577
|
+
// validatedData.input contains validated merged input
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
#### `withValidation(config, handler)`
|
|
583
|
+
|
|
584
|
+
Higher-order function wrapping a route handler with validation. Extracts route params from the context argument.
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
export const POST = withValidation(
|
|
588
|
+
{ body: createUserSchema },
|
|
589
|
+
async (request, validatedData, context) => {
|
|
590
|
+
const user = await createUser(validatedData['body']);
|
|
591
|
+
return NextResponse.json(user);
|
|
592
|
+
}
|
|
593
|
+
);
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
#### `createSecurityMiddleware(securityLevel?): (request) => NextResponse | null`
|
|
597
|
+
|
|
598
|
+
Returns synchronous middleware. Returns `null` to pass, or a 403 `NextResponse` on failure.
|
|
599
|
+
|
|
600
|
+
| Level | Checks |
|
|
601
|
+
| ---------- | --------------------------------------------------------------------------------------------------------- |
|
|
602
|
+
| `LOW` | Always passes |
|
|
603
|
+
| `MEDIUM` | Blocks bot/crawler/spider user agents |
|
|
604
|
+
| `HIGH` | Blocks bot/crawler/spider/curl/wget + requires `Origin` header |
|
|
605
|
+
| `CRITICAL` | Blocks bot/crawler/spider/curl/wget/python/ruby/php + requires `Origin` + requires `Authorization` header |
|
|
606
|
+
|
|
607
|
+
```typescript
|
|
608
|
+
const security = createSecurityMiddleware(SecurityLevel.HIGH);
|
|
609
|
+
const result = security(request);
|
|
610
|
+
if (result) return result; // 403 response
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
#### `createRateLimitMiddleware(requests?, windowMs?)`
|
|
614
|
+
|
|
615
|
+
IP-based rate limiting using `MemoryRateLimitStore`. Defaults: 100 requests per 60000ms. Extracts IP from `x-forwarded-for` or `x-real-ip` headers, falls back to `127.0.0.1`.
|
|
616
|
+
|
|
617
|
+
Returns `null` to pass, or a 429 `NextResponse` with rate limit headers:
|
|
618
|
+
|
|
619
|
+
- `X-RateLimit-Limit`
|
|
620
|
+
- `X-RateLimit-Remaining`
|
|
621
|
+
- `X-RateLimit-Reset`
|
|
622
|
+
- `Retry-After`
|
|
623
|
+
|
|
624
|
+
```typescript
|
|
625
|
+
const rateLimit = createRateLimitMiddleware(50, 30000); // 50 req / 30s
|
|
626
|
+
const result = rateLimit(request);
|
|
627
|
+
if (result) return result; // 429 response
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
#### `combineMiddleware(...middlewares)`
|
|
631
|
+
|
|
632
|
+
Composes multiple synchronous middleware functions. Runs each in order; returns the first non-null response (short-circuits).
|
|
633
|
+
|
|
634
|
+
```typescript
|
|
635
|
+
const combined = combineMiddleware(
|
|
636
|
+
createSecurityMiddleware(SecurityLevel.MEDIUM),
|
|
637
|
+
createRateLimitMiddleware(100, 60000)
|
|
638
|
+
);
|
|
639
|
+
const result = combined(request);
|
|
640
|
+
if (result) return result;
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
#### `createApiMiddleware(options)`
|
|
644
|
+
|
|
645
|
+
Comprehensive middleware stack combining security, rate limiting, and validation. Returns an async function `(request, handler, context?) => NextResponse`.
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
const apiMiddleware = createApiMiddleware({
|
|
649
|
+
security: SecurityLevel.HIGH,
|
|
650
|
+
rateLimit: { requests: 100, windowMs: 60000 },
|
|
651
|
+
validation: { body: createEventSchema },
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
return apiMiddleware(request, async (req, validatedData) => {
|
|
655
|
+
return NextResponse.json({ success: true });
|
|
656
|
+
});
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
#### `safeExtractValidatedData(result): Record<string, unknown>`
|
|
660
|
+
|
|
661
|
+
Extracts `validatedData` from a middleware result object. Returns empty object if not present.
|
|
662
|
+
|
|
663
|
+
### env.ts
|
|
664
|
+
|
|
665
|
+
#### `validateEnvironment()`
|
|
666
|
+
|
|
667
|
+
Validates `process.env` against the appropriate Zod schema based on `NODE_ENV`:
|
|
668
|
+
|
|
669
|
+
- `'production'` -> `productionEnvSchema` (strict: DATABASE_URL required, JWT_SECRET min 32, DEBUG must be false)
|
|
670
|
+
- `'development'` -> `developmentEnvSchema` (relaxed: optional fields, DEBUG defaults true)
|
|
671
|
+
- `'test'` or other -> `baseEnvSchema`
|
|
672
|
+
|
|
673
|
+
Also runs `scanEnvironmentSecurity()` and fails if security errors are found.
|
|
674
|
+
|
|
675
|
+
Returns: `{ success, data?, errors?, securityScan }`
|
|
676
|
+
|
|
677
|
+
#### `scanEnvironmentSecurity()`
|
|
678
|
+
|
|
679
|
+
Scans all `process.env` entries for security issues:
|
|
680
|
+
|
|
681
|
+
- **Production requirements**: checks DATABASE_URL and JWT_SECRET exist in production
|
|
682
|
+
- **Weak secrets**: detects variables with names matching `/secret|password|pass|pwd|key|token|auth|api|private|credential/i` that have weak values (test, dev, default, 123456, admin, root) or short length (<16 chars)
|
|
683
|
+
- **Recommendations**: always-present general security advice
|
|
684
|
+
|
|
685
|
+
Returns: `{ warnings: string[], errors: string[], recommendations: string[] }`
|
|
686
|
+
|
|
687
|
+
#### `logEnvironmentValidation(result)`
|
|
688
|
+
|
|
689
|
+
Logs validation results using the structured logger:
|
|
690
|
+
|
|
691
|
+
- Success/failure status
|
|
692
|
+
- Security errors (error level)
|
|
693
|
+
- Security warnings (warn level)
|
|
694
|
+
- Recommendations (info level)
|
|
695
|
+
|
|
696
|
+
#### `initializeEnvironment(): boolean`
|
|
697
|
+
|
|
698
|
+
Runs `validateEnvironment()` and `logEnvironmentValidation()`. Calls `process.exit(1)` if validation fails. Returns `true` on success.
|
|
699
|
+
|
|
700
|
+
#### `processValidationResult(validatedEnv, securityScan)`
|
|
701
|
+
|
|
702
|
+
Helper that combines Zod validation result with security scan. Fails if security scan has errors.
|
|
703
|
+
|
|
704
|
+
#### `processValidationError(error, securityScan)`
|
|
705
|
+
|
|
706
|
+
Helper that formats validation errors into the standard result structure.
|
|
707
|
+
|
|
708
|
+
### zod-schema-converter.ts
|
|
709
|
+
|
|
710
|
+
#### `zodToJsonSchema(schema): JsonSchema`
|
|
711
|
+
|
|
712
|
+
Converts a Zod schema to JSON Schema using Zod 4's native `toJSONSchema()` method. Throws if the schema doesn't support this method.
|
|
713
|
+
|
|
714
|
+
```typescript
|
|
715
|
+
const userSchema = z.object({ name: z.string(), age: z.number() });
|
|
716
|
+
const jsonSchema = zodToJsonSchema(userSchema);
|
|
717
|
+
// { type: 'object', properties: { name: { type: 'string' }, age: { type: 'number' } }, ... }
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
#### `safeZodToJsonSchema(schema?): JsonSchema | undefined`
|
|
721
|
+
|
|
722
|
+
Safe wrapper around `zodToJsonSchema`. Returns `undefined` if schema is undefined or conversion fails.
|
|
723
|
+
|
|
724
|
+
#### `extractMethodSchemas(inputSchema?, outputSchema?): MethodSchemas`
|
|
725
|
+
|
|
726
|
+
Convenience function that converts both input and output Zod schemas to JSON Schema.
|
|
727
|
+
|
|
728
|
+
```typescript
|
|
729
|
+
interface MethodSchemas {
|
|
730
|
+
input?: JsonSchema | undefined;
|
|
731
|
+
output?: JsonSchema | undefined;
|
|
732
|
+
}
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
#### `isZodSchemaWithJsonSupport(value): value is ZodSchema`
|
|
736
|
+
|
|
737
|
+
Runtime check for whether a value is a Zod schema with `toJSONSchema()`, `parse()`, and `safeParse()` methods.
|
|
738
|
+
|
|
739
|
+
## Error Handling Patterns
|
|
740
|
+
|
|
741
|
+
### Discriminated Union Results
|
|
742
|
+
|
|
743
|
+
All validation functions return `ValidationResult<T>`, a discriminated union. Always check `result.success` before accessing `.data` or `.errors`.
|
|
744
|
+
|
|
745
|
+
```typescript
|
|
746
|
+
const result = await validateData(schema, data);
|
|
747
|
+
if (result.success) {
|
|
748
|
+
// TypeScript narrows: result.data is T
|
|
749
|
+
processData(result.data);
|
|
750
|
+
} else {
|
|
751
|
+
// TypeScript narrows: result.errors is ValidationError[]
|
|
752
|
+
const formatted = formatValidationErrors(result.errors);
|
|
753
|
+
return NextResponse.json(
|
|
754
|
+
{ success: false, details: formatted },
|
|
755
|
+
{ status: 400 }
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
### Middleware Error Responses
|
|
761
|
+
|
|
762
|
+
Validation middleware returns structured JSON error responses:
|
|
763
|
+
|
|
764
|
+
```json
|
|
765
|
+
{
|
|
766
|
+
"success": false,
|
|
767
|
+
"error": "Validation failed",
|
|
768
|
+
"details": {
|
|
769
|
+
"email": ["Invalid email format"],
|
|
770
|
+
"age": ["Must be a positive integer"]
|
|
771
|
+
},
|
|
772
|
+
"timestamp": "2025-01-29T12:00:00.000Z"
|
|
773
|
+
}
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
Security middleware returns:
|
|
777
|
+
|
|
778
|
+
```json
|
|
779
|
+
{
|
|
780
|
+
"success": false,
|
|
781
|
+
"error": "Security check failed",
|
|
782
|
+
"timestamp": "2025-01-29T12:00:00.000Z"
|
|
783
|
+
}
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
Rate limit middleware returns (with headers):
|
|
787
|
+
|
|
788
|
+
```json
|
|
789
|
+
{
|
|
790
|
+
"success": false,
|
|
791
|
+
"error": "Rate limit exceeded",
|
|
792
|
+
"retryAfter": 45,
|
|
793
|
+
"timestamp": "2025-01-29T12:00:00.000Z"
|
|
794
|
+
}
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
### Environment Validation Errors
|
|
798
|
+
|
|
799
|
+
`validateEnvironment()` returns a structured result. `initializeEnvironment()` exits the process on failure, so callers never see the error.
|
|
800
|
+
|
|
801
|
+
```typescript
|
|
802
|
+
const result = validateEnvironment();
|
|
803
|
+
if (!result.success) {
|
|
804
|
+
// result.errors: string[] - human-readable error messages
|
|
805
|
+
// result.securityScan.warnings: string[] - security warnings
|
|
806
|
+
// result.securityScan.recommendations: string[] - improvement suggestions
|
|
807
|
+
}
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
## Use Cases
|
|
811
|
+
|
|
812
|
+
### 1. Validate API Request Body
|
|
813
|
+
|
|
814
|
+
```typescript
|
|
815
|
+
import { validateData } from '@scallywag/kernel/validation';
|
|
816
|
+
import { z } from 'zod';
|
|
817
|
+
|
|
818
|
+
const CreateEventSchema = z.object({
|
|
819
|
+
title: z.string().min(1).max(200),
|
|
820
|
+
date: z.string().datetime(),
|
|
821
|
+
capacity: z.number().int().positive().max(10000),
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
async function handleRequest(body: unknown) {
|
|
825
|
+
const result = await validateData(CreateEventSchema, body);
|
|
826
|
+
if (!result.success) {
|
|
827
|
+
return { status: 400, errors: result.errors };
|
|
828
|
+
}
|
|
829
|
+
return createEvent(result.data);
|
|
830
|
+
}
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
### 2. Sanitize User Input Before Storage
|
|
834
|
+
|
|
835
|
+
```typescript
|
|
836
|
+
import {
|
|
837
|
+
sanitizeString,
|
|
838
|
+
sanitizeObjectStrings,
|
|
839
|
+
} from '@scallywag/kernel/validation';
|
|
840
|
+
|
|
841
|
+
// Single field
|
|
842
|
+
const safeName = sanitizeString(rawName, { xss: true, sql: true, trim: true });
|
|
843
|
+
|
|
844
|
+
// Entire request body
|
|
845
|
+
const safeBody = sanitizeObjectStrings(requestBody);
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
### 3. Validate and Sanitize Together
|
|
849
|
+
|
|
850
|
+
```typescript
|
|
851
|
+
import { validateRequest, SecurityLevel } from '@scallywag/kernel/validation';
|
|
852
|
+
|
|
853
|
+
const result = await validateRequest(commentSchema, requestBody, {
|
|
854
|
+
interface: 'api',
|
|
855
|
+
securityLevel: SecurityLevel.HIGH,
|
|
856
|
+
});
|
|
857
|
+
// Body is sanitized (XSS/SQL stripped) THEN validated against schema
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
### 4. Validate File Uploads
|
|
861
|
+
|
|
862
|
+
```typescript
|
|
863
|
+
import { validateFileUpload } from '@scallywag/kernel/validation';
|
|
864
|
+
|
|
865
|
+
const result = validateFileUpload(
|
|
866
|
+
uploadedFile,
|
|
867
|
+
['image/jpeg', 'image/png', 'image/webp'],
|
|
868
|
+
5 * 1024 * 1024
|
|
869
|
+
);
|
|
870
|
+
if (!result.success) {
|
|
871
|
+
// result.errors may contain type AND size errors simultaneously
|
|
872
|
+
}
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
### 5. Next.js API Route with Middleware
|
|
876
|
+
|
|
877
|
+
```typescript
|
|
878
|
+
import { withValidation } from '@scallywag/kernel/validation';
|
|
879
|
+
import { z } from 'zod';
|
|
880
|
+
|
|
881
|
+
const schema = z.object({
|
|
882
|
+
email: z.string().email(),
|
|
883
|
+
password: z.string().min(8),
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
export const POST = withValidation(
|
|
887
|
+
{ body: schema },
|
|
888
|
+
async (request, validatedData) => {
|
|
889
|
+
const { email, password } = validatedData['body'] as {
|
|
890
|
+
email: string;
|
|
891
|
+
password: string;
|
|
892
|
+
};
|
|
893
|
+
return NextResponse.json({ success: true });
|
|
894
|
+
}
|
|
895
|
+
);
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
### 6. Unified Input Validation (Preferred for New Endpoints)
|
|
899
|
+
|
|
900
|
+
```typescript
|
|
901
|
+
import { createValidationMiddleware } from '@scallywag/kernel/validation';
|
|
902
|
+
import { z } from 'zod';
|
|
903
|
+
|
|
904
|
+
const middleware = createValidationMiddleware({
|
|
905
|
+
input: z.object({
|
|
906
|
+
id: z.string().uuid(), // from URL params
|
|
907
|
+
title: z.string().min(1), // from body
|
|
908
|
+
format: z.string().optional(), // from query string
|
|
909
|
+
}),
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// Middleware merges query + params + body into one object
|
|
913
|
+
const result = await middleware(request, undefined, { id: routeParamId });
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
### 7. Security Middleware
|
|
917
|
+
|
|
918
|
+
```typescript
|
|
919
|
+
import {
|
|
920
|
+
createSecurityMiddleware,
|
|
921
|
+
SecurityLevel,
|
|
922
|
+
} from '@scallywag/kernel/validation';
|
|
923
|
+
|
|
924
|
+
const security = createSecurityMiddleware(SecurityLevel.CRITICAL);
|
|
925
|
+
|
|
926
|
+
export async function GET(request: NextRequest) {
|
|
927
|
+
const secResult = security(request);
|
|
928
|
+
if (secResult) return secResult; // 403 for bots, missing origin, missing auth
|
|
929
|
+
// ... handle request
|
|
930
|
+
}
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
### 8. Rate Limiting
|
|
934
|
+
|
|
935
|
+
```typescript
|
|
936
|
+
import { createRateLimitMiddleware } from '@scallywag/kernel/validation';
|
|
937
|
+
|
|
938
|
+
const rateLimit = createRateLimitMiddleware(100, 60000);
|
|
939
|
+
|
|
940
|
+
export async function POST(request: NextRequest) {
|
|
941
|
+
const limited = rateLimit(request);
|
|
942
|
+
if (limited) return limited; // 429 with Retry-After
|
|
943
|
+
// ... handle request
|
|
944
|
+
}
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
### 9. Combined Middleware Stack
|
|
948
|
+
|
|
949
|
+
```typescript
|
|
950
|
+
import {
|
|
951
|
+
combineMiddleware,
|
|
952
|
+
createSecurityMiddleware,
|
|
953
|
+
createRateLimitMiddleware,
|
|
954
|
+
SecurityLevel,
|
|
955
|
+
} from '@scallywag/kernel/validation';
|
|
956
|
+
|
|
957
|
+
const middleware = combineMiddleware(
|
|
958
|
+
createSecurityMiddleware(SecurityLevel.HIGH),
|
|
959
|
+
createRateLimitMiddleware(50, 30000)
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
const blocked = middleware(request);
|
|
963
|
+
if (blocked) return blocked;
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
### 10. Full API Middleware
|
|
967
|
+
|
|
968
|
+
```typescript
|
|
969
|
+
import {
|
|
970
|
+
createApiMiddleware,
|
|
971
|
+
SecurityLevel,
|
|
972
|
+
} from '@scallywag/kernel/validation';
|
|
973
|
+
|
|
974
|
+
const api = createApiMiddleware({
|
|
975
|
+
security: SecurityLevel.HIGH,
|
|
976
|
+
rateLimit: { requests: 100, windowMs: 60000 },
|
|
977
|
+
validation: {
|
|
978
|
+
body: z.object({ name: z.string() }),
|
|
979
|
+
query: z.object({ page: z.coerce.number().default(1) }),
|
|
980
|
+
},
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
export async function POST(request: NextRequest) {
|
|
984
|
+
return api(request, async (req, validatedData) => {
|
|
985
|
+
return NextResponse.json({ data: validatedData });
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
### 11. Batch Validation
|
|
991
|
+
|
|
992
|
+
```typescript
|
|
993
|
+
import { batchValidate } from '@scallywag/kernel/validation';
|
|
994
|
+
|
|
995
|
+
const results = await batchValidate(userSchema, [user1, user2, user3]);
|
|
996
|
+
const allValid = results.every((r) => r.success);
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
### 12. Type Guards from Schemas
|
|
1000
|
+
|
|
1001
|
+
```typescript
|
|
1002
|
+
import {
|
|
1003
|
+
createValidator,
|
|
1004
|
+
createAsyncValidator,
|
|
1005
|
+
} from '@scallywag/kernel/validation';
|
|
1006
|
+
|
|
1007
|
+
const isUser = createValidator(userSchema);
|
|
1008
|
+
const items: unknown[] = [
|
|
1009
|
+
/* ... */
|
|
1010
|
+
];
|
|
1011
|
+
const users = items.filter(isUser); // User[]
|
|
1012
|
+
|
|
1013
|
+
const isValidAsync = createAsyncValidator(asyncRefinedSchema);
|
|
1014
|
+
if (await isValidAsync(data)) {
|
|
1015
|
+
// data passes async validation
|
|
1016
|
+
}
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
### 13. Validation Decorator on Class Methods
|
|
1020
|
+
|
|
1021
|
+
```typescript
|
|
1022
|
+
import { createValidationDecorator } from '@scallywag/kernel/validation';
|
|
1023
|
+
|
|
1024
|
+
class PaymentService {
|
|
1025
|
+
@createValidationDecorator(paymentSchema)
|
|
1026
|
+
async processPayment(input: PaymentInput) {
|
|
1027
|
+
// input is guaranteed to be valid
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
### 14. Route Decorator
|
|
1033
|
+
|
|
1034
|
+
```typescript
|
|
1035
|
+
import { validateRoute } from '@scallywag/kernel/validation';
|
|
1036
|
+
|
|
1037
|
+
class EventsController {
|
|
1038
|
+
@validateRoute({
|
|
1039
|
+
input: z.object({ title: z.string(), category: z.string() }),
|
|
1040
|
+
headers: z.object({ 'x-api-key': z.string() }),
|
|
1041
|
+
})
|
|
1042
|
+
async POST(request: NextRequest, validatedData: Record<string, unknown>) {
|
|
1043
|
+
const input = validatedData['input'];
|
|
1044
|
+
const headers = validatedData['headers'];
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
### 15. JSON String Parse + Validate
|
|
1050
|
+
|
|
1051
|
+
```typescript
|
|
1052
|
+
import {
|
|
1053
|
+
parseAndValidateJSON,
|
|
1054
|
+
safeParseAndValidateJSON,
|
|
1055
|
+
} from '@scallywag/kernel/validation';
|
|
1056
|
+
|
|
1057
|
+
// Throwing version
|
|
1058
|
+
try {
|
|
1059
|
+
const user = parseAndValidateJSON(userSchema, jsonString);
|
|
1060
|
+
} catch (e) {
|
|
1061
|
+
// SyntaxError or ZodError
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Safe version
|
|
1065
|
+
const result = safeParseAndValidateJSON(userSchema, jsonString);
|
|
1066
|
+
if (!result.success) {
|
|
1067
|
+
// Check result.errors[0].code === 'invalid_json' for parse errors
|
|
1068
|
+
}
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
### 16. Environment Validation at Startup
|
|
1072
|
+
|
|
1073
|
+
```typescript
|
|
1074
|
+
import { initializeEnvironment } from '@scallywag/kernel/validation/env';
|
|
1075
|
+
|
|
1076
|
+
// Exits process if environment is invalid
|
|
1077
|
+
initializeEnvironment();
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
### 17. Manual Environment Validation
|
|
1081
|
+
|
|
1082
|
+
```typescript
|
|
1083
|
+
import {
|
|
1084
|
+
validateEnvironment,
|
|
1085
|
+
logEnvironmentValidation,
|
|
1086
|
+
} from '@scallywag/kernel/validation/env';
|
|
1087
|
+
|
|
1088
|
+
const result = validateEnvironment();
|
|
1089
|
+
logEnvironmentValidation(result);
|
|
1090
|
+
|
|
1091
|
+
if (!result.success) {
|
|
1092
|
+
console.error('Env errors:', result.errors);
|
|
1093
|
+
console.warn('Security warnings:', result.securityScan.warnings);
|
|
1094
|
+
}
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
### 18. Environment Security Scan
|
|
1098
|
+
|
|
1099
|
+
```typescript
|
|
1100
|
+
import { scanEnvironmentSecurity } from '@scallywag/kernel/validation/env';
|
|
1101
|
+
|
|
1102
|
+
const scan = scanEnvironmentSecurity();
|
|
1103
|
+
if (scan.errors.length > 0) {
|
|
1104
|
+
// Missing required production variables
|
|
1105
|
+
}
|
|
1106
|
+
if (scan.warnings.length > 0) {
|
|
1107
|
+
// Weak secrets, short keys, etc.
|
|
1108
|
+
}
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
### 19. Using Pre-built Security Schemas
|
|
1112
|
+
|
|
1113
|
+
```typescript
|
|
1114
|
+
import {
|
|
1115
|
+
securitySchemas,
|
|
1116
|
+
primitiveSchemas,
|
|
1117
|
+
} from '@scallywag/kernel/validation';
|
|
1118
|
+
|
|
1119
|
+
const commentSchema = z.object({
|
|
1120
|
+
author: securitySchemas.safeString.min(1).max(100),
|
|
1121
|
+
body: securitySchemas.safeString.min(1).max(5000),
|
|
1122
|
+
email: primitiveSchemas.email,
|
|
1123
|
+
});
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
### 20. Using Pre-built API Schemas
|
|
1127
|
+
|
|
1128
|
+
```typescript
|
|
1129
|
+
import { apiSchemas } from '@scallywag/kernel/validation';
|
|
1130
|
+
|
|
1131
|
+
// Pagination with coercion (query strings come as strings)
|
|
1132
|
+
const query = apiSchemas.pagination.parse({
|
|
1133
|
+
page: '3', // coerced to 3
|
|
1134
|
+
limit: '50', // coerced to 50
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
// Generic API response wrapper
|
|
1138
|
+
const responseSchema = apiSchemas.apiResponse(z.object({ id: z.string() }));
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
### 21. Zod to JSON Schema Conversion
|
|
1142
|
+
|
|
1143
|
+
```typescript
|
|
1144
|
+
import {
|
|
1145
|
+
zodToJsonSchema,
|
|
1146
|
+
extractMethodSchemas,
|
|
1147
|
+
} from '@scallywag/kernel/validation';
|
|
1148
|
+
|
|
1149
|
+
const inputSchema = z.object({ title: z.string() });
|
|
1150
|
+
const jsonSchema = zodToJsonSchema(inputSchema);
|
|
1151
|
+
// Use for OpenAPI docs, MCP tool definitions, etc.
|
|
1152
|
+
|
|
1153
|
+
const schemas = extractMethodSchemas(inputSchema, outputSchema);
|
|
1154
|
+
// { input: JsonSchema, output: JsonSchema }
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
### 22. Error Inspection Utilities
|
|
1158
|
+
|
|
1159
|
+
```typescript
|
|
1160
|
+
import {
|
|
1161
|
+
hasErrorCode,
|
|
1162
|
+
getFieldErrors,
|
|
1163
|
+
formatValidationErrors,
|
|
1164
|
+
} from '@scallywag/kernel/validation';
|
|
1165
|
+
|
|
1166
|
+
const result = await validateData(schema, data);
|
|
1167
|
+
if (!result.success) {
|
|
1168
|
+
if (hasErrorCode(result, 'too_small')) {
|
|
1169
|
+
// Handle minimum constraint violation
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
const emailErrors = getFieldErrors(result, 'email');
|
|
1173
|
+
// All errors specifically for the 'email' field
|
|
1174
|
+
|
|
1175
|
+
const formatted = formatValidationErrors(result.errors);
|
|
1176
|
+
// { email: ['Invalid email format'], name: ['Required'] }
|
|
1177
|
+
}
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
### 23. Using File Schemas
|
|
1181
|
+
|
|
1182
|
+
```typescript
|
|
1183
|
+
import { fileSchemas } from '@scallywag/kernel/validation';
|
|
1184
|
+
|
|
1185
|
+
const result = fileSchemas.imageUpload.safeParse({
|
|
1186
|
+
filename: 'photo.jpg',
|
|
1187
|
+
mimetype: 'image/jpeg',
|
|
1188
|
+
size: 1024000,
|
|
1189
|
+
buffer: fileBuffer,
|
|
1190
|
+
});
|
|
1191
|
+
```
|
|
1192
|
+
|
|
1193
|
+
### 24. XSS Detection Without Modification
|
|
1194
|
+
|
|
1195
|
+
```typescript
|
|
1196
|
+
import { containsXssPatterns } from '@scallywag/kernel/validation';
|
|
1197
|
+
|
|
1198
|
+
// Pure detection - reject rather than sanitize
|
|
1199
|
+
if (containsXssPatterns(userInput)) {
|
|
1200
|
+
throw new Error('Input contains unsafe patterns');
|
|
1201
|
+
}
|
|
1202
|
+
```
|
|
1203
|
+
|
|
1204
|
+
## Testing Patterns
|
|
1205
|
+
|
|
1206
|
+
Tests are located in `packages/kernel/__tests__/core/validation/`.
|
|
1207
|
+
|
|
1208
|
+
### Testing Validation Functions
|
|
1209
|
+
|
|
1210
|
+
```typescript
|
|
1211
|
+
import { validateData } from '@scallywag/kernel/validation';
|
|
1212
|
+
import { z } from 'zod';
|
|
1213
|
+
|
|
1214
|
+
describe('validateData', () => {
|
|
1215
|
+
const schema = z.object({
|
|
1216
|
+
email: z.string().email(),
|
|
1217
|
+
age: z.number().min(18),
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
it('returns success with valid data', async () => {
|
|
1221
|
+
const result = await validateData(schema, { email: 'a@b.com', age: 25 });
|
|
1222
|
+
expect(result.success).toBe(true);
|
|
1223
|
+
if (result.success) {
|
|
1224
|
+
expect(result.data.email).toBe('a@b.com');
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
it('returns errors with invalid data', async () => {
|
|
1229
|
+
const result = await validateData(schema, { email: 'bad', age: 5 });
|
|
1230
|
+
expect(result.success).toBe(false);
|
|
1231
|
+
if (!result.success) {
|
|
1232
|
+
expect(result.errors).toHaveLength(2);
|
|
1233
|
+
expect(result.errors[0]?.field).toBe('email');
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
});
|
|
1237
|
+
```
|
|
1238
|
+
|
|
1239
|
+
### Testing Sanitization
|
|
1240
|
+
|
|
1241
|
+
```typescript
|
|
1242
|
+
import {
|
|
1243
|
+
sanitizeString,
|
|
1244
|
+
containsXssPatterns,
|
|
1245
|
+
sanitizeObjectStrings,
|
|
1246
|
+
} from '@scallywag/kernel/validation';
|
|
1247
|
+
|
|
1248
|
+
describe('sanitizeString', () => {
|
|
1249
|
+
it('removes script tags', () => {
|
|
1250
|
+
expect(sanitizeString('<script>alert(1)</script>Hello')).toBe('Hello');
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
it('removes SQL special chars by default', () => {
|
|
1254
|
+
expect(sanitizeString("Robert'; DROP TABLE users;--")).not.toContain("'");
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
it('encodes HTML entities when html option enabled', () => {
|
|
1258
|
+
expect(
|
|
1259
|
+
sanitizeString('<b>bold</b>', { html: true, xss: false, sql: false })
|
|
1260
|
+
).toBe('<b>bold</b>');
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
describe('sanitizeObjectStrings', () => {
|
|
1265
|
+
it('recursively sanitizes nested objects', () => {
|
|
1266
|
+
const result = sanitizeObjectStrings({
|
|
1267
|
+
nested: { value: '<script>xss</script>safe' },
|
|
1268
|
+
});
|
|
1269
|
+
expect(
|
|
1270
|
+
(result as Record<string, Record<string, string>>).nested.value
|
|
1271
|
+
).toBe('safe');
|
|
1272
|
+
});
|
|
1273
|
+
});
|
|
1274
|
+
```
|
|
1275
|
+
|
|
1276
|
+
### Testing Middleware
|
|
1277
|
+
|
|
1278
|
+
```typescript
|
|
1279
|
+
import {
|
|
1280
|
+
createValidationMiddleware,
|
|
1281
|
+
createSecurityMiddleware,
|
|
1282
|
+
SecurityLevel,
|
|
1283
|
+
} from '@scallywag/kernel/validation';
|
|
1284
|
+
import { NextRequest } from 'next/server';
|
|
1285
|
+
import { z } from 'zod';
|
|
1286
|
+
|
|
1287
|
+
describe('createValidationMiddleware', () => {
|
|
1288
|
+
it('returns 400 for invalid body', async () => {
|
|
1289
|
+
const middleware = createValidationMiddleware({
|
|
1290
|
+
body: z.object({ name: z.string() }),
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
const request = new NextRequest('http://localhost/api/test', {
|
|
1294
|
+
method: 'POST',
|
|
1295
|
+
body: JSON.stringify({ name: 123 }),
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
const result = await middleware(request);
|
|
1299
|
+
expect('status' in result && result.status).toBe(400);
|
|
1300
|
+
});
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
describe('createSecurityMiddleware', () => {
|
|
1304
|
+
it('blocks bot user agents at MEDIUM level', () => {
|
|
1305
|
+
const security = createSecurityMiddleware(SecurityLevel.MEDIUM);
|
|
1306
|
+
const request = new NextRequest('http://localhost/api/test', {
|
|
1307
|
+
headers: { 'user-agent': 'Googlebot/2.1' },
|
|
1308
|
+
});
|
|
1309
|
+
const result = security(request);
|
|
1310
|
+
expect(result).not.toBeNull();
|
|
1311
|
+
expect(result?.status).toBe(403);
|
|
1312
|
+
});
|
|
1313
|
+
});
|
|
1314
|
+
```
|
|
1315
|
+
|
|
1316
|
+
### Testing Environment Validation
|
|
1317
|
+
|
|
1318
|
+
```typescript
|
|
1319
|
+
import {
|
|
1320
|
+
scanEnvironmentSecurity,
|
|
1321
|
+
validateEnvironment,
|
|
1322
|
+
} from '@scallywag/kernel/validation/env';
|
|
1323
|
+
|
|
1324
|
+
describe('scanEnvironmentSecurity', () => {
|
|
1325
|
+
const originalEnv = process.env;
|
|
1326
|
+
|
|
1327
|
+
beforeEach(() => {
|
|
1328
|
+
process.env = { ...originalEnv };
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
afterAll(() => {
|
|
1332
|
+
process.env = originalEnv;
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
it('reports missing required vars in production', () => {
|
|
1336
|
+
process.env['NODE_ENV'] = 'production';
|
|
1337
|
+
delete process.env['DATABASE_URL'];
|
|
1338
|
+
const result = scanEnvironmentSecurity();
|
|
1339
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
```
|
|
1343
|
+
|
|
1344
|
+
### Testing Zod Schema Converter
|
|
1345
|
+
|
|
1346
|
+
```typescript
|
|
1347
|
+
import {
|
|
1348
|
+
zodToJsonSchema,
|
|
1349
|
+
isZodSchemaWithJsonSupport,
|
|
1350
|
+
} from '@scallywag/kernel/validation';
|
|
1351
|
+
|
|
1352
|
+
describe('zodToJsonSchema', () => {
|
|
1353
|
+
it('throws for schemas without toJSONSchema', () => {
|
|
1354
|
+
const fakeSchema = { parse: () => {}, safeParse: () => {} };
|
|
1355
|
+
expect(() => zodToJsonSchema(fakeSchema as never)).toThrow();
|
|
1356
|
+
});
|
|
1357
|
+
});
|
|
1358
|
+
```
|
|
1359
|
+
|
|
1360
|
+
## Exports
|
|
1361
|
+
|
|
1362
|
+
`index.ts` re-exports from:
|
|
1363
|
+
|
|
1364
|
+
- `./middleware` - all middleware functions, `EndpointValidation`, `safeExtractValidatedData`
|
|
1365
|
+
- `./schemas` - all schema collections
|
|
1366
|
+
- `./types` - all type definitions and enums
|
|
1367
|
+
- `./validators` - all validation functions and sanitization re-exports
|
|
1368
|
+
- `./zod-schema-converter` - all JSON Schema conversion utilities
|
|
1369
|
+
|
|
1370
|
+
Note: `./sanitization` is not directly re-exported from index but is re-exported through `./validators` (`sanitizeString`, `sanitizeObjectStrings`, `containsXssPatterns`, `XSS_PATTERNS`, `SQL_PATTERNS`, `HTML_ENTITIES`).
|
|
1371
|
+
|
|
1372
|
+
Note: `./env` is NOT re-exported from index. Import directly from `@scallywag/kernel/validation/env`.
|