#!/usr/bin/env node // Run an atlibs wasm module server side (javascript/src/server, no emdash, no npm deps): // node javascript/src/server/cli.mjs run /[@pin] [''] (or /, an at:// URI) // node javascript/src/server/cli.mjs run [''] // The input JSON: an argument, or - (read from stdin) or @file, so secrets (a stdin input) stay out of ps. // Exits 1 when the run failed (its error is in the printed result). // Who it acts as, explicit only (no flag: signed out, DID-service reads only; the environment is never read for it): // --app-password (--password-file | --password-env ) [--pds https://bsky.social] // (an app password: the simple / testing case; a user's OAuth session is a server's, javascript/src/oauth-server) // env: ATLIBS_SERVER_STATE (default ~/.atlibs-server: // store.db and secret.key, which seals the attestation keys and the saved session; keep it // private and reuse it so bind_key and later signatures share a key, and so runs refresh // the saved session instead of calling the rate-limited createSession). // ATLIBS_BUDGET (JSON merged over runModule's BUDGET, for example // '{"wallMs":300000,"events":5000}'; "unlimited" for calls, callsPerMinute, events, wallMs; ioBytes up to 16 MiB); // Ctrl-C ends a run and still prints it. // ATLIBS_TRANSPORTS (comma-separated module paths, each exporting `transport`: the host pseudo-calls // this command line serves, e.g. atlibs-iroh's js/index.js; unset: none, so iroh.connect is refused; // and/or `fetchers({ env, stateDir })`: { "iroh:": fn }, runModule's fetchers for http.fetch to those URLs). // ATLIBS_IROH_KEY (the iroh transport's ssh private key; default /iroh_ed25519). // ATLIBS_ALLOW_PRIVATE=1 lets DID lookups reach http and private addresses (local dev only). // ATLIBS_HTTP (JSON, or @file of it: the origins com.fedfork.exec.http.fetch may reach, each with the // headers this host adds, e.g. '{"https://api.anthropic.com":{"headers":{"x-api-key":"..."}}}'; runModule's http). // ATLIBS_HELD (JSON, or @file of it): { name: value }, the held values the run starts with (runModule's held). // A module.json (or module record) with "didkey": { "services": [https origins] } gets a P-256 // did:key, sealed in the state dir per module identity: its at:// URI, or the wasm's absolute path // (same did, same ingress host name, every run; never the name a module gives itself), and the // com.fedfork.exec.didkey.* pseudo-calls (javascript/src/didkey). // Prints runModule's { entry, result, error?, events, calls } as JSON on stdout. import { readFileSync } from "node:fs"; import { realpath } from "node:fs/promises"; import { homedir } from "node:os"; import { basename, dirname, join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { attestHost } from "../attest/index.js"; import { didkeyCall, ingressHost } from "../didkey/index.js"; import { loadModule, localModule, runModule } from "../run-module/index.js"; import { contextOptions, createNodeContext, didkeyOf, fileFetch, hostCall, modulePolicy, openSession, secp256k1, sessionCall, sessionFlags } from "./index.js"; const USAGE = `usage: cli.mjs run /[@pin] | / | ['' | - | @file] [--app-password (--password-file | --password-env ) [--pds ]] cli.mjs run ['' | - | @file] [...]`; // read: the input source (- is stdin, @path a file); swappable for tests. export function parse(argv, read = (source) => readFileSync(source === "-" ? 0 : source.slice(1), "utf8")) { const [command, ...rest] = argv; if (command !== "run") throw new Error(USAGE); // published: at://, or / (Rust registry::is_remote); else const head = /^([^/@\s]+)\/[^/@\s]+(@[A-Za-z0-9]+)?$/.exec(rest[0] ?? "")?.[1]; const remote = rest[0]?.startsWith("at://") || (head !== undefined && !rest[0].endsWith(".wasm") && (head.startsWith("did:") || (head.includes(".") && !head.startsWith(".")))); const [entry, inputJson = "{}", ...extra] = rest.slice(remote ? 1 : 2); if (!entry || extra.length || (!remote && !rest[1])) throw new Error(USAGE); const text = inputJson === "-" || inputJson.startsWith("@") ? read(inputJson) : inputJson; let input; try { input = JSON.parse(text); } catch { throw new Error("input is not JSON"); // never echoed: it may hold a secret } if (typeof input !== "object" || input === null || Array.isArray(input)) throw new Error("input must be a JSON object"); return { ...(remote ? { uri: rest[0] } : { wasm: rest[0], manifest: rest[1] }), entry, input }; } // transports: the host pseudo-calls this CLI was given, by NSID -> ({ env, stateDir }) => handler (hostCall's // calls; a transport module's `transport`, ATLIBS_TRANSPORTS on the command line). Their NSIDs are what this host provides, checked when hooks load. // A module that lists no calls may call anything, so it gets no transport (the host's keys stay out of it). // One not given is refused: ": not available on this host (built without )". // fetchers: those modules' fetchers exports, each ({ env, stateDir }) => { ":": fn } (runModule's fetchers). export async function main(argv, env = process.env, signal, transports = {}, fetchers = []) { const { config, rest } = sessionFlags(argv, { env }); const { uri, wasm, manifest, entry, input } = parse(rest); const stateDir = env.ATLIBS_SERVER_STATE || join(homedir(), ".atlibs-server"); const ctx = createNodeContext(contextOptions(stateDir)); ctx.allowPrivate = env.ATLIBS_ALLOW_PRIVATE === "1"; try { // key: its rkey, or the wasm's file stem (rust/cli pair_lib); dir: where a local module.json's session record is looked up const module = uri ? await loadModule(ctx, uri).then((mod) => ({ ...mod, key: mod.id.split("/").pop() })) : { ...(await localModule(pathToFileURL(wasm), pathToFileURL(manifest), fileFetch)), id: await realpath(wasm), key: basename(wasm, ".wasm"), dir: dirname(manifest) }; const session = await openSession(ctx, config); // JSON has no Infinity: "unlimited" is how ATLIBS_BUDGET says it (budgetValue keeps it only for UNLIMITED keys). const budget = env.ATLIBS_BUDGET ? JSON.parse(env.ATLIBS_BUDGET, (key, value) => (value === "unlimited" ? Infinity : value)) : undefined; const envJson = (name) => env[name] && JSON.parse(env[name].startsWith("@") ? readFileSync(env[name].slice(1), "utf8") : env[name]); const http = envJson("ATLIBS_HTTP"); const held = envJson("ATLIBS_HELD"); const schemes = Object.assign({}, ...(await Promise.all(fetchers.map((make) => make({ env, stateDir }))))); const refuse = (nsid) => async () => Promise.reject(new Error(`${nsid} needs a module record that lists its calls`)); // The call one module runs on: the run's, and each hook default's own, never another's. // A module that lists no calls may call anything, so it gets no signing key and no transport. const callOf = async (module) => { const attest = session && module.calls?.length ? attestHost(ctx, secp256k1, session.did) : undefined; const calls = Object.fromEntries(Object.entries(transports).map(([nsid, make]) => [nsid, module.calls?.length ? make({ env, stateDir }) : refuse(nsid)])); let call = hostCall({ ctx, xrpc: session?.xrpc, attest, pds: session?.pds, calls, fetchers: module.calls?.length ? schemes : {} }); // signed in: only what the module's own session record's scope allows const key = module.key ?? basename(module.id ?? "", ".wasm"); if (session) call = sessionCall(await modulePolicy(ctx, key, module, module.dir), key, call); if (module.didkey && module.calls?.length) { const key = await didkeyOf(ctx, module.id); for (const service of module.didkey.services ?? []) console.error(`${key.did}: https://${ingressHost(key.did, service)}/`); call = didkeyCall(key, call, { services: module.didkey.services, lease: (name) => ctx.lease(name) }); } return call; }; const call = await callOf(module); // A hook default of a local build: examples/rust//module.json and its wasm beside this one's. const local = async (key) => { const file = join(dirname(wasm), `${key.replaceAll("-", "_")}.wasm`); const dir = join(dirname(manifest), "..", key); return { ...(await localModule(pathToFileURL(file), pathToFileURL(join(dir, "module.json")), fileFetch)), id: await realpath(file), key, dir }; }; return await runModule(ctx, module, entry, { ...(session && { did: session.did }), ...input }, call, { budget, signal, callFor: callOf, provides: Object.keys(transports), http, fetchers: schemes, held, ...(!uri && { load: local }) }); } finally { ctx.close(); } } if (import.meta.main) { const stop = new AbortController(); process.once("SIGINT", () => stop.abort()); // ATLIBS_TRANSPORTS: the transport modules this command line loads (paths from the cwd); no core path imports one. const transports = async () => { const all = {}; const fetchers = []; for (const path of (process.env.ATLIBS_TRANSPORTS ?? "").split(",").map((p) => p.trim()).filter(Boolean)) { const plugin = await import(pathToFileURL(resolve(path)).href); if (!plugin.transport && typeof plugin.fetchers !== "function") throw new Error(`ATLIBS_TRANSPORTS: ${path} exports no transport`); Object.assign(all, plugin.transport); if (typeof plugin.fetchers === "function") fetchers.push(plugin.fetchers); } return [all, fetchers]; }; transports().then(([given, fetchers]) => main(process.argv.slice(2), process.env, stop.signal, given, fetchers)).then( (ran) => { console.log(JSON.stringify(ran, null, "\t")); if (ran.error !== undefined) process.exitCode = 1; }, (error) => { console.error(error.message); process.exitCode = 1; }, ); }