33const fs = require ( 'fs' ) . promises ;
44const path = require ( 'path' ) ;
55
6+ function isObject ( value ) {
7+ return typeof value === 'object' && value !== null ;
8+ }
9+
10+ /**
11+ * Resolve a JSON Pointer (RFC6901) against an object. Returns { ok: boolean, value?: any, error?: string }
12+ */
13+ function resolveJsonPointer ( root , pointer ) {
14+ if ( typeof pointer !== 'string' || pointer . length === 0 ) {
15+ return { ok : false , error : 'Empty JSON Pointer' } ;
16+ }
17+ // Allow pointers like "#/..." or "/..."; strip leading '#'
18+ let p = pointer . startsWith ( '#' ) ? pointer . slice ( 1 ) : pointer ;
19+ if ( p === '' ) return { ok : true , value : root } ;
20+ if ( ! p . startsWith ( '/' ) ) {
21+ return { ok : false , error : `Pointer must start with '/': ${ pointer } ` } ;
22+ }
23+ const parts = p . split ( '/' ) . slice ( 1 ) . map ( seg => seg . replace ( / ~ 1 / g, '/' ) . replace ( / ~ 0 / g, '~' ) ) ;
24+ let current = root ;
25+ for ( const key of parts ) {
26+ if ( ! isObject ( current ) && ! Array . isArray ( current ) ) {
27+ return { ok : false , error : `Non-object encountered before end at '${ key } ' in ${ pointer } ` } ;
28+ }
29+ if ( ! ( key in current ) ) {
30+ return { ok : false , error : `Missing key '${ key } ' in ${ pointer } ` } ;
31+ }
32+ current = current [ key ] ;
33+ }
34+ return { ok : true , value : current } ;
35+ }
36+
37+ /**
38+ * Traverse an object and collect all ref-like keyword values matching a predicate
39+ */
40+ function collectRefKeywords ( obj , keys , predicate , pathStack = [ ] ) {
41+ const result = [ ] ;
42+ if ( ! isObject ( obj ) ) return result ;
43+
44+ if ( Array . isArray ( obj ) ) {
45+ obj . forEach ( ( item , idx ) => {
46+ result . push ( ...collectRefKeywords ( item , keys , predicate , pathStack . concat ( `[${ idx } ]` ) ) ) ;
47+ } ) ;
48+ return result ;
49+ }
50+
51+ for ( const [ k , v ] of Object . entries ( obj ) ) {
52+ const nextPath = pathStack . concat ( k ) ;
53+ if ( keys . includes ( k ) && typeof v === 'string' && ( ! predicate || predicate ( v , k ) ) ) {
54+ result . push ( { ref : v , key : k , path : nextPath . join ( '.' ) } ) ;
55+ }
56+ result . push ( ...collectRefKeywords ( v , keys , predicate , nextPath ) ) ;
57+ }
58+ return result ;
59+ }
60+
661/**
762 * Recursively walks through an object and rewrites $ref paths
863 */
@@ -162,6 +217,24 @@ async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
162217
163218 console . log ( `\nUsing schema version: ${ schemaVersion } ` ) ;
164219 console . log ( `Using keyword: ${ defsKeyword } ` ) ;
220+
221+ // Pre-check: external file $ref targets must exist among loaded schemas
222+ console . log ( 'Validating external $ref targets...' ) ;
223+ const allowedFiles = new Set ( [ ...schemaFiles , rootSchemaFilename ] ) ;
224+ for ( const [ name , schema ] of Object . entries ( schemas ) ) {
225+ // Only $ref can be external; $dynamicRef/$recursiveRef are JSON Pointers by spec
226+ const refs = collectRefKeywords ( schema , [ '$ref' ] , ( v ) => / ^ ( .+ \. s c h e m a \. j s o n ) ( # .* ) ? $ / . test ( v ) ) ;
227+ for ( const { ref, key, path : refPath } of refs ) {
228+ const m = ref . match ( / ^ ( .+ \. s c h e m a \. j s o n ) ( # .* ) ? $ / ) ;
229+ if ( ! m ) continue ;
230+ const target = m [ 1 ] ;
231+ const base = path . basename ( target ) ;
232+ if ( ! allowedFiles . has ( base ) ) {
233+ throw new Error ( `Unresolved external ${ key } target file '${ target } ' referenced from schema '${ name } ' at '${ refPath } '` ) ;
234+ }
235+ }
236+ }
237+
165238 console . log ( 'Rewriting $ref pointers...' ) ;
166239
167240 // Rewrite all $refs in all schemas
@@ -181,6 +254,20 @@ async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
181254 [ defsKeyword ] : rewrittenDefinitions
182255 } ;
183256
257+ // Post-check: ensure all internal JSON Pointer refs resolve in the final bundle
258+ console . log ( 'Validating internal ref pointers ($ref, $dynamicRef, $recursiveRef)...' ) ;
259+ const internalRefs = collectRefKeywords (
260+ finalSchema ,
261+ [ '$ref' , '$dynamicRef' , '$recursiveRef' ] ,
262+ ( v ) => typeof v === 'string' && v . startsWith ( '#' )
263+ ) ;
264+ for ( const { ref, key, path : refPath } of internalRefs ) {
265+ const resolved = resolveJsonPointer ( finalSchema , ref ) ;
266+ if ( ! resolved . ok ) {
267+ throw new Error ( `Unresolved internal ${ key } '${ ref } ' at '${ refPath } ': ${ resolved . error } ` ) ;
268+ }
269+ }
270+
184271 // Optionally validate with AJV
185272 if ( options . validate ) {
186273 console . log ( '\nValidating with AJV...' ) ;
0 commit comments