@twelvehart/supermemory-runtime 1.0.0-next.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/.env.example +57 -0
- package/README.md +374 -0
- package/dist/index.js +189 -0
- package/dist/mcp/index.js +1132 -0
- package/docker-compose.prod.yml +91 -0
- package/docker-compose.yml +358 -0
- package/drizzle/0000_dapper_the_professor.sql +159 -0
- package/drizzle/0001_api_keys.sql +51 -0
- package/drizzle/meta/0000_snapshot.json +1532 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +20 -0
- package/package.json +114 -0
- package/scripts/add-extraction-job.ts +122 -0
- package/scripts/benchmark-pgvector.ts +122 -0
- package/scripts/bootstrap.sh +209 -0
- package/scripts/check-runtime-pack.ts +111 -0
- package/scripts/claude-mcp-config.ts +336 -0
- package/scripts/docker-entrypoint.sh +183 -0
- package/scripts/doctor.ts +377 -0
- package/scripts/init-db.sql +33 -0
- package/scripts/install.sh +1110 -0
- package/scripts/mcp-setup.ts +271 -0
- package/scripts/migrations/001_create_pgvector_extension.sql +31 -0
- package/scripts/migrations/002_create_memory_embeddings_table.sql +75 -0
- package/scripts/migrations/003_create_hnsw_index.sql +94 -0
- package/scripts/migrations/004_create_memory_embeddings_standalone.sql +70 -0
- package/scripts/migrations/005_create_chunks_table.sql +95 -0
- package/scripts/migrations/006_create_processing_queue.sql +45 -0
- package/scripts/migrations/generate_test_data.sql +42 -0
- package/scripts/migrations/phase1_comprehensive_test.sql +204 -0
- package/scripts/migrations/run_migrations.sh +286 -0
- package/scripts/migrations/test_hnsw_index.sql +255 -0
- package/scripts/pre-commit-secrets +282 -0
- package/scripts/run-extraction-worker.ts +46 -0
- package/scripts/run-phase1-tests.sh +291 -0
- package/scripts/setup.ts +222 -0
- package/scripts/smoke-install.sh +12 -0
- package/scripts/test-health-endpoint.sh +328 -0
- package/src/api/index.ts +2 -0
- package/src/api/middleware/auth.ts +80 -0
- package/src/api/middleware/csrf.ts +308 -0
- package/src/api/middleware/errorHandler.ts +166 -0
- package/src/api/middleware/rateLimit.ts +360 -0
- package/src/api/middleware/validation.ts +514 -0
- package/src/api/routes/documents.ts +286 -0
- package/src/api/routes/profiles.ts +237 -0
- package/src/api/routes/search.ts +71 -0
- package/src/api/stores/index.ts +58 -0
- package/src/config/bootstrap-env.ts +3 -0
- package/src/config/env.ts +71 -0
- package/src/config/feature-flags.ts +25 -0
- package/src/config/index.ts +140 -0
- package/src/config/secrets.config.ts +291 -0
- package/src/db/client.ts +92 -0
- package/src/db/index.ts +73 -0
- package/src/db/postgres.ts +72 -0
- package/src/db/schema/chunks.schema.ts +31 -0
- package/src/db/schema/containers.schema.ts +46 -0
- package/src/db/schema/documents.schema.ts +49 -0
- package/src/db/schema/embeddings.schema.ts +32 -0
- package/src/db/schema/index.ts +11 -0
- package/src/db/schema/memories.schema.ts +72 -0
- package/src/db/schema/profiles.schema.ts +34 -0
- package/src/db/schema/queue.schema.ts +59 -0
- package/src/db/schema/relationships.schema.ts +42 -0
- package/src/db/schema.ts +223 -0
- package/src/db/worker-connection.ts +47 -0
- package/src/index.ts +235 -0
- package/src/mcp/CLAUDE.md +1 -0
- package/src/mcp/index.ts +1380 -0
- package/src/mcp/legacyState.ts +22 -0
- package/src/mcp/rateLimit.ts +358 -0
- package/src/mcp/resources.ts +309 -0
- package/src/mcp/results.ts +104 -0
- package/src/mcp/tools.ts +401 -0
- package/src/queues/config.ts +119 -0
- package/src/queues/index.ts +289 -0
- package/src/sdk/client.ts +225 -0
- package/src/sdk/errors.ts +266 -0
- package/src/sdk/http.ts +560 -0
- package/src/sdk/index.ts +244 -0
- package/src/sdk/resources/base.ts +65 -0
- package/src/sdk/resources/connections.ts +204 -0
- package/src/sdk/resources/documents.ts +163 -0
- package/src/sdk/resources/index.ts +10 -0
- package/src/sdk/resources/memories.ts +150 -0
- package/src/sdk/resources/search.ts +60 -0
- package/src/sdk/resources/settings.ts +36 -0
- package/src/sdk/types.ts +674 -0
- package/src/services/chunking/index.ts +451 -0
- package/src/services/chunking.service.ts +650 -0
- package/src/services/csrf.service.ts +252 -0
- package/src/services/documents.repository.ts +219 -0
- package/src/services/documents.service.ts +191 -0
- package/src/services/embedding.service.ts +404 -0
- package/src/services/extraction.service.ts +300 -0
- package/src/services/extractors/code.extractor.ts +451 -0
- package/src/services/extractors/index.ts +9 -0
- package/src/services/extractors/markdown.extractor.ts +461 -0
- package/src/services/extractors/pdf.extractor.ts +315 -0
- package/src/services/extractors/text.extractor.ts +118 -0
- package/src/services/extractors/url.extractor.ts +243 -0
- package/src/services/index.ts +235 -0
- package/src/services/ingestion.service.ts +177 -0
- package/src/services/llm/anthropic.ts +400 -0
- package/src/services/llm/base.ts +460 -0
- package/src/services/llm/contradiction-detector.service.ts +526 -0
- package/src/services/llm/heuristics.ts +148 -0
- package/src/services/llm/index.ts +309 -0
- package/src/services/llm/memory-classifier.service.ts +383 -0
- package/src/services/llm/memory-extension-detector.service.ts +523 -0
- package/src/services/llm/mock.ts +470 -0
- package/src/services/llm/openai.ts +398 -0
- package/src/services/llm/prompts.ts +438 -0
- package/src/services/llm/types.ts +373 -0
- package/src/services/memory.repository.ts +1769 -0
- package/src/services/memory.service.ts +1338 -0
- package/src/services/memory.types.ts +234 -0
- package/src/services/persistence/index.ts +295 -0
- package/src/services/pipeline.service.ts +509 -0
- package/src/services/profile.repository.ts +436 -0
- package/src/services/profile.service.ts +560 -0
- package/src/services/profile.types.ts +270 -0
- package/src/services/relationships/detector.ts +1128 -0
- package/src/services/relationships/index.ts +268 -0
- package/src/services/relationships/memory-integration.ts +459 -0
- package/src/services/relationships/strategies.ts +132 -0
- package/src/services/relationships/types.ts +370 -0
- package/src/services/search.service.ts +761 -0
- package/src/services/search.types.ts +220 -0
- package/src/services/secrets.service.ts +384 -0
- package/src/services/vectorstore/base.ts +327 -0
- package/src/services/vectorstore/index.ts +444 -0
- package/src/services/vectorstore/memory.ts +286 -0
- package/src/services/vectorstore/migration.ts +295 -0
- package/src/services/vectorstore/mock.ts +403 -0
- package/src/services/vectorstore/pgvector.ts +695 -0
- package/src/services/vectorstore/types.ts +247 -0
- package/src/startup.ts +389 -0
- package/src/types/api.types.ts +193 -0
- package/src/types/document.types.ts +103 -0
- package/src/types/index.ts +241 -0
- package/src/types/profile.base.ts +133 -0
- package/src/utils/errors.ts +447 -0
- package/src/utils/id.ts +15 -0
- package/src/utils/index.ts +101 -0
- package/src/utils/logger.ts +313 -0
- package/src/utils/sanitization.ts +501 -0
- package/src/utils/secret-validation.ts +273 -0
- package/src/utils/synonyms.ts +188 -0
- package/src/utils/validation.ts +581 -0
- package/src/workers/chunking.worker.ts +242 -0
- package/src/workers/embedding.worker.ts +358 -0
- package/src/workers/extraction.worker.ts +346 -0
- package/src/workers/indexing.worker.ts +505 -0
- package/tsconfig.json +38 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Middleware for Supermemory Clone API
|
|
3
|
+
*
|
|
4
|
+
* Provides request validation middleware including:
|
|
5
|
+
* - Zod schema validation for request bodies
|
|
6
|
+
* - Content size limits (50KB default)
|
|
7
|
+
* - Path traversal protection
|
|
8
|
+
* - XSS content sanitization
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Context, MiddlewareHandler, Next } from 'hono'
|
|
12
|
+
import { ZodSchema, ZodError } from 'zod'
|
|
13
|
+
import { ErrorCodes, ErrorResponse } from '../../types/api.types.js'
|
|
14
|
+
import { sanitizeHtml, sanitizeForStorage, isPathSafe } from '../../utils/sanitization.js'
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Configuration
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Default maximum content size in bytes (50KB).
|
|
22
|
+
* Can be overridden via SUPERMEMORY_MAX_CONTENT_SIZE environment variable.
|
|
23
|
+
*/
|
|
24
|
+
export const MAX_CONTENT_SIZE = parseInt(process.env.SUPERMEMORY_MAX_CONTENT_SIZE || '', 10) || 50 * 1024
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Maximum JSON body size for metadata (10KB).
|
|
28
|
+
*/
|
|
29
|
+
export const MAX_METADATA_SIZE = 10 * 1024
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Maximum query string length (10KB).
|
|
33
|
+
*/
|
|
34
|
+
export const MAX_QUERY_LENGTH = 10 * 1024
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Maximum container tag length.
|
|
38
|
+
*/
|
|
39
|
+
export const MAX_CONTAINER_TAG_LENGTH = 100
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Error Helpers
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Creates a standardized validation error response.
|
|
47
|
+
*/
|
|
48
|
+
function createValidationErrorResponse(
|
|
49
|
+
message: string,
|
|
50
|
+
details?: Record<string, unknown>
|
|
51
|
+
): {
|
|
52
|
+
response: ErrorResponse
|
|
53
|
+
status: 400
|
|
54
|
+
} {
|
|
55
|
+
return {
|
|
56
|
+
response: {
|
|
57
|
+
error: {
|
|
58
|
+
code: ErrorCodes.VALIDATION_ERROR,
|
|
59
|
+
message,
|
|
60
|
+
...(details && { details }),
|
|
61
|
+
},
|
|
62
|
+
status: 400,
|
|
63
|
+
},
|
|
64
|
+
status: 400,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Creates a security error response for path traversal or similar attacks.
|
|
70
|
+
*/
|
|
71
|
+
function createSecurityErrorResponse(message: string): { response: ErrorResponse; status: 400 } {
|
|
72
|
+
return {
|
|
73
|
+
response: {
|
|
74
|
+
error: {
|
|
75
|
+
code: ErrorCodes.BAD_REQUEST,
|
|
76
|
+
message: `Security violation: ${message}`,
|
|
77
|
+
},
|
|
78
|
+
status: 400,
|
|
79
|
+
},
|
|
80
|
+
status: 400,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Content Size Middleware
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Options for content size limit middleware.
|
|
90
|
+
*/
|
|
91
|
+
interface ContentSizeLimitOptions {
|
|
92
|
+
/** Maximum content size in bytes */
|
|
93
|
+
maxSize?: number
|
|
94
|
+
/** Whether to include the limit in error messages */
|
|
95
|
+
includeLimit?: boolean
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Middleware that enforces content size limits on request bodies.
|
|
100
|
+
*
|
|
101
|
+
* Prevents denial-of-service attacks via extremely large payloads.
|
|
102
|
+
* Returns 400 Bad Request if the content exceeds the limit.
|
|
103
|
+
*
|
|
104
|
+
* @param options - Configuration options
|
|
105
|
+
* @returns Hono middleware handler
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* app.post('/documents', contentSizeLimit({ maxSize: 100 * 1024 }), async (c) => {
|
|
110
|
+
* // Handler only runs if content <= 100KB
|
|
111
|
+
* });
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export function contentSizeLimit(options: ContentSizeLimitOptions = {}): MiddlewareHandler {
|
|
115
|
+
const maxSize = options.maxSize ?? MAX_CONTENT_SIZE
|
|
116
|
+
const includeLimit = options.includeLimit ?? true
|
|
117
|
+
|
|
118
|
+
return async (c: Context, next: Next) => {
|
|
119
|
+
const contentLength = c.req.header('content-length')
|
|
120
|
+
|
|
121
|
+
if (contentLength) {
|
|
122
|
+
const size = parseInt(contentLength, 10)
|
|
123
|
+
|
|
124
|
+
if (!Number.isNaN(size) && size > maxSize) {
|
|
125
|
+
const { response, status } = createValidationErrorResponse(
|
|
126
|
+
includeLimit
|
|
127
|
+
? `Content size ${formatBytes(size)} exceeds maximum allowed size of ${formatBytes(maxSize)}`
|
|
128
|
+
: 'Content size exceeds maximum allowed size'
|
|
129
|
+
)
|
|
130
|
+
return c.json(response, status)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return next()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Default content size limit middleware (50KB).
|
|
140
|
+
*/
|
|
141
|
+
export const defaultContentSizeLimit = contentSizeLimit()
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Large content size limit middleware (1MB) for file uploads.
|
|
145
|
+
*/
|
|
146
|
+
export const largeContentSizeLimit = contentSizeLimit({ maxSize: 1024 * 1024 })
|
|
147
|
+
|
|
148
|
+
// ============================================================================
|
|
149
|
+
// Schema Validation Middleware
|
|
150
|
+
// ============================================================================
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Options for schema validation middleware.
|
|
154
|
+
*/
|
|
155
|
+
interface ValidateSchemaOptions {
|
|
156
|
+
/** Whether to sanitize string values in the body */
|
|
157
|
+
sanitize?: boolean
|
|
158
|
+
/** Whether to strip HTML from string values */
|
|
159
|
+
stripHtml?: boolean
|
|
160
|
+
/** Fields that should preserve HTML (not be sanitized) */
|
|
161
|
+
preserveHtmlFields?: string[]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Middleware that validates request body against a Zod schema.
|
|
166
|
+
*
|
|
167
|
+
* Parses and validates the JSON body, attaching the validated data
|
|
168
|
+
* to the context for use in handlers.
|
|
169
|
+
*
|
|
170
|
+
* @param schema - Zod schema to validate against
|
|
171
|
+
* @param options - Validation options
|
|
172
|
+
* @returns Hono middleware handler
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```typescript
|
|
176
|
+
* const CreateUserSchema = z.object({
|
|
177
|
+
* name: z.string().min(1),
|
|
178
|
+
* email: z.string().email()
|
|
179
|
+
* });
|
|
180
|
+
*
|
|
181
|
+
* app.post('/users', validateSchema(CreateUserSchema), async (c) => {
|
|
182
|
+
* const body = c.get('validatedBody');
|
|
183
|
+
* // body is typed and validated
|
|
184
|
+
* });
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
export function validateSchema<T>(schema: ZodSchema<T>, options: ValidateSchemaOptions = {}): MiddlewareHandler {
|
|
188
|
+
const { sanitize = true, stripHtml: shouldStripHtml = false, preserveHtmlFields = [] } = options
|
|
189
|
+
|
|
190
|
+
return async (c: Context, next: Next) => {
|
|
191
|
+
try {
|
|
192
|
+
let body = await c.req.json()
|
|
193
|
+
|
|
194
|
+
// Apply sanitization if enabled
|
|
195
|
+
if (sanitize && typeof body === 'object' && body !== null) {
|
|
196
|
+
body = sanitizeRequestBody(body, {
|
|
197
|
+
stripHtml: shouldStripHtml,
|
|
198
|
+
preserveFields: preserveHtmlFields,
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Validate against schema
|
|
203
|
+
const validated = schema.parse(body)
|
|
204
|
+
|
|
205
|
+
// Store validated body for handler access
|
|
206
|
+
c.set('validatedBody', validated)
|
|
207
|
+
|
|
208
|
+
return next()
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (error instanceof ZodError) {
|
|
211
|
+
const formattedErrors = formatZodErrors(error)
|
|
212
|
+
const { response, status } = createValidationErrorResponse(`Validation failed: ${formattedErrors}`, {
|
|
213
|
+
fieldErrors: extractFieldErrors(error),
|
|
214
|
+
})
|
|
215
|
+
return c.json(response, status)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (error instanceof SyntaxError) {
|
|
219
|
+
const { response, status } = createValidationErrorResponse('Invalid JSON in request body')
|
|
220
|
+
return c.json(response, status)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
throw error
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============================================================================
|
|
229
|
+
// Path Validation Middleware
|
|
230
|
+
// ============================================================================
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Middleware that validates path parameters for path traversal attacks.
|
|
234
|
+
*
|
|
235
|
+
* Checks all path parameters and rejects requests containing:
|
|
236
|
+
* - Parent directory references (..)
|
|
237
|
+
* - Absolute paths
|
|
238
|
+
* - URL-encoded traversal sequences
|
|
239
|
+
*
|
|
240
|
+
* @param paramNames - Optional list of specific parameter names to validate
|
|
241
|
+
* @returns Hono middleware handler
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```typescript
|
|
245
|
+
* app.get('/files/:path', validatePathParams(), async (c) => {
|
|
246
|
+
* const path = c.req.param('path');
|
|
247
|
+
* // path is guaranteed to be safe
|
|
248
|
+
* });
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
export function validatePathParams(paramNames?: string[]): MiddlewareHandler {
|
|
252
|
+
return async (c: Context, next: Next) => {
|
|
253
|
+
const params = c.req.param()
|
|
254
|
+
|
|
255
|
+
const paramsToCheck = paramNames || Object.keys(params)
|
|
256
|
+
|
|
257
|
+
for (const name of paramsToCheck) {
|
|
258
|
+
const value = params[name]
|
|
259
|
+
|
|
260
|
+
if (value && !isPathSafe(value)) {
|
|
261
|
+
const { response, status } = createSecurityErrorResponse(
|
|
262
|
+
'Path contains invalid characters or traversal sequences'
|
|
263
|
+
)
|
|
264
|
+
return c.json(response, status)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return next()
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Middleware that validates query parameters for dangerous content.
|
|
274
|
+
*
|
|
275
|
+
* Checks query string length and validates specific parameters.
|
|
276
|
+
*
|
|
277
|
+
* @returns Hono middleware handler
|
|
278
|
+
*/
|
|
279
|
+
export function validateQueryParams(): MiddlewareHandler {
|
|
280
|
+
return async (c: Context, next: Next) => {
|
|
281
|
+
const url = new URL(c.req.url)
|
|
282
|
+
const queryString = url.search
|
|
283
|
+
|
|
284
|
+
// Check total query string length
|
|
285
|
+
if (queryString.length > MAX_QUERY_LENGTH) {
|
|
286
|
+
const { response, status } = createValidationErrorResponse(
|
|
287
|
+
`Query string exceeds maximum length of ${MAX_QUERY_LENGTH} characters`
|
|
288
|
+
)
|
|
289
|
+
return c.json(response, status)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return next()
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ============================================================================
|
|
297
|
+
// Content Sanitization Middleware
|
|
298
|
+
// ============================================================================
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Options for content sanitization.
|
|
302
|
+
*/
|
|
303
|
+
interface SanitizeOptions {
|
|
304
|
+
/** Whether to strip all HTML tags */
|
|
305
|
+
stripHtml: boolean
|
|
306
|
+
/** Fields to preserve (not sanitize) */
|
|
307
|
+
preserveFields: string[]
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Sanitizes a request body object, removing XSS vectors from string values.
|
|
312
|
+
*
|
|
313
|
+
* @param body - Request body to sanitize
|
|
314
|
+
* @param options - Sanitization options
|
|
315
|
+
* @returns Sanitized body
|
|
316
|
+
*/
|
|
317
|
+
function sanitizeRequestBody(body: Record<string, unknown>, options: SanitizeOptions): Record<string, unknown> {
|
|
318
|
+
const result: Record<string, unknown> = {}
|
|
319
|
+
|
|
320
|
+
for (const [key, value] of Object.entries(body)) {
|
|
321
|
+
if (options.preserveFields.includes(key)) {
|
|
322
|
+
result[key] = value
|
|
323
|
+
} else if (typeof value === 'string') {
|
|
324
|
+
result[key] = options.stripHtml ? sanitizeForStorage(value) : sanitizeHtml(value)
|
|
325
|
+
} else if (Array.isArray(value)) {
|
|
326
|
+
result[key] = value.map((item) =>
|
|
327
|
+
typeof item === 'object' && item !== null
|
|
328
|
+
? sanitizeRequestBody(item as Record<string, unknown>, options)
|
|
329
|
+
: typeof item === 'string'
|
|
330
|
+
? options.stripHtml
|
|
331
|
+
? sanitizeForStorage(item)
|
|
332
|
+
: sanitizeHtml(item)
|
|
333
|
+
: item
|
|
334
|
+
)
|
|
335
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
336
|
+
result[key] = sanitizeRequestBody(value as Record<string, unknown>, options)
|
|
337
|
+
} else {
|
|
338
|
+
result[key] = value
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return result
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Middleware that sanitizes request body content for XSS.
|
|
347
|
+
*
|
|
348
|
+
* Applies HTML sanitization to all string values in the request body.
|
|
349
|
+
* Does not reject content, just cleans it.
|
|
350
|
+
*
|
|
351
|
+
* @param options - Sanitization options
|
|
352
|
+
* @returns Hono middleware handler
|
|
353
|
+
*/
|
|
354
|
+
export function sanitizeContent(options: Partial<SanitizeOptions> = {}): MiddlewareHandler {
|
|
355
|
+
const sanitizeOptions: SanitizeOptions = {
|
|
356
|
+
stripHtml: options.stripHtml ?? false,
|
|
357
|
+
preserveFields: options.preserveFields ?? [],
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return async (c: Context, next: Next) => {
|
|
361
|
+
// Only process JSON bodies
|
|
362
|
+
const contentType = c.req.header('content-type')
|
|
363
|
+
if (!contentType?.includes('application/json')) {
|
|
364
|
+
return next()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const body = await c.req.json()
|
|
369
|
+
|
|
370
|
+
if (typeof body === 'object' && body !== null) {
|
|
371
|
+
const sanitized = sanitizeRequestBody(body, sanitizeOptions)
|
|
372
|
+
c.set('sanitizedBody', sanitized)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return next()
|
|
376
|
+
} catch {
|
|
377
|
+
// If JSON parsing fails, let the next middleware handle it
|
|
378
|
+
return next()
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ============================================================================
|
|
384
|
+
// Combined Validation Middleware
|
|
385
|
+
// ============================================================================
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Options for combined request validation.
|
|
389
|
+
*/
|
|
390
|
+
interface RequestValidationOptions<T> {
|
|
391
|
+
/** Zod schema for body validation */
|
|
392
|
+
schema: ZodSchema<T>
|
|
393
|
+
/** Maximum content size in bytes */
|
|
394
|
+
maxSize?: number
|
|
395
|
+
/** Whether to sanitize string values */
|
|
396
|
+
sanitize?: boolean
|
|
397
|
+
/** Whether to strip HTML from strings */
|
|
398
|
+
stripHtml?: boolean
|
|
399
|
+
/** Fields to preserve HTML in */
|
|
400
|
+
preserveHtmlFields?: string[]
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Combined middleware that applies size limit, sanitization, and schema validation.
|
|
405
|
+
*
|
|
406
|
+
* This is the recommended middleware for most endpoints.
|
|
407
|
+
*
|
|
408
|
+
* @param options - Validation configuration
|
|
409
|
+
* @returns Hono middleware handler
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* ```typescript
|
|
413
|
+
* app.post('/documents',
|
|
414
|
+
* validateRequest({
|
|
415
|
+
* schema: CreateDocumentSchema,
|
|
416
|
+
* maxSize: 50 * 1024,
|
|
417
|
+
* sanitize: true
|
|
418
|
+
* }),
|
|
419
|
+
* async (c) => {
|
|
420
|
+
* const body = c.get('validatedBody');
|
|
421
|
+
* }
|
|
422
|
+
* );
|
|
423
|
+
* ```
|
|
424
|
+
*/
|
|
425
|
+
export function validateRequest<T>(options: RequestValidationOptions<T>): MiddlewareHandler {
|
|
426
|
+
const sizeLimitMiddleware = contentSizeLimit({ maxSize: options.maxSize })
|
|
427
|
+
const schemaMiddleware = validateSchema(options.schema, {
|
|
428
|
+
sanitize: options.sanitize,
|
|
429
|
+
stripHtml: options.stripHtml,
|
|
430
|
+
preserveHtmlFields: options.preserveHtmlFields,
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
return async (c: Context, next: Next) => {
|
|
434
|
+
// Apply size limit
|
|
435
|
+
const sizeResult = await sizeLimitMiddleware(c, async () => {})
|
|
436
|
+
if (sizeResult) return sizeResult
|
|
437
|
+
|
|
438
|
+
// Apply schema validation
|
|
439
|
+
return schemaMiddleware(c, next)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ============================================================================
|
|
444
|
+
// Utility Functions
|
|
445
|
+
// ============================================================================
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Formats Zod validation errors into a human-readable string.
|
|
449
|
+
*/
|
|
450
|
+
function formatZodErrors(error: ZodError): string {
|
|
451
|
+
return error.issues
|
|
452
|
+
.map((issue) => {
|
|
453
|
+
const path = issue.path.join('.')
|
|
454
|
+
return path ? `${path}: ${issue.message}` : issue.message
|
|
455
|
+
})
|
|
456
|
+
.join('; ')
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Extracts field-level errors from a ZodError.
|
|
461
|
+
*/
|
|
462
|
+
function extractFieldErrors(error: ZodError): Record<string, string[]> {
|
|
463
|
+
const fieldErrors: Record<string, string[]> = {}
|
|
464
|
+
|
|
465
|
+
for (const issue of error.issues) {
|
|
466
|
+
const path = issue.path.join('.') || '_root'
|
|
467
|
+
if (!fieldErrors[path]) {
|
|
468
|
+
fieldErrors[path] = []
|
|
469
|
+
}
|
|
470
|
+
fieldErrors[path].push(issue.message)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return fieldErrors
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Formats bytes into a human-readable string.
|
|
478
|
+
*/
|
|
479
|
+
function formatBytes(bytes: number): string {
|
|
480
|
+
if (bytes < 1024) return `${bytes} bytes`
|
|
481
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
482
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ============================================================================
|
|
486
|
+
// Context Type Extensions
|
|
487
|
+
// ============================================================================
|
|
488
|
+
|
|
489
|
+
// Extend Hono's context type to include validated body
|
|
490
|
+
declare module 'hono' {
|
|
491
|
+
interface ContextVariableMap {
|
|
492
|
+
validatedBody: unknown
|
|
493
|
+
sanitizedBody: Record<string, unknown>
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ============================================================================
|
|
498
|
+
// Exports
|
|
499
|
+
// ============================================================================
|
|
500
|
+
|
|
501
|
+
export default {
|
|
502
|
+
contentSizeLimit,
|
|
503
|
+
defaultContentSizeLimit,
|
|
504
|
+
largeContentSizeLimit,
|
|
505
|
+
validateSchema,
|
|
506
|
+
validatePathParams,
|
|
507
|
+
validateQueryParams,
|
|
508
|
+
sanitizeContent,
|
|
509
|
+
validateRequest,
|
|
510
|
+
MAX_CONTENT_SIZE,
|
|
511
|
+
MAX_METADATA_SIZE,
|
|
512
|
+
MAX_QUERY_LENGTH,
|
|
513
|
+
MAX_CONTAINER_TAG_LENGTH,
|
|
514
|
+
}
|