2026-05-12 02:20:55 -04:00
//! JavaScript / TypeScript harness emitter.
//!
//! Generates a Node.js script that:
//! 1. Reads the payload from `NYX_PAYLOAD` / `NYX_PAYLOAD_B64` env vars.
//! 2. Requires the entry module from the workdir (`entry.js`).
//! 3. Calls the entry function with the payload routed to the correct slot.
//! 4. Catches all exceptions to prevent harness crashes from masking results.
//!
//! Sink-reachability probe: the fixture itself emits `__NYX_SINK_HIT__` before
//! the actual sink call (same pattern as Rust fixtures). The harness is a pure
//! runner with no line-level tracing.
//!
//! Payload slot support:
//! - `PayloadSlot::Param(n)` — n-th positional argument.
//! - `PayloadSlot::EnvVar(name)` — set env var before calling.
//! - `PayloadSlot::Stdin` — pipe payload to process.stdin.
//! - Other slots produce `UnsupportedReason::EntryKindUnsupported`.
//!
//! Build: no compilation step. Command is `node harness.js`.
//! Build container: `nyx-build-node:{toolchain_id}` (deferred; §19.1).
2026-05-14 03:22:30 -05:00
use crate ::dynamic ::lang ::{ HarnessSource , LangEmitter } ;
use crate ::dynamic ::spec ::{ EntryKind , HarnessSpec , PayloadSlot } ;
2026-05-12 02:20:55 -04:00
use crate ::evidence ::UnsupportedReason ;
2026-05-14 03:22:30 -05:00
/// Zero-sized [`LangEmitter`] handle for JavaScript / TypeScript (one
/// emitter, both langs share the same Node.js dispatch). Method bodies
/// delegate to the existing free functions in this module.
pub struct JavaScriptEmitter ;
/// Entry kinds the JS / TS emitter currently understands. Extended in
/// Phase 13 (Track B JS + TS vertical) to include `HttpRoute` (Express /
/// Koa / Next), `CliSubcommand`, etc.
const SUPPORTED : & [ EntryKind ] = & [ EntryKind ::Function ] ;
impl LangEmitter for JavaScriptEmitter {
fn emit ( & self , spec : & HarnessSpec ) -> Result < HarnessSource , UnsupportedReason > {
emit ( spec )
}
fn entry_kinds_supported ( & self ) -> & 'static [ EntryKind ] {
SUPPORTED
}
fn entry_kind_hint ( & self , attempted : EntryKind ) -> String {
format! (
" javascript / typescript emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add Express / Koa / Next shapes in phase 13 "
)
}
}
2026-05-12 02:20:55 -04:00
/// Emit a Node.js harness for `spec`.
pub fn emit ( spec : & HarnessSpec ) -> Result < HarnessSource , UnsupportedReason > {
match & spec . payload_slot {
PayloadSlot ::Param ( _ ) | PayloadSlot ::EnvVar ( _ ) | PayloadSlot ::Stdin = > { }
_ = > return Err ( UnsupportedReason ::EntryKindUnsupported ) ,
}
let source = generate_source ( spec ) ;
let entry_filename = entry_module_filename ( & spec . entry_file ) ;
Ok ( HarnessSource {
source ,
filename : " harness.js " . to_owned ( ) ,
command : vec ! [ " node " . to_owned ( ) , " harness.js " . to_owned ( ) ] ,
extra_files : vec ! [ ] ,
entry_subpath : Some ( entry_filename ) ,
} )
}
fn generate_source ( spec : & HarnessSpec ) -> String {
let entry_module = entry_module_name ( & spec . entry_file ) ;
let entry_fn = & spec . entry_name ;
let ( pre_call , call_expr ) = build_call ( spec , & entry_module , entry_fn ) ;
format! (
r #" 'use strict';
// Nyx dynamic harness — auto-generated, do not edit.
// ── Payload loading ────────────────────────────────────────────────────────────
const _nyx_payload = ( ( ) = > { {
if ( process . env . NYX_PAYLOAD & & process . env . NYX_PAYLOAD . length > 0 ) { {
return process . env . NYX_PAYLOAD ;
} }
if ( process . env . NYX_PAYLOAD_B64 & & process . env . NYX_PAYLOAD_B64 . length > 0 ) { {
return Buffer . from ( process . env . NYX_PAYLOAD_B64 , ' base64 ' ) . toString ( ' utf8 ' ) ;
} }
return ' ' ;
} } ) ( ) ;
// ── Entry module import ────────────────────────────────────────────────────────
let _entry ;
try { {
_entry = require ( ' . / { entry_module } ' ) ;
} } catch ( e ) { {
process . stderr . write ( ' NYX_IMPORT_ERROR : ' + e . message + '\n' ) ;
process . exit ( 77 ) ;
} }
const payload = _nyx_payload ;
// ── Pre-call setup ─────────────────────────────────────────────────────────────
{ pre_call }
// ── Call entry point ──────────────────────────────────────────────────────────
try { {
const _result = { call_expr } ;
if ( _result ! = = undefined & & _result ! = = null ) { {
if ( _result & & typeof _result . then = = = ' function ' ) { {
_result
. then ( r = > { { if ( r ! = null ) process . stdout . write ( String ( r ) + '\n' ) ; } } )
. catch ( e = > { { process . stderr . write ( ' NYX_EXCEPTION : ' + e . message + '\n' ) ; } } ) ;
} } else { {
process . stdout . write ( String ( _result ) + '\n' ) ;
} }
} }
} } catch ( e ) { {
process . stderr . write ( ' NYX_EXCEPTION : ' + ( e . constructor ? e . constructor . name : ' Error ' ) + ' : ' + e . message + '\n' ) ;
} }
" #,
entry_module = entry_module ,
pre_call = pre_call ,
call_expr = call_expr ,
)
}
/// Build `(pre_call_setup, call_expression)` for the chosen payload slot.
fn build_call ( spec : & HarnessSpec , _module : & str , func : & str ) -> ( String , String ) {
match & spec . payload_slot {
PayloadSlot ::Param ( idx ) = > {
let pre = String ::new ( ) ;
let call = if * idx = = 0 {
format! ( " _entry. {func} (payload) " )
} else {
let pads = ( 0 .. * idx ) . map ( | _ | " '' " ) . collect ::< Vec < _ > > ( ) . join ( " , " ) ;
format! ( " _entry. {func} ( {pads} , payload) " )
} ;
( pre , call )
}
PayloadSlot ::EnvVar ( name ) = > {
let pre = format! ( " process.env[ {name:?} ] = payload; \n " ) ;
let call = format! ( " _entry. {func} () " ) ;
( pre , call )
}
PayloadSlot ::Stdin = > {
// Synchronous stdin replacement via Buffer.
let pre = format! (
" const {{ Readable }} = require('stream'); \n \
process . stdin = Readable . from ( [ Buffer . from ( payload , ' utf8 ' ) ] ) ; \ n "
) ;
let call = format! ( " _entry. {func} () " ) ;
( pre , call )
}
_ = > {
let pre = String ::new ( ) ;
let call = format! ( " _entry. {func} (payload) " ) ;
( pre , call )
}
}
}
/// Derive the JS module name from an entry file path.
///
/// `"src/handlers/login.js"` → `"login"` (basename without extension).
2026-05-14 02:37:01 -05:00
pub fn entry_module_name ( _entry_file : & str ) -> String {
// The harness always `require('./entry')` because `entry_module_filename`
// unconditionally copies the source to `entry.js` in the workdir. Keeping
// these two helpers in sync prevents a "Cannot find module" import error
// when the fixture's on-disk filename is anything other than `entry.js`.
" entry " . to_owned ( )
2026-05-12 02:20:55 -04:00
}
/// Derive the filename for `entry_subpath` from an entry file path.
///
/// Always returns `"entry.js"` — fixture files are copied here regardless of
/// their original name so the harness can always `require('./entry')`.
pub fn entry_module_filename ( _entry_file : & str ) -> String {
" entry.js " . to_owned ( )
}
#[ cfg(test) ]
mod tests {
use super ::* ;
use crate ::dynamic ::spec ::{ EntryKind , HarnessSpec , PayloadSlot } ;
use crate ::labels ::Cap ;
use crate ::symbol ::Lang ;
fn make_spec ( payload_slot : PayloadSlot ) -> HarnessSpec {
HarnessSpec {
finding_id : " js000000000001 " . into ( ) ,
entry_file : " src/app.js " . into ( ) ,
entry_name : " login " . into ( ) ,
entry_kind : EntryKind ::Function ,
lang : Lang ::JavaScript ,
toolchain_id : " node-20 " . into ( ) ,
payload_slot ,
expected_cap : Cap ::SQL_QUERY ,
constraint_hints : vec ! [ ] ,
sink_file : " src/app.js " . into ( ) ,
sink_line : 15 ,
spec_hash : " js000000000001 " . into ( ) ,
2026-05-13 13:03:44 -04:00
derivation : crate ::dynamic ::spec ::SpecDerivationStrategy ::FromFlowSteps ,
2026-05-12 02:20:55 -04:00
}
}
#[ test ]
fn emit_produces_source ( ) {
let spec = make_spec ( PayloadSlot ::Param ( 0 ) ) ;
let harness = emit ( & spec ) . unwrap ( ) ;
assert! ( harness . source . contains ( " NYX_PAYLOAD " ) ) ;
assert! ( harness . source . contains ( " require " ) ) ;
assert! ( harness . source . contains ( " login " ) ) ;
assert_eq! ( harness . filename , " harness.js " ) ;
assert_eq! ( harness . command , vec! [ " node " , " harness.js " ] ) ;
}
#[ test ]
fn emit_param_index_0 ( ) {
let spec = make_spec ( PayloadSlot ::Param ( 0 ) ) ;
let harness = emit ( & spec ) . unwrap ( ) ;
assert! ( harness . source . contains ( " _entry.login(payload) " ) ) ;
}
#[ test ]
fn emit_param_index_1 ( ) {
let spec = make_spec ( PayloadSlot ::Param ( 1 ) ) ;
let harness = emit ( & spec ) . unwrap ( ) ;
assert! ( harness . source . contains ( " _entry.login('', payload) " ) ) ;
}
#[ test ]
fn emit_env_var_slot ( ) {
let spec = make_spec ( PayloadSlot ::EnvVar ( " DB_HOST " . into ( ) ) ) ;
let harness = emit ( & spec ) . unwrap ( ) ;
assert! ( harness . source . contains ( " process.env[ \" DB_HOST \" ] = payload " ) ) ;
}
#[ test ]
fn emit_stdin_slot ( ) {
let spec = make_spec ( PayloadSlot ::Stdin ) ;
let harness = emit ( & spec ) . unwrap ( ) ;
assert! ( harness . source . contains ( " Readable " ) ) ;
assert! ( harness . source . contains ( " process.stdin " ) ) ;
}
#[ test ]
fn emit_http_body_is_unsupported ( ) {
let spec = make_spec ( PayloadSlot ::HttpBody ) ;
let err = emit ( & spec ) . unwrap_err ( ) ;
assert_eq! ( err , UnsupportedReason ::EntryKindUnsupported ) ;
}
#[ test ]
fn emit_entry_subpath_is_entry_js ( ) {
let spec = make_spec ( PayloadSlot ::Param ( 0 ) ) ;
let harness = emit ( & spec ) . unwrap ( ) ;
assert_eq! ( harness . entry_subpath , Some ( " entry.js " . to_owned ( ) ) ) ;
}
2026-05-14 03:22:30 -05:00
#[ test ]
fn entry_kinds_supported_is_non_empty ( ) {
assert! ( ! JavaScriptEmitter . entry_kinds_supported ( ) . is_empty ( ) ) ;
assert! ( JavaScriptEmitter
. entry_kinds_supported ( )
. contains ( & EntryKind ::Function ) ) ;
}
#[ test ]
fn entry_kind_hint_names_attempted_and_phase ( ) {
let hint = JavaScriptEmitter . entry_kind_hint ( EntryKind ::HttpRoute ) ;
assert! ( hint . contains ( " HttpRoute " ) ) ;
assert! ( hint . contains ( " phase 13 " ) ) ;
}
2026-05-12 02:20:55 -04:00
#[ test ]
2026-05-14 02:37:01 -05:00
fn entry_module_name_is_always_entry_to_match_copy_destination ( ) {
// `copy_entry_file` (via `entry_module_filename`) stages every fixture
// at `workdir/entry.js`, so `require('./entry')` is the only path the
// harness can use without missing-module errors at runtime, regardless
// of the source file's original name.
assert_eq! ( entry_module_name ( " src/handlers/login.js " ) , " entry " ) ;
assert_eq! ( entry_module_name ( " app.ts " ) , " entry " ) ;
assert_eq! ( entry_module_name ( " handler.mjs " ) , " entry " ) ;
assert_eq! ( entry_module_name ( " no_ext " ) , " entry " ) ;
2026-05-12 02:20:55 -04:00
}
}