//! `atlibs bridge`: the Rust run-module engine driven over stdio (JSON lines), so the JS //! conformance harness (rust/host/conformance.mjs) can hand it the same ctx and call stubs it //! hands the JS runModule, and compare. Not a user surface. //! //! stdin, first line: { module: { wasm?: path, calls?, anyRecords?, sha256?, owner? }, entry, input, //! budget?, inputs?: bool, ledger?: bool, now?: iso, didkey?: { jwk, services }, http?: runModule's http, held?: runModule's held } //! (or { rfpPolicyRoutes: { manifest, from, records } }: one { t: done, manifest | error } line, no run) //! then replies { k, ... } and unsolicited { t: input, data } | { t: inputs-end, error? } | { t: abort }. //! stdout: { t: run | call | send | close | abort | ledger | fetch, k, ... } and last { t: done, ran | error }. //! Without module.wasm, ctx.wasm.run goes over the bridge too (the JS harness's own runner). use std::collections::HashMap; use std::io::Write; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use serde_json::{Map, Value, json}; use tokio::io::AsyncBufReadExt; use tokio::sync::{mpsc, oneshot}; use atlibs_host::crypto::DidKey; use atlibs_host::host::{DidkeyCall, Fetch, Response}; use atlibs_host::run::{self, Call, Ctx, Ledger, Module, NOSEND, Options, Request, Runner, Sub, SubCmd}; use atlibs_host::wasm::{Bound, Wasm}; use atlibs_host::{BoxFut, Signal, js_string}; enum Waiter { Once(oneshot::Sender), Stream(mpsc::UnboundedSender), } #[derive(Default)] struct Io { next: AtomicU64, waiters: Mutex>, /// request.emit of pending calls, for { t: emit, k, event } emits: Mutex>, } impl Io { fn write(&self, message: Value) { let mut out = std::io::stdout().lock(); let _ = writeln!(out, "{message}"); let _ = out.flush(); } fn key(&self) -> u64 { self.next.fetch_add(1, Ordering::SeqCst) + 1 } async fn ask(&self, mut message: Value) -> Value { let (k, (tx, rx)) = (self.key(), oneshot::channel()); self.waiters.lock().unwrap().insert(k, Waiter::Once(tx)); message["k"] = json!(k); self.write(message); rx.await .unwrap_or_else(|_| json!({ "ok": false, "error": "bridge closed" })) } fn deliver(&self, reply: Value) { let Some(k) = reply.get("k").and_then(Value::as_u64) else { return; }; let mut waiters = self.waiters.lock().unwrap(); match waiters.get(&k) { Some(Waiter::Once(_)) => { if let Some(Waiter::Once(tx)) = waiters.remove(&k) { let _ = tx.send(reply); } } Some(Waiter::Stream(tx)) => { let _ = tx.send(reply); } None => {} } } } fn outcome(reply: &Value) -> Result, String> { if reply.get("ok") == Some(&Value::Bool(true)) { Ok(reply.get("output").cloned()) } else { Err(js_string(reply.get("error"))) } } struct BridgeRunner(Arc); impl Runner for BridgeRunner { fn run(&self, entry: String, input: Value, io_bytes: usize) -> BoxFut<'_, Result> { Box::pin(async move { outcome( &self .0 .ask(json!({ "t": "run", "entry": entry, "input": input, "ioBytes": io_bytes })) .await, ) .map(|o| o.unwrap_or(Value::Null)) }) } } struct BridgeLedger(Arc); // The JS ledger's errors come back as { ok: false, error } and are the ledger's errors here too. fn ledger_reply(reply: Value) -> Result { match reply.get("ok") { Some(Value::Bool(false)) => Err(js_string(reply.get("error"))), _ => Ok(reply), } } impl Ledger for BridgeLedger { fn get(&self, uri: &str) -> BoxFut<'_, Result, String>> { let uri = uri.to_string(); Box::pin(async move { let reply = ledger_reply(self.0.ask(json!({ "t": "ledger", "op": "get", "uri": uri })).await)?; Ok(reply.get("value").filter(|v| !v.is_null()).cloned()) }) } fn put(&self, uri: String, value: Value) -> BoxFut<'_, Result<(), String>> { Box::pin(async move { ledger_reply(self.0.ask(json!({ "t": "ledger", "op": "put", "uri": uri, "value": value })).await).map(drop) }) } fn delete(&self, uri: String) -> BoxFut<'_, Result<(), String>> { Box::pin(async move { ledger_reply(self.0.ask(json!({ "t": "ledger", "op": "delete", "uri": uri })).await).map(drop) }) } } /// The JS side's ctx.http.fetch; with a scheme ("webrtc:"), its fetchers[scheme]. struct BridgeFetch(Arc, Option); impl Fetch for BridgeFetch { fn fetch( &self, method: &str, url: &str, headers: Vec<(String, String)>, body: Option>, ) -> BoxFut<'_, Result> { let headers: Map = headers.into_iter().map(|(k, v)| (k, json!(v))).collect(); let message = json!({ "t": "fetch", "scheme": self.1, "method": method, "url": url, "headers": headers, "body": body.map(|b| String::from_utf8_lossy(&b).to_string()) }); Box::pin(async move { let reply = self.0.ask(message).await; Ok(Response { status: reply.get("status").and_then(Value::as_u64).unwrap_or(200) as u16, headers: reply.get("headers").and_then(Value::as_object).map(|h| h.iter().map(|(k, v)| (k.clone(), js_string(Some(v)))).collect()).unwrap_or_default(), body: js_string(reply.get("body")).into_bytes(), }) }) } } /// The JS side's call; session: its `session` option's (runModule's, which mints http serviceAuth) instead. struct BridgeCall(Arc, bool); impl BridgeCall { // The JS stub sees signal.aborted (with this reason) while the call is pending, and when the // Rust side drops the call unfinished (a subscription stopped while opening), as dropping a real // call's future cancels it. fn watch_abort(&self, k: u64, signal: Signal, done: oneshot::Receiver<()>) { let io = self.0.clone(); tokio::spawn(async move { tokio::select! { biased; reason = signal.aborted() => io.write(json!({ "t": "abort", "k": k, "reason": reason })), done = done => if done.is_err() { io.write(json!({ "t": "abort", "k": k, "reason": signal.reason().unwrap_or_else(|| "cancelled".into()) })); }, } }); } } impl Call for BridgeCall { fn call(&self, nsid: String, request: Request) -> BoxFut<'_, Result, String>> { Box::pin(async move { let (k, (tx, rx)) = (self.0.key(), oneshot::channel()); self.0.waiters.lock().unwrap().insert(k, Waiter::Once(tx)); let (done_tx, done_rx) = oneshot::channel(); self.watch_abort(k, request.signal.clone(), done_rx); if let Some(emit) = request.emit.clone() { self.0.emits.lock().unwrap().insert(k, emit); } self.0 .write(json!({ "t": "call", "k": k, "nsid": nsid, "request": request.to_json(), "session": self.1 })); let reply = rx .await .unwrap_or_else(|_| json!({ "ok": false, "error": "bridge closed" })); self.0.emits.lock().unwrap().remove(&k); let _ = done_tx.send(()); outcome(&reply) }) } fn subscribe(&self, nsid: String, request: Request) -> BoxFut<'_, Result> { Box::pin(async move { let k = self.0.key(); let (tx, mut replies) = mpsc::unbounded_channel(); self.0.waiters.lock().unwrap().insert(k, Waiter::Stream(tx)); let (done_tx, done_rx) = oneshot::channel(); self.watch_abort(k, request.signal.clone(), done_rx); self.0 .write(json!({ "t": "call", "k": k, "nsid": nsid, "request": request.to_json(), "session": self.1 })); let first = replies .recv() .await .unwrap_or_else(|| json!({ "ok": false, "error": "bridge closed" })); if first.get("open") != Some(&Value::Bool(true)) { self.0.waiters.lock().unwrap().remove(&k); return Err(js_string(first.get("error"))); } let (msg_tx, messages) = mpsc::unbounded_channel(); let (commands, mut cmd_rx) = mpsc::unbounded_channel::(); let io = self.0.clone(); tokio::spawn(async move { while let Some(reply) = replies.recv().await { if let Some(data) = reply.get("message") { let _ = msg_tx.send(Ok(data.clone())); } else if reply.get("end").is_some() { if let Some(e) = reply.get("error") { let _ = msg_tx.send(Err(js_string(Some(e)))); } break; } } io.waiters.lock().unwrap().remove(&k); }); let io = self.0.clone(); tokio::spawn(async move { while let Some(command) = cmd_rx.recv().await { match command { SubCmd::Send(data, ack) => { let reply = io.ask(json!({ "t": "send", "sub": k, "data": data })).await; let _ = ack.send(if reply.get("nosend").is_some() { Err(NOSEND.into()) } else { outcome(&reply).map(drop) }); } SubCmd::Close => break, } } io.write(json!({ "t": "close", "sub": k })); let _ = done_tx.send(()); }); Ok(Sub { messages, commands }) }) } } pub async fn main() -> Result<(), String> { let io = Arc::new(Io::default()); let mut lines = tokio::io::BufReader::new(tokio::io::stdin()).lines(); let start: Value = serde_json::from_str( &lines .next_line() .await .map_err(|e| e.to_string())? .ok_or("bridge: no start line")?, ) .map_err(|e| e.to_string())?; // { rfpPolicyRoutes: { manifest, from, records } }: compose::rfp_policy_routes alone, no run if let Some(ask) = start.get("rfpPolicyRoutes") { let records = ask["records"].as_array().cloned().unwrap_or_default(); io.write(match atlibs_host::compose::rfp_policy_routes(&ask["manifest"], &js_string(ask.get("from")), &records) { Ok(manifest) => json!({ "t": "done", "manifest": manifest }), Err(error) => json!({ "t": "done", "error": error }), }); return Ok(()); } let (input_tx, input_rx) = mpsc::unbounded_channel(); let signal = Signal::new(); { let (io, signal) = (io.clone(), signal.clone()); tokio::spawn(async move { let mut input_tx = Some(input_tx); while let Ok(Some(line)) = lines.next_line().await { let Ok(message) = serde_json::from_str::(&line) else { continue; }; match message.get("t").and_then(Value::as_str) { Some("input") => drop(input_tx.as_ref().map(|tx| { tx.send(Ok(message.get("data").cloned().unwrap_or(Value::Null))) })), Some("inputs-end") => { if let (Some(tx), Some(e)) = (&input_tx, message.get("error")) { let _ = tx.send(Err(js_string(Some(e)))); } input_tx = None; } Some("abort") => signal.abort("aborted"), Some("emit") => { let emit = message .get("k") .and_then(Value::as_u64) .and_then(|k| io.emits.lock().unwrap().get(&k).cloned()); if let Some(emit) = emit { emit.emit(message.get("event").cloned().unwrap_or(Value::Null)); } } _ => io.deliver(message), } } }); } let spec = &start["module"]; // a refused module is the run's error, as the JS runModule throws it let mut module = match Module::from_manifest(vec![], spec) { Ok(module) => module, Err(error) => { io.write(json!({ "t": "done", "error": error })); return Ok(()); } }; module.sha256 = spec.get("sha256").and_then(Value::as_str).map(String::from); // the JS side's moduleOwner (it has the bytes when ctx.wasm.run is bridged) module.owner = spec.get("owner").and_then(Value::as_str).map(String::from); let runner: Arc = match spec.get("wasm").and_then(Value::as_str) { Some(path) => { module.bytes = Arc::new(std::fs::read(path).map_err(|e| format!("{path}: {e}"))?); Arc::new(Bound { wasm: Wasm::new(atlibs_host::wasm::WALL_TIME_MS), bytes: module.bytes.clone(), sha256: module.sha256.clone(), }) } None => Arc::new(BridgeRunner(io.clone())), }; let ledger: Option> = if start.get("ledger") == Some(&Value::Bool(true)) { Some(Arc::new(BridgeLedger(io.clone()))) } else { None }; let ctx = Ctx { runner, ledger, now: start.get("now").and_then(Value::as_str).map(String::from), }; let mut call: Arc = Arc::new(BridgeCall(io.clone(), false)); if let Some(didkey) = start.get("didkey") { let services = didkey .get("services") .and_then(Value::as_array) .map(|s| { s.iter() .filter_map(|x| x.as_str().map(String::from)) .collect() }) .unwrap_or_default(); call = Arc::new(DidkeyCall { key: DidKey::new(didkey.get("jwk"))?, services, fetch: Arc::new(BridgeFetch(io.clone(), None)), inner: call, store: None, // the suite's own did:key, no state dir }); } let entry = js_string(start.get("entry")); let input = start .get("input") .and_then(Value::as_object) .cloned() .unwrap_or_default(); let options = Options { signal: Some(signal), inputs: (start.get("inputs") == Some(&Value::Bool(true))).then_some(input_rx), budget: start.get("budget").cloned(), on_event: (start.get("onEvent") == Some(&Value::Bool(true))).then(|| { let io = io.clone(); Arc::new(move |event: &Value, out: &Value| { io.write(json!({ "t": "event", "event": event, "out": out })) }) as run::OnEvent }), load: None, provides: None, http: match start.get("http") { Some(allow) => { let mut http = run::Http::from_json(Arc::new(BridgeFetch(io.clone(), None)), allow)?; for scheme in start["fetchers"].as_array().into_iter().flatten().filter_map(Value::as_str) { http.schemes.insert(scheme.into(), Arc::new(BridgeFetch(io.clone(), Some(scheme.into())))); } Some(Arc::new(http)) } None => None, }, held: start.get("held").and_then(Value::as_object).cloned(), session: (start.get("session") == Some(&Value::Bool(true))).then(|| Arc::new(BridgeCall(io.clone(), true)) as Arc), }; let ran = run::run_module(&ctx, &module, &entry, &input, call, options).await; // Let the run end's aborts and closes reach the stubs before the process exits. tokio::time::sleep(std::time::Duration::from_millis(5)).await; io.write(json!({ "t": "done", "ran": ran })); Ok(()) }