@@ -2,6 +2,11 @@ import { DatabaseAdapter } from './database-adapter';
22import { ParsedNode } from '../parsers/node-parser' ;
33import { SQLiteStorageService } from '../services/sqlite-storage-service' ;
44import { NodeTypeNormalizer } from '../utils/node-type-normalizer' ;
5+ import { logger } from '../utils/logger' ;
6+
7+ // Default retention window for workflow version backups (days). Configurable
8+ // via WORKFLOW_VERSION_RETENTION_DAYS; set to 0 to disable age-based pruning.
9+ const DEFAULT_WORKFLOW_VERSION_RETENTION_DAYS = 30 ;
510
611/**
712 * Community node extension fields
@@ -28,6 +33,31 @@ export class NodeRepository {
2833
2934 this . db = dbOrService ;
3035 }
36+
37+ /**
38+ * Age-based housekeeping: remove version backups past the retention window.
39+ * Called once during database initialization. Internal maintenance only —
40+ * not callable by tenants and not tenant-scoped (deterministic retention,
41+ * not selective destruction).
42+ */
43+ pruneExpiredWorkflowVersions ( ) : void {
44+ const days = parseInt (
45+ process . env . WORKFLOW_VERSION_RETENTION_DAYS || String ( DEFAULT_WORKFLOW_VERSION_RETENTION_DAYS ) ,
46+ 10
47+ ) ;
48+ if ( ! Number . isFinite ( days ) || days <= 0 ) {
49+ return ; // Retention disabled.
50+ }
51+ try {
52+ const cutoffIso = new Date ( Date . now ( ) - days * 24 * 60 * 60 * 1000 ) . toISOString ( ) ;
53+ const removed = this . deleteWorkflowVersionsOlderThan ( cutoffIso ) ;
54+ if ( removed > 0 ) {
55+ logger . info ( `Pruned ${ removed } workflow version backup(s) older than ${ days } days` ) ;
56+ }
57+ } catch ( error ) {
58+ logger . warn ( 'Could not prune expired workflow versions' , { error } ) ;
59+ }
60+ }
3161
3262 /**
3363 * Save node with proper JSON serialization
@@ -1058,10 +1088,16 @@ export class NodeRepository {
10581088 // Workflow Versioning Methods
10591089 // ========================================
10601090
1091+ // All workflow_versions queries are scoped by instance_id to isolate
1092+ // tenants in multi-tenant deployments (GHSA-j6r7-6fhx-77wx). instanceId is
1093+ // a required, derived tenant key (see getInstanceScopeId); '' is the single
1094+ // logical tenant for single-user / stdio deployments.
1095+
10611096 /**
10621097 * Create a new workflow version (backup before modification)
10631098 */
10641099 createWorkflowVersion ( data : {
1100+ instanceId : string ;
10651101 workflowId : string ;
10661102 versionNumber : number ;
10671103 workflowName : string ;
@@ -1073,12 +1109,13 @@ export class NodeRepository {
10731109 } ) : number {
10741110 const stmt = this . db . prepare ( `
10751111 INSERT INTO workflow_versions (
1076- workflow_id, version_number, workflow_name, workflow_snapshot,
1112+ instance_id, workflow_id, version_number, workflow_name, workflow_snapshot,
10771113 trigger, operations, fix_types, metadata
1078- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1114+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ? )
10791115 ` ) ;
10801116
10811117 const result = stmt . run (
1118+ data . instanceId ,
10821119 data . workflowId ,
10831120 data . versionNumber ,
10841121 data . workflowName ,
@@ -1095,30 +1132,30 @@ export class NodeRepository {
10951132 /**
10961133 * Get workflow versions ordered by version number (newest first)
10971134 */
1098- getWorkflowVersions ( workflowId : string , limit ?: number ) : any [ ] {
1135+ getWorkflowVersions ( workflowId : string , instanceId : string , limit ?: number ) : any [ ] {
10991136 let sql = `
11001137 SELECT * FROM workflow_versions
1101- WHERE workflow_id = ?
1138+ WHERE workflow_id = ? AND instance_id = ?
11021139 ORDER BY version_number DESC
11031140 ` ;
11041141
11051142 if ( limit ) {
11061143 sql += ` LIMIT ?` ;
1107- const rows = this . db . prepare ( sql ) . all ( workflowId , limit ) as any [ ] ;
1144+ const rows = this . db . prepare ( sql ) . all ( workflowId , instanceId , limit ) as any [ ] ;
11081145 return rows . map ( row => this . parseWorkflowVersionRow ( row ) ) ;
11091146 }
11101147
1111- const rows = this . db . prepare ( sql ) . all ( workflowId ) as any [ ] ;
1148+ const rows = this . db . prepare ( sql ) . all ( workflowId , instanceId ) as any [ ] ;
11121149 return rows . map ( row => this . parseWorkflowVersionRow ( row ) ) ;
11131150 }
11141151
11151152 /**
1116- * Get a specific workflow version by ID
1153+ * Get a specific workflow version by ID, scoped to the caller's tenant
11171154 */
1118- getWorkflowVersion ( versionId : number ) : any | null {
1155+ getWorkflowVersion ( versionId : number , instanceId : string ) : any | null {
11191156 const row = this . db . prepare ( `
1120- SELECT * FROM workflow_versions WHERE id = ?
1121- ` ) . get ( versionId ) as any ;
1157+ SELECT * FROM workflow_versions WHERE id = ? AND instance_id = ?
1158+ ` ) . get ( versionId , instanceId ) as any ;
11221159
11231160 if ( ! row ) return null ;
11241161 return this . parseWorkflowVersionRow ( row ) ;
@@ -1127,34 +1164,37 @@ export class NodeRepository {
11271164 /**
11281165 * Get the latest workflow version for a workflow
11291166 */
1130- getLatestWorkflowVersion ( workflowId : string ) : any | null {
1167+ getLatestWorkflowVersion ( workflowId : string , instanceId : string ) : any | null {
11311168 const row = this . db . prepare ( `
11321169 SELECT * FROM workflow_versions
1133- WHERE workflow_id = ?
1170+ WHERE workflow_id = ? AND instance_id = ?
11341171 ORDER BY version_number DESC
11351172 LIMIT 1
1136- ` ) . get ( workflowId ) as any ;
1173+ ` ) . get ( workflowId , instanceId ) as any ;
11371174
11381175 if ( ! row ) return null ;
11391176 return this . parseWorkflowVersionRow ( row ) ;
11401177 }
11411178
11421179 /**
1143- * Delete a specific workflow version
1180+ * Delete a specific workflow version, scoped to the caller's tenant.
1181+ * Returns the number of rows deleted (0 if not owned by this tenant).
11441182 */
1145- deleteWorkflowVersion ( versionId : number ) : void {
1146- this . db . prepare ( `
1147- DELETE FROM workflow_versions WHERE id = ?
1148- ` ) . run ( versionId ) ;
1183+ deleteWorkflowVersion ( versionId : number , instanceId : string ) : number {
1184+ const result = this . db . prepare ( `
1185+ DELETE FROM workflow_versions WHERE id = ? AND instance_id = ?
1186+ ` ) . run ( versionId , instanceId ) ;
1187+
1188+ return result . changes ;
11491189 }
11501190
11511191 /**
11521192 * Delete all versions for a specific workflow
11531193 */
1154- deleteWorkflowVersionsByWorkflowId ( workflowId : string ) : number {
1194+ deleteWorkflowVersionsByWorkflowId ( workflowId : string , instanceId : string ) : number {
11551195 const result = this . db . prepare ( `
1156- DELETE FROM workflow_versions WHERE workflow_id = ?
1157- ` ) . run ( workflowId ) ;
1196+ DELETE FROM workflow_versions WHERE workflow_id = ? AND instance_id = ?
1197+ ` ) . run ( workflowId , instanceId ) ;
11581198
11591199 return result . changes ;
11601200 }
@@ -1163,13 +1203,13 @@ export class NodeRepository {
11631203 * Prune old workflow versions, keeping only the most recent N versions
11641204 * Returns number of versions deleted
11651205 */
1166- pruneWorkflowVersions ( workflowId : string , keepCount : number ) : number {
1206+ pruneWorkflowVersions ( workflowId : string , keepCount : number , instanceId : string ) : number {
11671207 // Get all versions ordered by version_number DESC
11681208 const versions = this . db . prepare ( `
11691209 SELECT id FROM workflow_versions
1170- WHERE workflow_id = ?
1210+ WHERE workflow_id = ? AND instance_id = ?
11711211 ORDER BY version_number DESC
1172- ` ) . all ( workflowId ) as any [ ] ;
1212+ ` ) . all ( workflowId , instanceId ) as any [ ] ;
11731213
11741214 // If we have fewer versions than keepCount, no pruning needed
11751215 if ( versions . length <= keepCount ) {
@@ -1193,41 +1233,42 @@ export class NodeRepository {
11931233 }
11941234
11951235 /**
1196- * Truncate the entire workflow_versions table
1197- * Returns number of rows deleted
1236+ * Delete all version backups older than the given ISO timestamp, across all
1237+ * tenants. Internal age-based retention sweep — deterministic housekeeping
1238+ * that exposes no data and is not callable by tenants. Returns rows deleted.
11981239 */
1199- truncateWorkflowVersions ( ) : number {
1240+ deleteWorkflowVersionsOlderThan ( cutoffIso : string ) : number {
12001241 const result = this . db . prepare ( `
1201- DELETE FROM workflow_versions
1202- ` ) . run ( ) ;
1242+ DELETE FROM workflow_versions WHERE created_at < ?
1243+ ` ) . run ( cutoffIso ) ;
12031244
12041245 return result . changes ;
12051246 }
12061247
12071248 /**
12081249 * Get count of versions for a specific workflow
12091250 */
1210- getWorkflowVersionCount ( workflowId : string ) : number {
1251+ getWorkflowVersionCount ( workflowId : string , instanceId : string ) : number {
12111252 const result = this . db . prepare ( `
1212- SELECT COUNT(*) as count FROM workflow_versions WHERE workflow_id = ?
1213- ` ) . get ( workflowId ) as any ;
1253+ SELECT COUNT(*) as count FROM workflow_versions WHERE workflow_id = ? AND instance_id = ?
1254+ ` ) . get ( workflowId , instanceId ) as any ;
12141255
12151256 return result . count ;
12161257 }
12171258
12181259 /**
1219- * Get storage statistics for workflow versions
1260+ * Get storage statistics for workflow versions, scoped to the caller's tenant
12201261 */
1221- getVersionStorageStats ( ) : any {
1262+ getVersionStorageStats ( instanceId : string ) : any {
12221263 // Total versions
12231264 const totalResult = this . db . prepare ( `
1224- SELECT COUNT(*) as count FROM workflow_versions
1225- ` ) . get ( ) as any ;
1265+ SELECT COUNT(*) as count FROM workflow_versions WHERE instance_id = ?
1266+ ` ) . get ( instanceId ) as any ;
12261267
12271268 // Total size (approximate - sum of JSON lengths)
12281269 const sizeResult = this . db . prepare ( `
1229- SELECT SUM(LENGTH(workflow_snapshot)) as total_size FROM workflow_versions
1230- ` ) . get ( ) as any ;
1270+ SELECT SUM(LENGTH(workflow_snapshot)) as total_size FROM workflow_versions WHERE instance_id = ?
1271+ ` ) . get ( instanceId ) as any ;
12311272
12321273 // Per-workflow breakdown
12331274 const byWorkflow = this . db . prepare ( `
@@ -1238,9 +1279,10 @@ export class NodeRepository {
12381279 SUM(LENGTH(workflow_snapshot)) as total_size,
12391280 MAX(created_at) as last_backup
12401281 FROM workflow_versions
1282+ WHERE instance_id = ?
12411283 GROUP BY workflow_id
12421284 ORDER BY version_count DESC
1243- ` ) . all ( ) as any [ ] ;
1285+ ` ) . all ( instanceId ) as any [ ] ;
12441286
12451287 return {
12461288 totalVersions : totalResult . count ,
0 commit comments