johnandersen777.bsky.social/atlibs-socialweb-computer-requester
socialweb.computer market requester and policy modules for atlibs, with its web pages and lexicons
0
0
use std::collections::BTreeMap;
use abi::{Call, Datetime, Did, StrongRef, TypedStrongRef};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value, json};
use crate::ids::CallId;
pub type Step<S> = abi::Step<Args, S, CallId>;
pub type Out = abi::Out<CallId>;
pub const MAX_PEERS: usize = 10;
static NULL: Value = Value::Null;
/// An action's fields: `did` (absent when signed out), `now`, and the form fields, which arrive as
/// strings from a form and as JSON from a caller.
#[derive(Deserialize)]
#[serde(try_from = "Map<String, Value>")]
pub struct Args {
fields: Map<String, Value>,
}
impl TryFrom<Map<String, Value>> for Args {
type Error = String;
fn try_from(fields: Map<String, Value>) -> Result<Self, String> {
match fields.get("did") {
None | Some(Value::Null) => Ok(Args { fields }),
Some(did) => Did::try_from(did.as_str().unwrap_or_default().to_string()).map(|_| Args { fields }),
}
}
}
impl Args {
pub fn signed_in(&self) -> Result<(), String> {
self.arg("did").as_str().map(|_| ()).ok_or("invalid input: missing field `did`".into())
}
pub fn me(&self) -> &str {
self.arg("did").as_str().unwrap_or_default()
}
pub fn arg(&self, key: &str) -> &Value {
self.fields.get(key).unwrap_or(&NULL)
}
pub fn created_at(&self) -> Result<Datetime, String> {
Datetime::new(self.arg("now").as_str().unwrap_or_default()).map_err(|e| format!("invalid now: {e}"))
}
pub fn number(&self, key: &str, default: u64, max: u64) -> u64 {
self.arg(key).as_u64().unwrap_or(default).min(max)
}
}
/// Where a peer XRPC goes: an https origin, called directly; did#service, through the PDS (atproto-proxy);
/// or a DID's own endpoints (a bidder's offering.endpoints, as { did, endpoints }): the host picks one it
/// reaches (iroh, webrtc, https) and sends service auth aud did#pdr_temp_market.
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Route {
Service(String),
Proxy(String),
Peer(String, Value),
}
impl Route {
pub fn call(&self, nsid: &str, input: impl Serialize) -> Call {
let call = Call::new("", nsid).input(json!(input));
match self {
Route::Service(origin) => call.service(origin),
Route::Proxy(target) => call.proxy(target),
Route::Peer(target, endpoints) => call.endpoints(target, endpoints.clone()),
}
}
}
pub fn did(value: &Value, label: &str) -> Result<String, String> {
match value.as_str() {
Some(s) if s.starts_with("did:") && s.len() > 6 && !s.contains(['#', '?', '/']) && !s.chars().any(char::is_whitespace) => Ok(s.to_string()),
_ => Err(format!("{label} must be a DID")),
}
}
/// did#service; a bare DID gets `service`.
pub fn target(value: &Value, service: &str, label: &str) -> Result<String, String> {
let s = value.as_str().ok_or(format!("{label} must be a DID or did#service"))?;
let (d, fragment) = s.split_once('#').unwrap_or((s, service));
let d = did(&json!(d), label)?;
if fragment.is_empty() || !fragment.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') {
return Err(format!("{label} must be a DID or did#service"));
}
Ok(format!("{d}#{fragment}"))
}
/// { did, endpoints } for a peer whose offering lists endpoints; None when it lists none.
pub fn peer(did: &str, offering: &Value) -> Option<Value> {
let endpoints = offering["endpoints"].as_array().filter(|e| !e.is_empty())?;
Some(json!({ "did": did, "endpoints": endpoints }))
}
pub fn route(value: &Value, label: &str) -> Result<Route, String> {
if value.is_object() {
let endpoints = value["endpoints"].as_array().filter(|e| !e.is_empty() && e.len() <= 8).ok_or(format!("{label} endpoints must be 1 to 8 {{ kind, address }}"))?;
return Ok(Route::Peer(target(&value["did"], "pdr_temp_market", label)?, json!(endpoints)));
}
match value.as_str() {
Some(url) if url.starts_with("https://") => {
let origin = url.trim_end_matches('/');
let host = &origin["https://".len()..];
let (name, port) = host.split_once(':').unwrap_or((host, "443"));
if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') || port.is_empty() || !port.chars().all(|c| c.is_ascii_digit()) {
return Err(format!("{label} must be an https origin or a DID"));
}
Ok(Route::Service(origin.to_string()))
}
_ => Ok(Route::Proxy(target(value, "pdr_temp_market", label)?)),
}
}
pub fn optional_route(value: &Value, label: &str) -> Result<Option<Route>, String> {
match value {
Value::Null => Ok(None),
value => route(value, label).map(Some),
}
}
/// A strong ref, or its JSON (a form field).
pub fn strong(value: &Value, label: &str) -> Result<TypedStrongRef, String> {
if let Some(text) = value.as_str() {
return strong(&serde_json::from_str(text).map_err(|_| format!("{label} must be a strong ref {{uri, cid}}"))?, label);
}
strong_ref(value["uri"].as_str(), value["cid"].as_str(), label)
}
pub fn strong_ref(uri: Option<&str>, cid: Option<&str>, label: &str) -> Result<TypedStrongRef, String> {
let uri = uri.filter(|u| u.starts_with("at://did:"));
let cid = cid.filter(|c| !c.is_empty());
match (uri, cid) {
(Some(uri), Some(cid)) => Ok(TypedStrongRef::new(StrongRef { uri: uri.into(), cid: cid.into() })),
_ => Err(format!("{label} must be a strong ref {{uri, cid}}")),
}
}
pub fn at_uri<'a>(value: &'a Value, label: &str) -> Result<&'a str, String> {
value.as_str().filter(|u| u.starts_with("at://")).ok_or(format!("{label} must be an at:// URI"))
}
/// What a command reads on stdin: secrets go here, never in the command (an argument of ssh on the
/// host and of the login shell on the guest, so ps shows it on both).
pub fn stdin(value: &Value) -> Result<Option<String>, String> {
match value {
Value::Null => Ok(None),
Value::String(s) if s.len() <= 65536 => Ok(Some(s.clone())),
_ => Err("stdin must be a string of at most 65536 bytes".into()),
}
}
pub fn tag(value: &Value) -> Result<String, String> {
let tag = value.as_str().filter(|t| !t.is_empty() && t.len() <= 32 && t.bytes().all(|b| b.is_ascii_graphic()));
tag.map(str::to_string).ok_or("tag must be 1 to 32 printable ASCII bytes".into())
}
pub fn tags(value: &Value) -> Result<Vec<String>, String> {
match value {
Value::Null => Ok(vec!["ssh".into(), "shell".into()]),
Value::Array(list) if list.len() <= 6 => list.iter().map(tag).collect(),
_ => Err("tags must be a list of at most 6 tags".into()),
}
}
/// A list field: a JSON list, or a string of comma, space or newline separated items.
/// Peers to call: a list of https origins and DIDs (list), or of { did, endpoints } (offerings' output).
pub fn routes(value: &Value, label: &str) -> Result<Vec<Route>, String> {
match value {
Value::Array(peers) if peers.iter().any(Value::is_object) => {
if peers.len() > MAX_PEERS {
return Err(format!("at most {MAX_PEERS} {label}"));
}
peers.iter().map(|p| route(p, label)).collect()
}
value => list(value, label)?.iter().map(|p| route(&json!(p), label)).collect(),
}
}
pub fn list(value: &Value, label: &str) -> Result<Vec<String>, String> {
let list = match value {
Value::Null => return Ok(vec![]),
Value::String(text) => &text.split([',', ' ', '\n']).filter(|s| !s.is_empty()).map(|s| json!(s)).collect(),
Value::Array(list) => list,
_ => return Err(format!("{label} must be a list")),
};
if list.len() > MAX_PEERS {
return Err(format!("at most {MAX_PEERS} {label}"));
}
list.iter().map(|v| Ok(v.as_str().ok_or(format!("{label} must be DIDs"))?.to_string())).collect()
}
pub fn bidders(args: &Args) -> Result<Vec<String>, String> {
let bidders = list(args.arg("bidders"), "bidders")?.iter().map(|b| did(&json!(b), "bidders")).collect::<Result<Vec<_>, _>>()?;
if bidders.is_empty() {
return Err("bidders is required".into());
}
Ok(bidders)
}
/// Waits re-read on a timer (`poll_ms`) until the budget (`max_polls`) runs out, counted per timer.
#[derive(Default, Serialize, Deserialize)]
pub struct Polls(BTreeMap<String, u64>);
impl Polls {
/// Count this wait and set its timer when the budget allows another poll.
pub fn again(&mut self, args: &Args, out: &mut Out, timer: CallId) -> bool {
let polls = self.0.entry(timer.to_string()).or_default();
*polls += 1;
let again = *polls < args.number("max_polls", 40, 200);
if again {
out.timer(timer, args.number("poll_ms", 3000, 60_000));
}
again
}
}