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

bee-wasm-js

v0.1.1

Published

Build, test and run Swarm WASM modules for Bee's experimental WASM compute engine

Readme

bee-wasm-js

Write, build, test and run WebAssembly modules for Swarm.

A Bee node can download a WASM module from Swarm and run it: POST /@/{address} hands the request body to the module on stdin and returns what it wrote to stdout. While it runs, the module may call back into the node to read and write Swarm data through a host module named swarm.

This SDK gives you the whole loop in JavaScript tooling:

write AssemblyScript  →  bee-wasm build  →  runLocal (in-memory node)  →  upload  →  execute

Experimental. The execute endpoint is off by default; an operator enables it with --wasm-execute-enable. Output is not reproducible across nodes — a host call reads what that node happens to hold — there is no gas metering, and modules run in the node's own process. Don't enable it on a public gateway.

Install

npm install bee-wasm-js

Node 22.18 or newer. The runtime dependencies are the AssemblyScript compiler and bee-js, which the node client is built on.

Quickstart

1. Write a module. It reads its input from stdin and writes its result to stdout. _start is the entrypoint, like any WASI command.

// assembly/index.ts
import { bytesGet, codeName, readInput, writeOutput, writeOutputString } from "bee-wasm-js/assembly";


export function _start(): void {
  const reference = readInput();            // the request body: a 32-byte reference
  const result = bytesGet(reference);       // fetch what it points at
  if (!result.ok) {
    writeOutputString("error: " + codeName(result.code));
    return;
  }
  writeOutput(result.unwrap());
}

2. Build it. The output is a few kilobytes and imports nothing but wasi_snapshot_preview1 and swarm, which is what the sandbox demands.

npx bee-wasm build assembly/index.ts -o build/module.wasm
# build/module.wasm  3849 bytes

3. Run it against an in-memory node, with no Bee running anywhere:

import { MemoryHost, runLocal, toHex } from "bee-wasm-js";
import { readFileSync } from "node:fs";

const host = new MemoryHost();
const reference = host.seedBytes("hello swarm");

const result = runLocal(readFileSync("build/module.wasm"), { input: reference, host });

console.log(result.status);                        // "ok"
console.log(Buffer.from(result.output).toString()); // "hello swarm"
console.log(result.hostCallsUsed, result.hostBytesUsed); // 1 11

4. Put it on Swarm and run it there:

export BEE_API_URL=http://localhost:1633
export BEE_POSTAGE_BATCH=<your batch id>

REF=$(npx bee-wasm upload build/module.wasm)
npx bee-wasm exec "$REF" --input-hex <a reference to fetch>

or from JavaScript:

import { BeeClient } from "bee-wasm-js";

const client = new BeeClient({ url: "http://localhost:1633", batchId: process.env.BEE_POSTAGE_BATCH });
const address = await client.uploadModule(readFileSync("build/module.wasm"));
const result = await client.execute(address, { input: someReference });

Writing a module

Guest code is AssemblyScript: TypeScript syntax, WebAssembly semantics. It is not JavaScript — there is no any, numbers are typed (i32, u32, usize, f64), and there are no exceptions, which is why every call here returns a result code instead of throwing.

Import the guest library as bee-wasm-js/assembly.

Input and output

| Function | Meaning | |---|---| | readInput(): Uint8Array | the whole request body, from stdin | | writeOutput(data: Uint8Array) | append to the response body | | writeOutputString(text: string) | the same, UTF-8 encoded | | writeError(text: string) | stderr — discarded by the node, captured by the harness | | getEnv(name: string): string | any request-derived variable; the host environment is never inherited |

Reading the request

The rest of the request arrives through the environment, CGI-style:

| Function | Variable | Meaning | |---|---|---| | requestMethod() | REQUEST_METHOD | the HTTP method | | pathInfo() | PATH_INFO | the path after the address — this is what you route on | | queryString() | QUERY_STRING | the raw, undecoded query | | scriptName() | SCRIPT_NAME | where the module is mounted, e.g. /@/{address} | | requestURI() | REQUEST_URI | the full request target | | requestContentType() | CONTENT_TYPE | of the request body | | requestContentLength() | CONTENT_LENGTH | -1 when the node reported none | | requestHeader(name) | HTTP_* | an allowlisted request header, by its ordinary name |

/@/{address} and /@/{address}/{path} both serve, and PATH_INFO is what distinguishes them: empty for the bare form, / for a trailing slash, /x/y beyond. scriptName() + pathInfo() is always the path the client asked for, which is what to build self-links from — hardcoding an address breaks behind the /v1/@/... alias.

The node forwards only a fixed set of request headers — Accept, User-Agent, Swarm-Postage-Batch-Id and a handful more — and never Authorization or Cookie, whatever the operator configures. So an empty requestHeader may mean the client did not send it or that the node does not forward it.

Every HTTP method reaches the module, so one module can serve several verbs:

export function _start(): void {
  if (requestMethod() == "GET") writeOutputString("<h1>hello</h1>");
  else writeOutputString("send me a GET");
}

OPTIONS is the exception: the node answers it itself as a CORS preflight, so untrusted code never sees it.

Responding

By default the node decides how your output is rendered: the status comes from the verdict and the content type from the caller's Accept. A module serving a web page needs both itself — a stylesheet is not text/html just because the browser asked for text/html.

setStatus(code: i32): Code
setHeader(name: string, value: string): Code
setContentType(value: string): Code
redirect(location: string, code: i32 = 303): Code
export function _start(): void {
  if (pathInfo() == "/style.css") {
    setContentType("text/css");
    setHeader("Cache-Control", "max-age=3600");
    writeOutputString("body { font: 16px system-ui }");
    return;
  }
  setStatus(404);
  setContentType("text/plain");
  writeOutputString("not found\n");
}

Neither call charges the host-call budget — they cause no node work — and both work on a node with node access switched off. They have their own bounds instead: 32 headers and 8 KiB of name-plus-value, BUDGET_EXHAUSTED beyond.

Four rules worth internalising:

  • Only the outermost execution has a response. A module reached through executeModule gets DENIED, so a module you fetched from Swarm cannot rewrite the content type you are serving.
  • Only a clean run commits. A module that traps sets no headers, exactly as it stores nothing. Its partial output does survive — that is evidence about what went wrong, while a header would be an instruction to follow.
  • A status must be 200–599. 5xx is allowed: Swarm-Wasm-Status still says ok, which is what distinguishes your 500 from the node's.
  • Some names are refused with DENIED: Swarm-Wasm-*, Access-Control-*, Set-Cookie and the origin-wide security headers — every module shares the node's origin with the node's own authenticated API — plus the hop-by-hop and framing headers. A malformed name or a control character in a value is INVALID, which is also what stops CR/LF injection.

How this reaches a client depends on what it asked for. Accept: application/json reports your status and headers in the envelope as httpStatus and headers without applying them. Every other representation applies them. A wildcard Accept reports by default and applies once you have set something — which is what lets a browser load your stylesheet, since a subresource request never names text/html.

Reaching the node

bytesGet(address: Uint8Array, hint: i32 = 4096): GetResult
bytesPut(batchId: Uint8Array, data: Uint8Array): PutResult
chunkGet(address: Uint8Array): GetResult
chunkPut(batchId: Uint8Array, data: Uint8Array): PutResult
executeModule(address: Uint8Array, input: Uint8Array, hint: i32 = 4096): ExecResult

bytes* moves data of arbitrary length through the same splitter and joiner the /bytes endpoints use. chunk* is the raw single-chunk pair: a chunk is an 8-byte little-endian span followed by at most 4096 bytes, 4104 in total.

Addresses, references and batch ids are always 32 bytes.

Every call returns a result carrying a code, an ok flag, and the payload:

const result = bytesGet(reference);
if (result.ok) {
  writeOutput(result.unwrap());       // Uint8Array
} else if (result.code == Code.NOT_FOUND) {
  writeOutputString("nothing there");
}

unwrap() returns the payload, or aborts with a message on stderr when the call did not succeed. Aborting is a legitimate choice — it is a clean trap verdict — but note that a trap discards anything the execution uploaded, so branch on the code if you have already stored something you want to keep.

Result codes

| Code | Name | Meaning | |---|---|---| | 0 | OK | the call succeeded | | 1 | NOT_FOUND | nothing is stored at that address | | 2 | DENIED | the node refused: an unusable batch, or a second batch in one execution | | 3 | BUDGET_EXHAUSTED | the call, byte or depth budget is spent | | 4 | BUFFER_TOO_SMALL | the payload did not fit — handled for you, see below | | 5 | INVALID | a pointer is out of bounds, or the arguments are malformed | | 6 | EXEC_FAILED | a nested module trapped or was invalid |

Nothing traps. A pointer outside linear memory is INVALID, not a crash: the node bounds-checks every offset a module hands it.

Failures inside the node — its storer erroring, a watchdog firing — are never reported through these codes. They end the execution with status host-error and a 500, because a node-local failure is not a verdict on your program.

Budgets

The caller provides the buffer for a read, so the node never grows guest memory mid-call. bytesGet handles the two-call protocol for you: it asks once with a hint-sized buffer and, if the payload did not fit, asks again with the exact length the node reported.

| Bound | Default | Header | Stops | |---|---|---|---| | host calls | 64 | Swarm-Wasm-Host-Calls-Limit | fetch amplification | | host bytes | 32 MiB | Swarm-Wasm-Host-Bytes-Limit | memory and bandwidth blowup | | depth | 4 | Swarm-Wasm-Depth-Limit | runaway recursion | | response headers | 32 | — | unbounded response metadata | | response header bytes | 8 KiB | — | the same, by size |

Headers may only lower a limit; the operator's configured maximum wins.

Spending them well:

  • a read that fits the hint costs one call; a read that does not costs two, but the bytes are charged only on delivery, so the payload is paid for once either way.
  • chunkGet always fits — a chunk has a known maximum — so it is always one call. Prefer it when you know you are addressing a single chunk.
  • hint: 0 is a pure probe, worth it only when a payload is expected to be far larger than the default 4 KiB and you want to allocate exactly once.
  • an upload is charged its declared length before the splitter runs, so an oversized put is refused without the node ever chunking it.
  • the byte budget is one pool counting both directions.

Uploads

A module can only upload with a batch id it was given — it has no way to enumerate the node's batches, so this path grants no authority the HTTP API does not already grant. The node resolves the batch exactly as POST /chunks does; an unusable or unknown one is DENIED.

const batchId = input.subarray(0, 32);
const payload = input.subarray(32);

const result = bytesPut(batchId, payload);
if (result.ok) writeOutput(result.unwrap());   // the 32-byte reference

Three rules worth internalising:

  • One execution gets one batch. A put naming a different batch than the first is DENIED.
  • Uploads are deferred. A put returns once the data is stored locally; the pusher syncs it afterwards. When the HTTP response arrives the data is in the node's upload store, not yet acknowledged by the network.
  • Only a clean run commits. A module that traps, or is cut off, leaves nothing behind.

Encryption and redundancy are not exposed: bytesPut writes unencrypted at the default redundancy level, which is what keeps a reference 32 bytes wide.

Nested execution

executeModule fetches another module from Swarm and runs it, handing it your input on stdin and returning its stdout:

const result = executeModule(moduleAddress, input);
if (result.ok) writeOutput(result.unwrap());

The budgets are shared across the whole call tree, so a module cannot multiply its allowance by recursing; a cycle simply runs out of depth. The depth limit counts levels including the outermost, so 1 permits no nesting at all. A nested module is always run as a WASI command — an entrypoint header applies to the outermost module only.

Building

bee-wasm build <entry.ts> [options]
  -o, --out <file>      output path (default build/module.wasm)
  --text                also emit the WebAssembly text format
  --debug               unoptimized build with debug info
  --entrypoint <name>   verify against this export instead of _start
  --max-memory <bytes>  cap linear memory
  --runtime <name>      stub | minimal | incremental (default stub)

or programmatically:

import { build } from "bee-wasm-js";

const { wasm } = await build({ entry: "assembly/index.ts", outFile: "build/module.wasm" });

Two things the build does beyond calling asc:

It removes the env imports. AssemblyScript imports abort, seed and trace from a module named env, and the sandbox allows no namespace but swarm and wasi_snapshot_preview1. The build redirects them to WASI-based replacements shipped in assembly/runtime.ts — abort writes its message to stderr and exits, which the node reports as a trap. A module that does not import this library at all has nothing to redirect to, so the imports are dropped instead and an abort becomes a bare unreachable.

It applies the sandbox's own rules. After compiling, the module is checked the way checkImports in pkg/compute/wazero.go checks it: no import outside the two allowed namespaces, no unknown swarm.* function, no imported memory, an exported _start (or the entrypoint you named), and a memory exported as memory — which is how the harness reads guest buffers, and which AssemblyScript emits by default. A module that would come back invalid-module from a node fails the build with an explanation instead.

You can run that check on any module, whoever built it:

bee-wasm inspect build/module.wasm
import { checkModule } from "bee-wasm-js";

const problems = checkModule(wasm);   // [] means the sandbox accepts it

The default stub runtime is a bump allocator that never frees, which is what you want for a module that runs once and exits. Choose incremental if your module allocates in a loop and needs the memory back.

Testing locally

runLocal runs a module the way the node does — same errno table, same budgets, same commit-on-success rule — against an in-memory Swarm:

import { MemoryHost, runLocal } from "bee-wasm-js";

const host = new MemoryHost();
const reference = host.seedBytes("some data");

const result = runLocal(wasm, {
  input: reference,          // Uint8Array or string
  method: "POST",            // becomes REQUEST_METHOD
  url: "/blob/abc?raw=1",    // PATH_INFO and QUERY_STRING in one
  scriptName: "/@/abc",      // where the module is mounted
  headers: { accept: "text/css" },
  host,                      // omit for a fresh MemoryHost, null for no node access
  limits: { hostCalls: 8, hostBytes: 1 << 20, depth: 2 },
});

result.status;         // "ok" | "trap" | "invalid-module" | "host-error"
result.output;         // Uint8Array
result.response;       // { status, headers } the module set, or undefined
result.stderr;         // what the module wrote to stderr, including abort messages
result.hostCallsUsed;  // budget actually spent
result.hostBytesUsed;

MemoryHost is a real little Swarm: seedBytes splits data into chunks the way the node does and returns a root reference, seedChunk stores one chunk verbatim, and bytes(reference) / chunk(address) read back what a module uploaded — but only after a successful run, because uploads are staged and committed exactly as they are on a node.

const result = runLocal(uploadModule, { input: concat(batchId, payload), host });
host.bytes(result.output);      // what the module stored, if it succeeded

Options worth knowing:

new MemoryHost({
  batches: ["<hex batch id>"],       // anything else is DENIED; by default any non-zero id works
  address: (chunk) => myBmtHash(chunk),
});

What the harness does not model

  • Addressing. References are SHA-256 digests, not Swarm BMT references, so a reference computed locally will not resolve on a real node. Pass address if you have a real implementation. The structure is faithful: spans, 4096-byte leaves, 128-reference intermediate chunks.
  • The memory ceiling. The node enforces it in-engine; the harness does not.
  • WASI. The harness implements the syscalls this SDK's modules use, plus the handful a Rust or TinyGo runtime touches on startup. A module reaching for anything else fails loudly with the function's name rather than pretending it is invalid — the node provides all of preview1, so such a module may still run there. HARNESS_WASI_FUNCTIONS is the list.

A custom host is just an object; it must be synchronous, because a host call is: the guest blocks inside swarm_bytes_get until the node answers.

import { HostRefusal, type SwarmHost } from "bee-wasm-js";

const host: SwarmHost = {
  bytesGet(address) {
    const data = myStore.get(toHex(address));
    if (!data) throw new HostRefusal("not-found");   // the module sees NOT_FOUND
    return data;
  },
  // throwing anything else ends the run as host-error, as a node-local failure should
  ...
};

Talking to a node

import { BeeClient } from "bee-wasm-js";

const client = new BeeClient({ url: "http://localhost:1633", batchId });

const address = await client.uploadModule(wasm);
const result = await client.execute(address, {
  input: body,                 // Uint8Array or string
  method: "POST",
  path: "/style.css",          // reaches the module as PATH_INFO
  query: "v=2",                // QUERY_STRING
  headers: { "Swarm-Postage-Batch-Id": batchId },
  entrypoint: "run",           // Swarm-Wasm-Entrypoint
  hostCalls: 16,               // Swarm-Wasm-Host-Calls-Limit
  hostBytes: 1 << 20,          // Swarm-Wasm-Host-Bytes-Limit
  depth: 2,                    // Swarm-Wasm-Depth-Limit
  memory: 16 << 20,            // Swarm-Wasm-Memory-Limit
});

BeeClient is a thin layer over bee-js: uploads and downloads are bee.data.upload and bee.data.download, and client.bee is the Bee instance itself, so the rest of the node API — stamps, pinning, feeds, manifests — is available through it. Only execute is spelled out here, because the endpoint is experimental and bee-js does not cover it.

const batches = await client.bee.stamp.getAll();   // the whole bee-js API

execute returns a verdict on the program and throws only when the node itself refused. A trap or an invalid module comes back as HTTP 400 carrying the same JSON envelope a 200 does, so status, output and trapMessage are available either way; 404, 406, 413, 429 and 500 raise bee-js's BeeResponseError.

Ask for a different representation with accept: application/json (the default) returns the envelope, application/octet-stream and text/html return the module's raw output, which is what makes a module servable as a web page.

CLI

bee-wasm build <entry.ts> [options]      compile AssemblyScript to a module
bee-wasm inspect <module.wasm>           list imports, exports and objections
bee-wasm run <module.wasm> [options]     run against an in-memory node
bee-wasm upload <module.wasm> [options]  store a module on Swarm
bee-wasm exec <address> [options]        run a module on a node

run takes --seed <file> (store a file in the in-memory node first, printing its reference), --input <file> or --input-hex <hex>, --method, --url, --script-name, --header name:value, --batch and the three budget flags. It prints the status and headers the module set on stderr, so stdout stays pipeable. exec takes --path, --query, --header and --accept as well. upload and exec read $BEE_API_URL and $BEE_POSTAGE_BATCH when --bee and --batch are absent.

echo "swarm data" > data.txt
bee-wasm run build/module.wasm --seed data.txt
# seeded data.txt at 7154e973...254b
bee-wasm run build/module.wasm --seed data.txt --input-hex 7154e973...254b
# status=ok host_calls=1 host_bytes=11
# SWARM DATA

Examples

Each one is a complete module in examples/, exercised by the tests in test/:

| Example | What it shows | |---|---| | echo | stdin, stdout and REQUEST_METHOD; no host calls at all | | transform | fetching a reference, branching on NOT_FOUND instead of trapping | | upload | storing the request body and returning its reference | | nested | running another module with swarm_execute | | webpage | routing on PATH_INFO, per-path content types, a redirect and a 404 |

npm run examples   # build them all into examples/*/build/
npm test           # build them, then run the suite

Staying in sync with Bee

The ABI, the result codes and the budgets are bee's, not this SDK's. When they move, these are the files to read:

| bee | what it defines | |---|---| | pkg/compute/README.md | the guest ABI, in prose | | pkg/compute/host.go | the host functions, result codes and budget arithmetic | | pkg/compute/limits.go | the default budgets | | pkg/compute/wazero.go | the import rules and how a run is classified | | pkg/api/execute.go | the endpoint, its headers and the response envelope | | pkg/api/host.go | what a host call actually does to the node | | pkg/api/execute_env.go | the CGI environment and the request-header allowlist | | pkg/api/router.go | the two routes and how PATH_INFO is derived |

src/types.ts mirrors the constants, src/verify.ts mirrors the import rules, src/cgi.ts mirrors the environment derivation and src/harness/run.ts mirrors the budget accounting and the response caps.