🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@vercel/sandbox

Package Overview
Dependencies
Maintainers
4
Versions
96
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@vercel/sandbox - npm Package Compare versions

Comparing version
2.6.0
to
2.6.1
+1
-0
dist/index.cjs

@@ -16,2 +16,3 @@ const require_api_error = require('./api-client/api-error.cjs');

exports.SandboxUser = require_sandbox_user.SandboxUser;
exports.SandboxUserAlreadyExistsError = require_sandbox_user.SandboxUserAlreadyExistsError;
exports.Session = require_session.Session;

@@ -18,0 +19,0 @@ exports.Snapshot = require_snapshot.Snapshot;

+2
-2

@@ -9,5 +9,5 @@ import { APIError, StreamError } from "./api-client/api-error.cjs";

import { FileSystem } from "./filesystem.cjs";
import { SandboxUser } from "./sandbox-user.cjs";
import { SandboxUser, SandboxUserAlreadyExistsError } from "./sandbox-user.cjs";
import { Sandbox, SerializedSandbox } from "./sandbox.cjs";
import { InvalidRequestProxyHandler, ProxyHandler, ProxyMeta, defineSandboxProxy } from "./proxy.cjs";
export { APIError, Command, CommandFinished, type CommandOutput, type ExecutionContext, FileSystem, type InvalidRequestProxyHandler, type NetworkPolicy, type NetworkPolicyKeyValueMatcher, type NetworkPolicyMatch, type NetworkPolicyMatcher, type NetworkPolicyRule, type NetworkTransformer, type ProxyHandler, type ProxyMeta, Sandbox, SandboxUser, type SerializedCommand, type SerializedCommandFinished, type SerializedSandbox, type SerializedSnapshot, Session, Snapshot, type SnapshotTreeNodeData, StreamError, defineSandboxProxy };
export { APIError, Command, CommandFinished, type CommandOutput, type ExecutionContext, FileSystem, type InvalidRequestProxyHandler, type NetworkPolicy, type NetworkPolicyKeyValueMatcher, type NetworkPolicyMatch, type NetworkPolicyMatcher, type NetworkPolicyRule, type NetworkTransformer, type ProxyHandler, type ProxyMeta, Sandbox, SandboxUser, SandboxUserAlreadyExistsError, type SerializedCommand, type SerializedCommandFinished, type SerializedSandbox, type SerializedSnapshot, Session, Snapshot, type SnapshotTreeNodeData, StreamError, defineSandboxProxy };

@@ -9,5 +9,5 @@ import { APIError, StreamError } from "./api-client/api-error.js";

import { FileSystem } from "./filesystem.js";
import { SandboxUser } from "./sandbox-user.js";
import { SandboxUser, SandboxUserAlreadyExistsError } from "./sandbox-user.js";
import { Sandbox, SerializedSandbox } from "./sandbox.js";
import { InvalidRequestProxyHandler, ProxyHandler, ProxyMeta, defineSandboxProxy } from "./proxy.js";
export { APIError, Command, CommandFinished, type CommandOutput, type ExecutionContext, FileSystem, type InvalidRequestProxyHandler, type NetworkPolicy, type NetworkPolicyKeyValueMatcher, type NetworkPolicyMatch, type NetworkPolicyMatcher, type NetworkPolicyRule, type NetworkTransformer, type ProxyHandler, type ProxyMeta, Sandbox, SandboxUser, type SerializedCommand, type SerializedCommandFinished, type SerializedSandbox, type SerializedSnapshot, Session, Snapshot, type SnapshotTreeNodeData, StreamError, defineSandboxProxy };
export { APIError, Command, CommandFinished, type CommandOutput, type ExecutionContext, FileSystem, type InvalidRequestProxyHandler, type NetworkPolicy, type NetworkPolicyKeyValueMatcher, type NetworkPolicyMatch, type NetworkPolicyMatcher, type NetworkPolicyRule, type NetworkTransformer, type ProxyHandler, type ProxyMeta, Sandbox, SandboxUser, SandboxUserAlreadyExistsError, type SerializedCommand, type SerializedCommandFinished, type SerializedSandbox, type SerializedSnapshot, Session, Snapshot, type SnapshotTreeNodeData, StreamError, defineSandboxProxy };

@@ -6,6 +6,6 @@ import { APIError, StreamError } from "./api-client/api-error.js";

import { FileSystem } from "./filesystem.js";
import { SandboxUser } from "./sandbox-user.js";
import { SandboxUser, SandboxUserAlreadyExistsError } from "./sandbox-user.js";
import { Sandbox } from "./sandbox.js";
import { defineSandboxProxy } from "./proxy.js";
export { APIError, Command, CommandFinished, FileSystem, Sandbox, SandboxUser, Session, Snapshot, StreamError, defineSandboxProxy };
export { APIError, Command, CommandFinished, FileSystem, Sandbox, SandboxUser, SandboxUserAlreadyExistsError, Session, Snapshot, StreamError, defineSandboxProxy };

@@ -11,2 +11,12 @@ const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');

/**
* Thrown when {@link Sandbox.createUser} is called for an existing username.
*/
var SandboxUserAlreadyExistsError = class extends Error {
constructor(username) {
super(`Failed to create user "${username}": user already exists`);
this.name = "SandboxUserAlreadyExistsError";
this.username = username;
}
};
/**
* A user context within a sandbox.

@@ -293,2 +303,3 @@ *

exports.SandboxUser = SandboxUser;
exports.SandboxUserAlreadyExistsError = SandboxUserAlreadyExistsError;
//# sourceMappingURL=sandbox-user.cjs.map

@@ -1,1 +0,1 @@

{"version":3,"file":"sandbox-user.cjs","names":["path","wrapped","Readable","stream"],"sources":["../src/sandbox-user.ts"],"sourcesContent":["import { Readable } from \"stream\";\nimport { pipeline } from \"stream/promises\";\nimport { createWriteStream } from \"fs\";\nimport { mkdir } from \"fs/promises\";\nimport { dirname, resolve } from \"path\";\nimport type { Sandbox } from \"./sandbox.js\";\nimport type { RunCommandParams } from \"./session.js\";\nimport type { Command, CommandFinished } from \"./command.js\";\nimport type { ExecutionContext } from \"./execution-context.js\";\nimport { validateName } from \"./utils/validate-name.js\";\n\n/**\n * A user context within a sandbox.\n *\n * All file and command operations default to running as this user.\n * Created via {@link Sandbox.createUser} or {@link Sandbox.asUser}.\n *\n * @hideconstructor\n */\nexport class SandboxUser implements ExecutionContext {\n /**\n * The Linux username.\n */\n readonly username: string;\n\n /**\n * The user's home directory (e.g., `/home/alice`).\n */\n readonly homeDir: string;\n\n private readonly sandbox: Sandbox;\n\n /**\n * Memoized lookup of this user's primary group.\n * See {@link SandboxUser.primaryGroup}.\n */\n private primaryGroupPromise?: Promise<string>;\n\n constructor({\n sandbox,\n username,\n }: {\n sandbox: Sandbox;\n username: string;\n }) {\n this.sandbox = sandbox;\n this.username = username;\n // `root`'s home is `/root`, not `/home/root`; every other user's home\n // follows the `/home/<username>` convention.\n this.homeDir = username === \"root\" ? \"/root\" : `/home/${username}`;\n }\n\n /**\n * Build the wrapped command args to run as this user via `sudo -u`.\n *\n * When `env` is provided, injects `env KEY=VAL ...` so that environment\n * variables survive the `sudo -u` transition.\n */\n private buildUserCommand(params: {\n cmd: string;\n args?: string[];\n env?: Record<string, string>;\n cwd?: string;\n }): { cmd: string; args: string[] } {\n const envEntries = Object.entries(params.env ?? {});\n const envArgs =\n envEntries.length > 0\n ? [\"env\", ...envEntries.map(([k, v]) => `${k}=${v}`)]\n : [];\n\n const cwd = params.cwd ?? this.homeDir;\n\n // Run as the target user via `sudo -u`, changing into `cwd` first.\n //\n // We can't set the directory via the sandbox API's `cwd` (the backend cd's\n // there before exec, and SUID binaries like sudo cannot start from a `770`\n // home dir), nor via `sudo --chdir` (the sandbox's sudoers policy forbids\n // the `-D` option). Instead we cd inside a `bash -c` wrapper.\n //\n // `cwd`, the command, its args, and any `env KEY=VAL` are passed as\n // separate positional parameters to `bash -c` (`$1` is `cwd`; `$@` after\n // the shift is the command). Because they are argv elements rather than\n // text spliced into the script, they are never re-parsed by the shell —\n // injection-safe by construction.\n return {\n cmd: \"sudo\",\n args: [\n \"-u\",\n this.username,\n \"--\",\n \"bash\",\n \"-c\",\n 'cd \"$1\" || exit 1; shift; exec \"$@\"',\n \"bash\", // $0 placeholder for `bash -c`\n cwd,\n ...envArgs,\n params.cmd,\n ...(params.args ?? []),\n ],\n };\n }\n\n /**\n * Resolve a path relative to this user's home directory.\n * Absolute paths are returned as-is.\n */\n private resolvePath(path: string): string {\n return path.startsWith(\"/\") ? path : `${this.homeDir}/${path}`;\n }\n\n /**\n * Start executing a command as this user.\n *\n * @param command - The command to execute.\n * @param args - Arguments to pass to the command.\n * @param opts - Optional parameters.\n * @returns A {@link CommandFinished} result once execution is done.\n */\n async runCommand(\n command: string,\n args?: string[],\n opts?: { signal?: AbortSignal; timeoutMs?: number },\n ): Promise<CommandFinished>;\n\n /**\n * Start executing a command as this user in detached mode.\n *\n * @param params - The command parameters.\n * @returns A {@link Command} instance for the running command.\n */\n async runCommand(\n params: RunCommandParams & { detached: true },\n ): Promise<Command>;\n\n /**\n * Start executing a command as this user.\n *\n * @param params - The command parameters.\n * @returns A {@link CommandFinished} result once execution is done.\n */\n async runCommand(params: RunCommandParams): Promise<CommandFinished>;\n\n async runCommand(\n commandOrParams: string | RunCommandParams,\n args?: string[],\n opts?: { signal?: AbortSignal; timeoutMs?: number },\n ): Promise<Command | CommandFinished> {\n if (typeof commandOrParams === \"string\") {\n const wrapped = this.buildUserCommand({\n cmd: commandOrParams,\n args,\n });\n // Don't pass cwd to the sandbox API — the bash -c wrapper cd's for us.\n // Don't pass sudo: true — the default user already has sudo privileges.\n return this.sandbox.runCommand({\n ...wrapped,\n signal: opts?.signal,\n timeoutMs: opts?.timeoutMs,\n });\n }\n\n const params = commandOrParams;\n\n // When sudo: true is passed, delegate directly to root (skip user wrapping).\n // Don't default cwd to homeDir — the backend can't exec SUID binaries\n // from directories with restricted permissions.\n if (params.sudo) {\n return this.sandbox.runCommand({\n ...params,\n } as RunCommandParams & { detached: true });\n }\n\n const wrapped = this.buildUserCommand({\n cmd: params.cmd,\n args: params.args,\n env: params.env,\n cwd: params.cwd,\n });\n\n return this.sandbox.runCommand({\n cmd: wrapped.cmd,\n args: wrapped.args,\n // Don't pass cwd — the bash -c wrapper cd's for us (see buildUserCommand)\n // Don't pass sudo: true — the default user already has sudo privileges\n // env is already baked into the wrapped command via `env KEY=VAL`\n detached: params.detached,\n stdout: params.stdout,\n stderr: params.stderr,\n signal: params.signal,\n timeoutMs: params.timeoutMs,\n } as RunCommandParams & { detached: true });\n }\n\n /**\n * Write files to this user's home directory (or absolute paths).\n * Files are written via the sandbox HTTP API then chowned to this user.\n *\n * The HTTP API can write to user home dirs because they are group-owned\n * by the sandbox's default user group with `770` permissions.\n *\n * @param files - Array of files with path, content, and optional mode\n * @param opts - Optional parameters.\n */\n async writeFiles(\n files: { path: string; content: string | Uint8Array; mode?: number }[],\n opts?: { signal?: AbortSignal },\n ) {\n // Resolve relative paths to user's home directory\n const absoluteFiles = files.map((f) => ({\n ...f,\n path: this.resolvePath(f.path),\n }));\n\n // Write via the HTTP API (works because home dirs are group-owned\n // by the default user's group)\n await this.sandbox.writeFiles(absoluteFiles, opts);\n\n const paths = absoluteFiles.map((f) => f.path);\n if (paths.length === 0) return;\n\n // The files themselves belong to the user outright.\n await this.chownOrThrow(\n paths,\n `${this.username}:${await this.primaryGroup(opts?.signal)}`,\n opts?.signal,\n );\n\n // Any directories the write implicitly created (e.g. `data/` in\n // `data/config.json`) are owned by the default user. Hand them to the user\n // but keep them group-owned by the default user's group with mode 770 —\n // matching the home dir — so the HTTP API can still traverse and write into\n // them later.\n const dirs = this.ancestorDirsUnderHome(paths);\n if (dirs.length > 0) {\n const { group } = await this.sandbox.getDefaultUser(opts);\n await this.chownOrThrow(\n dirs,\n `${this.username}:${group}`,\n opts?.signal,\n );\n await this.chmodOrThrow(dirs, \"770\", opts?.signal);\n }\n }\n\n /**\n * Read a file from this user's context as a stream.\n *\n * @param file - File to read, with path and optional cwd\n * @param opts - Optional parameters.\n * @returns A ReadableStream of the file contents, or null if not found\n */\n async readFile(\n file: { path: string; cwd?: string },\n opts?: { signal?: AbortSignal },\n ): Promise<NodeJS.ReadableStream | null> {\n // `\"use step\"`: touches a Node built-in (`stream`), which the @workflow\n // compiler only permits inside step functions (matches Session.readFile).\n \"use step\";\n const buffer = await this.catAsUser(file, opts);\n return buffer === null ? null : Readable.from([buffer]);\n }\n\n /**\n * Read a file from this user's context as a Buffer.\n *\n * @param file - File to read, with path and optional cwd\n * @param opts - Optional parameters.\n * @returns The file contents as a Buffer, or null if not found\n */\n async readFileToBuffer(\n file: { path: string; cwd?: string },\n opts?: { signal?: AbortSignal },\n ): Promise<Buffer | null> {\n return this.catAsUser(file, opts);\n }\n\n /**\n * Download a file from this user's context to the local filesystem.\n *\n * @param src - Source file in the sandbox\n * @param dst - Destination on the local machine\n * @param opts - Optional parameters.\n * @returns The absolute path to the written file, or null if not found\n */\n async downloadFile(\n src: { path: string; cwd?: string },\n dst: { path: string; cwd?: string },\n opts?: { mkdirRecursive?: boolean; signal?: AbortSignal },\n ): Promise<string | null> {\n // `\"use step\"`: touches Node built-ins (`fs`, `path`), which the @workflow\n // compiler only permits inside step functions (matches Session.downloadFile).\n \"use step\";\n const stream = await this.readFile(src, opts);\n if (stream === null) return null;\n\n const dstPath = resolve(dst.cwd ?? \"\", dst.path);\n if (opts?.mkdirRecursive) {\n await mkdir(dirname(dstPath), { recursive: true });\n }\n await pipeline(stream, createWriteStream(dstPath), {\n signal: opts?.signal,\n });\n return dstPath;\n }\n\n /**\n * Read a file as this user and return its bytes, or null if it does not\n * exist.\n *\n * Reads via `sudo -u <user> base64` rather than the HTTP file API: on stock\n * runtimes the API runs as `vercel-sandbox` and cannot read files this user\n * has kept private (e.g. mode `600`), whereas reading as the user always\n * honours the user's own permissions. The payload is base64-encoded because\n * the command output channel is UTF-8 only and would otherwise corrupt\n * binary files.\n */\n private async catAsUser(\n file: { path: string; cwd?: string },\n opts?: { signal?: AbortSignal },\n ): Promise<Buffer | null> {\n const path = file.path.startsWith(\"/\")\n ? file.path\n : `${file.cwd ?? this.homeDir}/${file.path}`;\n const result = await this.runCommand({\n cmd: \"base64\",\n args: [path],\n signal: opts?.signal,\n });\n if (result.exitCode !== 0) {\n const stderr = await result.stderr();\n if (/No such file or directory/i.test(stderr)) return null;\n throw new Error(`Failed to read ${path}: ${stderr}`);\n }\n return Buffer.from(await result.stdout(), \"base64\");\n }\n\n /**\n * Create a directory owned by this user.\n *\n * @param path - Path of the directory to create\n * @param opts - Optional parameters.\n */\n async mkDir(path: string, opts?: { signal?: AbortSignal }): Promise<void> {\n const absPath = this.resolvePath(path);\n await this.sandbox.mkDir(absPath, opts);\n // Own the created directory plus any parents the mkdir created under the\n // home dir. Group-owned by the default user's group with mode 770 so the\n // HTTP file API can write into it (matching the home dir).\n const dirs = [absPath, ...this.ancestorDirsUnderHome([absPath])];\n const { group } = await this.sandbox.getDefaultUser(opts);\n await this.chownOrThrow(\n dirs,\n `${this.username}:${group}`,\n opts?.signal,\n );\n await this.chmodOrThrow(dirs, \"770\", opts?.signal);\n }\n\n /**\n * This user's primary group. Users created via {@link Sandbox.createUser}\n * get a group named after them, but {@link Sandbox.asUser} accepts\n * pre-existing users whose primary group can differ (e.g. system users), so\n * we resolve it from the sandbox rather than assuming `<username>`.\n *\n * The result is memoized for the lifetime of this instance.\n */\n private primaryGroup(signal?: AbortSignal): Promise<string> {\n if (!this.primaryGroupPromise) {\n this.primaryGroupPromise = this.resolvePrimaryGroup(signal).catch(\n (err) => {\n // Don't cache failures — allow a later call to retry.\n this.primaryGroupPromise = undefined;\n throw err;\n },\n );\n }\n return this.primaryGroupPromise;\n }\n\n private async resolvePrimaryGroup(signal?: AbortSignal): Promise<string> {\n const result = await this.sandbox.runCommand({\n cmd: \"id\",\n args: [\"-gn\", this.username],\n signal,\n });\n if (result.exitCode !== 0) {\n const stderr = await result.stderr();\n throw new Error(\n `Failed to resolve the primary group of \"${this.username}\": ${stderr}`,\n );\n }\n const group = (await result.stdout()).trim();\n if (!group) {\n throw new Error(\n `Failed to resolve the primary group of \"${this.username}\"`,\n );\n }\n return group;\n }\n\n /**\n * Run `chown <ownership> <paths...>` as root, throwing on failure.\n */\n private async chownOrThrow(\n paths: string[],\n ownership: string,\n signal?: AbortSignal,\n ): Promise<void> {\n const chown = await this.sandbox.runCommand({\n cmd: \"chown\",\n args: [ownership, ...paths],\n sudo: true,\n signal,\n });\n if (chown.exitCode !== 0) {\n const stderr = await chown.stderr();\n throw new Error(\n `Failed to set ownership on ${paths.join(\", \")}: ${stderr}`,\n );\n }\n }\n\n /**\n * Run `chmod <mode> <paths...>` as root, throwing on failure.\n */\n private async chmodOrThrow(\n paths: string[],\n mode: string,\n signal?: AbortSignal,\n ): Promise<void> {\n const chmod = await this.sandbox.runCommand({\n cmd: \"chmod\",\n args: [mode, ...paths],\n sudo: true,\n signal,\n });\n if (chmod.exitCode !== 0) {\n const stderr = await chmod.stderr();\n throw new Error(\n `Failed to set permissions on ${paths.join(\", \")}: ${stderr}`,\n );\n }\n }\n\n /**\n * Given absolute leaf paths, return the directories strictly between this\n * user's home directory and each leaf. The home dir itself is excluded, as\n * are any paths that fall outside the home dir.\n */\n private ancestorDirsUnderHome(paths: string[]): string[] {\n const dirs = new Set<string>();\n const homePrefix = `${this.homeDir}/`;\n for (const p of paths) {\n let dir = p.slice(0, p.lastIndexOf(\"/\"));\n while (dir.length > this.homeDir.length && dir.startsWith(homePrefix)) {\n dirs.add(dir);\n dir = dir.slice(0, dir.lastIndexOf(\"/\"));\n }\n }\n return [...dirs];\n }\n\n /**\n * Add this user to a group.\n *\n * @param groupname - Name of the group to join\n * @param opts - Optional parameters.\n */\n async addToGroup(\n groupname: string,\n opts?: { signal?: AbortSignal },\n ): Promise<void> {\n validateName(groupname, \"group name\");\n await this.sandbox.addUserToGroup(this.username, groupname, opts);\n }\n\n /**\n * Remove this user from a group.\n *\n * @param groupname - Name of the group to leave\n * @param opts - Optional parameters.\n */\n async removeFromGroup(\n groupname: string,\n opts?: { signal?: AbortSignal },\n ): Promise<void> {\n validateName(groupname, \"group name\");\n await this.sandbox.removeUserFromGroup(this.username, groupname, opts);\n }\n}\n"],"mappings":"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"}
{"version":3,"file":"sandbox-user.cjs","names":["path","wrapped","Readable","stream"],"sources":["../src/sandbox-user.ts"],"sourcesContent":["import { Readable } from \"stream\";\nimport { pipeline } from \"stream/promises\";\nimport { createWriteStream } from \"fs\";\nimport { mkdir } from \"fs/promises\";\nimport { dirname, resolve } from \"path\";\nimport type { Sandbox } from \"./sandbox.js\";\nimport type { RunCommandParams } from \"./session.js\";\nimport type { Command, CommandFinished } from \"./command.js\";\nimport type { ExecutionContext } from \"./execution-context.js\";\nimport { validateName } from \"./utils/validate-name.js\";\n\n/**\n * Thrown when {@link Sandbox.createUser} is called for an existing username.\n */\nexport class SandboxUserAlreadyExistsError extends Error {\n readonly username: string;\n\n constructor(username: string) {\n super(`Failed to create user \"${username}\": user already exists`);\n this.name = \"SandboxUserAlreadyExistsError\";\n this.username = username;\n }\n}\n\n/**\n * A user context within a sandbox.\n *\n * All file and command operations default to running as this user.\n * Created via {@link Sandbox.createUser} or {@link Sandbox.asUser}.\n *\n * @hideconstructor\n */\nexport class SandboxUser implements ExecutionContext {\n /**\n * The Linux username.\n */\n readonly username: string;\n\n /**\n * The user's home directory (e.g., `/home/alice`).\n */\n readonly homeDir: string;\n\n private readonly sandbox: Sandbox;\n\n /**\n * Memoized lookup of this user's primary group.\n * See {@link SandboxUser.primaryGroup}.\n */\n private primaryGroupPromise?: Promise<string>;\n\n constructor({\n sandbox,\n username,\n }: {\n sandbox: Sandbox;\n username: string;\n }) {\n this.sandbox = sandbox;\n this.username = username;\n // `root`'s home is `/root`, not `/home/root`; every other user's home\n // follows the `/home/<username>` convention.\n this.homeDir = username === \"root\" ? \"/root\" : `/home/${username}`;\n }\n\n /**\n * Build the wrapped command args to run as this user via `sudo -u`.\n *\n * When `env` is provided, injects `env KEY=VAL ...` so that environment\n * variables survive the `sudo -u` transition.\n */\n private buildUserCommand(params: {\n cmd: string;\n args?: string[];\n env?: Record<string, string>;\n cwd?: string;\n }): { cmd: string; args: string[] } {\n const envEntries = Object.entries(params.env ?? {});\n const envArgs =\n envEntries.length > 0\n ? [\"env\", ...envEntries.map(([k, v]) => `${k}=${v}`)]\n : [];\n\n const cwd = params.cwd ?? this.homeDir;\n\n // Run as the target user via `sudo -u`, changing into `cwd` first.\n //\n // We can't set the directory via the sandbox API's `cwd` (the backend cd's\n // there before exec, and SUID binaries like sudo cannot start from a `770`\n // home dir), nor via `sudo --chdir` (the sandbox's sudoers policy forbids\n // the `-D` option). Instead we cd inside a `bash -c` wrapper.\n //\n // `cwd`, the command, its args, and any `env KEY=VAL` are passed as\n // separate positional parameters to `bash -c` (`$1` is `cwd`; `$@` after\n // the shift is the command). Because they are argv elements rather than\n // text spliced into the script, they are never re-parsed by the shell —\n // injection-safe by construction.\n return {\n cmd: \"sudo\",\n args: [\n \"-u\",\n this.username,\n \"--\",\n \"bash\",\n \"-c\",\n 'cd \"$1\" || exit 1; shift; exec \"$@\"',\n \"bash\", // $0 placeholder for `bash -c`\n cwd,\n ...envArgs,\n params.cmd,\n ...(params.args ?? []),\n ],\n };\n }\n\n /**\n * Resolve a path relative to this user's home directory.\n * Absolute paths are returned as-is.\n */\n private resolvePath(path: string): string {\n return path.startsWith(\"/\") ? path : `${this.homeDir}/${path}`;\n }\n\n /**\n * Start executing a command as this user.\n *\n * @param command - The command to execute.\n * @param args - Arguments to pass to the command.\n * @param opts - Optional parameters.\n * @returns A {@link CommandFinished} result once execution is done.\n */\n async runCommand(\n command: string,\n args?: string[],\n opts?: { signal?: AbortSignal; timeoutMs?: number },\n ): Promise<CommandFinished>;\n\n /**\n * Start executing a command as this user in detached mode.\n *\n * @param params - The command parameters.\n * @returns A {@link Command} instance for the running command.\n */\n async runCommand(\n params: RunCommandParams & { detached: true },\n ): Promise<Command>;\n\n /**\n * Start executing a command as this user.\n *\n * @param params - The command parameters.\n * @returns A {@link CommandFinished} result once execution is done.\n */\n async runCommand(params: RunCommandParams): Promise<CommandFinished>;\n\n async runCommand(\n commandOrParams: string | RunCommandParams,\n args?: string[],\n opts?: { signal?: AbortSignal; timeoutMs?: number },\n ): Promise<Command | CommandFinished> {\n if (typeof commandOrParams === \"string\") {\n const wrapped = this.buildUserCommand({\n cmd: commandOrParams,\n args,\n });\n // Don't pass cwd to the sandbox API — the bash -c wrapper cd's for us.\n // Don't pass sudo: true — the default user already has sudo privileges.\n return this.sandbox.runCommand({\n ...wrapped,\n signal: opts?.signal,\n timeoutMs: opts?.timeoutMs,\n });\n }\n\n const params = commandOrParams;\n\n // When sudo: true is passed, delegate directly to root (skip user wrapping).\n // Don't default cwd to homeDir — the backend can't exec SUID binaries\n // from directories with restricted permissions.\n if (params.sudo) {\n return this.sandbox.runCommand({\n ...params,\n } as RunCommandParams & { detached: true });\n }\n\n const wrapped = this.buildUserCommand({\n cmd: params.cmd,\n args: params.args,\n env: params.env,\n cwd: params.cwd,\n });\n\n return this.sandbox.runCommand({\n cmd: wrapped.cmd,\n args: wrapped.args,\n // Don't pass cwd — the bash -c wrapper cd's for us (see buildUserCommand)\n // Don't pass sudo: true — the default user already has sudo privileges\n // env is already baked into the wrapped command via `env KEY=VAL`\n detached: params.detached,\n stdout: params.stdout,\n stderr: params.stderr,\n signal: params.signal,\n timeoutMs: params.timeoutMs,\n } as RunCommandParams & { detached: true });\n }\n\n /**\n * Write files to this user's home directory (or absolute paths).\n * Files are written via the sandbox HTTP API then chowned to this user.\n *\n * The HTTP API can write to user home dirs because they are group-owned\n * by the sandbox's default user group with `770` permissions.\n *\n * @param files - Array of files with path, content, and optional mode\n * @param opts - Optional parameters.\n */\n async writeFiles(\n files: { path: string; content: string | Uint8Array; mode?: number }[],\n opts?: { signal?: AbortSignal },\n ) {\n // Resolve relative paths to user's home directory\n const absoluteFiles = files.map((f) => ({\n ...f,\n path: this.resolvePath(f.path),\n }));\n\n // Write via the HTTP API (works because home dirs are group-owned\n // by the default user's group)\n await this.sandbox.writeFiles(absoluteFiles, opts);\n\n const paths = absoluteFiles.map((f) => f.path);\n if (paths.length === 0) return;\n\n // The files themselves belong to the user outright.\n await this.chownOrThrow(\n paths,\n `${this.username}:${await this.primaryGroup(opts?.signal)}`,\n opts?.signal,\n );\n\n // Any directories the write implicitly created (e.g. `data/` in\n // `data/config.json`) are owned by the default user. Hand them to the user\n // but keep them group-owned by the default user's group with mode 770 —\n // matching the home dir — so the HTTP API can still traverse and write into\n // them later.\n const dirs = this.ancestorDirsUnderHome(paths);\n if (dirs.length > 0) {\n const { group } = await this.sandbox.getDefaultUser(opts);\n await this.chownOrThrow(\n dirs,\n `${this.username}:${group}`,\n opts?.signal,\n );\n await this.chmodOrThrow(dirs, \"770\", opts?.signal);\n }\n }\n\n /**\n * Read a file from this user's context as a stream.\n *\n * @param file - File to read, with path and optional cwd\n * @param opts - Optional parameters.\n * @returns A ReadableStream of the file contents, or null if not found\n */\n async readFile(\n file: { path: string; cwd?: string },\n opts?: { signal?: AbortSignal },\n ): Promise<NodeJS.ReadableStream | null> {\n // `\"use step\"`: touches a Node built-in (`stream`), which the @workflow\n // compiler only permits inside step functions (matches Session.readFile).\n \"use step\";\n const buffer = await this.catAsUser(file, opts);\n return buffer === null ? null : Readable.from([buffer]);\n }\n\n /**\n * Read a file from this user's context as a Buffer.\n *\n * @param file - File to read, with path and optional cwd\n * @param opts - Optional parameters.\n * @returns The file contents as a Buffer, or null if not found\n */\n async readFileToBuffer(\n file: { path: string; cwd?: string },\n opts?: { signal?: AbortSignal },\n ): Promise<Buffer | null> {\n return this.catAsUser(file, opts);\n }\n\n /**\n * Download a file from this user's context to the local filesystem.\n *\n * @param src - Source file in the sandbox\n * @param dst - Destination on the local machine\n * @param opts - Optional parameters.\n * @returns The absolute path to the written file, or null if not found\n */\n async downloadFile(\n src: { path: string; cwd?: string },\n dst: { path: string; cwd?: string },\n opts?: { mkdirRecursive?: boolean; signal?: AbortSignal },\n ): Promise<string | null> {\n // `\"use step\"`: touches Node built-ins (`fs`, `path`), which the @workflow\n // compiler only permits inside step functions (matches Session.downloadFile).\n \"use step\";\n const stream = await this.readFile(src, opts);\n if (stream === null) return null;\n\n const dstPath = resolve(dst.cwd ?? \"\", dst.path);\n if (opts?.mkdirRecursive) {\n await mkdir(dirname(dstPath), { recursive: true });\n }\n await pipeline(stream, createWriteStream(dstPath), {\n signal: opts?.signal,\n });\n return dstPath;\n }\n\n /**\n * Read a file as this user and return its bytes, or null if it does not\n * exist.\n *\n * Reads via `sudo -u <user> base64` rather than the HTTP file API: on stock\n * runtimes the API runs as `vercel-sandbox` and cannot read files this user\n * has kept private (e.g. mode `600`), whereas reading as the user always\n * honours the user's own permissions. The payload is base64-encoded because\n * the command output channel is UTF-8 only and would otherwise corrupt\n * binary files.\n */\n private async catAsUser(\n file: { path: string; cwd?: string },\n opts?: { signal?: AbortSignal },\n ): Promise<Buffer | null> {\n const path = file.path.startsWith(\"/\")\n ? file.path\n : `${file.cwd ?? this.homeDir}/${file.path}`;\n const result = await this.runCommand({\n cmd: \"base64\",\n args: [path],\n signal: opts?.signal,\n });\n if (result.exitCode !== 0) {\n const stderr = await result.stderr();\n if (/No such file or directory/i.test(stderr)) return null;\n throw new Error(`Failed to read ${path}: ${stderr}`);\n }\n return Buffer.from(await result.stdout(), \"base64\");\n }\n\n /**\n * Create a directory owned by this user.\n *\n * @param path - Path of the directory to create\n * @param opts - Optional parameters.\n */\n async mkDir(path: string, opts?: { signal?: AbortSignal }): Promise<void> {\n const absPath = this.resolvePath(path);\n await this.sandbox.mkDir(absPath, opts);\n // Own the created directory plus any parents the mkdir created under the\n // home dir. Group-owned by the default user's group with mode 770 so the\n // HTTP file API can write into it (matching the home dir).\n const dirs = [absPath, ...this.ancestorDirsUnderHome([absPath])];\n const { group } = await this.sandbox.getDefaultUser(opts);\n await this.chownOrThrow(\n dirs,\n `${this.username}:${group}`,\n opts?.signal,\n );\n await this.chmodOrThrow(dirs, \"770\", opts?.signal);\n }\n\n /**\n * This user's primary group. Users created via {@link Sandbox.createUser}\n * get a group named after them, but {@link Sandbox.asUser} accepts\n * pre-existing users whose primary group can differ (e.g. system users), so\n * we resolve it from the sandbox rather than assuming `<username>`.\n *\n * The result is memoized for the lifetime of this instance.\n */\n private primaryGroup(signal?: AbortSignal): Promise<string> {\n if (!this.primaryGroupPromise) {\n this.primaryGroupPromise = this.resolvePrimaryGroup(signal).catch(\n (err) => {\n // Don't cache failures — allow a later call to retry.\n this.primaryGroupPromise = undefined;\n throw err;\n },\n );\n }\n return this.primaryGroupPromise;\n }\n\n private async resolvePrimaryGroup(signal?: AbortSignal): Promise<string> {\n const result = await this.sandbox.runCommand({\n cmd: \"id\",\n args: [\"-gn\", this.username],\n signal,\n });\n if (result.exitCode !== 0) {\n const stderr = await result.stderr();\n throw new Error(\n `Failed to resolve the primary group of \"${this.username}\": ${stderr}`,\n );\n }\n const group = (await result.stdout()).trim();\n if (!group) {\n throw new Error(\n `Failed to resolve the primary group of \"${this.username}\"`,\n );\n }\n return group;\n }\n\n /**\n * Run `chown <ownership> <paths...>` as root, throwing on failure.\n */\n private async chownOrThrow(\n paths: string[],\n ownership: string,\n signal?: AbortSignal,\n ): Promise<void> {\n const chown = await this.sandbox.runCommand({\n cmd: \"chown\",\n args: [ownership, ...paths],\n sudo: true,\n signal,\n });\n if (chown.exitCode !== 0) {\n const stderr = await chown.stderr();\n throw new Error(\n `Failed to set ownership on ${paths.join(\", \")}: ${stderr}`,\n );\n }\n }\n\n /**\n * Run `chmod <mode> <paths...>` as root, throwing on failure.\n */\n private async chmodOrThrow(\n paths: string[],\n mode: string,\n signal?: AbortSignal,\n ): Promise<void> {\n const chmod = await this.sandbox.runCommand({\n cmd: \"chmod\",\n args: [mode, ...paths],\n sudo: true,\n signal,\n });\n if (chmod.exitCode !== 0) {\n const stderr = await chmod.stderr();\n throw new Error(\n `Failed to set permissions on ${paths.join(\", \")}: ${stderr}`,\n );\n }\n }\n\n /**\n * Given absolute leaf paths, return the directories strictly between this\n * user's home directory and each leaf. The home dir itself is excluded, as\n * are any paths that fall outside the home dir.\n */\n private ancestorDirsUnderHome(paths: string[]): string[] {\n const dirs = new Set<string>();\n const homePrefix = `${this.homeDir}/`;\n for (const p of paths) {\n let dir = p.slice(0, p.lastIndexOf(\"/\"));\n while (dir.length > this.homeDir.length && dir.startsWith(homePrefix)) {\n dirs.add(dir);\n dir = dir.slice(0, dir.lastIndexOf(\"/\"));\n }\n }\n return [...dirs];\n }\n\n /**\n * Add this user to a group.\n *\n * @param groupname - Name of the group to join\n * @param opts - Optional parameters.\n */\n async addToGroup(\n groupname: string,\n opts?: { signal?: AbortSignal },\n ): Promise<void> {\n validateName(groupname, \"group name\");\n await this.sandbox.addUserToGroup(this.username, groupname, opts);\n }\n\n /**\n * Remove this user from a group.\n *\n * @param groupname - Name of the group to leave\n * @param opts - Optional parameters.\n */\n async removeFromGroup(\n groupname: string,\n opts?: { signal?: AbortSignal },\n ): Promise<void> {\n validateName(groupname, \"group name\");\n await this.sandbox.removeUserFromGroup(this.username, groupname, opts);\n }\n}\n"],"mappings":"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"}

@@ -9,2 +9,9 @@ import { Command, CommandFinished } from "./command.cjs";

/**
* Thrown when {@link Sandbox.createUser} is called for an existing username.
*/
declare class SandboxUserAlreadyExistsError extends Error {
readonly username: string;
constructor(username: string);
}
/**
* A user context within a sandbox.

@@ -205,3 +212,3 @@ *

//#endregion
export { SandboxUser };
export { SandboxUser, SandboxUserAlreadyExistsError };
//# sourceMappingURL=sandbox-user.d.cts.map

@@ -9,2 +9,9 @@ import { Command, CommandFinished } from "./command.js";

/**
* Thrown when {@link Sandbox.createUser} is called for an existing username.
*/
declare class SandboxUserAlreadyExistsError extends Error {
readonly username: string;
constructor(username: string);
}
/**
* A user context within a sandbox.

@@ -205,3 +212,3 @@ *

//#endregion
export { SandboxUser };
export { SandboxUser, SandboxUserAlreadyExistsError };
//# sourceMappingURL=sandbox-user.d.ts.map

@@ -10,2 +10,12 @@ import { validateName } from "./utils/validate-name.js";

/**
* Thrown when {@link Sandbox.createUser} is called for an existing username.
*/
var SandboxUserAlreadyExistsError = class extends Error {
constructor(username) {
super(`Failed to create user "${username}": user already exists`);
this.name = "SandboxUserAlreadyExistsError";
this.username = username;
}
};
/**
* A user context within a sandbox.

@@ -291,3 +301,3 @@ *

//#endregion
export { SandboxUser };
export { SandboxUser, SandboxUserAlreadyExistsError };
//# sourceMappingURL=sandbox-user.js.map

@@ -1,1 +0,1 @@

{"version":3,"file":"sandbox-user.js","names":["path","wrapped"],"sources":["../src/sandbox-user.ts"],"sourcesContent":["import { Readable } from \"stream\";\nimport { pipeline } from \"stream/promises\";\nimport { createWriteStream } from \"fs\";\nimport { mkdir } from \"fs/promises\";\nimport { dirname, resolve } from \"path\";\nimport type { Sandbox } from \"./sandbox.js\";\nimport type { RunCommandParams } from \"./session.js\";\nimport type { Command, CommandFinished } from \"./command.js\";\nimport type { ExecutionContext } from \"./execution-context.js\";\nimport { validateName } from \"./utils/validate-name.js\";\n\n/**\n * A user context within a sandbox.\n *\n * All file and command operations default to running as this user.\n * Created via {@link Sandbox.createUser} or {@link Sandbox.asUser}.\n *\n * @hideconstructor\n */\nexport class SandboxUser implements ExecutionContext {\n /**\n * The Linux username.\n */\n readonly username: string;\n\n /**\n * The user's home directory (e.g., `/home/alice`).\n */\n readonly homeDir: string;\n\n private readonly sandbox: Sandbox;\n\n /**\n * Memoized lookup of this user's primary group.\n * See {@link SandboxUser.primaryGroup}.\n */\n private primaryGroupPromise?: Promise<string>;\n\n constructor({\n sandbox,\n username,\n }: {\n sandbox: Sandbox;\n username: string;\n }) {\n this.sandbox = sandbox;\n this.username = username;\n // `root`'s home is `/root`, not `/home/root`; every other user's home\n // follows the `/home/<username>` convention.\n this.homeDir = username === \"root\" ? \"/root\" : `/home/${username}`;\n }\n\n /**\n * Build the wrapped command args to run as this user via `sudo -u`.\n *\n * When `env` is provided, injects `env KEY=VAL ...` so that environment\n * variables survive the `sudo -u` transition.\n */\n private buildUserCommand(params: {\n cmd: string;\n args?: string[];\n env?: Record<string, string>;\n cwd?: string;\n }): { cmd: string; args: string[] } {\n const envEntries = Object.entries(params.env ?? {});\n const envArgs =\n envEntries.length > 0\n ? [\"env\", ...envEntries.map(([k, v]) => `${k}=${v}`)]\n : [];\n\n const cwd = params.cwd ?? this.homeDir;\n\n // Run as the target user via `sudo -u`, changing into `cwd` first.\n //\n // We can't set the directory via the sandbox API's `cwd` (the backend cd's\n // there before exec, and SUID binaries like sudo cannot start from a `770`\n // home dir), nor via `sudo --chdir` (the sandbox's sudoers policy forbids\n // the `-D` option). Instead we cd inside a `bash -c` wrapper.\n //\n // `cwd`, the command, its args, and any `env KEY=VAL` are passed as\n // separate positional parameters to `bash -c` (`$1` is `cwd`; `$@` after\n // the shift is the command). Because they are argv elements rather than\n // text spliced into the script, they are never re-parsed by the shell —\n // injection-safe by construction.\n return {\n cmd: \"sudo\",\n args: [\n \"-u\",\n this.username,\n \"--\",\n \"bash\",\n \"-c\",\n 'cd \"$1\" || exit 1; shift; exec \"$@\"',\n \"bash\", // $0 placeholder for `bash -c`\n cwd,\n ...envArgs,\n params.cmd,\n ...(params.args ?? []),\n ],\n };\n }\n\n /**\n * Resolve a path relative to this user's home directory.\n * Absolute paths are returned as-is.\n */\n private resolvePath(path: string): string {\n return path.startsWith(\"/\") ? path : `${this.homeDir}/${path}`;\n }\n\n /**\n * Start executing a command as this user.\n *\n * @param command - The command to execute.\n * @param args - Arguments to pass to the command.\n * @param opts - Optional parameters.\n * @returns A {@link CommandFinished} result once execution is done.\n */\n async runCommand(\n command: string,\n args?: string[],\n opts?: { signal?: AbortSignal; timeoutMs?: number },\n ): Promise<CommandFinished>;\n\n /**\n * Start executing a command as this user in detached mode.\n *\n * @param params - The command parameters.\n * @returns A {@link Command} instance for the running command.\n */\n async runCommand(\n params: RunCommandParams & { detached: true },\n ): Promise<Command>;\n\n /**\n * Start executing a command as this user.\n *\n * @param params - The command parameters.\n * @returns A {@link CommandFinished} result once execution is done.\n */\n async runCommand(params: RunCommandParams): Promise<CommandFinished>;\n\n async runCommand(\n commandOrParams: string | RunCommandParams,\n args?: string[],\n opts?: { signal?: AbortSignal; timeoutMs?: number },\n ): Promise<Command | CommandFinished> {\n if (typeof commandOrParams === \"string\") {\n const wrapped = this.buildUserCommand({\n cmd: commandOrParams,\n args,\n });\n // Don't pass cwd to the sandbox API — the bash -c wrapper cd's for us.\n // Don't pass sudo: true — the default user already has sudo privileges.\n return this.sandbox.runCommand({\n ...wrapped,\n signal: opts?.signal,\n timeoutMs: opts?.timeoutMs,\n });\n }\n\n const params = commandOrParams;\n\n // When sudo: true is passed, delegate directly to root (skip user wrapping).\n // Don't default cwd to homeDir — the backend can't exec SUID binaries\n // from directories with restricted permissions.\n if (params.sudo) {\n return this.sandbox.runCommand({\n ...params,\n } as RunCommandParams & { detached: true });\n }\n\n const wrapped = this.buildUserCommand({\n cmd: params.cmd,\n args: params.args,\n env: params.env,\n cwd: params.cwd,\n });\n\n return this.sandbox.runCommand({\n cmd: wrapped.cmd,\n args: wrapped.args,\n // Don't pass cwd — the bash -c wrapper cd's for us (see buildUserCommand)\n // Don't pass sudo: true — the default user already has sudo privileges\n // env is already baked into the wrapped command via `env KEY=VAL`\n detached: params.detached,\n stdout: params.stdout,\n stderr: params.stderr,\n signal: params.signal,\n timeoutMs: params.timeoutMs,\n } as RunCommandParams & { detached: true });\n }\n\n /**\n * Write files to this user's home directory (or absolute paths).\n * Files are written via the sandbox HTTP API then chowned to this user.\n *\n * The HTTP API can write to user home dirs because they are group-owned\n * by the sandbox's default user group with `770` permissions.\n *\n * @param files - Array of files with path, content, and optional mode\n * @param opts - Optional parameters.\n */\n async writeFiles(\n files: { path: string; content: string | Uint8Array; mode?: number }[],\n opts?: { signal?: AbortSignal },\n ) {\n // Resolve relative paths to user's home directory\n const absoluteFiles = files.map((f) => ({\n ...f,\n path: this.resolvePath(f.path),\n }));\n\n // Write via the HTTP API (works because home dirs are group-owned\n // by the default user's group)\n await this.sandbox.writeFiles(absoluteFiles, opts);\n\n const paths = absoluteFiles.map((f) => f.path);\n if (paths.length === 0) return;\n\n // The files themselves belong to the user outright.\n await this.chownOrThrow(\n paths,\n `${this.username}:${await this.primaryGroup(opts?.signal)}`,\n opts?.signal,\n );\n\n // Any directories the write implicitly created (e.g. `data/` in\n // `data/config.json`) are owned by the default user. Hand them to the user\n // but keep them group-owned by the default user's group with mode 770 —\n // matching the home dir — so the HTTP API can still traverse and write into\n // them later.\n const dirs = this.ancestorDirsUnderHome(paths);\n if (dirs.length > 0) {\n const { group } = await this.sandbox.getDefaultUser(opts);\n await this.chownOrThrow(\n dirs,\n `${this.username}:${group}`,\n opts?.signal,\n );\n await this.chmodOrThrow(dirs, \"770\", opts?.signal);\n }\n }\n\n /**\n * Read a file from this user's context as a stream.\n *\n * @param file - File to read, with path and optional cwd\n * @param opts - Optional parameters.\n * @returns A ReadableStream of the file contents, or null if not found\n */\n async readFile(\n file: { path: string; cwd?: string },\n opts?: { signal?: AbortSignal },\n ): Promise<NodeJS.ReadableStream | null> {\n // `\"use step\"`: touches a Node built-in (`stream`), which the @workflow\n // compiler only permits inside step functions (matches Session.readFile).\n \"use step\";\n const buffer = await this.catAsUser(file, opts);\n return buffer === null ? null : Readable.from([buffer]);\n }\n\n /**\n * Read a file from this user's context as a Buffer.\n *\n * @param file - File to read, with path and optional cwd\n * @param opts - Optional parameters.\n * @returns The file contents as a Buffer, or null if not found\n */\n async readFileToBuffer(\n file: { path: string; cwd?: string },\n opts?: { signal?: AbortSignal },\n ): Promise<Buffer | null> {\n return this.catAsUser(file, opts);\n }\n\n /**\n * Download a file from this user's context to the local filesystem.\n *\n * @param src - Source file in the sandbox\n * @param dst - Destination on the local machine\n * @param opts - Optional parameters.\n * @returns The absolute path to the written file, or null if not found\n */\n async downloadFile(\n src: { path: string; cwd?: string },\n dst: { path: string; cwd?: string },\n opts?: { mkdirRecursive?: boolean; signal?: AbortSignal },\n ): Promise<string | null> {\n // `\"use step\"`: touches Node built-ins (`fs`, `path`), which the @workflow\n // compiler only permits inside step functions (matches Session.downloadFile).\n \"use step\";\n const stream = await this.readFile(src, opts);\n if (stream === null) return null;\n\n const dstPath = resolve(dst.cwd ?? \"\", dst.path);\n if (opts?.mkdirRecursive) {\n await mkdir(dirname(dstPath), { recursive: true });\n }\n await pipeline(stream, createWriteStream(dstPath), {\n signal: opts?.signal,\n });\n return dstPath;\n }\n\n /**\n * Read a file as this user and return its bytes, or null if it does not\n * exist.\n *\n * Reads via `sudo -u <user> base64` rather than the HTTP file API: on stock\n * runtimes the API runs as `vercel-sandbox` and cannot read files this user\n * has kept private (e.g. mode `600`), whereas reading as the user always\n * honours the user's own permissions. The payload is base64-encoded because\n * the command output channel is UTF-8 only and would otherwise corrupt\n * binary files.\n */\n private async catAsUser(\n file: { path: string; cwd?: string },\n opts?: { signal?: AbortSignal },\n ): Promise<Buffer | null> {\n const path = file.path.startsWith(\"/\")\n ? file.path\n : `${file.cwd ?? this.homeDir}/${file.path}`;\n const result = await this.runCommand({\n cmd: \"base64\",\n args: [path],\n signal: opts?.signal,\n });\n if (result.exitCode !== 0) {\n const stderr = await result.stderr();\n if (/No such file or directory/i.test(stderr)) return null;\n throw new Error(`Failed to read ${path}: ${stderr}`);\n }\n return Buffer.from(await result.stdout(), \"base64\");\n }\n\n /**\n * Create a directory owned by this user.\n *\n * @param path - Path of the directory to create\n * @param opts - Optional parameters.\n */\n async mkDir(path: string, opts?: { signal?: AbortSignal }): Promise<void> {\n const absPath = this.resolvePath(path);\n await this.sandbox.mkDir(absPath, opts);\n // Own the created directory plus any parents the mkdir created under the\n // home dir. Group-owned by the default user's group with mode 770 so the\n // HTTP file API can write into it (matching the home dir).\n const dirs = [absPath, ...this.ancestorDirsUnderHome([absPath])];\n const { group } = await this.sandbox.getDefaultUser(opts);\n await this.chownOrThrow(\n dirs,\n `${this.username}:${group}`,\n opts?.signal,\n );\n await this.chmodOrThrow(dirs, \"770\", opts?.signal);\n }\n\n /**\n * This user's primary group. Users created via {@link Sandbox.createUser}\n * get a group named after them, but {@link Sandbox.asUser} accepts\n * pre-existing users whose primary group can differ (e.g. system users), so\n * we resolve it from the sandbox rather than assuming `<username>`.\n *\n * The result is memoized for the lifetime of this instance.\n */\n private primaryGroup(signal?: AbortSignal): Promise<string> {\n if (!this.primaryGroupPromise) {\n this.primaryGroupPromise = this.resolvePrimaryGroup(signal).catch(\n (err) => {\n // Don't cache failures — allow a later call to retry.\n this.primaryGroupPromise = undefined;\n throw err;\n },\n );\n }\n return this.primaryGroupPromise;\n }\n\n private async resolvePrimaryGroup(signal?: AbortSignal): Promise<string> {\n const result = await this.sandbox.runCommand({\n cmd: \"id\",\n args: [\"-gn\", this.username],\n signal,\n });\n if (result.exitCode !== 0) {\n const stderr = await result.stderr();\n throw new Error(\n `Failed to resolve the primary group of \"${this.username}\": ${stderr}`,\n );\n }\n const group = (await result.stdout()).trim();\n if (!group) {\n throw new Error(\n `Failed to resolve the primary group of \"${this.username}\"`,\n );\n }\n return group;\n }\n\n /**\n * Run `chown <ownership> <paths...>` as root, throwing on failure.\n */\n private async chownOrThrow(\n paths: string[],\n ownership: string,\n signal?: AbortSignal,\n ): Promise<void> {\n const chown = await this.sandbox.runCommand({\n cmd: \"chown\",\n args: [ownership, ...paths],\n sudo: true,\n signal,\n });\n if (chown.exitCode !== 0) {\n const stderr = await chown.stderr();\n throw new Error(\n `Failed to set ownership on ${paths.join(\", \")}: ${stderr}`,\n );\n }\n }\n\n /**\n * Run `chmod <mode> <paths...>` as root, throwing on failure.\n */\n private async chmodOrThrow(\n paths: string[],\n mode: string,\n signal?: AbortSignal,\n ): Promise<void> {\n const chmod = await this.sandbox.runCommand({\n cmd: \"chmod\",\n args: [mode, ...paths],\n sudo: true,\n signal,\n });\n if (chmod.exitCode !== 0) {\n const stderr = await chmod.stderr();\n throw new Error(\n `Failed to set permissions on ${paths.join(\", \")}: ${stderr}`,\n );\n }\n }\n\n /**\n * Given absolute leaf paths, return the directories strictly between this\n * user's home directory and each leaf. The home dir itself is excluded, as\n * are any paths that fall outside the home dir.\n */\n private ancestorDirsUnderHome(paths: string[]): string[] {\n const dirs = new Set<string>();\n const homePrefix = `${this.homeDir}/`;\n for (const p of paths) {\n let dir = p.slice(0, p.lastIndexOf(\"/\"));\n while (dir.length > this.homeDir.length && dir.startsWith(homePrefix)) {\n dirs.add(dir);\n dir = dir.slice(0, dir.lastIndexOf(\"/\"));\n }\n }\n return [...dirs];\n }\n\n /**\n * Add this user to a group.\n *\n * @param groupname - Name of the group to join\n * @param opts - Optional parameters.\n */\n async addToGroup(\n groupname: string,\n opts?: { signal?: AbortSignal },\n ): Promise<void> {\n validateName(groupname, \"group name\");\n await this.sandbox.addUserToGroup(this.username, groupname, opts);\n }\n\n /**\n * Remove this user from a group.\n *\n * @param groupname - Name of the group to leave\n * @param opts - Optional parameters.\n */\n async removeFromGroup(\n groupname: string,\n opts?: { signal?: AbortSignal },\n ): Promise<void> {\n validateName(groupname, \"group name\");\n await this.sandbox.removeUserFromGroup(this.username, groupname, opts);\n }\n}\n"],"mappings":"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"}
{"version":3,"file":"sandbox-user.js","names":["path","wrapped"],"sources":["../src/sandbox-user.ts"],"sourcesContent":["import { Readable } from \"stream\";\nimport { pipeline } from \"stream/promises\";\nimport { createWriteStream } from \"fs\";\nimport { mkdir } from \"fs/promises\";\nimport { dirname, resolve } from \"path\";\nimport type { Sandbox } from \"./sandbox.js\";\nimport type { RunCommandParams } from \"./session.js\";\nimport type { Command, CommandFinished } from \"./command.js\";\nimport type { ExecutionContext } from \"./execution-context.js\";\nimport { validateName } from \"./utils/validate-name.js\";\n\n/**\n * Thrown when {@link Sandbox.createUser} is called for an existing username.\n */\nexport class SandboxUserAlreadyExistsError extends Error {\n readonly username: string;\n\n constructor(username: string) {\n super(`Failed to create user \"${username}\": user already exists`);\n this.name = \"SandboxUserAlreadyExistsError\";\n this.username = username;\n }\n}\n\n/**\n * A user context within a sandbox.\n *\n * All file and command operations default to running as this user.\n * Created via {@link Sandbox.createUser} or {@link Sandbox.asUser}.\n *\n * @hideconstructor\n */\nexport class SandboxUser implements ExecutionContext {\n /**\n * The Linux username.\n */\n readonly username: string;\n\n /**\n * The user's home directory (e.g., `/home/alice`).\n */\n readonly homeDir: string;\n\n private readonly sandbox: Sandbox;\n\n /**\n * Memoized lookup of this user's primary group.\n * See {@link SandboxUser.primaryGroup}.\n */\n private primaryGroupPromise?: Promise<string>;\n\n constructor({\n sandbox,\n username,\n }: {\n sandbox: Sandbox;\n username: string;\n }) {\n this.sandbox = sandbox;\n this.username = username;\n // `root`'s home is `/root`, not `/home/root`; every other user's home\n // follows the `/home/<username>` convention.\n this.homeDir = username === \"root\" ? \"/root\" : `/home/${username}`;\n }\n\n /**\n * Build the wrapped command args to run as this user via `sudo -u`.\n *\n * When `env` is provided, injects `env KEY=VAL ...` so that environment\n * variables survive the `sudo -u` transition.\n */\n private buildUserCommand(params: {\n cmd: string;\n args?: string[];\n env?: Record<string, string>;\n cwd?: string;\n }): { cmd: string; args: string[] } {\n const envEntries = Object.entries(params.env ?? {});\n const envArgs =\n envEntries.length > 0\n ? [\"env\", ...envEntries.map(([k, v]) => `${k}=${v}`)]\n : [];\n\n const cwd = params.cwd ?? this.homeDir;\n\n // Run as the target user via `sudo -u`, changing into `cwd` first.\n //\n // We can't set the directory via the sandbox API's `cwd` (the backend cd's\n // there before exec, and SUID binaries like sudo cannot start from a `770`\n // home dir), nor via `sudo --chdir` (the sandbox's sudoers policy forbids\n // the `-D` option). Instead we cd inside a `bash -c` wrapper.\n //\n // `cwd`, the command, its args, and any `env KEY=VAL` are passed as\n // separate positional parameters to `bash -c` (`$1` is `cwd`; `$@` after\n // the shift is the command). Because they are argv elements rather than\n // text spliced into the script, they are never re-parsed by the shell —\n // injection-safe by construction.\n return {\n cmd: \"sudo\",\n args: [\n \"-u\",\n this.username,\n \"--\",\n \"bash\",\n \"-c\",\n 'cd \"$1\" || exit 1; shift; exec \"$@\"',\n \"bash\", // $0 placeholder for `bash -c`\n cwd,\n ...envArgs,\n params.cmd,\n ...(params.args ?? []),\n ],\n };\n }\n\n /**\n * Resolve a path relative to this user's home directory.\n * Absolute paths are returned as-is.\n */\n private resolvePath(path: string): string {\n return path.startsWith(\"/\") ? path : `${this.homeDir}/${path}`;\n }\n\n /**\n * Start executing a command as this user.\n *\n * @param command - The command to execute.\n * @param args - Arguments to pass to the command.\n * @param opts - Optional parameters.\n * @returns A {@link CommandFinished} result once execution is done.\n */\n async runCommand(\n command: string,\n args?: string[],\n opts?: { signal?: AbortSignal; timeoutMs?: number },\n ): Promise<CommandFinished>;\n\n /**\n * Start executing a command as this user in detached mode.\n *\n * @param params - The command parameters.\n * @returns A {@link Command} instance for the running command.\n */\n async runCommand(\n params: RunCommandParams & { detached: true },\n ): Promise<Command>;\n\n /**\n * Start executing a command as this user.\n *\n * @param params - The command parameters.\n * @returns A {@link CommandFinished} result once execution is done.\n */\n async runCommand(params: RunCommandParams): Promise<CommandFinished>;\n\n async runCommand(\n commandOrParams: string | RunCommandParams,\n args?: string[],\n opts?: { signal?: AbortSignal; timeoutMs?: number },\n ): Promise<Command | CommandFinished> {\n if (typeof commandOrParams === \"string\") {\n const wrapped = this.buildUserCommand({\n cmd: commandOrParams,\n args,\n });\n // Don't pass cwd to the sandbox API — the bash -c wrapper cd's for us.\n // Don't pass sudo: true — the default user already has sudo privileges.\n return this.sandbox.runCommand({\n ...wrapped,\n signal: opts?.signal,\n timeoutMs: opts?.timeoutMs,\n });\n }\n\n const params = commandOrParams;\n\n // When sudo: true is passed, delegate directly to root (skip user wrapping).\n // Don't default cwd to homeDir — the backend can't exec SUID binaries\n // from directories with restricted permissions.\n if (params.sudo) {\n return this.sandbox.runCommand({\n ...params,\n } as RunCommandParams & { detached: true });\n }\n\n const wrapped = this.buildUserCommand({\n cmd: params.cmd,\n args: params.args,\n env: params.env,\n cwd: params.cwd,\n });\n\n return this.sandbox.runCommand({\n cmd: wrapped.cmd,\n args: wrapped.args,\n // Don't pass cwd — the bash -c wrapper cd's for us (see buildUserCommand)\n // Don't pass sudo: true — the default user already has sudo privileges\n // env is already baked into the wrapped command via `env KEY=VAL`\n detached: params.detached,\n stdout: params.stdout,\n stderr: params.stderr,\n signal: params.signal,\n timeoutMs: params.timeoutMs,\n } as RunCommandParams & { detached: true });\n }\n\n /**\n * Write files to this user's home directory (or absolute paths).\n * Files are written via the sandbox HTTP API then chowned to this user.\n *\n * The HTTP API can write to user home dirs because they are group-owned\n * by the sandbox's default user group with `770` permissions.\n *\n * @param files - Array of files with path, content, and optional mode\n * @param opts - Optional parameters.\n */\n async writeFiles(\n files: { path: string; content: string | Uint8Array; mode?: number }[],\n opts?: { signal?: AbortSignal },\n ) {\n // Resolve relative paths to user's home directory\n const absoluteFiles = files.map((f) => ({\n ...f,\n path: this.resolvePath(f.path),\n }));\n\n // Write via the HTTP API (works because home dirs are group-owned\n // by the default user's group)\n await this.sandbox.writeFiles(absoluteFiles, opts);\n\n const paths = absoluteFiles.map((f) => f.path);\n if (paths.length === 0) return;\n\n // The files themselves belong to the user outright.\n await this.chownOrThrow(\n paths,\n `${this.username}:${await this.primaryGroup(opts?.signal)}`,\n opts?.signal,\n );\n\n // Any directories the write implicitly created (e.g. `data/` in\n // `data/config.json`) are owned by the default user. Hand them to the user\n // but keep them group-owned by the default user's group with mode 770 —\n // matching the home dir — so the HTTP API can still traverse and write into\n // them later.\n const dirs = this.ancestorDirsUnderHome(paths);\n if (dirs.length > 0) {\n const { group } = await this.sandbox.getDefaultUser(opts);\n await this.chownOrThrow(\n dirs,\n `${this.username}:${group}`,\n opts?.signal,\n );\n await this.chmodOrThrow(dirs, \"770\", opts?.signal);\n }\n }\n\n /**\n * Read a file from this user's context as a stream.\n *\n * @param file - File to read, with path and optional cwd\n * @param opts - Optional parameters.\n * @returns A ReadableStream of the file contents, or null if not found\n */\n async readFile(\n file: { path: string; cwd?: string },\n opts?: { signal?: AbortSignal },\n ): Promise<NodeJS.ReadableStream | null> {\n // `\"use step\"`: touches a Node built-in (`stream`), which the @workflow\n // compiler only permits inside step functions (matches Session.readFile).\n \"use step\";\n const buffer = await this.catAsUser(file, opts);\n return buffer === null ? null : Readable.from([buffer]);\n }\n\n /**\n * Read a file from this user's context as a Buffer.\n *\n * @param file - File to read, with path and optional cwd\n * @param opts - Optional parameters.\n * @returns The file contents as a Buffer, or null if not found\n */\n async readFileToBuffer(\n file: { path: string; cwd?: string },\n opts?: { signal?: AbortSignal },\n ): Promise<Buffer | null> {\n return this.catAsUser(file, opts);\n }\n\n /**\n * Download a file from this user's context to the local filesystem.\n *\n * @param src - Source file in the sandbox\n * @param dst - Destination on the local machine\n * @param opts - Optional parameters.\n * @returns The absolute path to the written file, or null if not found\n */\n async downloadFile(\n src: { path: string; cwd?: string },\n dst: { path: string; cwd?: string },\n opts?: { mkdirRecursive?: boolean; signal?: AbortSignal },\n ): Promise<string | null> {\n // `\"use step\"`: touches Node built-ins (`fs`, `path`), which the @workflow\n // compiler only permits inside step functions (matches Session.downloadFile).\n \"use step\";\n const stream = await this.readFile(src, opts);\n if (stream === null) return null;\n\n const dstPath = resolve(dst.cwd ?? \"\", dst.path);\n if (opts?.mkdirRecursive) {\n await mkdir(dirname(dstPath), { recursive: true });\n }\n await pipeline(stream, createWriteStream(dstPath), {\n signal: opts?.signal,\n });\n return dstPath;\n }\n\n /**\n * Read a file as this user and return its bytes, or null if it does not\n * exist.\n *\n * Reads via `sudo -u <user> base64` rather than the HTTP file API: on stock\n * runtimes the API runs as `vercel-sandbox` and cannot read files this user\n * has kept private (e.g. mode `600`), whereas reading as the user always\n * honours the user's own permissions. The payload is base64-encoded because\n * the command output channel is UTF-8 only and would otherwise corrupt\n * binary files.\n */\n private async catAsUser(\n file: { path: string; cwd?: string },\n opts?: { signal?: AbortSignal },\n ): Promise<Buffer | null> {\n const path = file.path.startsWith(\"/\")\n ? file.path\n : `${file.cwd ?? this.homeDir}/${file.path}`;\n const result = await this.runCommand({\n cmd: \"base64\",\n args: [path],\n signal: opts?.signal,\n });\n if (result.exitCode !== 0) {\n const stderr = await result.stderr();\n if (/No such file or directory/i.test(stderr)) return null;\n throw new Error(`Failed to read ${path}: ${stderr}`);\n }\n return Buffer.from(await result.stdout(), \"base64\");\n }\n\n /**\n * Create a directory owned by this user.\n *\n * @param path - Path of the directory to create\n * @param opts - Optional parameters.\n */\n async mkDir(path: string, opts?: { signal?: AbortSignal }): Promise<void> {\n const absPath = this.resolvePath(path);\n await this.sandbox.mkDir(absPath, opts);\n // Own the created directory plus any parents the mkdir created under the\n // home dir. Group-owned by the default user's group with mode 770 so the\n // HTTP file API can write into it (matching the home dir).\n const dirs = [absPath, ...this.ancestorDirsUnderHome([absPath])];\n const { group } = await this.sandbox.getDefaultUser(opts);\n await this.chownOrThrow(\n dirs,\n `${this.username}:${group}`,\n opts?.signal,\n );\n await this.chmodOrThrow(dirs, \"770\", opts?.signal);\n }\n\n /**\n * This user's primary group. Users created via {@link Sandbox.createUser}\n * get a group named after them, but {@link Sandbox.asUser} accepts\n * pre-existing users whose primary group can differ (e.g. system users), so\n * we resolve it from the sandbox rather than assuming `<username>`.\n *\n * The result is memoized for the lifetime of this instance.\n */\n private primaryGroup(signal?: AbortSignal): Promise<string> {\n if (!this.primaryGroupPromise) {\n this.primaryGroupPromise = this.resolvePrimaryGroup(signal).catch(\n (err) => {\n // Don't cache failures — allow a later call to retry.\n this.primaryGroupPromise = undefined;\n throw err;\n },\n );\n }\n return this.primaryGroupPromise;\n }\n\n private async resolvePrimaryGroup(signal?: AbortSignal): Promise<string> {\n const result = await this.sandbox.runCommand({\n cmd: \"id\",\n args: [\"-gn\", this.username],\n signal,\n });\n if (result.exitCode !== 0) {\n const stderr = await result.stderr();\n throw new Error(\n `Failed to resolve the primary group of \"${this.username}\": ${stderr}`,\n );\n }\n const group = (await result.stdout()).trim();\n if (!group) {\n throw new Error(\n `Failed to resolve the primary group of \"${this.username}\"`,\n );\n }\n return group;\n }\n\n /**\n * Run `chown <ownership> <paths...>` as root, throwing on failure.\n */\n private async chownOrThrow(\n paths: string[],\n ownership: string,\n signal?: AbortSignal,\n ): Promise<void> {\n const chown = await this.sandbox.runCommand({\n cmd: \"chown\",\n args: [ownership, ...paths],\n sudo: true,\n signal,\n });\n if (chown.exitCode !== 0) {\n const stderr = await chown.stderr();\n throw new Error(\n `Failed to set ownership on ${paths.join(\", \")}: ${stderr}`,\n );\n }\n }\n\n /**\n * Run `chmod <mode> <paths...>` as root, throwing on failure.\n */\n private async chmodOrThrow(\n paths: string[],\n mode: string,\n signal?: AbortSignal,\n ): Promise<void> {\n const chmod = await this.sandbox.runCommand({\n cmd: \"chmod\",\n args: [mode, ...paths],\n sudo: true,\n signal,\n });\n if (chmod.exitCode !== 0) {\n const stderr = await chmod.stderr();\n throw new Error(\n `Failed to set permissions on ${paths.join(\", \")}: ${stderr}`,\n );\n }\n }\n\n /**\n * Given absolute leaf paths, return the directories strictly between this\n * user's home directory and each leaf. The home dir itself is excluded, as\n * are any paths that fall outside the home dir.\n */\n private ancestorDirsUnderHome(paths: string[]): string[] {\n const dirs = new Set<string>();\n const homePrefix = `${this.homeDir}/`;\n for (const p of paths) {\n let dir = p.slice(0, p.lastIndexOf(\"/\"));\n while (dir.length > this.homeDir.length && dir.startsWith(homePrefix)) {\n dirs.add(dir);\n dir = dir.slice(0, dir.lastIndexOf(\"/\"));\n }\n }\n return [...dirs];\n }\n\n /**\n * Add this user to a group.\n *\n * @param groupname - Name of the group to join\n * @param opts - Optional parameters.\n */\n async addToGroup(\n groupname: string,\n opts?: { signal?: AbortSignal },\n ): Promise<void> {\n validateName(groupname, \"group name\");\n await this.sandbox.addUserToGroup(this.username, groupname, opts);\n }\n\n /**\n * Remove this user from a group.\n *\n * @param groupname - Name of the group to leave\n * @param opts - Optional parameters.\n */\n async removeFromGroup(\n groupname: string,\n opts?: { signal?: AbortSignal },\n ): Promise<void> {\n validateName(groupname, \"group name\");\n await this.sandbox.removeUserFromGroup(this.username, groupname, opts);\n }\n}\n"],"mappings":"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"}

@@ -855,2 +855,3 @@ const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');

if (useradd.exitCode !== 0) {
if (useradd.exitCode === 9) throw new require_sandbox_user.SandboxUserAlreadyExistsError(username);
const stderr = await useradd.stderr();

@@ -857,0 +858,0 @@ throw new Error(`Failed to create user "${username}": ${stderr}`);

@@ -11,3 +11,3 @@ import { APIError } from "./api-client/api-error.js";

import { validateName } from "./utils/validate-name.js";
import { SandboxUser } from "./sandbox-user.js";
import { SandboxUser, SandboxUserAlreadyExistsError } from "./sandbox-user.js";
import { WORKFLOW_DESERIALIZE, WORKFLOW_SERIALIZE } from "@workflow/serde";

@@ -855,2 +855,3 @@ import { setTimeout } from "node:timers/promises";

if (useradd.exitCode !== 0) {
if (useradd.exitCode === 9) throw new SandboxUserAlreadyExistsError(username);
const stderr = await useradd.stderr();

@@ -857,0 +858,0 @@ throw new Error(`Failed to create user "${username}": ${stderr}`);

//#region src/version.ts
const VERSION = "2.6.0";
const VERSION = "2.6.1";

@@ -5,0 +5,0 @@ //#endregion

@@ -1,1 +0,1 @@

{"version":3,"file":"version.cjs","names":[],"sources":["../src/version.ts"],"sourcesContent":["// Autogenerated by inject-version.ts\nexport const VERSION = \"2.6.0\";\n"],"mappings":";;AACA,MAAa,UAAU"}
{"version":3,"file":"version.cjs","names":[],"sources":["../src/version.ts"],"sourcesContent":["// Autogenerated by inject-version.ts\nexport const VERSION = \"2.6.1\";\n"],"mappings":";;AACA,MAAa,UAAU"}
//#region src/version.ts
const VERSION = "2.6.0";
const VERSION = "2.6.1";

@@ -4,0 +4,0 @@ //#endregion

@@ -1,1 +0,1 @@

{"version":3,"file":"version.js","names":[],"sources":["../src/version.ts"],"sourcesContent":["// Autogenerated by inject-version.ts\nexport const VERSION = \"2.6.0\";\n"],"mappings":";AACA,MAAa,UAAU"}
{"version":3,"file":"version.js","names":[],"sources":["../src/version.ts"],"sourcesContent":["// Autogenerated by inject-version.ts\nexport const VERSION = \"2.6.1\";\n"],"mappings":";AACA,MAAa,UAAU"}
{
"name": "@vercel/sandbox",
"version": "2.6.0",
"version": "2.6.1",
"description": "Software Development Kit for Vercel Sandbox",

@@ -5,0 +5,0 @@ "type": "module",

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display