Skip to main content

techne_sdk/
lib.rs

1//! techne-sdk (working name; crates.io `techne-sdk` per PLAN ยง Repos): the TechneSDK logic that must
2//! answer the same in every language, written once here and shipped to TypeScript as WebAssembly
3//! (`sdk/src/wasm`, built by `sdk/scripts/build-wasm.sh`).
4//!
5//! - [`versions`]: how versions rank and compare, and `eventState` (what a screen shows for one event
6//!   or place). The rules curator composes curated records with the same functions.
7//! - [`verify`]: what an app can check offline about a record it read.
8//! - The pure check itself is `techne_lexicon_check` (validity, placement, provenance, `decide`, the
9//!   credential chain), re-exported as [`check`].
10//! - [`put`]: `put`'s plan, the order its writes go out in (private first, public last) and their
11//!   deterministic rkeys and CIDs (P13.2), and its deletes placed by the same rule (`putPlan`).
12//!
13//! No platform crate: by default only the published check and serde/chrono; `crypto`/`net`/`tap` add crates.io ones.
14//!
15//! [`call`] is the one boundary every other language crosses: a function name and JSON arguments in,
16//! JSON out. The WebAssembly exports it unchanged ([`wasm`], wasm32 only), and the shared fixtures
17//! (`fixtures/cases.json`) run through it in Rust and through the TypeScript package in Node.
18
19#![deny(unsafe_code)]
20
21pub use techne_lexicon_check as check;
22
23pub mod put;
24mod renames;
25pub mod verify;
26pub mod versions;
27
28// The WebAssembly exports are the crate's only unsafe code: raw pointers across linear memory.
29#[cfg(target_arch = "wasm32")]
30#[allow(unsafe_code)]
31mod wasm;
32
33use serde::de::DeserializeOwned;
34use serde_json::{json, Value};
35
36fn arg<T: DeserializeOwned>(args: &Value, name: &str) -> Result<T, String> {
37    T::deserialize(args.get(name).unwrap_or(&Value::Null)).map_err(|e| format!("{name}: {e}"))
38}
39
40fn str_arg<'a>(args: &'a Value, name: &str) -> Result<&'a str, String> {
41    args.get(name).and_then(Value::as_str).ok_or_else(|| format!("{name}: expected a string"))
42}
43
44/// `now` (unix seconds), which `function` cannot do without: the WebAssembly has no clock.
45fn now_arg(args: &Value, function: &str) -> Result<u64, String> {
46    arg::<Option<u64>>(args, "now")?.ok_or_else(|| {
47        format!("{function}: now (unix seconds) is required: the WebAssembly has no clock, and an old name's lexicon depends on the day")
48    })
49}
50
51/// Run one SDK function by name over JSON arguments (an object of named arguments).
52pub fn call(function: &str, args: &Value) -> Result<Value, String> {
53    let out = match function {
54        "eventState" => {
55            let data: versions::GetVersions = arg(args, "data")?;
56            let picked: Option<String> = arg(args, "picked")?;
57            json!(versions::event_state(&data, picked.as_deref()))
58        }
59        "comparable" => versions::shown_as(str_arg(args, "field")?, args.get("value").unwrap_or(&Value::Null)),
60        "validate" => {
61            let collection = str_arg(args, "collection")?;
62            let problems = check::validate(collection, args.get("body").unwrap_or(&Value::Null)).err().unwrap_or_default();
63            json!({"valid": problems.is_empty(), "problems": problems, "publicHome": check::has_public_home(collection)})
64        }
65        "decide" => {
66            let claims: verify::Claims = arg(args, "claims")?;
67            let body = args.get("body").unwrap_or(&Value::Null);
68            let space: Option<String> = arg(args, "space")?;
69            let version = check::Version {
70                collection: str_arg(args, "collection")?,
71                author: str_arg(args, "author")?,
72                rkey: str_arg(args, "rkey")?,
73                body,
74                space: space.as_deref(),
75            };
76            let first_seen: Option<String> = arg(args, "firstSeen")?;
77            json!(check::decide(&version, first_seen.as_deref().unwrap_or_default(), &claims))
78        }
79        // `now` (unix seconds), required: the caller's clock, which the WebAssembly lacks
80        // (`put::plan_at`). Without it the plan would be as of 0, when every old name still had its
81        // rename and so a lexicon; refused instead. Native callers have `put::plan`, on the system clock.
82        "put" => {
83            let records: Vec<put::Record> = arg(args, "records")?;
84            json!(put::plan_at(str_arg(args, "repo")?, records, now_arg(args, "put")?)?)
85        }
86        "putPlan" => {
87            let records: Vec<put::Record> = arg(args, "records")?;
88            let deletes: Option<Vec<put::DeleteRecord>> = arg(args, "deletes")?;
89            let (repo, deletes) = (str_arg(args, "repo")?, deletes.unwrap_or_default());
90            json!(put::plan_with_deletes_at(repo, records, deletes, now_arg(args, "putPlan")?)?)
91        }
92        "verify" => {
93            let record: verify::Record = arg(args, "record")?;
94            let claims: verify::Claims = arg(args, "claims")?;
95            json!(verify::verify(&record, &claims))
96        }
97        other => return Err(format!("no SDK function {other}")),
98    };
99    Ok(out)
100}
101
102// Opt-in atproto plumbing services share (P20.7 G4), off by default and never in the WebAssembly. Declared
103// last so the default build's code keeps its line numbers (panic locations are in the shipped wasm).
104// `crypto` (pure): DID documents, service-auth JWTs, signed labels, record proofs, service-account
105// passwords. `net` (I/O): the DID key resolver, `ServiceAuth`, verified record fetches, password sessions.
106
107#[cfg(feature = "crypto")]
108pub mod account;
109#[cfg(feature = "crypto")]
110pub mod did;
111#[cfg(feature = "crypto")]
112pub mod labels;
113#[cfg(feature = "crypto")]
114pub mod service_jwt;
115#[cfg(feature = "crypto")]
116pub mod sync;
117
118#[cfg(feature = "net")]
119pub mod fetch;
120#[cfg(feature = "net")]
121pub mod service_auth;
122#[cfg(feature = "net")]
123pub mod session;
124
125/// Service auth in one place: verify an inbound atproto service JWT (`crypto`), and with `net` resolve
126/// its issuer's key and refuse replays ([`ServiceAuth`](auth::ServiceAuth)).
127#[cfg(feature = "crypto")]
128pub mod auth {
129    pub use crate::service_jwt::{mint_service_jwt, precheck_claims, verify_service_jwt, ServiceJwtError, VerifiedClaims};
130    #[cfg(feature = "net")]
131    pub use crate::did::{CommonDidKeyResolver, DidKeyResolver, InMemoryDidKeyResolver};
132    #[cfg(feature = "net")]
133    pub use crate::service_auth::{AuthError, InProcessJtiCache, JtiCache, ServiceAuth};
134}
135
136/// The stock client for a Tap's `/channel` (feature `tap`, native only): verified events delivered to
137/// a sink, each frame acked once they are durable (P20.7). Kept last so the WebAssembly is unchanged.
138#[cfg(feature = "tap")]
139pub mod tap;