1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# 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 `<did>/<name>`, an at:// URI; `@<pin>` 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/<module>/<entry>`. 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 <path><session>/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)