~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs

0
0
main
169 lines · 6.6 KB Raw History
# 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)