techne-sdk 0.3.5

TechneSDK's logic written once: version ranking (eventState) and offline record verification over the published check, shipped to TypeScript as WebAssembly; with opt-in atproto plumbing (service auth, DID resolution, signed labels, proven record fetches, PDS password sessions) behind the `crypto` and `net` features.
Documentation
//! techne-sdk (working name; crates.io `techne-sdk` per PLAN ยง Repos): the TechneSDK logic that must
//! answer the same in every language, written once here and shipped to TypeScript as WebAssembly
//! (`sdk/src/wasm`, built by `sdk/scripts/build-wasm.sh`).
//!
//! - [`versions`]: how versions rank and compare, and `eventState` (what a screen shows for one event
//!   or place). The rules curator composes curated records with the same functions.
//! - [`verify`]: what an app can check offline about a record it read.
//! - The pure check itself is `techne_lexicon_check` (validity, placement, provenance, `decide`, the
//!   credential chain), re-exported as [`check`].
//! - [`put`]: `put`'s plan, the order its writes go out in (private first, public last) and their
//!   deterministic rkeys and CIDs (P13.2), and its deletes placed by the same rule (`putPlan`).
//!
//! No platform crate: by default only the published check and serde/chrono; `crypto`/`net`/`tap` add crates.io ones.
//!
//! [`call`] is the one boundary every other language crosses: a function name and JSON arguments in,
//! JSON out. The WebAssembly exports it unchanged ([`wasm`], wasm32 only), and the shared fixtures
//! (`fixtures/cases.json`) run through it in Rust and through the TypeScript package in Node.

#![deny(unsafe_code)]

pub use techne_lexicon_check as check;

pub mod put;
mod renames;
pub mod verify;
pub mod versions;

// The WebAssembly exports are the crate's only unsafe code: raw pointers across linear memory.
#[cfg(target_arch = "wasm32")]
#[allow(unsafe_code)]
mod wasm;

use serde::de::DeserializeOwned;
use serde_json::{json, Value};

fn arg<T: DeserializeOwned>(args: &Value, name: &str) -> Result<T, String> {
    T::deserialize(args.get(name).unwrap_or(&Value::Null)).map_err(|e| format!("{name}: {e}"))
}

fn str_arg<'a>(args: &'a Value, name: &str) -> Result<&'a str, String> {
    args.get(name).and_then(Value::as_str).ok_or_else(|| format!("{name}: expected a string"))
}

/// `now` (unix seconds), which `function` cannot do without: the WebAssembly has no clock.
fn now_arg(args: &Value, function: &str) -> Result<u64, String> {
    arg::<Option<u64>>(args, "now")?.ok_or_else(|| {
        format!("{function}: now (unix seconds) is required: the WebAssembly has no clock, and an old name's lexicon depends on the day")
    })
}

/// Run one SDK function by name over JSON arguments (an object of named arguments).
pub fn call(function: &str, args: &Value) -> Result<Value, String> {
    let out = match function {
        "eventState" => {
            let data: versions::GetVersions = arg(args, "data")?;
            let picked: Option<String> = arg(args, "picked")?;
            json!(versions::event_state(&data, picked.as_deref()))
        }
        "comparable" => versions::shown_as(str_arg(args, "field")?, args.get("value").unwrap_or(&Value::Null)),
        "validate" => {
            let collection = str_arg(args, "collection")?;
            let problems = check::validate(collection, args.get("body").unwrap_or(&Value::Null)).err().unwrap_or_default();
            json!({"valid": problems.is_empty(), "problems": problems, "publicHome": check::has_public_home(collection)})
        }
        "decide" => {
            let claims: verify::Claims = arg(args, "claims")?;
            let body = args.get("body").unwrap_or(&Value::Null);
            let space: Option<String> = arg(args, "space")?;
            let version = check::Version {
                collection: str_arg(args, "collection")?,
                author: str_arg(args, "author")?,
                rkey: str_arg(args, "rkey")?,
                body,
                space: space.as_deref(),
            };
            let first_seen: Option<String> = arg(args, "firstSeen")?;
            json!(check::decide(&version, first_seen.as_deref().unwrap_or_default(), &claims))
        }
        // `now` (unix seconds), required: the caller's clock, which the WebAssembly lacks
        // (`put::plan_at`). Without it the plan would be as of 0, when every old name still had its
        // rename and so a lexicon; refused instead. Native callers have `put::plan`, on the system clock.
        "put" => {
            let records: Vec<put::Record> = arg(args, "records")?;
            json!(put::plan_at(str_arg(args, "repo")?, records, now_arg(args, "put")?)?)
        }
        "putPlan" => {
            let records: Vec<put::Record> = arg(args, "records")?;
            let deletes: Option<Vec<put::DeleteRecord>> = arg(args, "deletes")?;
            let (repo, deletes) = (str_arg(args, "repo")?, deletes.unwrap_or_default());
            json!(put::plan_with_deletes_at(repo, records, deletes, now_arg(args, "putPlan")?)?)
        }
        "verify" => {
            let record: verify::Record = arg(args, "record")?;
            let claims: verify::Claims = arg(args, "claims")?;
            json!(verify::verify(&record, &claims))
        }
        other => return Err(format!("no SDK function {other}")),
    };
    Ok(out)
}

// Opt-in atproto plumbing services share (P20.7 G4), off by default and never in the WebAssembly. Declared
// last so the default build's code keeps its line numbers (panic locations are in the shipped wasm).
// `crypto` (pure): DID documents, service-auth JWTs, signed labels, record proofs, service-account
// passwords. `net` (I/O): the DID key resolver, `ServiceAuth`, verified record fetches, password sessions.

#[cfg(feature = "crypto")]
pub mod account;
#[cfg(feature = "crypto")]
pub mod did;
#[cfg(feature = "crypto")]
pub mod labels;
#[cfg(feature = "crypto")]
pub mod service_jwt;
#[cfg(feature = "crypto")]
pub mod sync;

#[cfg(feature = "net")]
pub mod fetch;
#[cfg(feature = "net")]
pub mod service_auth;
#[cfg(feature = "net")]
pub mod session;

/// Service auth in one place: verify an inbound atproto service JWT (`crypto`), and with `net` resolve
/// its issuer's key and refuse replays ([`ServiceAuth`](auth::ServiceAuth)).
#[cfg(feature = "crypto")]
pub mod auth {
    pub use crate::service_jwt::{mint_service_jwt, precheck_claims, verify_service_jwt, ServiceJwtError, VerifiedClaims};
    #[cfg(feature = "net")]
    pub use crate::did::{CommonDidKeyResolver, DidKeyResolver, InMemoryDidKeyResolver};
    #[cfg(feature = "net")]
    pub use crate::service_auth::{AuthError, InProcessJtiCache, JtiCache, ServiceAuth};
}

/// The stock client for a Tap's `/channel` (feature `tap`, native only): verified events delivered to
/// a sink, each frame acked once they are durable (P20.7). Kept last so the WebAssembly is unchanged.
#[cfg(feature = "tap")]
pub mod tap;