npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

steamwand.js

v0.5.0

Published

TypeScript bindings for the Steamworks SDK, generated from steam_api.json over the flat C API via koffi FFI. No native compilation.

Readme

npm CI node license Discord

TypeScript bindings for the Steamworks SDK with no native build step. The binding layer is generated from steam_api.json, Valve's own machine-readable description of the flat C API, and called through koffi FFI. When a new SDK ships, you regenerate; when you need a function nobody wrapped yet, it is already there.

Why another Steamworks binding

Every existing Node binding makes you compile someone else's native code to add a function. greenworks is NAN-era C++ built on the pre-2014 RemoteStorage workshop API. steamworks.js is solid but every contribution means Rust, napi-rs, and a three-platform build matrix; adding one setter (SetItemUpdateLanguage) took two forked repos and a patch file. steamwand has no native code of its own. One generic FFI dependency, one generated TS layer, and the whole flat API surface: 25 interfaces, 807 functions, 191 callback structs with per-platform offset tables.

Install

pnpm add steamwand.js

Node 18+. Windows x64, Linux x64, Linux ARM64, and macOS (x64 and Apple silicon, one universal library) are wired up; the Steam client must be running. The steam_api redistributables ship in the package. koffi's own native binary comes as a separate @koromix/koffi-<os>-<arch> package next to koffi in node_modules, so a bundler or Electron packager rule that only keeps steamwand.js/** must also keep koffi/** and @koromix/**.

Documentation

Full docs are in the wiki: getting started, the core API, the curated workshop, stats, cloud, leaderboards, and lobbies, social, overlay, auth, system, capture, controllers, and DLC layers, the raw flat API, recipes, and troubleshooting. Every generated function also carries its original C signature, its out-buffer sizes, and a link to Valve's documentation, so hovering it in an editor is usually enough.

Use

import { init } from 'steamwand.js';

const steam = init({ appId: 480 });

// Curated workshop layer
const { fileId } = await steam.workshop.createItem();
await steam.workshop.submitUpdate(fileId, {
  title: 'My mod',
  description: 'Full description',
  contentPath: 'C:/mods/my-mod',
  tags: ['gameplay'],
  changeNote: 'first upload',
}, { onProgress: (p) => console.log(p.status, p.bytesProcessed) });

// Per-language text, the thing that started this project
await steam.workshop.submitUpdate(fileId, {
  language: 'german',
  title: 'Mein Mod',
  description: 'Deutsche Beschreibung',
});

const item = await steam.workshop.getItem(fileId, { language: 'german' });

// Workshop item wiring: required DLC, collection children, extra previews,
// metadata and key/value tags, plus the running app's DLC list
await steam.workshop.addAppDependency(fileId, 1_234_567);
await steam.workshop.addDependency(9_876_543_210n, fileId); // collection, child
await steam.workshop.submitUpdate(fileId, {
  metadata: JSON.stringify({ buildId: 42 }),
  keyValueTags: { engineVersion: '1.14' },
  previewImages: ['C:/mods/my-mod/screenshot.png'],
  previewVideos: ['dQw4w9WgXcQ'],
});
const full = await steam.workshop.getItem(fileId, { children: true, additionalPreviews: true });
const dlc = steam.dlc.listDlc();

// Achievements, stats, cloud saves, leaderboards, lobbies: same treatment
steam.stats.unlock('ACH_WIN_ONE_GAME');
await steam.cloud.writeFile('save01.json', JSON.stringify({ level: 3 }));
const board = await steam.leaderboards.findOrCreate('Fastest Lap', 1, 3);
await steam.leaderboards.uploadScore(board.handle, 91_240);
const lobbyId = await steam.lobbies.create(2, 4);
steam.lobbies.setData(lobbyId, 'map', 'de_dust2');

// Friends, rich presence, overlay, auth tickets, system facts, Steam Input
const friends = steam.social.listFriends();
steam.social.setRichPresence('status', 'In the menu');
steam.overlay.activateInviteDialog(lobbyId);
const { hex } = await steam.auth.getWebApiTicket('my-backend');
if (steam.system.isSteamDeck()) steam.controllers.init();

// The raw generated layer, when the curated ones stop
const ticket = steam.apps.GetAppOwner();
steam.on('ItemInstalled_t', (data) => console.log('installed', data));
const found = await steam.async.userStats.FindLeaderboard('Fastest Lap');

steam.close();

Twelve curated layers (workshop, stats, cloud, leaderboards, lobbies, social, overlay, auth, system, capture, controllers, dlc) cover the flows most games need, with typed errors that carry the EResult. For everything else, steam.async wraps each of the 76 call-result functions as a promise, steam.on and steam.once give typed callbacks by struct name, and the out helpers make the flat API's out-buffers safe. 64-bit values (Steam ids, file ids, handles) are bigint everywhere. The dispatch pump checks every async result against the callback id the caller expected, so a mixed-up completion rejects instead of decoding garbage.

What is generated, what is not

scripts/generate.ts reads steam_api.json and emits src/generated/: enums, consts, one class per interface with its versioned accessor (SteamAPI_SteamUGC_v021, so an SDK bump is a regeneration, not an archaeology project), and struct layouts as explicit per-platform offset tables. Windows packs callback structs at 8 bytes, Linux and macOS at 4, CSteamID is pack(1); the generator encodes those rules and its output has been verified against steamworks-sys's bindgen layouts, 428 comparisons with zero differences. test/offsets.test.ts pins the workshop set so a future SDK bump cannot silently shift an offset.

Handwritten and small: the library loader, the dispatch pump, the struct decoder, and the twelve curated layers under src/api/. After close(), every call through the session throws instead of reaching the unloaded API.

The SDK itself is not in this repo and must not be committed; Valve's license does not allow redistributing the headers or steam_api.json. To regenerate, download the SDK from partner.steamgames.com, unpack it to sdk/ (gitignored apart from the STEAMWAND.md file), and run pnpm generate. sdk/STEAMWAND.md has the exact paths, the hash checks, and what to update on a version bump. sdk.lock.json records which SDK version and steam_api.json hash the committed output came from. Shipping the redistributable binaries (steam_api64.dll and friends) is normal practice and allowed.

Known limits

  • 12 of the 819 flat functions are skipped: 9 that take C function pointers (debug hooks, netsockets status callbacks) and 3 whose by-value struct the generator cannot prove safe (SteamIPAddress_t is a C union, SteamPartyBeaconLocation_t packs differently per platform). The Steam Input action-data calls, skipped before 0.3, are bound. The generator lists every remaining skip when it runs.
  • Structs containing C unions (SteamNetworkingIdentity and relatives) get no layout table, because steam_api.json cannot express unions and a guessed layout would read garbage. They are excluded loudly, not wrongly.
  • An FFI mistake crashes the process instead of throwing. If you embed this in something that must survive (a VS Code extension, an editor), run it in a child process. That is how the CK3 modding toolkit uses it.
  • Game server APIs are not wired up. There is no curated networking layer either: ISteamNetworkingSockets and ISteamNetworkingMessages take SteamNetworkingIdentity, one of the union structs above, so only the generated methods that avoid it are usable today.
  • The Steam overlay draws into the game's own renderer. Node has none, so an Electron app gets no overlay from this package; that needs a native hook.

Tests

pnpm test runs offline (layout regression, dispatch pump, platform, and close-guard tests). pnpm test:live runs against the running Steam client on appid 480 (Spacewar): the full workshop round trip (create a private item, upload content and a preview image, set a German translation, metadata and key/value tags, an app dependency, query everything back, delete the item) plus checks for the stats, cloud, leaderboards, lobbies, social, auth, system, capture, and controllers layers (one temporary cloud file, one private throwaway lobby, one cancelled auth ticket). It cleans up after itself.

License

MIT for the code here. The Steamworks redistributables in runtime/ are Valve's, under the Steamworks SDK Access Agreement.