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

johnandersen777.bsky.social/atlibs

0
0
main
177 lines · 7.0 KB Raw History

Server sessions

A server acts for each signed-in user with that user's own OAuth session: any number of users, any number of runs at once. Each run may only make the calls its module's session record allows. An app password is used only when you configure one.

Sign users in

atlibs serve is its own OAuth client. With --origin it is a confidential client on that origin; without one it is the loopback development client, for 127.0.0.1 only.

atlibs serve examples/rust/statusphere --origin https://atlibs.example.com --port 8792 &
curl -s localhost:8792/oauth/client-metadata.json | jq -c '{client_id, redirect_uris, token_endpoint_auth_method}'
curl -s localhost:8792/oauth/jwks.json | jq -r '.keys[0].alg'
{"client_id":"https://atlibs.example.com/oauth/client-metadata.json","redirect_uris":["https://atlibs.example.com/oauth/callback"],"token_endpoint_auth_method":"private_key_jwt"}
ES256
routewhat
GET /oauth/login?handle=<handle>redirects to the user's authorization server
GET /oauth/callbackfinishes sign-in, sets the atlibs_session cookie (also usable as a bearer token)
POST /oauth/logoutrevokes the user's tokens
POST /run/<entry>runs as the request's user; with no session, as --as/--app-password if given, else signed out

The scope it asks for is the union of the served modules' session records (--scope overrides it).

In Node, javascript/src/oauth-server (optional; npm install there) is the same thing behind a fetch-style handler; examples/node-hono mounts it:

examples/node-hono/server.mjs

	const oauth = await import("../../javascript/src/oauth-server/index.js").then(
		({ oauthServer }) => oauthServer(ctx, { origin: process.env.ATLIBS_ORIGIN || `http://127.0.0.1:${port}`, scope }),
		(error) => console.error(`no sign-in (npm install in javascript/src/oauth-server): ${error.message}`),
	);

examples/node-hono/server.mjs

	app.all("/oauth/*", async (c) => {
		const doc = c.req.method === "GET" && metadata(c.req.raw);
		if (doc) return doc;
		if (!oauth) return c.notFound();
		const docs = /\/oauth\/(client-metadata|jwks)\.json$/.test(c.req.path);
		const refused = !docs && runRefusal(c.req.raw, origin, { json: false });
		if (refused) return c.json({ error: "Forbidden", message: refused[1] }, refused[0]);
		return (await oauth.handle(c.req.raw)) ?? c.notFound();
	});

Both are tested against a fake authorization server and PDS (PAR, PKCE, DPoP, refresh rotation):

node --test javascript/src/oauth-server/oauth-server.test.mjs rust/host/oauth.test.mjs 2>&1 | grep -E '^ℹ fail'
ℹ fail 0

Each run is held to its module's scope

The user granted the union of every served module's scope, but a run of statusphere may only make calls records/statusphere.json's scope allows: one module cannot write another's collections. A call outside it fails with the same text in both hosts:

statusphere may not call com.example.other.write with sessions statusphere

Calls that need no scope (public reads, host calls) always go through. A module with no session record makes no scoped call. The scope comes from the module record's session, or records/<session>.json beside a local module.json, or per instance in a composition.

A service-auth token (com.atproto.server.getServiceAuth, which the host also mints for an http allowlist's serviceAuth) needs rpc:<lxm>?aud=<aud> for the token's lxm and aud, as an OAuth PDS requires.

An operator account with an app password

For tests, cron jobs, or a server's own account. The password never goes in argv: a file (first line; - is stdin) or the variable --password-env names.

atlibs run examples/rust/statusphere set_status '{"status":"🦋"}' --app-password you.bsky.social --password-file ~/.atlibs-app-password
node javascript/src/server/cli.mjs run target/wasm32-unknown-unknown/release/statusphere.wasm examples/rust/statusphere/module.json set_status '{"status":"🦋"}' --app-password you.bsky.social --password-env APP_PASSWORD

In code, the config is explicit:

javascript/src/server/index.js

 * Who a run acts as, from explicit config only, never the environment (README "Who a run acts as"):
 *   { appPassword: { handle, password?, service? } }  an app password (the simple / testing case; its session
 *                                                     is saved in the state dir, so password may be omitted then)
 *   { oauth: { did } }                                a user's OAuth session (oauthServer from javascript/src/oauth-server)
 *   undefined                                         no session: signed out
 * @param {{ oauth?: { session: (did: string) => Promise<object> } }} [hosts]
 */
export async function openSession(ctx, config, { oauth } = {}) {

examples/node-hono/server.mjs

	const { config } = sessionFlags(process.argv.slice(2));
	const session = await openSession(ctx, config);

Rust: SessionConfig::AppPassword / SessionConfig::OAuth (Rust servers). The session is saved, sealed, in the state dir and refreshed from then on.

Many runs at once

Runs share one session per user. An expired token is refreshed once for all of them: once per process, and once across processes sharing a state dir (a lease in the store). A revoked session fails every run that uses it.

At most ATLIBS_WASM_CONCURRENCY wasm runs execute at once per process (default: the core count); the rest queue, and queued time does not count against a run's wall time.

ATLIBS_WASM_CONCURRENCY=0 atlibs help || true
ATLIBS_WASM_CONCURRENCY must be a positive integer, not "0"

Behind a reverse proxy

The origin is configuration, never the request: --origin (or ATLIBS_ORIGIN) sets the client id, redirect URI and callback. The server binds 127.0.0.1 and answers only Hosts that are loopback, an IP, or the origin's.

atlibs serve bobvmbuilder.bsky.social/statusphere --origin https://atlibs.example.com --port 8787
server {
	server_name atlibs.example.com;
	location / {
		proxy_pass http://127.0.0.1:8787;
		proxy_set_header Host $host;
	}
}
atlibs.example.com {
	reverse_proxy 127.0.0.1:8787
}

Troubleshooting

You seeDo this
403 Host is not this server's (Node: Host … is not this server)Request through the --origin host, or 127.0.0.1.
403 cross-origin requests are refused (Node: Origin … may not run modules here)Runs come from the server's own pages or non-browser clients only.
401 unknown or expired session: sign in again …GET /oauth/login?handle=… again.
… may not call … with sessions …Add the call to the module's session record scope and republish both.
… needs a session (sign in: atlibs login + --as, or --app-password)The call needs an account; sign in.

Previous: Rust servers | Next: App identity