@@ -26,19 +26,101 @@ export const MAX_ROWS = {
2626 scenes : 2000 ,
2727} ;
2828
29+ // String caps stop a hostile backup from smuggling a tiny row count with huge
30+ // values (e.g. a 1 MB repoId / note) that would pin the IDB lock or blow quota.
31+ export const MAX_STRING_LENGTHS = {
32+ id : 512 ,
33+ repoId : 256 ,
34+ label : 128 ,
35+ scalar : 20_000 ,
36+ } ;
37+
2938// Per-repo snapshot ring-buffer cap — single source of truth in snapshots.js; each
3039// imported snapshots row is trimmed to its most recent SNAP_CAP entries.
3140const SNAP_CAP = SNAPSHOT_CAP ;
3241
3342const arr = ( x ) => ( Array . isArray ( x ) ? x : [ ] ) ;
34- const rowHasRepo = ( r ) => ! ! ( r && r . id != null && r . payload && r . payload . repoId ) ;
35- const rowHasId = ( r ) => ! ! ( r && r . id != null && r . payload != null ) ;
36- const edgeOk = ( e ) => ! ! ( e && e . id != null && e . source != null && e . target != null && e . label ) ;
37- const cacheOk = ( c ) => ! ! ( c && c . repoId && c . platform ) ;
38- const collectionOk = ( c ) => ! ! ( c && c . id != null && c . payload && typeof c . payload . name === 'string' ) ;
39- const decisionOk = ( d ) => ! ! ( d && d . id != null && d . payload && d . payload . repoId && d . payload . decision ) ;
40- const snapshotOk = ( r ) => ! ! ( r && r . id != null && r . repoId && Array . isArray ( r . snaps ) ) ;
41- const sceneOk = ( s ) => ! ! ( s && s . id && s . scope && Array . isArray ( s . nodes ) && Array . isArray ( s . edges ) ) ;
43+ const shortString = ( x , max = MAX_STRING_LENGTHS . scalar ) => typeof x !== 'string' || x . length <= max ;
44+ const shortId = ( x ) => shortString ( String ( x ?? '' ) , MAX_STRING_LENGTHS . id ) ;
45+ const shortRepoId = ( x ) => typeof x === 'string' && x . length > 0 && x . length <= MAX_STRING_LENGTHS . repoId ;
46+ const shortLabel = ( x ) => typeof x === 'string' && x . length > 0 && x . length <= MAX_STRING_LENGTHS . label ;
47+ function stringsWithin ( value , max = MAX_STRING_LENGTHS . scalar ) {
48+ if ( typeof value === 'string' ) return value . length <= max ;
49+ if ( ! value || typeof value !== 'object' ) return true ;
50+ if ( Array . isArray ( value ) ) return value . every ( ( v ) => stringsWithin ( v , max ) ) ;
51+ return Object . values ( value ) . every ( ( v ) => stringsWithin ( v , max ) ) ;
52+ }
53+ const rowHasRepo = ( r ) =>
54+ ! ! (
55+ r &&
56+ r . id != null &&
57+ shortId ( r . id ) &&
58+ r . payload &&
59+ shortRepoId ( r . payload . repoId ) &&
60+ stringsWithin ( r . payload )
61+ ) ;
62+ const rowHasId = ( r ) =>
63+ ! ! ( r && r . id != null && shortId ( r . id ) && r . payload != null && stringsWithin ( r . payload ) ) ;
64+ const edgeOk = ( e ) =>
65+ ! ! (
66+ e &&
67+ e . id != null &&
68+ shortId ( e . id ) &&
69+ e . source != null &&
70+ shortId ( e . source ) &&
71+ e . target != null &&
72+ shortId ( e . target ) &&
73+ shortLabel ( e . label ) &&
74+ stringsWithin ( e . properties || { } )
75+ ) ;
76+ const cacheOk = ( c ) =>
77+ ! ! (
78+ c &&
79+ shortRepoId ( c . repoId ) &&
80+ typeof c . platform === 'string' &&
81+ shortString ( c . platform , 64 ) &&
82+ stringsWithin ( c )
83+ ) ;
84+ const collectionOk = ( c ) =>
85+ ! ! (
86+ c &&
87+ c . id != null &&
88+ shortId ( c . id ) &&
89+ c . payload &&
90+ typeof c . payload . name === 'string' &&
91+ shortString ( c . payload . name , 120 ) &&
92+ stringsWithin ( c . payload )
93+ ) ;
94+ const decisionOk = ( d ) =>
95+ ! ! (
96+ d &&
97+ d . id != null &&
98+ shortId ( d . id ) &&
99+ d . payload &&
100+ shortRepoId ( d . payload . repoId ) &&
101+ d . payload . decision &&
102+ stringsWithin ( d . payload )
103+ ) ;
104+ const snapshotOk = ( r ) =>
105+ ! ! (
106+ r &&
107+ r . id != null &&
108+ shortId ( r . id ) &&
109+ shortRepoId ( r . repoId ) &&
110+ Array . isArray ( r . snaps ) &&
111+ stringsWithin ( r . snaps )
112+ ) ;
113+ const sceneOk = ( s ) =>
114+ ! ! (
115+ s &&
116+ s . id &&
117+ shortString ( s . id , MAX_STRING_LENGTHS . id ) &&
118+ s . scope &&
119+ shortString ( s . scope , 64 ) &&
120+ Array . isArray ( s . nodes ) &&
121+ Array . isArray ( s . edges ) &&
122+ stringsWithin ( s )
123+ ) ;
42124
43125/** Empty normalized shape — the safe fallback when a file can't be parsed. */
44126function emptyValue ( ) {
@@ -152,24 +234,22 @@ export function validateBackup(obj) {
152234 return kept ;
153235 } ;
154236 const value = {
155- repos : clamp ( 'repos' , arr ( obj . repos ) . filter ( rowHasRepo ) ) ,
156- nodes : clamp ( 'nodes' , arr ( obj . nodes ) . filter ( rowHasId ) ) ,
157- edges : clamp ( 'edges' , arr ( obj . edges ) . filter ( edgeOk ) ) ,
158- cache : clamp ( 'cache' , arr ( obj . cache ) . filter ( cacheOk ) ) ,
159- collections : clamp ( 'collections' , arr ( obj . collections ) . filter ( collectionOk ) ) ,
160- decisions : clamp ( 'decisions' , arr ( obj . decisions ) . filter ( decisionOk ) ) ,
237+ repos : clamp ( 'repos' , filterWarn ( 'repo' , arr ( obj . repos ) , rowHasRepo ) ) ,
238+ nodes : clamp ( 'nodes' , filterWarn ( 'node' , arr ( obj . nodes ) , rowHasId ) ) ,
239+ edges : clamp ( 'edges' , filterWarn ( 'edge' , arr ( obj . edges ) , edgeOk ) ) ,
240+ cache : clamp ( 'cache' , filterWarn ( 'cache' , arr ( obj . cache ) , cacheOk ) ) ,
241+ collections : clamp ( 'collections' , filterWarn ( 'collection' , arr ( obj . collections ) , collectionOk ) ) ,
242+ decisions : clamp ( 'decisions' , filterWarn ( 'decision' , arr ( obj . decisions ) , decisionOk ) ) ,
161243 snapshots : clamp (
162244 'snapshots' ,
163- arr ( obj . snapshots )
164- . filter ( snapshotOk )
165- . map ( ( r ) => ( {
166- ...r ,
167- // Trim to the cap and coerce each snap's flags to an array — a corrupt/hostile
168- // file may carry a non-array `flags` that would later throw in snapshotTrend.
169- snaps : arr ( r . snaps )
170- . slice ( - SNAP_CAP )
171- . map ( ( s ) => ( s && typeof s === 'object' ? { ...s , flags : arr ( s . flags ) } : s ) ) ,
172- } ) )
245+ filterWarn ( 'snapshot' , arr ( obj . snapshots ) , snapshotOk ) . map ( ( r ) => ( {
246+ ...r ,
247+ // Trim to the cap and coerce each snap's flags to an array — a corrupt/hostile
248+ // file may carry a non-array `flags` that would later throw in snapshotTrend.
249+ snaps : arr ( r . snaps )
250+ . slice ( - SNAP_CAP )
251+ . map ( ( s ) => ( s && typeof s === 'object' ? { ...s , flags : arr ( s . flags ) } : s ) ) ,
252+ } ) )
173253 ) ,
174254 scenes : clamp ( 'scenes' , filterWarn ( 'scene' , arr ( obj . scenes ) , sceneOk ) ) ,
175255 } ;
0 commit comments