2026-07-16 05:29:40 +08:00
import { createHash } from "node:crypto" ;
2026-10-13 05:04:21 +08:00
2026-10-13 15:58:25 +08:00
import { moduleResolutionModeForJavaScriptReference } from "../../domain/javascript/javascriptModuleResolutionMode.js" ;
2026-10-08 16:15:35 +08:00
import { setImmediate } from "node:timers/promises" ;
2026-07-16 05:29:40 +08:00
2026-10-13 13:48:24 +08:00
import { resolveJavaScriptSourceMapReferences } from "../../domain/javascript/javascriptSourceMapPaths.js" ;
2026-10-10 05:27:41 -07:00
import { analyzeParsedJavaScriptStaticSourceSteps } from "../../domain/javascript/javascriptStaticAnalysis.js" ;
2026-10-14 17:59:44 -07:00
import { analyzeParsedJavaScriptSemanticsSteps } from "../../domain/javascript/javascriptSemanticAnalysis.js" ;
2026-10-08 16:15:35 +08:00
import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSemanticIr.js" ;
2026-10-11 07:22:50 +08:00
import { projectJavaScriptModuleSemantics } from "../../domain/javascript/javascriptModuleSemanticIr.js" ;
2026-10-07 17:16:58 +08:00
import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js" ;
2026-10-07 16:03:37 +08:00
import { hasValidSourceMapContents } from "../../domain/sourceMapContents.js" ;
2026-10-12 04:09:17 +08:00
import {
flattenSourceMapLeaves ,
sourceMapJsonText ,
} from "../../domain/sourceMapEnvelope.js" ;
2026-10-14 08:46:50 +08:00
import type { JavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisTypes.js" ;
2026-10-07 17:16:58 +08:00
import { failedJavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js" ;
2026-07-16 05:29:40 +08:00
import type {
JavaScriptArtifactFile ,
JavaScriptArtifactFileSet ,
2026-10-07 17:57:33 +08:00
} from "../../domain/javascript/javascriptArtifactFiles.js" ;
2026-07-16 05:29:40 +08:00
import type {
AnalyzedJavaScriptArtifactFile ,
JavaScriptArtifactAnalysis ,
2026-10-08 16:15:35 +08:00
JavaScriptModuleArtifactAnalysis ,
JavaScriptModuleSemanticIr ,
2026-10-14 08:46:50 +08:00
JavaScriptHtmlInlineScriptsObservation ,
2026-07-16 05:29:40 +08:00
JavaScriptHtmlScriptObservation ,
2026-07-17 23:57:23 +08:00
JavaScriptJsonModuleObservation ,
2026-07-16 05:29:40 +08:00
JavaScriptPackageObservation ,
JavaScriptSourceMapObservation ,
JavaScriptSourceMapOriginal ,
} from "./JavaScriptArtifactAnalysisTypes.js" ;
2026-07-17 23:57:23 +08:00
import { analyzeJavaScriptJsonModule } from "./JavaScriptJsonModules.js" ;
2026-10-10 03:14:37 -07:00
import { completeJavaScriptAnalysisSteps } from "./JavaScriptAnalysisControl.js" ;
2026-10-08 01:24:24 -04:00
import { htmlArtifactReferences } from "../../domain/javascript/htmlArtifactReferences.js" ;
2026-10-11 07:22:50 +08:00
import type { JavaScriptSourceAnalysisPort } from "../../domain/javascript/javascriptSourceAnalysis.js" ;
import type { JavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js" ;
import { AnalysisError } from "../../domain/analysisErrorBase.js" ;
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js" ;
import {
AnalysisCancelledError ,
AnalysisResourceConstraintError ,
AnalysisTimeoutError ,
} from "../../domain/analysisErrorCore.js" ;
2026-07-16 05:29:40 +08:00
2026-10-08 16:15:35 +08:00
interface MutableArtifactAnalysis <
SemanticIr extends JavaScriptModuleSemanticIr ,
> {
readonly files : AnalyzedJavaScriptArtifactFile < SemanticIr >[];
2026-07-16 05:29:40 +08:00
readonly packages : JavaScriptPackageObservation [];
2026-07-17 23:57:23 +08:00
readonly jsonModules : JavaScriptJsonModuleObservation [];
2026-07-16 05:29:40 +08:00
readonly htmlScripts : JavaScriptHtmlScriptObservation [];
2026-10-14 08:46:50 +08:00
readonly htmlInlineScripts : JavaScriptHtmlInlineScriptsObservation [];
2026-07-16 05:29:40 +08:00
readonly sourceMaps : JavaScriptSourceMapObservation [];
visitedNodes : number ;
findings : number ;
modules : number ;
parseFailures : number ;
}
2026-10-08 16:15:35 +08:00
interface ArtifactAnalysisContext <
SemanticIr extends JavaScriptModuleSemanticIr ,
> {
readonly state : MutableArtifactAnalysis < SemanticIr >;
readonly projectSemantics : (
file : JavaScriptArtifactFile ,
ir : JavaScriptSemanticIr ,
2026-10-10 03:14:37 -07:00
) => Generator < void , SemanticIr >;
2026-07-16 05:29:40 +08:00
}
2026-09-29 14:53:50 +08:00
/** Analyze every text source from the artifact without a result quota. */
2026-07-16 05:29:40 +08:00
export const analyzeJavaScriptArtifactFiles = (
fileSet : JavaScriptArtifactFileSet ,
2026-10-08 16:15:35 +08:00
) : JavaScriptArtifactAnalysis =>
2026-10-10 03:14:37 -07:00
analyzeArtifactFiles ( fileSet , function * ( _file , ir ) {
// The full IR needs no projection; one step keeps the shared protocol.
yield ;
return ir ;
});
2026-10-08 16:15:35 +08:00
2026-10-10 03:14:37 -07:00
/**
* Project each file's full IR immediately, retaining only later module facts.
* Parsing, analysis and each projection pass run as separate steps, so control
* messages and cancellation are handled within one large file as well.
*/
2026-10-08 16:15:35 +08:00
export const analyzeAndProjectJavaScriptArtifactFiles = async (
fileSet : JavaScriptArtifactFileSet ,
projectSemantics : (
file : JavaScriptArtifactFile ,
ir : JavaScriptSemanticIr ,
2026-10-10 03:14:37 -07:00
) => Iterator < void , void > | void ,
2026-10-08 16:15:35 +08:00
beforeFile ?: (
file : JavaScriptArtifactFile ,
completed : number ,
total : number ,
) => Promise < void >,
2026-10-10 03:14:37 -07:00
signal? : AbortSignal ,
2026-10-08 16:15:35 +08:00
) : Promise < JavaScriptModuleArtifactAnalysis > => {
const state = emptyArtifactAnalysis < JavaScriptModuleSemanticIr >();
2026-10-10 03:14:37 -07:00
const projectFileSemantics = function * (
2026-10-08 16:15:35 +08:00
file : JavaScriptArtifactFile ,
ir : JavaScriptSemanticIr ,
2026-10-10 03:14:37 -07:00
) : Generator < void , JavaScriptModuleSemanticIr > {
const projection = projectSemantics ( file , ir );
if ( projection !== undefined ) while ( projection . next (). done !== true ) yield ;
2026-10-11 07:22:50 +08:00
return projectJavaScriptModuleSemantics ( ir );
2026-10-08 16:15:35 +08:00
};
const context = { state , projectSemantics : projectFileSemantics };
for ( const [ index , file ] of fileSet . files . entries ()) {
// Let progress, cancellation and garbage collection run between files.
await setImmediate ();
await beforeFile ? .( file , index , fileSet . files . length );
2026-10-10 03:14:37 -07:00
await completeJavaScriptAnalysisSteps (
analyzeArtifactFileSteps ( file , context ),
signal ,
);
2026-10-08 16:15:35 +08:00
}
return finalizeArtifactAnalysis ( state );
};
2026-10-11 07:22:50 +08:00
/** Analyze sources behind a hard heap boundary, retaining collected facts on interruption. */
export const analyzeAndAdoptJavaScriptArtifactFiles = async (
fileSet : JavaScriptArtifactFileSet ,
worker : Pick < JavaScriptSourceAnalysisPort , "analyze" >,
projection : JavaScriptSemanticGraphProjection ,
beforeFile : (
file : JavaScriptArtifactFile ,
completed : number ,
total : number ,
) => Promise < void >,
signal? : AbortSignal ,
) : Promise < {
readonly analysis : JavaScriptModuleArtifactAnalysis ;
readonly interruption : AnalysisError | null ;
} > => {
const state = emptyArtifactAnalysis < JavaScriptModuleSemanticIr >();
let interruption : AnalysisError | null = null ;
2026-10-12 04:07:14 +08:00
const selectedFileCount =
fileSet . files . length - ( fileSet . source_selection ? . unselected_files ?? 0 );
let selectedFileIndex = 0 ;
for ( const file of fileSet . files ) {
2026-10-11 07:22:50 +08:00
try {
await setImmediate ();
2026-10-12 06:29:27 +08:00
if ( classifyOutsideAnalysisScope ( file , state , projection )) {
if ( signal ? . aborted === true ) {
interruption = new AnalysisCancelledError (
"analyze_javascript_application" ,
2026-10-12 04:07:14 +08:00
);
2026-10-12 06:29:27 +08:00
break ;
}
2026-10-12 04:07:14 +08:00
continue ;
}
2026-10-12 06:29:27 +08:00
if ( signal ? . aborted === true )
throw new AnalysisCancelledError ( "analyze_javascript_application" );
2026-10-12 04:07:14 +08:00
await beforeFile ( file , selectedFileIndex ++ , selectedFileCount );
2026-10-11 07:22:50 +08:00
if ( file . kind !== "javascript" || ! file . text . included ) {
const steps = analyzeArtifactFileSteps ( file , {
state ,
projectSemantics : function * ( _file , ir ) {
yield ;
return projectJavaScriptModuleSemantics ( ir );
},
});
await completeJavaScriptAnalysisSteps ( steps , signal );
continue ;
}
const result = await worker . analyze (
file ,
projection . remainingFileNodeBudget (),
signal ,
);
const facts = result . ok ? result.value : result.error ;
const applicationProjectionFailure = facts . applicationProjectionFailure ;
if ( facts . projection !== null )
projection . adoptFileProjection ( facts . projection );
const error = result . ok ? null : result . error . error ;
const limits =
error instanceof AnalysisResourceConstraintError
? error.reportedLimits
: error instanceof AnalysisTimeoutError
? { worker_timeout_ms : error.timeoutMs }
: null ;
const failure =
error === null
? undefined
: {
reason : ` ${ file . path } : ${ error . message } ` ,
error : projectAnalysisError ( error ),
limits ,
};
state . files . push ({
file ,
javascript : facts.javascript ,
semantic : facts.module === null ? null : { ir : facts.module },
...( failure === undefined ? {} : { analysis_failure : failure }),
...( applicationProjectionFailure === undefined
? {}
: {
application_projection_failure : {
reason : applicationProjectionFailure.message ,
error : projectAnalysisError ( applicationProjectionFailure ),
limits : applicationProjectionFailure.reportedLimits ,
},
}),
});
if ( failure !== undefined && facts . projection === null )
projection . recordUnavailableFile (
file ,
failure . reason ,
failure . limits ,
error instanceof AnalysisResourceConstraintError ||
error instanceof AnalysisTimeoutError
? "resource-limit"
: "incomplete-module" ,
);
if ( facts . javascript !== null ) {
state . visitedNodes += facts . javascript . visited_ast_nodes ;
state . findings +=
findingCount ( facts . javascript ) +
( facts . module ? . moduleLinks . length ?? 0 );
state . modules += facts . javascript . bundler_registrations . reduce (
( count , item ) => count + item . modules . length ,
0 ,
);
if ( facts . javascript . parse_status === "failed" )
state . parseFailures += 1 ;
}
if (
error !== null &&
( ! ( error instanceof AnalysisResourceConstraintError ) ||
error . reportedLimits ? . parent_result_budget_exhausted === true )
) {
interruption = error ;
break ;
}
} catch ( cause : unknown ) {
if ( signal ? . aborted !== true && ! ( cause instanceof AnalysisError ))
throw cause ;
interruption =
signal ? . aborted === true
? new AnalysisCancelledError ( "analyze_javascript_application" )
: cause instanceof AnalysisError
? cause
: null ;
if ( interruption !== null ) {
state . files . push ({
file ,
javascript : null ,
semantic : null ,
analysis_failure : {
reason : ` ${ file . path } : ${ interruption . message } ` ,
error : projectAnalysisError ( interruption ),
limits : null ,
},
});
projection . recordUnavailableFile (
file ,
interruption . message ,
null ,
"incomplete-module" ,
);
}
break ;
}
}
if ( interruption !== null ) {
const analyzedPaths = new Set ( state . files . map (({ file }) => file . path ));
const failure = {
reason : `Source analysis was interrupted: ${ interruption . message } ` ,
error : projectAnalysisError ( interruption ),
limits :
interruption instanceof AnalysisResourceConstraintError
? interruption.reportedLimits
: interruption instanceof AnalysisTimeoutError
? { worker_timeout_ms : interruption.timeoutMs }
: null ,
};
for ( const file of fileSet . files ) {
if ( analyzedPaths . has ( file . path )) continue ;
2026-10-12 06:29:27 +08:00
if ( classifyOutsideAnalysisScope ( file , state , projection )) continue ;
2026-10-11 07:22:50 +08:00
state . files . push ({
file ,
javascript : null ,
semantic : null ,
analysis_failure : failure ,
});
if ( file . kind === "javascript" )
projection . recordUnavailableFile (
file ,
failure . reason ,
failure . limits ,
interruption instanceof AnalysisResourceConstraintError ||
interruption instanceof AnalysisTimeoutError
? "resource-limit"
: "incomplete-module" ,
);
}
}
return { analysis : finalizeArtifactAnalysis ( state ), interruption };
};
2026-10-08 16:15:35 +08:00
const analyzeArtifactFiles = < SemanticIr extends JavaScriptModuleSemanticIr >(
fileSet : JavaScriptArtifactFileSet ,
projectSemantics : ArtifactAnalysisContext < SemanticIr >[ "projectSemantics" ],
) : JavaScriptArtifactAnalysis < SemanticIr > => {
const state = emptyArtifactAnalysis < SemanticIr >();
const context = { state , projectSemantics };
2026-10-10 03:14:37 -07:00
for ( const file of fileSet . files ) {
const steps = analyzeArtifactFileSteps ( file , context );
while ( steps . next (). done !== true );
}
2026-09-29 10:53:45 +08:00
return finalizeArtifactAnalysis ( state );
2026-07-16 05:29:40 +08:00
};
2026-10-08 16:15:35 +08:00
const finalizeArtifactAnalysis = <
SemanticIr extends JavaScriptModuleSemanticIr ,
> (
state : MutableArtifactAnalysis < SemanticIr >,
) : JavaScriptArtifactAnalysis < SemanticIr > => {
2026-07-16 05:29:40 +08:00
return {
files : state.files ,
packages : state.packages ,
2026-07-17 23:57:23 +08:00
json_modules : state.jsonModules ,
2026-09-29 10:53:45 +08:00
html_scripts : state.htmlScripts ,
2026-10-14 08:46:50 +08:00
html_inline_scripts : state.htmlInlineScripts ,
2026-07-16 05:29:40 +08:00
source_maps : state.sourceMaps ,
visited_ast_nodes : state.visitedNodes ,
2026-10-14 08:46:50 +08:00
findings :
state.findings +
state . htmlScripts . length +
state . htmlInlineScripts . reduce (
( count , { script_count }) => count + script_count ,
0 ,
),
2026-07-16 05:29:40 +08:00
modules : state.modules ,
parse_failures : state.parseFailures ,
limitations : [
"JavaScript and HTML were parsed as inert text; bundle bootstrap code was never executed." ,
"Static paths and relationships may remain unresolved when expressions are dynamic or obfuscated." ,
2026-10-14 08:46:50 +08:00
... state . htmlInlineScripts . map ( htmlInlineScriptLimitation ),
2026-10-11 07:22:50 +08:00
... state . files . flatMap (
({ analysis_failure , application_projection_failure }) =>
[ analysis_failure , application_projection_failure ]. flatMap (
( failure ) => ( failure === undefined ? [] : [ failure . reason ]),
),
),
2026-07-16 05:29:40 +08:00
],
};
};
2026-10-10 03:14:37 -07:00
function * analyzeArtifactFileSteps <
SemanticIr extends JavaScriptModuleSemanticIr ,
> (
2026-07-16 05:29:40 +08:00
file : JavaScriptArtifactFile ,
2026-10-08 16:15:35 +08:00
context : ArtifactAnalysisContext < SemanticIr >,
2026-10-10 03:14:37 -07:00
) : Generator < void , void > {
2026-09-29 10:53:45 +08:00
const { state } = context ;
2026-10-12 06:29:27 +08:00
if ( classifyOutsideAnalysisScope ( file , state )) return ;
2026-07-17 23:57:23 +08:00
addStructuredObservations ( file , state );
2026-10-14 08:46:50 +08:00
if ( file . kind === "html" && file . text . included ) {
const html = parseHtmlScripts ( file . path , file . text . value );
// Append without spreading: argument count is bounded by the engine.
for ( const script of html . scripts ) state . htmlScripts . push ( script );
if ( html . inlineScripts !== null )
state . htmlInlineScripts . push ( html . inlineScripts );
}
2026-07-16 05:29:40 +08:00
if ( file . kind === "source-map" ) addSourceMap ( file , context );
if ( file . kind !== "javascript" || ! file . text . included ) {
2026-07-17 23:57:23 +08:00
state . files . push ({ file , javascript : null , semantic : null });
2026-07-16 05:29:40 +08:00
return ;
}
2026-10-08 13:24:47 -04:00
const parsed = parseJavaScriptSource ( file . text . value , file . path );
2026-10-10 03:14:37 -07:00
yield ;
2026-07-25 09:10:51 +00:00
if ( parsed === null ) {
const analysis = failedJavaScriptStaticAnalysis ();
state . files . push ({ file , javascript : analysis , semantic : null });
state . parseFailures += 1 ;
return ;
}
2026-10-10 05:27:41 -07:00
const analysis = yield * analyzeParsedJavaScriptStaticSourceSteps (
2026-10-09 00:20:48 +08:00
file . text . value ,
parsed ,
);
2026-07-17 23:57:23 +08:00
const staticFindings = findingCount ( analysis );
2026-10-10 03:14:37 -07:00
yield ;
2026-07-17 23:57:23 +08:00
const semantics =
analysis . parse_status === "complete" || analysis . parse_status === "partial"
2026-10-13 05:04:21 +08:00
? yield * analyzeParsedJavaScriptSemanticsSteps (
parsed ,
2026-10-13 15:58:25 +08:00
moduleResolutionModeForJavaScriptReference (
file . path ,
"static-import" ,
),
2026-10-13 05:04:21 +08:00
)
2026-07-17 23:57:23 +08:00
: null ;
2026-10-10 03:14:37 -07:00
const projected =
semantics === null
? null
: yield * context . projectSemantics ( file , semantics );
2026-07-17 23:57:23 +08:00
state . files . push ({
file ,
javascript : analysis ,
2026-10-10 03:14:37 -07:00
semantic : projected === null ? null : { ir : projected },
2026-07-17 23:57:23 +08:00
});
2026-07-16 05:29:40 +08:00
state . visitedNodes += analysis . visited_ast_nodes ;
2026-07-17 23:57:23 +08:00
state . findings += staticFindings + ( semantics ? . moduleLinks . length ?? 0 );
2026-07-16 05:29:40 +08:00
state . modules += analysis . bundler_registrations . reduce (
( count , registration ) => count + registration . modules . length ,
0 ,
);
if ( analysis . parse_status === "failed" ) state . parseFailures += 1 ;
2026-10-10 03:14:37 -07:00
}
2026-07-16 05:29:40 +08:00
2026-10-12 06:29:27 +08:00
const classifyOutsideAnalysisScope = <
SemanticIr extends JavaScriptModuleSemanticIr ,
> (
file : JavaScriptArtifactFile ,
state : MutableArtifactAnalysis < SemanticIr >,
projection? : JavaScriptSemanticGraphProjection ,
) : boolean => {
if ( file . text . included || file . text . reason !== "outside-analysis-scope" )
return false ;
state . files . push ({ file , javascript : null , semantic : null });
if ( file . kind === "javascript" )
projection ? . recordUnavailableFile (
file ,
"Source was not analyzed because it is outside the caller-selected source_paths." ,
null ,
"incomplete-module" ,
);
return true ;
};
2026-07-16 05:29:40 +08:00
const addSourceMap = (
file : JavaScriptArtifactFile ,
2026-10-08 16:15:35 +08:00
context : ArtifactAnalysisContext < JavaScriptModuleSemanticIr >,
2026-07-16 05:29:40 +08:00
) : void => {
2026-09-29 14:53:50 +08:00
const sourceMap = parseSourceMap ( file );
2026-07-16 05:29:40 +08:00
context . state . sourceMaps . push ( sourceMap );
};
2026-10-08 16:15:35 +08:00
const emptyArtifactAnalysis = <
SemanticIr extends JavaScriptModuleSemanticIr ,
> () : MutableArtifactAnalysis < SemanticIr > => ({
2026-07-16 05:29:40 +08:00
files : [],
packages : [],
2026-07-17 23:57:23 +08:00
jsonModules : [],
2026-07-16 05:29:40 +08:00
htmlScripts : [],
2026-10-14 08:46:50 +08:00
htmlInlineScripts : [],
2026-07-16 05:29:40 +08:00
sourceMaps : [],
visitedNodes : 0 ,
findings : 0 ,
modules : 0 ,
parseFailures : 0 ,
});
2026-07-17 23:57:23 +08:00
const addStructuredObservations = (
file : JavaScriptArtifactFile ,
2026-10-08 16:15:35 +08:00
state : MutableArtifactAnalysis < JavaScriptModuleSemanticIr >,
2026-07-17 23:57:23 +08:00
) : void => {
if ( file . kind === "package-json" ) state . packages . push ( parsePackage ( file ));
if ( file . kind !== "json" ) return ;
const json = analyzeJavaScriptJsonModule ( file );
state . jsonModules . push ( json );
if ( json . status === "invalid" ) state . parseFailures += 1 ;
};
2026-07-16 05:29:40 +08:00
const parsePackage = (
file : JavaScriptArtifactFile ,
) : JavaScriptPackageObservation => {
if ( ! file . text . included )
return unavailablePackage ( file , "Package metadata text was unavailable." );
let value : unknown ;
try {
value = JSON . parse ( file . text . value );
2026-10-06 05:58:46 +08:00
} catch ( cause : unknown ) {
void cause ;
2026-07-16 05:29:40 +08:00
return invalidPackage ( file , "package.json is not valid JSON." );
}
if ( ! isRecord ( value ))
return invalidPackage ( file , "package.json root is not an object." );
return {
path : file.path ,
sha256 : file.sha256 ,
status : "included" ,
2026-09-29 13:57:24 +08:00
name : optionalString ( value . name ),
version : optionalString ( value . version ),
main : optionalString ( value . main ),
2026-07-16 05:29:40 +08:00
renderer :
2026-09-29 13:57:24 +08:00
optionalString ( value . renderer ) ??
optionalString ( value . browser ) ??
optionalString ( value . module ),
2026-07-16 05:29:40 +08:00
limitation : null ,
};
};
const invalidPackage = (
file : JavaScriptArtifactFile ,
limitation : string ,
) : JavaScriptPackageObservation => ({
... unavailablePackage ( file , limitation ),
status : "invalid" ,
});
const unavailablePackage = (
file : JavaScriptArtifactFile ,
limitation : string ,
2026-08-10 14:57:44 +08:00
) : JavaScriptPackageObservation & { readonly status : "unavailable" } => ({
2026-07-16 05:29:40 +08:00
path : file.path ,
sha256 : file.sha256 ,
status : "unavailable" ,
name : null ,
version : null ,
main : null ,
renderer : null ,
limitation ,
});
const parseHtmlScripts = (
path : string ,
text : string ,
2026-10-14 08:46:50 +08:00
) : {
readonly scripts : JavaScriptHtmlScriptObservation [];
readonly inlineScripts : JavaScriptHtmlInlineScriptsObservation | null ;
} => {
const { scripts , inlineScripts , inlineScriptCount , baseHref } =
htmlArtifactReferences ( text );
return {
scripts : scripts.map (({ scriptPath , location }) => ({
html_path : path ,
script_path : scriptPath ,
base_href : baseHref ,
location ,
})),
inlineScripts :
inlineScriptCount === 0
? null
: {
path ,
script_count : inlineScriptCount ,
scripts : inlineScripts.map (({ scriptType , location }) => ({
script_type : scriptType ,
location ,
})),
},
};
2026-07-17 21:30:24 +08:00
};
2026-07-16 05:29:40 +08:00
const parseSourceMap = (
file : JavaScriptArtifactFile ,
) : JavaScriptSourceMapObservation => {
if ( ! file . text . included ) return unavailableSourceMap ( file );
let value : unknown ;
try {
2026-10-12 04:09:17 +08:00
value = JSON . parse ( sourceMapJsonText ( file . text . value ));
2026-10-06 05:58:46 +08:00
} catch ( cause : unknown ) {
void cause ;
2026-07-16 05:29:40 +08:00
return invalidSourceMap ( file , "Source map is not valid JSON." );
}
2026-10-12 04:33:38 +08:00
const maps = flattenSourceMapLeaves ( value );
2026-07-16 05:29:40 +08:00
if ( maps === undefined )
2026-09-29 14:53:50 +08:00
return invalidSourceMap ( file , "Source map is not a version 3 map." );
return collectSourceMapOriginals ( file , maps );
2026-07-16 05:29:40 +08:00
};
const collectSourceMapOriginals = (
file : JavaScriptArtifactFile ,
maps : readonly Readonly < Record < string , unknown >>[],
) : JavaScriptSourceMapObservation => {
const sources : JavaScriptSourceMapOriginal [] = [];
2026-10-12 11:10:49 +08:00
let declarationIndex = 0 ;
2026-07-16 05:29:40 +08:00
for ( const map of maps ) {
const names = map . sources ;
if ( ! Array . isArray ( names ))
return invalidSourceMap ( file , "Source map has no sources array." );
2026-10-05 20:16:44 +08:00
const contents = map . sourcesContent ;
2026-10-13 09:52:47 -06:00
if ( ! hasValidSourceMapContents ( contents ))
2026-10-05 20:16:44 +08:00
return invalidSourceMap (
file ,
2026-10-13 09:52:47 -06:00
"Source map sourcesContent must be an array of strings or null entries." ,
2026-10-05 20:16:44 +08:00
);
2026-10-09 20:02:52 +08:00
if ( map . sourceRoot != null && typeof map . sourceRoot !== "string" )
return invalidSourceMap (
file ,
"Source map sourceRoot must be a string or null." ,
);
const root = typeof map . sourceRoot === "string" ? map.sourceRoot : null ;
2026-10-13 13:48:24 +08:00
const sourceNames : ( string | null )[] = [];
for ( const raw of names ) {
2026-10-12 11:10:49 +08:00
if ( raw !== null && typeof raw !== "string" )
2026-07-16 05:29:40 +08:00
return invalidSourceMap (
file ,
"Source map contains a non-string source name." ,
);
2026-10-13 13:48:24 +08:00
sourceNames . push ( raw );
}
const references = resolveJavaScriptSourceMapReferences (
sourceNames ,
root ,
file . path ,
);
for ( const [ index , reference ] of references . entries ()) {
2026-10-05 20:16:44 +08:00
const rawContent = Array . isArray ( contents ) ? contents [ index ] : undefined ;
const content = typeof rawContent === "string" ? rawContent : null ;
2026-07-16 05:29:40 +08:00
sources . push ({
2026-10-12 11:10:49 +08:00
source_index : index ,
declaration_index : declarationIndex ++ ,
2026-10-13 13:48:24 +08:00
reference ,
2026-07-16 05:29:40 +08:00
content ,
content_sha256 : content === null ? null : sha256 ( content ),
});
}
}
2026-09-29 10:53:45 +08:00
return {
path : file.path ,
sha256 : file.sha256 ,
status : "included" ,
sources ,
limitation : null ,
};
2026-07-16 05:29:40 +08:00
};
2026-10-14 08:46:50 +08:00
/** Name the HTML file and lines whose inline JavaScript was not analyzed. */
export const htmlInlineScriptLimitation = ({
path ,
script_count : count ,
scripts ,
} : JavaScriptHtmlInlineScriptsObservation ) : string => {
const lines = scripts
. slice ( 0 , HTML_INLINE_SCRIPT_LIMITATION_LINES )
. map (({ location }) => String ( location . start . line ));
const unnamed = count - lines . length ;
const one = count === 1 ;
return ` ${ path } has ${ String ( count ) } inline JavaScript ${ one ? "body" : "bodies" } starting at line ${ one ? "" : "s" } ${ lines . join ( ", " ) }${ unnamed > 0 ? ` and ${ String ( unnamed ) } more` : "" } that ${ one ? "was" : "were" } not analyzed; endpoints, storage, imports and other facts in ${ one ? "it" : "them" } are absent from the evidence.` ;
};
// Keep the agent-facing limitation short; the unknown scope keeps the ranges.
const HTML_INLINE_SCRIPT_LIMITATION_LINES = 10 ;
2026-07-16 05:29:40 +08:00
const unavailableSourceMap = (
file : JavaScriptArtifactFile ,
) : JavaScriptSourceMapObservation => {
if ( file . text . included )
throw new TypeError ( "Expected unavailable source-map text" );
return {
path : file.path ,
sha256 : file.sha256 ,
2026-09-29 07:08:46 +08:00
status : "invalid" ,
2026-07-16 05:29:40 +08:00
sources : [],
2026-09-29 14:53:50 +08:00
limitation : "Source-map text could not be decoded as UTF-8." ,
2026-07-16 05:29:40 +08:00
};
};
const invalidSourceMap = (
file : JavaScriptArtifactFile ,
limitation : string ,
) : JavaScriptSourceMapObservation => ({
path : file.path ,
sha256 : file.sha256 ,
status : "invalid" ,
sources : [],
limitation ,
});
const findingCount = ( analysis : JavaScriptStaticAnalysis ) : number =>
analysis . references . length +
analysis . endpoints . length +
analysis . storage . length +
analysis . role_paths . length +
analysis . source_map_urls . length +
2026-07-16 06:45:34 +08:00
analysis . bundler_registrations . length +
analysis . electron . browser_windows . length +
analysis . electron . context_bridge_apis . length +
analysis . electron . ipc . length +
analysis . electron . sender_validations . length +
analysis . electron . utility_processes . length +
analysis . electron . native_addon_bindings . length ;
2026-07-16 05:29:40 +08:00
2026-09-29 13:57:24 +08:00
const optionalString = ( value : unknown ) : string | null =>
typeof value === "string" && value . length > 0 ? value : null ;
2026-07-16 05:29:40 +08:00
const sha256 = ( value : string ) : string =>
createHash ( "sha256" ). update ( value ). digest ( "hex" );
const isRecord = ( value : unknown ) : value is Readonly < Record < string , unknown >> =>
typeof value === "object" && value !== null && ! Array . isArray ( value );