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.
- package/dist/agentdb.min.js +4 -4
- package/dist/benchmarks/wasm-vector-benchmark.d.ts +10 -0
- package/dist/benchmarks/wasm-vector-benchmark.d.ts.map +1 -0
- package/dist/benchmarks/wasm-vector-benchmark.js +196 -0
- package/dist/benchmarks/wasm-vector-benchmark.js.map +1 -0
- package/dist/cli/agentdb-cli.d.ts +1 -1
- package/dist/cli/agentdb-cli.d.ts.map +1 -1
- package/dist/cli/agentdb-cli.js +74 -1
- package/dist/cli/agentdb-cli.js.map +1 -1
- package/dist/controllers/EmbeddingService.d.ts.map +1 -1
- package/dist/controllers/EmbeddingService.js +7 -3
- package/dist/controllers/EmbeddingService.js.map +1 -1
- package/dist/controllers/EnhancedEmbeddingService.d.ts +50 -0
- package/dist/controllers/EnhancedEmbeddingService.d.ts.map +1 -0
- package/dist/controllers/EnhancedEmbeddingService.js +119 -0
- package/dist/controllers/EnhancedEmbeddingService.js.map +1 -0
- package/dist/controllers/WASMVectorSearch.d.ts +89 -0
- package/dist/controllers/WASMVectorSearch.d.ts.map +1 -0
- package/dist/controllers/WASMVectorSearch.js +226 -0
- package/dist/controllers/WASMVectorSearch.js.map +1 -0
- package/dist/controllers/index.d.ts +4 -0
- package/dist/controllers/index.d.ts.map +1 -1
- package/dist/controllers/index.js +2 -0
- package/dist/controllers/index.js.map +1 -1
- package/dist/db-fallback.d.ts +4 -0
- package/dist/db-fallback.d.ts.map +1 -1
- package/dist/db-fallback.js +36 -10
- package/dist/db-fallback.js.map +1 -1
- package/dist/examples/wasm-vector-usage.d.ts +12 -0
- package/dist/examples/wasm-vector-usage.d.ts.map +1 -0
- package/dist/examples/wasm-vector-usage.js +190 -0
- package/dist/examples/wasm-vector-usage.js.map +1 -0
- package/dist/mcp/agentdb-mcp-server.js +54 -27
- package/dist/mcp/agentdb-mcp-server.js.map +1 -1
- package/dist/optimizations/BatchOperations.d.ts +7 -2
- package/dist/optimizations/BatchOperations.d.ts.map +1 -1
- package/dist/optimizations/BatchOperations.js +46 -19
- package/dist/optimizations/BatchOperations.js.map +1 -1
- package/dist/security/input-validation.d.ts +85 -0
- package/dist/security/input-validation.d.ts.map +1 -0
- package/dist/security/input-validation.js +292 -0
- package/dist/security/input-validation.js.map +1 -0
- package/package.json +10 -3
- package/src/benchmarks/wasm-vector-benchmark.ts +250 -0
- package/src/cli/agentdb-cli.ts +83 -1
- package/src/controllers/EmbeddingService.ts +7 -3
- package/src/controllers/EnhancedEmbeddingService.ts +159 -0
- package/src/controllers/WASMVectorSearch.ts +308 -0
- package/src/controllers/index.ts +4 -0
- package/src/db-fallback.ts +38 -10
- package/src/examples/wasm-vector-usage.ts +245 -0
- package/src/mcp/agentdb-mcp-server.ts +59 -28
- package/src/optimizations/BatchOperations.ts +55 -24
- package/src/security/input-validation.ts +377 -0
- 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
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
-
|
|
896
|
-
const stmt = db.prepare('DELETE FROM episodes WHERE
|
|
897
|
-
const result = stmt.run(
|
|
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 (
|
|
900
|
-
|
|
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
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
.join(' AND ');
|
|
225
|
+
// SECURITY: Build safe WHERE clause with validated column names
|
|
226
|
+
const whereResult = buildSafeWhereClause(validatedTable, conditions);
|
|
205
227
|
|
|
206
|
-
|
|
228
|
+
// Combine values from SET and WHERE clauses
|
|
229
|
+
const values = [...setResult.values, ...whereResult.values];
|
|
207
230
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
+
}
|