agentdb 1.4.3 → 1.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/agentdb.min.js +4 -4
  2. package/dist/benchmarks/wasm-vector-benchmark.d.ts +10 -0
  3. package/dist/benchmarks/wasm-vector-benchmark.d.ts.map +1 -0
  4. package/dist/benchmarks/wasm-vector-benchmark.js +196 -0
  5. package/dist/benchmarks/wasm-vector-benchmark.js.map +1 -0
  6. package/dist/cli/agentdb-cli.d.ts +1 -1
  7. package/dist/cli/agentdb-cli.d.ts.map +1 -1
  8. package/dist/cli/agentdb-cli.js +74 -1
  9. package/dist/cli/agentdb-cli.js.map +1 -1
  10. package/dist/controllers/EmbeddingService.d.ts.map +1 -1
  11. package/dist/controllers/EmbeddingService.js +7 -3
  12. package/dist/controllers/EmbeddingService.js.map +1 -1
  13. package/dist/controllers/EnhancedEmbeddingService.d.ts +50 -0
  14. package/dist/controllers/EnhancedEmbeddingService.d.ts.map +1 -0
  15. package/dist/controllers/EnhancedEmbeddingService.js +119 -0
  16. package/dist/controllers/EnhancedEmbeddingService.js.map +1 -0
  17. package/dist/controllers/WASMVectorSearch.d.ts +89 -0
  18. package/dist/controllers/WASMVectorSearch.d.ts.map +1 -0
  19. package/dist/controllers/WASMVectorSearch.js +226 -0
  20. package/dist/controllers/WASMVectorSearch.js.map +1 -0
  21. package/dist/controllers/index.d.ts +4 -0
  22. package/dist/controllers/index.d.ts.map +1 -1
  23. package/dist/controllers/index.js +2 -0
  24. package/dist/controllers/index.js.map +1 -1
  25. package/dist/db-fallback.d.ts +4 -0
  26. package/dist/db-fallback.d.ts.map +1 -1
  27. package/dist/db-fallback.js +36 -10
  28. package/dist/db-fallback.js.map +1 -1
  29. package/dist/examples/wasm-vector-usage.d.ts +12 -0
  30. package/dist/examples/wasm-vector-usage.d.ts.map +1 -0
  31. package/dist/examples/wasm-vector-usage.js +190 -0
  32. package/dist/examples/wasm-vector-usage.js.map +1 -0
  33. package/dist/mcp/agentdb-mcp-server.js +54 -27
  34. package/dist/mcp/agentdb-mcp-server.js.map +1 -1
  35. package/dist/optimizations/BatchOperations.d.ts +7 -2
  36. package/dist/optimizations/BatchOperations.d.ts.map +1 -1
  37. package/dist/optimizations/BatchOperations.js +46 -19
  38. package/dist/optimizations/BatchOperations.js.map +1 -1
  39. package/dist/security/input-validation.d.ts +85 -0
  40. package/dist/security/input-validation.d.ts.map +1 -0
  41. package/dist/security/input-validation.js +292 -0
  42. package/dist/security/input-validation.js.map +1 -0
  43. package/package.json +10 -3
  44. package/src/benchmarks/wasm-vector-benchmark.ts +250 -0
  45. package/src/cli/agentdb-cli.ts +83 -1
  46. package/src/controllers/EmbeddingService.ts +7 -3
  47. package/src/controllers/EnhancedEmbeddingService.ts +159 -0
  48. package/src/controllers/WASMVectorSearch.ts +308 -0
  49. package/src/controllers/index.ts +4 -0
  50. package/src/db-fallback.ts +38 -10
  51. package/src/examples/wasm-vector-usage.ts +245 -0
  52. package/src/mcp/agentdb-mcp-server.ts +59 -28
  53. package/src/optimizations/BatchOperations.ts +55 -24
  54. package/src/security/input-validation.ts +377 -0
  55. package/src/tests/wasm-vector-search.test.ts +240 -0
@@ -23,6 +23,13 @@ import { LearningSystem } from '../controllers/LearningSystem.js';
23
23
  import { EmbeddingService } from '../controllers/EmbeddingService.js';
24
24
  import { BatchOperations } from '../optimizations/BatchOperations.js';
25
25
  import { ReasoningBank } from '../controllers/ReasoningBank.js';
26
+ import {
27
+ validateId,
28
+ validateTimestamp,
29
+ validateSessionId,
30
+ ValidationError,
31
+ handleSecurityError,
32
+ } from '../security/input-validation.js';
26
33
  import * as path from 'path';
27
34
  import * as fs from 'fs';
28
35
 
@@ -879,38 +886,62 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
879
886
  const id = args?.id as number | undefined;
880
887
  const filters = args?.filters as any;
881
888
 
882
- if (id !== undefined) {
883
- // Delete single vector
884
- const stmt = db.prepare('DELETE FROM episodes WHERE id = ?');
885
- const result = stmt.run(id);
886
- deleted = result.changes;
887
- } else if (filters) {
888
- // Bulk delete with filters
889
- const conditions: Record<string, any> = {};
890
-
891
- if (filters.session_id) {
892
- conditions.session_id = filters.session_id;
893
- }
889
+ try {
890
+ if (id !== undefined) {
891
+ // Validate ID
892
+ const validatedId = validateId(id, 'id');
894
893
 
895
- if (filters.before_timestamp) {
896
- const stmt = db.prepare('DELETE FROM episodes WHERE ts < ?');
897
- const result = stmt.run(filters.before_timestamp);
894
+ // Delete single vector using parameterized query
895
+ const stmt = db.prepare('DELETE FROM episodes WHERE id = ?');
896
+ const result = stmt.run(validatedId);
898
897
  deleted = result.changes;
899
- } else if (Object.keys(conditions).length > 0) {
900
- deleted = batchOps.bulkDelete('episodes', conditions);
898
+ } else if (filters) {
899
+ // Bulk delete with validated filters
900
+ if (filters.session_id) {
901
+ // Validate session_id
902
+ const validatedSessionId = validateSessionId(filters.session_id);
903
+
904
+ // Use parameterized query
905
+ const stmt = db.prepare('DELETE FROM episodes WHERE session_id = ?');
906
+ const result = stmt.run(validatedSessionId);
907
+ deleted = result.changes;
908
+ } else if (filters.before_timestamp) {
909
+ // Validate timestamp
910
+ const validatedTimestamp = validateTimestamp(filters.before_timestamp, 'before_timestamp');
911
+
912
+ // Use parameterized query
913
+ const stmt = db.prepare('DELETE FROM episodes WHERE ts < ?');
914
+ const result = stmt.run(validatedTimestamp);
915
+ deleted = result.changes;
916
+ } else {
917
+ throw new ValidationError('Invalid or missing filter criteria', 'INVALID_FILTER');
918
+ }
919
+ } else {
920
+ throw new ValidationError('Either id or filters must be provided', 'MISSING_PARAMETER');
901
921
  }
902
- }
903
922
 
904
- return {
905
- content: [
906
- {
907
- type: 'text',
908
- text: `✅ Delete operation completed!\n` +
909
- `📊 Deleted: ${deleted} vector(s)\n` +
910
- `🗑️ ${id !== undefined ? `ID: ${id}` : 'Bulk deletion with filters'}`,
911
- },
912
- ],
913
- };
923
+ return {
924
+ content: [
925
+ {
926
+ type: 'text',
927
+ text: `✅ Delete operation completed!\n` +
928
+ `📊 Deleted: ${deleted} vector(s)\n` +
929
+ `🗑️ ${id !== undefined ? `ID: ${id}` : 'Bulk deletion with filters'}`,
930
+ },
931
+ ],
932
+ };
933
+ } catch (error: any) {
934
+ const safeMessage = handleSecurityError(error);
935
+ return {
936
+ content: [
937
+ {
938
+ type: 'text',
939
+ text: `❌ Delete operation failed: ${safeMessage}`,
940
+ },
941
+ ],
942
+ isError: true,
943
+ };
944
+ }
914
945
  }
915
946
 
916
947
  // ======================================================================
@@ -6,12 +6,23 @@
6
6
  * - Batch embedding generation
7
7
  * - Parallel processing
8
8
  * - Progress tracking
9
+ *
10
+ * SECURITY: Fixed SQL injection vulnerabilities:
11
+ * - Table names validated against whitelist
12
+ * - Column names validated against whitelist
13
+ * - All queries use parameterized values
9
14
  */
10
15
 
11
16
  // Database type from db-fallback
12
17
  type Database = any;
13
18
  import { EmbeddingService } from '../controllers/EmbeddingService';
14
19
  import { Episode } from '../controllers/ReflexionMemory';
20
+ import {
21
+ validateTableName,
22
+ buildSafeWhereClause,
23
+ buildSafeSetClause,
24
+ ValidationError,
25
+ } from '../security/input-validation.js';
15
26
 
16
27
  export interface BatchConfig {
17
28
  batchSize: number;
@@ -172,45 +183,65 @@ export class BatchOperations {
172
183
  }
173
184
 
174
185
  /**
175
- * Bulk delete with conditions
186
+ * Bulk delete with conditions (SQL injection safe)
176
187
  */
177
188
  bulkDelete(table: string, conditions: Record<string, any>): number {
178
- const whereClause = Object.keys(conditions)
179
- .map(key => `${key} = ?`)
180
- .join(' AND ');
181
-
182
- const values = Object.values(conditions);
183
-
184
- const stmt = this.db.prepare(`DELETE FROM ${table} WHERE ${whereClause}`);
185
- const result = stmt.run(...values);
186
-
187
- return result.changes;
189
+ try {
190
+ // SECURITY: Validate table name against whitelist
191
+ const validatedTable = validateTableName(table);
192
+
193
+ // SECURITY: Build safe WHERE clause with validated column names
194
+ const { clause, values } = buildSafeWhereClause(validatedTable, conditions);
195
+
196
+ // Execute with parameterized query
197
+ const stmt = this.db.prepare(`DELETE FROM ${validatedTable} WHERE ${clause}`);
198
+ const result = stmt.run(...values);
199
+
200
+ return result.changes;
201
+ } catch (error) {
202
+ if (error instanceof ValidationError) {
203
+ console.error(`❌ Bulk delete validation error: ${error.message}`);
204
+ throw error;
205
+ }
206
+ throw error;
207
+ }
188
208
  }
189
209
 
190
210
  /**
191
- * Bulk update with conditions
211
+ * Bulk update with conditions (SQL injection safe)
192
212
  */
193
213
  bulkUpdate(
194
214
  table: string,
195
215
  updates: Record<string, any>,
196
216
  conditions: Record<string, any>
197
217
  ): number {
198
- const setClause = Object.keys(updates)
199
- .map(key => `${key} = ?`)
200
- .join(', ');
218
+ try {
219
+ // SECURITY: Validate table name against whitelist
220
+ const validatedTable = validateTableName(table);
221
+
222
+ // SECURITY: Build safe SET clause with validated column names
223
+ const setResult = buildSafeSetClause(validatedTable, updates);
201
224
 
202
- const whereClause = Object.keys(conditions)
203
- .map(key => `${key} = ?`)
204
- .join(' AND ');
225
+ // SECURITY: Build safe WHERE clause with validated column names
226
+ const whereResult = buildSafeWhereClause(validatedTable, conditions);
205
227
 
206
- const values = [...Object.values(updates), ...Object.values(conditions)];
228
+ // Combine values from SET and WHERE clauses
229
+ const values = [...setResult.values, ...whereResult.values];
207
230
 
208
- const stmt = this.db.prepare(
209
- `UPDATE ${table} SET ${setClause} WHERE ${whereClause}`
210
- );
211
- const result = stmt.run(...values);
231
+ // Execute with parameterized query
232
+ const stmt = this.db.prepare(
233
+ `UPDATE ${validatedTable} SET ${setResult.clause} WHERE ${whereResult.clause}`
234
+ );
235
+ const result = stmt.run(...values);
212
236
 
213
- return result.changes;
237
+ return result.changes;
238
+ } catch (error) {
239
+ if (error instanceof ValidationError) {
240
+ console.error(`❌ Bulk update validation error: ${error.message}`);
241
+ throw error;
242
+ }
243
+ throw error;
244
+ }
214
245
  }
215
246
 
216
247
  /**
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Input Validation and Sanitization for AgentDB Security
3
+ *
4
+ * Provides comprehensive validation to prevent SQL injection and other attacks:
5
+ * - Whitelist-based validation for identifiers (tables, columns, PRAGMA commands)
6
+ * - Input sanitization for user data
7
+ * - Type validation and constraints
8
+ * - Error handling that doesn't leak sensitive information
9
+ */
10
+
11
+ /**
12
+ * Allowed table names in AgentDB (whitelist)
13
+ */
14
+ const ALLOWED_TABLES = new Set([
15
+ 'episodes',
16
+ 'episode_embeddings',
17
+ 'skills',
18
+ 'skill_embeddings',
19
+ 'causal_edges',
20
+ 'causal_experiments',
21
+ 'causal_observations',
22
+ 'provenance_certificates',
23
+ 'reasoning_patterns',
24
+ 'pattern_embeddings',
25
+ 'rl_sessions',
26
+ 'rl_experiences',
27
+ 'rl_policies',
28
+ 'rl_q_values',
29
+ ]);
30
+
31
+ /**
32
+ * Allowed column names by table (whitelist)
33
+ */
34
+ const ALLOWED_COLUMNS: Record<string, Set<string>> = {
35
+ episodes: new Set([
36
+ 'id', 'ts', 'session_id', 'task', 'input', 'output', 'critique',
37
+ 'reward', 'success', 'latency_ms', 'tokens_used', 'tags', 'metadata'
38
+ ]),
39
+ skills: new Set([
40
+ 'id', 'ts', 'name', 'description', 'signature', 'code',
41
+ 'success_rate', 'uses', 'avg_reward', 'avg_latency_ms', 'tags', 'metadata'
42
+ ]),
43
+ causal_edges: new Set([
44
+ 'id', 'ts', 'from_memory_id', 'from_memory_type', 'to_memory_id',
45
+ 'to_memory_type', 'similarity', 'uplift', 'confidence', 'sample_size', 'evidence_ids'
46
+ ]),
47
+ // Add more as needed
48
+ };
49
+
50
+ /**
51
+ * Allowed PRAGMA commands (whitelist)
52
+ */
53
+ const ALLOWED_PRAGMAS = new Set([
54
+ 'journal_mode',
55
+ 'synchronous',
56
+ 'cache_size',
57
+ 'page_size',
58
+ 'page_count',
59
+ 'user_version',
60
+ 'foreign_keys',
61
+ 'temp_store',
62
+ 'mmap_size',
63
+ 'wal_autocheckpoint',
64
+ ]);
65
+
66
+ /**
67
+ * Validation error with safe error messages
68
+ */
69
+ export class ValidationError extends Error {
70
+ public readonly code: string;
71
+ public readonly field?: string;
72
+
73
+ constructor(message: string, code: string = 'VALIDATION_ERROR', field?: string) {
74
+ super(message);
75
+ this.name = 'ValidationError';
76
+ this.code = code;
77
+ this.field = field;
78
+ }
79
+
80
+ /**
81
+ * Get safe error message (doesn't leak sensitive info)
82
+ */
83
+ getSafeMessage(): string {
84
+ return `Invalid input: ${this.field || 'unknown field'}`;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Validate table name against whitelist
90
+ */
91
+ export function validateTableName(tableName: string): string {
92
+ if (!tableName || typeof tableName !== 'string') {
93
+ throw new ValidationError('Table name must be a non-empty string', 'INVALID_TABLE', 'tableName');
94
+ }
95
+
96
+ const sanitized = tableName.trim().toLowerCase();
97
+
98
+ if (!ALLOWED_TABLES.has(sanitized)) {
99
+ throw new ValidationError(
100
+ `Invalid table name: ${sanitized}. Allowed tables: ${Array.from(ALLOWED_TABLES).join(', ')}`,
101
+ 'INVALID_TABLE',
102
+ 'tableName'
103
+ );
104
+ }
105
+
106
+ return sanitized;
107
+ }
108
+
109
+ /**
110
+ * Validate column name against whitelist
111
+ */
112
+ export function validateColumnName(tableName: string, columnName: string): string {
113
+ if (!columnName || typeof columnName !== 'string') {
114
+ throw new ValidationError('Column name must be a non-empty string', 'INVALID_COLUMN', 'columnName');
115
+ }
116
+
117
+ const sanitized = columnName.trim().toLowerCase();
118
+ const validatedTable = validateTableName(tableName);
119
+
120
+ const allowedColumns = ALLOWED_COLUMNS[validatedTable];
121
+ if (allowedColumns && !allowedColumns.has(sanitized)) {
122
+ throw new ValidationError(
123
+ `Invalid column name for table ${validatedTable}: ${sanitized}`,
124
+ 'INVALID_COLUMN',
125
+ 'columnName'
126
+ );
127
+ }
128
+
129
+ return sanitized;
130
+ }
131
+
132
+ /**
133
+ * Validate PRAGMA command against whitelist
134
+ */
135
+ export function validatePragmaCommand(pragma: string): string {
136
+ if (!pragma || typeof pragma !== 'string') {
137
+ throw new ValidationError('PRAGMA command must be a non-empty string', 'INVALID_PRAGMA', 'pragma');
138
+ }
139
+
140
+ // Extract the pragma name (before any = or space)
141
+ const pragmaName = pragma.trim().toLowerCase().split(/[=\s]/)[0];
142
+
143
+ if (!ALLOWED_PRAGMAS.has(pragmaName)) {
144
+ throw new ValidationError(
145
+ `Invalid PRAGMA command: ${pragmaName}. Allowed: ${Array.from(ALLOWED_PRAGMAS).join(', ')}`,
146
+ 'INVALID_PRAGMA',
147
+ 'pragma'
148
+ );
149
+ }
150
+
151
+ // Return the full pragma for execution (e.g., "journal_mode = WAL")
152
+ return pragma.trim();
153
+ }
154
+
155
+ /**
156
+ * Validate and sanitize session ID
157
+ */
158
+ export function validateSessionId(sessionId: string): string {
159
+ if (!sessionId || typeof sessionId !== 'string') {
160
+ throw new ValidationError('Session ID must be a non-empty string', 'INVALID_SESSION_ID', 'sessionId');
161
+ }
162
+
163
+ // Allow alphanumeric, hyphens, underscores (max 255 chars)
164
+ const sanitized = sessionId.trim();
165
+
166
+ if (sanitized.length > 255) {
167
+ throw new ValidationError('Session ID exceeds maximum length (255)', 'INVALID_SESSION_ID', 'sessionId');
168
+ }
169
+
170
+ if (!/^[a-zA-Z0-9_-]+$/.test(sanitized)) {
171
+ throw new ValidationError(
172
+ 'Session ID must contain only alphanumeric characters, hyphens, and underscores',
173
+ 'INVALID_SESSION_ID',
174
+ 'sessionId'
175
+ );
176
+ }
177
+
178
+ return sanitized;
179
+ }
180
+
181
+ /**
182
+ * Validate numeric ID
183
+ */
184
+ export function validateId(id: any, fieldName: string = 'id'): number {
185
+ const numId = Number(id);
186
+
187
+ if (!Number.isFinite(numId) || numId < 0 || !Number.isInteger(numId)) {
188
+ throw new ValidationError(`${fieldName} must be a non-negative integer`, 'INVALID_ID', fieldName);
189
+ }
190
+
191
+ return numId;
192
+ }
193
+
194
+ /**
195
+ * Validate timestamp
196
+ */
197
+ export function validateTimestamp(timestamp: any, fieldName: string = 'timestamp'): number {
198
+ const numTs = Number(timestamp);
199
+
200
+ if (!Number.isFinite(numTs) || numTs < 0) {
201
+ throw new ValidationError(`${fieldName} must be a non-negative number`, 'INVALID_TIMESTAMP', fieldName);
202
+ }
203
+
204
+ // Reasonable timestamp bounds (2000-01-01 to 2100-01-01)
205
+ const MIN_TIMESTAMP = 946684800; // 2000-01-01
206
+ const MAX_TIMESTAMP = 4102444800; // 2100-01-01
207
+
208
+ if (numTs < MIN_TIMESTAMP || numTs > MAX_TIMESTAMP) {
209
+ throw new ValidationError(
210
+ `${fieldName} is out of valid range (2000-2100)`,
211
+ 'INVALID_TIMESTAMP',
212
+ fieldName
213
+ );
214
+ }
215
+
216
+ return numTs;
217
+ }
218
+
219
+ /**
220
+ * Validate reward value (0-1)
221
+ */
222
+ export function validateReward(reward: any): number {
223
+ const numReward = Number(reward);
224
+
225
+ if (!Number.isFinite(numReward)) {
226
+ throw new ValidationError('Reward must be a number', 'INVALID_REWARD', 'reward');
227
+ }
228
+
229
+ if (numReward < 0 || numReward > 1) {
230
+ throw new ValidationError('Reward must be between 0 and 1', 'INVALID_REWARD', 'reward');
231
+ }
232
+
233
+ return numReward;
234
+ }
235
+
236
+ /**
237
+ * Validate success flag
238
+ */
239
+ export function validateSuccess(success: any): boolean {
240
+ if (typeof success === 'boolean') {
241
+ return success;
242
+ }
243
+
244
+ if (typeof success === 'number') {
245
+ return success !== 0;
246
+ }
247
+
248
+ if (typeof success === 'string') {
249
+ const lower = success.toLowerCase();
250
+ if (lower === 'true' || lower === '1' || lower === 'yes') return true;
251
+ if (lower === 'false' || lower === '0' || lower === 'no') return false;
252
+ }
253
+
254
+ throw new ValidationError('Success must be a boolean value', 'INVALID_BOOLEAN', 'success');
255
+ }
256
+
257
+ /**
258
+ * Sanitize text input (prevent extremely long strings, null bytes, etc.)
259
+ */
260
+ export function sanitizeText(text: string, maxLength: number = 100000, fieldName: string = 'text'): string {
261
+ if (typeof text !== 'string') {
262
+ throw new ValidationError(`${fieldName} must be a string`, 'INVALID_TEXT', fieldName);
263
+ }
264
+
265
+ // Remove null bytes
266
+ const sanitized = text.replace(/\0/g, '');
267
+
268
+ if (sanitized.length > maxLength) {
269
+ throw new ValidationError(
270
+ `${fieldName} exceeds maximum length (${maxLength})`,
271
+ 'TEXT_TOO_LONG',
272
+ fieldName
273
+ );
274
+ }
275
+
276
+ return sanitized;
277
+ }
278
+
279
+ /**
280
+ * Build safe WHERE clause with parameterized values
281
+ * Returns both the SQL clause and the parameter values
282
+ */
283
+ export function buildSafeWhereClause(
284
+ tableName: string,
285
+ conditions: Record<string, any>
286
+ ): { clause: string; values: any[] } {
287
+ const validatedTable = validateTableName(tableName);
288
+
289
+ if (!conditions || typeof conditions !== 'object' || Object.keys(conditions).length === 0) {
290
+ throw new ValidationError('Conditions must be a non-empty object', 'INVALID_CONDITIONS', 'conditions');
291
+ }
292
+
293
+ const clauses: string[] = [];
294
+ const values: any[] = [];
295
+
296
+ for (const [column, value] of Object.entries(conditions)) {
297
+ const validatedColumn = validateColumnName(validatedTable, column);
298
+ clauses.push(`${validatedColumn} = ?`);
299
+ values.push(value);
300
+ }
301
+
302
+ return {
303
+ clause: clauses.join(' AND '),
304
+ values,
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Build safe SET clause for UPDATE statements
310
+ */
311
+ export function buildSafeSetClause(
312
+ tableName: string,
313
+ updates: Record<string, any>
314
+ ): { clause: string; values: any[] } {
315
+ const validatedTable = validateTableName(tableName);
316
+
317
+ if (!updates || typeof updates !== 'object' || Object.keys(updates).length === 0) {
318
+ throw new ValidationError('Updates must be a non-empty object', 'INVALID_UPDATES', 'updates');
319
+ }
320
+
321
+ const clauses: string[] = [];
322
+ const values: any[] = [];
323
+
324
+ for (const [column, value] of Object.entries(updates)) {
325
+ const validatedColumn = validateColumnName(validatedTable, column);
326
+ clauses.push(`${validatedColumn} = ?`);
327
+ values.push(value);
328
+ }
329
+
330
+ return {
331
+ clause: clauses.join(', '),
332
+ values,
333
+ };
334
+ }
335
+
336
+ /**
337
+ * Validate JSON data
338
+ */
339
+ export function validateJSON(data: any, fieldName: string = 'json'): string {
340
+ try {
341
+ return JSON.stringify(data);
342
+ } catch (error) {
343
+ throw new ValidationError(`${fieldName} is not valid JSON`, 'INVALID_JSON', fieldName);
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Validate array of tags
349
+ */
350
+ export function validateTags(tags: any): string[] {
351
+ if (!Array.isArray(tags)) {
352
+ throw new ValidationError('Tags must be an array', 'INVALID_TAGS', 'tags');
353
+ }
354
+
355
+ const sanitized = tags.map((tag, i) => {
356
+ if (typeof tag !== 'string') {
357
+ throw new ValidationError(`Tag at index ${i} must be a string`, 'INVALID_TAG', `tags[${i}]`);
358
+ }
359
+ return sanitizeText(tag, 100, `tags[${i}]`);
360
+ });
361
+
362
+ return sanitized;
363
+ }
364
+
365
+ /**
366
+ * Safe error handler that doesn't leak sensitive information
367
+ */
368
+ export function handleSecurityError(error: any): string {
369
+ if (error instanceof ValidationError) {
370
+ // Safe to return validation errors
371
+ return error.message;
372
+ }
373
+
374
+ // For other errors, return generic message and log details internally
375
+ console.error('Security error:', error);
376
+ return 'An error occurred while processing your request. Please check your input and try again.';
377
+ }