steam-workshop-ts
v1.2.0
Published
Zero-dependency TypeScript client for the Steam Workshop Web API and SteamCMD, for Node.js and Bun. Query and search workshop items, resolve collections, and download/update items with caching, disk-space checks, high-resolution profiling, and a CLI.
Maintainers
Readme
About The Project
Managing Steam Workshop items (maps, mods, plugins) programmatically for game servers like Counter-Strike 2 (CS2), CS:GO, Garry's Mod (GMod), or Dota 2 is usually clunky.
This library is a modern, lightweight, pure TypeScript package to:
- Query Workshop item details using Steam's Web API.
- Search and filter the Workshop and resolve collections.
- Download and update workshop items via
steamcmd, with automatic path extraction, caching, and a CLI. - Profile execution and measure cache/network efficiency with zero-dependency async context telemetry.
It runs on Node.js and Bun with absolutely zero runtime dependencies.
Why this package is special
- Zero runtime dependencies - Only development dependencies are used.
- Smart path parser - Automatically starts a SteamCMD subprocess, downloads files, parses stdout, and returns the absolute directory path of the downloaded workshop item.
- Caching & incremental updates - Manifest-based cache with pre-flight disk-space checks, plus an optional persistent Steam depot cache (
steamCacheDir) for fast, low-bandwidth re-downloads. - Hot-path WeakMap optimization - Internal cache ensures repetitive query options deserialize in picoseconds with 0.00 B / iteration memory overhead.
- Built-in high-res profiling - Measure sub-operations with
startTimer(),measure(), and pass state effortlessly across async chains withrunWithContext(). - Batteries included - A
steam-workshop-tsCLI, automatic retries with backoff, Steam Guard 2FA support, collection resolution, and Docker/temp-dir sandbox modes (macOS-friendly). - Supports Bun and Node.js - Dual-format distribution (CommonJS
.jsand ESM.mjs) with dedicated.d.tsand.d.mtsdeclarations.
Installation
npm install steam-workshop-ts
pnpm add steam-workshop-ts
bun add steam-workshop-tsQuick example
1. Fetching Workshop Item Details
No API Key is required to fetch details for public items:
import { SteamWorkshopClient } from "steam-workshop-ts";
const client = new SteamWorkshopClient();
const details = await client.getItemDetails("3167383610");
if (details.length > 0) {
const map = details[0];
console.log(`Title: ${map.title}`);
console.log(`Size: ${map.file_size} bytes`);
console.log(`URL: ${map.file_url}`);
}2. Searching/Querying Workshop Items
Requires a Steam Web API Key:
import { SteamWorkshopClient } from "steam-workshop-ts";
const client = new SteamWorkshopClient("YOUR_STEAM_API_KEY");
const results = await client.queryItems({
appId: 730, // CS2
searchText: "surf",
numPerPage: 10,
});
console.log(`Found ${results.total} surf maps.`);
for (const item of results.items) {
console.log(`- ${item.title} (${item.publishedfileid})`);
}3. Programmatic Download via SteamCMD
import { SteamCmdWrapper } from "steam-workshop-ts";
const steamcmd = new SteamCmdWrapper({
binPath: "/usr/games/steamcmd", // Defaults to resolving "steamcmd" from PATH
});
// Downloads item 3167383610 (CS2 map VPK) and returns absolute folder path
const downloadPath = await steamcmd.downloadItem(730, 3167383610);
console.log(`Map downloaded and saved at: ${downloadPath}`);4. Resolving Workshop Collections
A workshop collection is a list of child items. You can resolve the children IDs and fetch details for all of them in one helper call:
import { SteamWorkshopClient } from "steam-workshop-ts";
const client = new SteamWorkshopClient();
// Get details for all maps inside a collection
const items = await client.getCollectionItems("YOUR_COLLECTION_ID");
for (const item of items) {
console.log(`Child Map: ${item.title} (${item.publishedfileid})`);
}5. SteamCMD Auto-Installation
If SteamCMD is not installed on the system, you can download and install it programmatically to a local directory:
import { SteamCmdWrapper } from "steam-workshop-ts";
const steamcmd = new SteamCmdWrapper();
// Automatically detects process.platform, downloads zip/tar.gz from Steam CDN,
// extracts it to the local directory, and makes the binary executable.
const binPath = await steamcmd.autoInstall("./bin/steamcmd");
console.log(`SteamCMD binary ready at: ${binPath}`);
// Now downloadItem will use the auto-installed binary automatically!
const path = await steamcmd.downloadItem(730, 3167383610);6. Sandbox Download Modes (Docker & Temp Dir)
Prevent host system pollution or compatibility issues (especially on macOS which cannot run the 32-bit SteamCMD locally):
import { SteamCmdWrapper, SteamWorkshopClient } from "steam-workshop-ts";
// 1. Docker Sandbox Mode (Uses official steamcmd/steamcmd Docker image)
const steamcmdDocker = new SteamCmdWrapper({
useDocker: true
});
// 2. Temp Directory Sandbox Mode (Installs SteamCMD to OS temp folder and auto-deletes it)
const steamcmdTemp = new SteamCmdWrapper({
useTempDir: true
});
// The wrapper automatically handles downloading and self-cleaning!
const client = new SteamWorkshopClient();
const path = await client.downloadItemCached(730, 3167383610, steamcmdDocker, "./addons");7. Pre-flight Disk Space Validation
Before starting any download via downloadItemCached, the client queries the Web API for file_size and compares it to the remaining disk space using native system commands (df or PowerShell):
import { SteamWorkshopClient, SteamCmdWrapper } from "steam-workshop-ts";
const client = new SteamWorkshopClient();
const steamcmd = new SteamCmdWrapper();
try {
// Throws an error before executing SteamCMD if disk is full
await client.downloadItemCached(730, 3167383610, steamcmd, "./addons");
} catch (err) {
console.error((err as Error).message); // "Insufficient disk space on the host machine..."
}8. Steam Guard 2FA Support & Timeout Options
Set process execution timeouts and supply Steam Guard 2FA authentication codes dynamically on demand to prevent hanging scripts:
import { SteamCmdWrapper } from "steam-workshop-ts";
const steamcmd = new SteamCmdWrapper({
username: "my_steam_account",
password: "my_password"
});
const path = await steamcmd.downloadItem(730, 3167383610, {
timeout: 60000, // Kill process if it takes longer than 60 seconds
onSteamGuardRequired: async (attempt) => {
// Fetch code from SMS, email, or stdin
return "12AB3";
}
});9. Download Progress, Retries & Caching
import { SteamWorkshopClient, SteamCmdWrapper } from "steam-workshop-ts";
// Retries transient 429/5xx with backoff, and memo-caches item details for 60s
const client = new SteamWorkshopClient(undefined, { maxRetries: 5, cacheTtlMs: 60_000 });
const steamcmd = new SteamCmdWrapper();
await steamcmd.downloadItem(730, 3167383610, {
onProgress: (p) => {
process.stdout.write(`\r${p.percent.toFixed(1)}% (${p.downloadedBytes}/${p.totalBytes})`);
},
});10. Persistent Cache & Pruning
// Reuse Steam's depot cache across runs (Docker mode) for incremental, low-bandwidth updates
const fast = new SteamCmdWrapper({ useDocker: true, steamCacheDir: "./.steam-cache" });
await client.downloadItemsCached(730, [3070244462], fast, "./addons");
// Remove every cached item except the ones still in use; returns the pruned IDs
const removed = client.pruneCache("./addons", [3070244462, 3167383610]);
console.log(`Freed ${removed.length} stale item(s).`);11. Profiling & Async Context Telemetry
Measure durations and trace API metrics across async execution chains without passing state manually:
import {
runWithContext,
createScanMetrics,
startTimer,
measure,
SteamWorkshopClient
} from "steam-workshop-ts";
// 1. High-resolution timer
const timer = startTimer();
// ... do work ...
console.log(`Elapsed: ${timer.elapsedMs().toFixed(2)} ms`);
const totalTime = timer.stop();
// 2. Measure synchronous blocks
const { result, durationMs } = measure(() => computeSomething());
// 3. Track async workflow metrics automatically
const context = {
id: "sync-job-42",
metrics: createScanMetrics(),
};
const client = new SteamWorkshopClient(undefined, { cacheTtlMs: 30_000 });
await runWithContext(context, async () => {
await client.getItemDetails(["3070244462", "3167383610"]);
// Metrics like apiCalls, cacheHits, cacheMisses update automatically
});
console.log(`API calls made: ${context.metrics.apiCalls}`);
console.log(`Cache hits: ${context.metrics.cacheHits}`);Performance & Benchmarks
Benchmarks measured on Apple M2 Pro with Bun 1.4.1 using mitata. You can reproduce these measurements anytime with bun run bench.
| Benchmark Operation | Average Latency | p99 Latency | Memory Allocation |
|---|---|---|---|
| WeakMap cached query string | 89.78 ps/iter | 122.07 ps | 0.00 B / iter |
| Raw URLSearchParams generation | 743.07 ns/iter | 2.58 µs | 486.98 B / iter |
| recordScanMetric (outside context no-op) | 222.54 ps/iter | 4.36 ns | 0.00 B / iter |
| measure(noop) | 67.26 ns/iter | 118.15 ns | ~2.61 B / iter |
| startTimer + elapsedMs + stop | 77.52 ns/iter | 167.71 ns | ~5.14 B / iter |
| runWithContext + recordScanMetric | 196.18 ns/iter | 666.00 ns | ~26.01 B / iter |
| getItemDetails (in-memory cached hit) | 241.75 ns/iter | 833.00 ns | ~3.14 B / iter |
| SteamCMD Progress regex match & parse | 73.94 ns/iter | 291.00 ns | ~35.78 B / iter |
Highlight: The internal
WeakMapquery cache eliminates allocations on hot loops, running ~8,276x faster than raw parameter serialization with flat 0.00 B / iter allocation.
CLI
Installed as the steam-workshop-ts command (or run without installing via npx steam-workshop-ts):
steam-workshop-ts info 3070244462 # metadata for one or more item IDs
steam-workshop-ts query 730 surf --per-page 10 # search (needs STEAM_API_KEY)
steam-workshop-ts collection 2753947063 # list a collection's items
steam-workshop-ts download 730 ./addons 3070244462 --docker # download into ./addons
steam-workshop-ts download 730 ./addons 3070244462 --docker --cache-dir ./.steam-cache # incrementalquery reads the Steam Web API key from the STEAM_API_KEY environment variable. download accepts --docker, --temp, --cache-dir <dir>, --username and --password, and shows a spinner with elapsed time plus the final average speed.
API
SteamWorkshopClient
| Member | Description |
|---|---|
| new SteamWorkshopClient(apiKey?, options?) | Create a client; the API key is only needed for queryItems. Options: maxRetries, retryDelayMs, cacheTtlMs |
| getItemDetails(ids) | Metadata for one or many item IDs (no key required); auto-chunks >100 IDs and caches when cacheTtlMs is set |
| queryItems(options) | Search/filter/page the Workshop (QueryFiles, key required) |
| getCollectionDetails(ids) | Raw child IDs of one or many collections |
| getCollectionItems(collectionId) | Resolve a collection to its children's full details |
| downloadItemCached(appId, itemId, steamcmd, targetDir) | Download one item into targetDir/itemId only if missing/outdated; validates disk space and cleans the SteamCMD sandbox |
| downloadItemsCached(appId, itemIds, steamcmd, targetDir) | Batched cached download; one Web API call + one SteamCMD session |
| pruneCache(targetDir, keepIds) | Delete cached items not in keepIds (dir + manifest entry); returns the pruned IDs |
SteamCmdWrapper
| Member | Description |
|---|---|
| new SteamCmdWrapper(options?) | Configure bin path, credentials, sandbox mode (useDocker / useTempDir), and steamCacheDir (persistent Docker cache for incremental updates) |
| downloadItem(appId, itemId, options?) | Download one item, returns its absolute path. Options: timeout, onProgress, onSteamGuardRequired |
| downloadItems(appId, itemIds, options?) | Batch download in a single session, returns an ID → path map |
| downloadItemsManaged(appId, itemIds, options?) | Like downloadItems, but returns { paths, cleanup } so you can remove the sandbox temp dir after copying |
| autoInstall(targetDir) | Download + extract SteamCMD for the current platform |
Profiling & Context Utilities
| Member | Description |
|---|---|
| startTimer() | Returns a high-resolution Timer instance with elapsedMs() and stop() |
| measure(fn) | Measures duration of a synchronous function execution |
| measureAsync(fn) | Measures duration of an asynchronous Promise execution |
| runWithContext(context, fn) | Runs a function inside an AsyncLocalStorage scope carrying ScanContext |
| getScanContext() | Retrieves the active ScanContext or undefined |
| createScanMetrics(initial?) | Instantiates a ScanMetrics tracker |
| recordScanMetric(updater) | Updates the active scan metrics if currently inside a context |
| getFreeDiskSpace(path) | Free bytes on the disk holding path, or Infinity if the probe fails |
All types are centralized in types.ts and re-exported from the package root.
Examples
Runnable directly with Bun:
bun run examples/basic.ts # fetch item details + a Docker sandbox download
STEAM_API_KEY=xxxx bun run examples/query.ts # search the Workshop (needs a Web API key)
bun run examples/collection.ts [collectionId] # resolve a collection to its items
bun run examples/cached-download.ts # cached download + progress bar + cache prune
bun run examples/sandbox.ts # Docker / temp-dir / explicit-install download modes
bun run bench # execute the mitata benchmark suiteTesting & Quality
bun test # unit & integration tests (33 tests across mocked SteamCMD & Web API)
bun run lint # sub-35ms linting with oxlint
bun run type-check # strict TypeScript check with noEmit
bun run bench # run performance benchmark suite
bun run build # native Bun multi-target build (ESM, CJS, d.ts, d.mts)Security & capabilities
Supply-chain scanners (e.g. Socket) flag this package for network and shell access. Both are intrinsic to what it does, and are limited to the following:
- Network: HTTPS requests to the official Steam Web API (
api.steampowered.com) for item/collection data, and to the Steam CDN (steamcdn-a.akamaihd.net) whenautoInstalldownloads SteamCMD. All URLs are hardcoded; nothing is fetched from user input. - Shell / child processes: it spawns the
steamcmd(ordocker) process to perform downloads, and runstar/powershellto extract SteamCMD anddf/powershellto check free disk space.
Hardening in place:
- Every subprocess uses
execFile/spawnwith argument arrays, never a shell string, so paths and options can't be interpreted as shell commands (no command injection). - No dynamic code evaluation, no obfuscation, no telemetry, and zero runtime dependencies.
If you don't need SteamCMD downloads, importing only SteamWorkshopClient keeps you on the Web API surface (network only, no child processes).
Contributing
Please check .github/CONTRIBUTING.md for details.
License
Distributed under the MIT License. See LICENSE for more information.
