11import { createClient } from '@libsql/client' ;
22import { Entity , Relation } from '../types/index.js' ;
33
4+ // Input limits
5+ const MAX_ENTITY_NAME_LENGTH = 256 ;
6+ const MAX_ENTITY_TYPE_LENGTH = 256 ;
7+ const MAX_OBSERVATION_LENGTH = 4096 ;
8+ const MAX_OBSERVATIONS_PER_ENTITY = 100 ;
9+ const MAX_RELATION_TYPE_LENGTH = 256 ;
10+
411// Types for configuration
512interface DatabaseConfig {
613 url : string ;
714 authToken ?: string ;
815}
916
17+ /**
18+ * Sanitize a string to mitigate prompt injection.
19+ * Strips control characters and common injection markers
20+ * while preserving normal content.
21+ */
22+ function sanitize_input ( input : string ) : string {
23+ return (
24+ input
25+ // Strip non-printable control chars (except newline, tab)
26+ . replace ( / [ ^ \P{ C} \n \t ] / gu, '' )
27+ // Collapse excessive whitespace/newlines
28+ . replace ( / \n { 3 , } / g, '\n\n' )
29+ . trim ( )
30+ ) ;
31+ }
32+
1033export class DatabaseManager {
1134 private static instance : DatabaseManager ;
1235 private client ;
@@ -41,23 +64,39 @@ export class DatabaseManager {
4164 ) : Promise < void > {
4265 try {
4366 for ( const entity of entities ) {
44- // Validate entity name
67+ // Validate and sanitize entity name
4568 if (
4669 ! entity . name ||
4770 typeof entity . name !== 'string' ||
4871 entity . name . trim ( ) === ''
4972 ) {
5073 throw new Error ( 'Entity name must be a non-empty string' ) ;
5174 }
75+ const safe_name = sanitize_input ( entity . name ) . slice (
76+ 0 ,
77+ MAX_ENTITY_NAME_LENGTH ,
78+ ) ;
79+ if ( safe_name === '' ) {
80+ throw new Error ( 'Entity name is empty after sanitization' ) ;
81+ }
5282
53- // Validate entity type
83+ // Validate and sanitize entity type
5484 if (
5585 ! entity . entityType ||
5686 typeof entity . entityType !== 'string' ||
5787 entity . entityType . trim ( ) === ''
5888 ) {
5989 throw new Error (
60- `Invalid entity type for entity "${ entity . name } "` ,
90+ `Invalid entity type for entity "${ safe_name } "` ,
91+ ) ;
92+ }
93+ const safe_type = sanitize_input ( entity . entityType ) . slice (
94+ 0 ,
95+ MAX_ENTITY_TYPE_LENGTH ,
96+ ) ;
97+ if ( safe_type === '' ) {
98+ throw new Error (
99+ `Entity type is empty after sanitization for entity "${ safe_name } "` ,
61100 ) ;
62101 }
63102
@@ -67,49 +106,66 @@ export class DatabaseManager {
67106 entity . observations . length === 0
68107 ) {
69108 throw new Error (
70- `Entity "${ entity . name } " must have at least one observation` ,
109+ `Entity "${ safe_name } " must have at least one observation` ,
71110 ) ;
72111 }
73112
74113 if (
75- ! entity . observations . every (
76- ( obs ) => typeof obs === 'string' && obs . trim ( ) !== '' ,
77- )
114+ entity . observations . length > MAX_OBSERVATIONS_PER_ENTITY
78115 ) {
79116 throw new Error (
80- `Entity "${ entity . name } " has invalid observations. All observations must be non-empty strings ` ,
117+ `Entity "${ safe_name } " exceeds maximum of ${ MAX_OBSERVATIONS_PER_ENTITY } observations` ,
81118 ) ;
82119 }
83120
121+ // Sanitize observations and validate
122+ const safe_observations = entity . observations . map ( ( obs ) => {
123+ if ( typeof obs !== 'string' || obs . trim ( ) === '' ) {
124+ throw new Error (
125+ `Entity "${ safe_name } " has invalid observations. All observations must be non-empty strings` ,
126+ ) ;
127+ }
128+ const sanitized = sanitize_input ( obs ) . slice (
129+ 0 ,
130+ MAX_OBSERVATION_LENGTH ,
131+ ) ;
132+ if ( sanitized === '' ) {
133+ throw new Error (
134+ `Entity "${ safe_name } " has an observation that is empty after sanitization` ,
135+ ) ;
136+ }
137+ return sanitized ;
138+ } ) ;
139+
84140 // Start a transaction
85141 const txn = await this . client . transaction ( 'write' ) ;
86142
87143 try {
88144 // First try to update
89145 const result = await txn . execute ( {
90146 sql : 'UPDATE entities SET entity_type = ? WHERE name = ?' ,
91- args : [ entity . entityType , entity . name ] ,
147+ args : [ safe_type , safe_name ] ,
92148 } ) ;
93149
94150 // If no rows affected, do insert
95151 if ( result . rowsAffected === 0 ) {
96152 await txn . execute ( {
97153 sql : 'INSERT INTO entities (name, entity_type) VALUES (?, ?)' ,
98- args : [ entity . name , entity . entityType ] ,
154+ args : [ safe_name , safe_type ] ,
99155 } ) ;
100156 }
101157
102158 // Clear old observations
103159 await txn . execute ( {
104160 sql : 'DELETE FROM observations WHERE entity_name = ?' ,
105- args : [ entity . name ] ,
161+ args : [ safe_name ] ,
106162 } ) ;
107163
108164 // Add new observations
109- for ( const observation of entity . observations ) {
165+ for ( const observation of safe_observations ) {
110166 await txn . execute ( {
111167 sql : 'INSERT INTO observations (entity_name, content) VALUES (?, ?)' ,
112- args : [ entity . name , observation ] ,
168+ args : [ safe_name , observation ] ,
113169 } ) ;
114170 }
115171
@@ -243,11 +299,32 @@ export class DatabaseManager {
243299 try {
244300 if ( relations . length === 0 ) return ;
245301
246- // Prepare batch statements for all relations
247- const batch_statements = relations . map ( ( relation ) => ( {
248- sql : 'INSERT INTO relations (source, target, relation_type) VALUES (?, ?, ?)' ,
249- args : [ relation . from , relation . to , relation . relationType ] ,
250- } ) ) ;
302+ // Sanitize and validate relation inputs
303+ const batch_statements = relations . map ( ( relation ) => {
304+ const safe_from = sanitize_input ( relation . from ) . slice (
305+ 0 ,
306+ MAX_ENTITY_NAME_LENGTH ,
307+ ) ;
308+ const safe_to = sanitize_input ( relation . to ) . slice (
309+ 0 ,
310+ MAX_ENTITY_NAME_LENGTH ,
311+ ) ;
312+ const safe_type = sanitize_input ( relation . relationType ) . slice (
313+ 0 ,
314+ MAX_RELATION_TYPE_LENGTH ,
315+ ) ;
316+
317+ if ( ! safe_from || ! safe_to || ! safe_type ) {
318+ throw new Error (
319+ 'Relation source, target, and type must be non-empty strings' ,
320+ ) ;
321+ }
322+
323+ return {
324+ sql : 'INSERT INTO relations (source, target, relation_type) VALUES (?, ?, ?)' ,
325+ args : [ safe_from , safe_to , safe_type ] ,
326+ } ;
327+ } ) ;
251328
252329 // Execute all inserts in a single batch transaction
253330 await this . client . batch ( batch_statements , 'write' ) ;
@@ -358,9 +435,8 @@ export class DatabaseManager {
358435 relations : Relation [ ] ;
359436 } > {
360437 const recent_entities = await this . get_recent_entities ( ) ;
361- const relations = await this . get_relations_for_entities (
362- recent_entities ,
363- ) ;
438+ const relations =
439+ await this . get_relations_for_entities ( recent_entities ) ;
364440 return { entities : recent_entities , relations } ;
365441 }
366442
@@ -385,9 +461,8 @@ export class DatabaseManager {
385461 return { entities : [ ] , relations : [ ] } ;
386462 }
387463
388- const relations = await this . get_relations_for_entities (
389- entities ,
390- ) ;
464+ const relations =
465+ await this . get_relations_for_entities ( entities ) ;
391466 return { entities, relations } ;
392467 } catch ( error ) {
393468 throw new Error (
0 commit comments