# Node servers `javascript/src/server` runs modules in Node 25 or later with built-ins only (`node:sqlite`, `node:crypto`, `node:worker_threads`, `fetch`). Everything is fetch-style (`Request` in, `Response` out), so any framework mounts it; `examples/node-hono` uses Hono. ## Run a module `createNodeContext` gives a state dir (sealed secrets, the records ledger) and a wasm runner. `hostCall({ ctx })` with no session makes public reads only. ```js run import { localModule, runModule } from "./javascript/src/run-module/index.js"; import { createNodeContext, fileFetch, hostCall } from "./javascript/src/server/index.js"; const ctx = createNodeContext({ id: "my-server", stateDir: ".state/my-server" }); const root = `file://${process.cwd()}/`; const module = await localModule(new URL("target/wasm32-unknown-unknown/release/compose.wasm", root), new URL("examples/rust/compose/module.json", root), fileFetch); const ran = await runModule(ctx, module, "lookup", { n: 21 }, hostCall({ ctx })); console.log(JSON.stringify(ran)); ``` ```output {"entry":"lookup","result":{"ack":null,"caller":null,"doubled":42},"events":1,"calls":[]} ``` A published module: `loadModule(ctx, "bobvmbuilder.bsky.social/hello")` (or `/`, an at:// URI; `@` checks it). ## From the command line The Node CLI runs one module, from a record or a local build, and prints the same JSON as `atlibs run`. ```bash run node javascript/src/server/cli.mjs run target/wasm32-unknown-unknown/release/compose.wasm examples/rust/compose/module.json lookup '{"n":2}' ``` ```output "doubled": 4 ``` It signs in only with an app password (`--app-password`, [Server sessions](sessions.md)); for a user's OAuth session use `atlibs login` and `atlibs run --as`. ## A Hono server `examples/node-hono/server.mjs` serves staged modules: each session's OAuth client metadata, user sign-in, and `POST /run//`. Install its two dependencies once (and those of the optional sign-in, `javascript/src/oauth-server`): ```bash run=network (cd examples/node-hono && npm install) (cd javascript/src/oauth-server && npm install) ``` Stage the modules, start it, run one: ```bash run=network atlibs stage --target server --out staged examples/rust/compose examples/rust/statusphere > /dev/null ATLIBS_STAGE=staged/stage.json node examples/node-hono/server.mjs & ``` ```bash run=network curl -s -X POST localhost:8788/run/compose/lookup -H 'content-type: application/json' -d '{"n":2}'; echo curl -s localhost:8788/oauth/statusphere/client-metadata.json | jq -r .client_id ``` ```output {"entry":"lookup","result":{"ack":null,"caller":null,"doubled":4},"events":1,"calls":[]} http://localhost:8788/oauth/statusphere/client-metadata.json ``` `PORT`, `ATLIBS_STAGE`, `ATLIBS_SERVER_STATE` and `ATLIBS_ORIGIN` (the public origin) configure it. It listens on 127.0.0.1; put a reverse proxy or tunnel in front ([Server sessions](sessions.md)). ### Mount the client metadata `clientMetadataHandler(stage, { origin })` answers `GET /client-metadata.json` and returns `undefined` for anything else, so your routes still run. `examples/node-hono/server.mjs` ```js const metadata = clientMetadataHandler(stage, { origin }); const app = new Hono(); … app.all("/oauth/*", async (c) => { const doc = c.req.method === "GET" && metadata(c.req.raw); if (doc) return doc; ``` With `ATLIBS_ORIGIN` and a session record that declares `app`, it also serves the app's own identity and read-only repo first (`appRepoHandler`, [App identity](app-identity.md)). ### Run a module per request Each request runs as its signed-in user (or the server's own account, or signed out), held to the module's own session scope by `sessionCall`. `examples/node-hono/server.mjs` ```js // who this run acts as: the request's user with their own session, else the server's default let session; try { session = await oauth?.sessionOf(c.req.raw); } catch (error) { return c.json({ error: "AuthRequired", message: error.message }, error.status ?? 401); } const wasm = await realpath(join(dir, staged.wasm)); // id: the wasm's path, as cli.mjs gives a local module (the same ledger owner in a shared state dir) const module = { ...(await localModule(pathToFileURL(wasm), pathToFileURL(join(dir, staged.spec)), fileFetch)), id: wasm }; const runDid = session?.did ?? did; // signed in (a user, or the operator account): only what the module's own session allows (stage.json is the policy) const runCall = runDid ? sessionCall(stage, c.req.param("module"), session ? hostCall({ ctx, xrpc: session.xrpc, pds: session.pds }) : call) : call; return c.json(await runModule(ctx, module, c.req.param("entry"), { ...(runDid && { did: runDid }), ...input }, runCall, { provides: [] })); ``` ### Refuse cross-site runs `runRefusal` is the same guard `atlibs serve` applies: a Host that is loopback or the configured origin's, no foreign `Origin`, and `content-type: application/json` (which a cross-site form cannot send without a preflight). ```bash run=network curl -s -X POST localhost:8788/run/compose/lookup -H 'Origin: https://evil.example' -H 'content-type: application/json' -d '{"n":2}'; echo ``` ```output {"error":"Forbidden","message":"Origin https://evil.example may not run modules here"} ``` ## Accept calls from modules: service auth A module calling your XRPC service with `service: "https://you.example"` sends a service-auth JWT. `verifyServiceAuth` returns the caller's DID or throws (answer 401); it checks the audience, method, expiry, the issuer's signing key, and replays. ```js run import { createDidKey } from "./javascript/src/didkey/index.js"; import { verifyServiceAuth } from "./javascript/src/serviceauth/index.js"; // a caller: a did:key mints a token for one method at one audience const caller = await createDidKey(); const token = await caller.serviceAuth({ aud: "did:web:you.example", lxm: "com.example.hello" }); // your route; resolve is only here to skip the network for a did:key const resolve = async () => ({ verificationMethod: [{ id: "#atproto", publicKeyMultibase: caller.did.slice("did:key:".length) }] }); const who = await verifyServiceAuth({ audiences: ["did:web:you.example"] }, `Bearer ${token}`, "com.example.hello", { resolve }); console.log(who === caller.did); await verifyServiceAuth({ audiences: ["did:web:you.example"] }, `Bearer ${token}`, "com.example.hello", { resolve }).catch((e) => console.log(e.message)); ``` ```output true the token was already used ``` ## Test it ```bash run node --test examples/node-hono/test.mjs 2>&1 | grep -E '^ℹ (pass|fail)' ``` ```output ℹ pass … ℹ fail 0 ``` Previous: [Sign in from a page](../browser/oauth.md) | Next: [Rust servers](rust.md)