Skip to content

Commit ce3cb07

Browse files
committed
fix(sea): fix sfw extraction from VFS with node_modules structure
Socket Firewall (sfw) was failing to extract from VFS in SEA binaries because: 1. node-smol VFS API requires all files under /snapshot/node_modules/ for security 2. sfw was packaged at VFS root, causing "Invalid VFS path" errors 3. Extracted binaries lacked executable permissions Changes: - Update downloads.mjs to package sfw in node_modules/@socketsecurity/sfw-bin/ - Add TOOL_STANDALONE_PATHS mapping for standalone binaries under node_modules - Create getToolFilePath() helper to centralize path resolution - Add chmod 0o755 for extracted standalone binaries on Unix - Remove unimplemented npm package tools from EXTERNAL_TOOLS (cdxgen, coana, etc.) VFS structure now: node_modules/@socketsecurity/sfw-bin/sfw (standalone binary) node_modules/@cyclonedx/cdxgen/ (future: npm packages) Verified: - ✓ SEA binary builds successfully with sfw in VFS - ✓ socket npm whoami command extracts and executes sfw correctly - ✓ Socket Firewall protection active for npm commands
1 parent 5026c51 commit ce3cb07

File tree

2 files changed

+63
-38
lines changed

2 files changed

+63
-38
lines changed

packages/cli/scripts/sea-build-utils/downloads.mjs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -322,24 +322,33 @@ export async function downloadExternalTools(platform, arch, isMusl = false) {
322322
const isStandalone = !isZip && !isTarGz
323323

324324
if (isStandalone) {
325-
// Standalone binary (e.g., sfw) - already downloaded, just rename if needed.
325+
// Standalone binary (e.g., sfw) - create node_modules structure for VFS compatibility.
326+
// node-smol VFS requires all files to be under node_modules/ for security.
326327
logger.log(` Preparing ${toolName}...`)
327-
if (archivePath !== binaryPath) {
328+
329+
// Create node_modules/@socketsecurity/sfw-bin/ structure.
330+
const packageDir = normalizePath(path.join(toolsDir, 'node_modules', '@socketsecurity', `${toolName}-bin`))
331+
await safeMkdir(packageDir)
332+
333+
const packageBinaryPath = normalizePath(path.join(packageDir, binaryName))
334+
335+
// Move binary into package directory.
336+
if (archivePath !== packageBinaryPath) {
328337
try {
329-
await fs.rename(archivePath, binaryPath)
338+
await fs.rename(archivePath, packageBinaryPath)
330339
} catch (e) {
331340
// Fallback to copy + delete for cross-device moves.
332-
await fs.copyFile(archivePath, binaryPath)
341+
await fs.copyFile(archivePath, packageBinaryPath)
333342
await fs.unlink(archivePath)
334343
}
335344
}
336345

337346
// Make executable on Unix.
338347
if (!isPlatWin) {
339-
await fs.chmod(binaryPath, 0o755)
348+
await fs.chmod(packageBinaryPath, 0o755)
340349
}
341350

342-
toolNames.push(binaryName)
351+
toolNames.push(`node_modules/@socketsecurity/${toolName}-bin`)
343352
logger.log(` ✓ ${toolName} ready`)
344353
continue
345354
}

packages/cli/src/utils/dlx/vfs-extract.mts

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -79,19 +79,17 @@ import { isSeaBinary } from '../sea/detect.mts'
7979
const logger = getDefaultLogger()
8080

8181
// External tool names bundled in VFS.
82-
// Includes both npm packages and standalone binaries.
82+
// Currently only includes standalone binaries that are packaged in the VFS tarball.
83+
// npm packages (cdxgen, coana, socket-patch, synp) will be added in future updates.
8384
export const EXTERNAL_TOOLS = [
8485
'sfw',
85-
'cdxgen',
86-
'coana',
87-
'socket-patch',
88-
'synp',
8986
] as const
9087

9188
export type ExternalTool = (typeof EXTERNAL_TOOLS)[number]
9289

9390
// Map of npm package tools to their node_modules/ paths.
94-
// Standalone binaries (like sfw) are NOT in this map and extract to direct paths.
91+
// These are full npm packages with dependencies and node_modules/ subdirectories.
92+
// Standalone binaries (like sfw) are NOT in this map - they use direct file paths.
9593
const TOOL_NPM_PATHS: Partial<Record<ExternalTool, { packageName: string; binPath: string }>> = {
9694
cdxgen: {
9795
packageName: '@cyclonedx/cdxgen',
@@ -111,6 +109,33 @@ const TOOL_NPM_PATHS: Partial<Record<ExternalTool, { packageName: string; binPat
111109
},
112110
}
113111

112+
// Map of standalone binary tools to their VFS paths.
113+
// These tools are stored under node_modules/ for VFS security but are single binaries without dependencies.
114+
const TOOL_STANDALONE_PATHS: Partial<Record<ExternalTool, string>> = {
115+
sfw: 'node_modules/@socketsecurity/sfw-bin/sfw',
116+
}
117+
118+
/**
119+
* Get the file system path for a tool based on its type (npm package or standalone binary).
120+
*
121+
* @param tool - Tool name.
122+
* @param nodeSmolBase - Base dlx directory path.
123+
* @returns Path to the tool binary (without .exe extension).
124+
*/
125+
function getToolFilePath(tool: ExternalTool, nodeSmolBase: string): string {
126+
const npmPath = TOOL_NPM_PATHS[tool]
127+
const standalonePath = TOOL_STANDALONE_PATHS[tool]
128+
129+
// For npm packages, use node_modules/ path with binPath.
130+
// For standalone binaries under node_modules/, use standalonePath.
131+
// For other standalone binaries, use direct tool name.
132+
return npmPath
133+
? normalizePath(path.join(nodeSmolBase, npmPath.binPath))
134+
: standalonePath
135+
? normalizePath(path.join(nodeSmolBase, standalonePath))
136+
: normalizePath(path.join(nodeSmolBase, tool))
137+
}
138+
114139
/**
115140
* Get the base dlx directory path for node-smol.
116141
* This is where both VFS-extracted tools and npm-installed packages live.
@@ -270,13 +295,23 @@ async function extractTool(tool: ExternalTool): Promise<string> {
270295
const toolPath = normalizePath(path.join(nodeSmolBase, npmPath.binPath))
271296
extractedPath = isPlatWin ? `${toolPath}.exe` : toolPath
272297
} else {
273-
// Extract standalone binary from VFS root.
274-
const vfsBinaryPath = `/snapshot/${tool}`
298+
// Extract standalone binary - check if it's under node_modules/ or VFS root.
299+
const standalonePath = TOOL_STANDALONE_PATHS[tool]
300+
const vfsBinaryPath = standalonePath ? `/snapshot/${standalonePath}` : `/snapshot/${tool}`
275301
const binaryPath = processWithSmol.smol.mount(vfsBinaryPath)
276302

277303
logger.info(` ✓ Extracted ${tool} binary to ${binaryPath}`)
278304

279305
extractedPath = isPlatWin ? `${binaryPath}.exe` : binaryPath
306+
307+
// Make executable on Unix.
308+
if (!isPlatWin && existsSync(extractedPath)) {
309+
try {
310+
await fs.chmod(extractedPath, 0o755)
311+
} catch {
312+
// Ignore chmod errors - file might already be executable.
313+
}
314+
}
280315
}
281316

282317
if (!existsSync(extractedPath)) {
@@ -397,10 +432,7 @@ export async function extractExternalTools(
397432
const toolPaths: Partial<Record<ExternalTool, string>> = {}
398433
let allValid = true
399434
for (const tool of EXTERNAL_TOOLS) {
400-
const npmPath = TOOL_NPM_PATHS[tool]
401-
const toolPath = npmPath
402-
? normalizePath(path.join(nodeSmolBase, npmPath.binPath))
403-
: normalizePath(path.join(nodeSmolBase, tool))
435+
const toolPath = getToolFilePath(tool, nodeSmolBase)
404436
const toolPathWithExt = isPlatWin ? `${toolPath}.exe` : toolPath
405437
// Validate tool exists and is executable.
406438
if (!existsSync(toolPathWithExt)) {
@@ -470,10 +502,7 @@ export async function extractExternalTools(
470502
const toolPaths: Partial<Record<ExternalTool, string>> = {}
471503
let allValid = true
472504
for (const tool of EXTERNAL_TOOLS) {
473-
const npmPath = TOOL_NPM_PATHS[tool]
474-
const toolPath = npmPath
475-
? normalizePath(path.join(nodeSmolBase, npmPath.binPath))
476-
: normalizePath(path.join(nodeSmolBase, tool))
505+
const toolPath = getToolFilePath(tool, nodeSmolBase)
477506
const toolPathWithExt = isPlatWin ? `${toolPath}.exe` : toolPath
478507
if (!existsSync(toolPathWithExt)) {
479508
allValid = false
@@ -504,10 +533,7 @@ export async function extractExternalTools(
504533
const toolPaths: Partial<Record<ExternalTool, string>> = {}
505534
let allValid = true
506535
for (const tool of EXTERNAL_TOOLS) {
507-
const npmPath = TOOL_NPM_PATHS[tool]
508-
const toolPath = npmPath
509-
? normalizePath(path.join(nodeSmolBase, npmPath.binPath))
510-
: normalizePath(path.join(nodeSmolBase, tool))
536+
const toolPath = getToolFilePath(tool, nodeSmolBase)
511537
const toolPathWithExt = isPlatWin ? `${toolPath}.exe` : toolPath
512538
// Validate tool exists before adding to paths.
513539
if (!existsSync(toolPathWithExt)) {
@@ -548,10 +574,7 @@ export async function extractExternalTools(
548574
const toolPaths: Partial<Record<ExternalTool, string>> = {}
549575

550576
for (const tool of EXTERNAL_TOOLS) {
551-
const npmPath = TOOL_NPM_PATHS[tool]
552-
const toolPath = npmPath
553-
? normalizePath(path.join(nodeSmolBase, npmPath.binPath))
554-
: normalizePath(path.join(nodeSmolBase, tool))
577+
const toolPath = getToolFilePath(tool, nodeSmolBase)
555578
const toolPathWithExt = isPlatWin ? `${toolPath}.exe` : toolPath
556579

557580
// Check if tool already exists and is executable.
@@ -624,14 +647,7 @@ export function getToolPaths(): Record<ExternalTool, string> {
624647
const paths: Partial<Record<ExternalTool, string>> = {}
625648

626649
for (const tool of EXTERNAL_TOOLS) {
627-
const npmPath = TOOL_NPM_PATHS[tool]
628-
629-
// For npm packages, use node_modules/ path.
630-
// For standalone binaries, use direct path.
631-
const toolPath = npmPath
632-
? normalizePath(path.join(nodeSmolBase, npmPath.binPath))
633-
: normalizePath(path.join(nodeSmolBase, tool))
634-
650+
const toolPath = getToolFilePath(tool, nodeSmolBase)
635651
paths[tool] = isPlatWin ? `${toolPath}.exe` : toolPath
636652
}
637653

0 commit comments

Comments
 (0)