maarcadetweet: initial commit

AT Protocol PDS + AppView + Tauri Desktop Client, 160-char post limit.

- PDS (Rust + axum + sqlx)
  - Auth: createAccount, createSession, refreshSession
  - Records: createRecord, deleteRecord (race-safe via SELECT FOR UPDATE)
  - Feed: feed.like.create, feed.repost.create
  - Sync: getRepo, getBlocks, getLatestCommit, getRecord (with MST proof), listRepos
  - Identity: resolveHandle
  - MST: spec-conformant (at-mst crate, 27 tests)
  - Repo: signed commits, TID counter (monotonic, 4096 wrap safe)

- AppView (Rust + axum + sqlx)
  - Jetstream consumer (WebSocket, exponential backoff, 38k+ events indexed)
  - REST API: timeline/home (graph-aware), profile, search, post (with thread hydration)
  - Handle-sync worker (did:plc + did:web)
  - JSONB embed storage + thread columns (migration 0003)
  - Like/repost counter cache (migration 0004)

- Tauri 2 + Svelte 5 Desktop Client
  - System tray (Show/Compose/Quit menu)
  - OS notifications (tauri-plugin-notification)
  - Auto-update (tauri-plugin-updater, placeholder endpoint)
  - Window-state (tauri-plugin-window-state)
  - 160-char compose with live counter
  - Image/Link embed rendering
  - LocalStorage-persisted like state
  - Timeline with poll (prepend new posts)
  - Custom TitleBar (transparent, no decorations)
  - Orange/IBM Plex Mono maarcade design

Tests: 231 Rust + 9 vitest = 240 passed.
This commit is contained in:
tomdebone
2026-07-05 20:01:31 +02:00
commit c586fd39c9
134 changed files with 35279 additions and 0 deletions
+204
View File
@@ -0,0 +1,204 @@
use anyhow::{anyhow, Result};
use at_crypto::cid::cid_for_cbor;
use at_crypto::signing::verify_dag_cbor;
use cid::Cid;
use k256::ecdsa::VerifyingKey;
use serde_json::Value;
/// A signed repository commit.
///
/// `Commit` carries the canonical DAG-CBOR serialization of a signed commit
/// block (including the `sig` field) together with the parsed fields. The
/// `cid` is the SHA-256 DAG-CBOR content-address of `signed_bytes`.
///
/// The `data` field is `Option<Cid>` to support commits on empty repositories
/// — an empty repo has no MST root to point at, so the JSON payload's `data`
/// is serialized as `null`.
#[derive(Debug, Clone)]
pub struct Commit {
pub cid: Cid,
pub signed_bytes: Vec<u8>,
pub did: String,
pub rev: String,
pub prev: Option<Cid>,
pub data: Option<Cid>,
}
impl Commit {
/// Verify the commit's signature.
///
/// Delegates the actual cryptographic check to [`at_crypto::signing::verify_dag_cbor`],
/// which uses the `pubkey` field embedded inside the signed commit. The
/// `signing_pubkey` argument is the caller-trusted key — we additionally
/// require the embedded pubkey to match it, so a malicious swap of the
/// `pubkey` field (followed by a forged signature under the swapped key)
/// is rejected.
pub fn verify(&self, signing_pubkey: &VerifyingKey) -> Result<()> {
let embedded = verify_dag_cbor(&self.signed_bytes)?;
if &embedded != signing_pubkey {
return Err(anyhow!(
"commit embedded pubkey does not match expected signing pubkey"
));
}
Ok(())
}
/// Parse a signed commit block out of raw DAG-CBOR bytes.
///
/// This is used by `Repo::load` to reconstruct the head commit when
/// re-hydrating a repository from a blockstore.
pub fn from_signed_bytes(signed_bytes: Vec<u8>) -> Result<Self> {
let cid = cid_for_cbor(&signed_bytes)?;
let value: Value = ciborium::from_reader(&signed_bytes[..])
.map_err(|e| anyhow!("invalid commit CBOR: {e}"))?;
let obj = value
.as_object()
.ok_or_else(|| anyhow!("commit CBOR is not an object"))?;
let did = obj
.get("did")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("commit missing `did`"))?
.to_string();
let rev = obj
.get("rev")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("commit missing `rev`"))?
.to_string();
let prev = parse_optional_cid(obj.get("prev"), "prev")?;
let data = parse_optional_cid(obj.get("data"), "data")?;
Ok(Self {
cid,
signed_bytes,
did,
rev,
prev,
data,
})
}
}
fn parse_optional_cid(value: Option<&Value>, field: &str) -> Result<Option<Cid>> {
match value {
None | Some(Value::Null) => Ok(None),
Some(Value::String(s)) => s
.parse::<Cid>()
.map(Some)
.map_err(|e| anyhow!("commit `{field}` is not a valid CID: {e}")),
Some(_) => Err(anyhow!("commit `{field}` must be null or string")),
}
}
#[cfg(test)]
mod tests {
use super::*;
use at_crypto::did_key::pubkey_to_multibase;
use k256::ecdsa::SigningKey;
use k256::SecretKey;
use k256::PublicKey;
fn make_test_commit(
sk: &SigningKey,
did: &str,
rev: &str,
prev: Option<&str>,
data: Option<&str>,
) -> Commit {
let pk: PublicKey = sk.verifying_key().into();
let mb = pubkey_to_multibase(&pk).unwrap();
let mut payload = serde_json::json!({
"did": did,
"version": 3,
"prev": prev.map(|s| serde_json::Value::String(s.to_string()))
.unwrap_or(serde_json::Value::Null),
"data": data.map(|s| serde_json::Value::String(s.to_string()))
.unwrap_or(serde_json::Value::Null),
"rev": rev,
"pubkey": mb,
});
let _ = payload.as_object_mut().unwrap().remove("sig");
let signed = at_crypto::signing::sign_dag_cbor(sk, &payload).unwrap();
Commit::from_signed_bytes(signed.signed_bytes).unwrap()
}
#[test]
fn self_signed_commit_verifies() {
let sk = SigningKey::from(SecretKey::from_slice(&[7u8; 32]).unwrap());
let commit = make_test_commit(&sk, "did:plc:abc", "0", None, None);
assert!(commit.verify(&sk.verifying_key()).is_ok());
}
#[test]
fn wrong_key_fails_verify() {
let sk = SigningKey::from(SecretKey::from_slice(&[7u8; 32]).unwrap());
let sk2 = SigningKey::from(SecretKey::from_slice(&[9u8; 32]).unwrap());
let commit = make_test_commit(&sk, "did:plc:abc", "0", None, None);
assert!(commit.verify(&sk2.verifying_key()).is_err());
}
#[test]
fn commit_with_prev_and_data_roundtrip() {
let sk = SigningKey::from(SecretKey::from_slice(&[11u8; 32]).unwrap());
let prev_cid: Cid = "bafyreig7qfkqdk5v3jy3z6xgc4n3yh6ycjxqrvt5pqjpwxgvvcxyzw7tqy"
.parse()
.unwrap();
let data_cid: Cid = "bafyreihzfgvyuwdq5i3qaqj2vnlv4bgw2xhcsoa2uh2pqkpcw55nuefzzi"
.parse()
.unwrap();
let commit = make_test_commit(
&sk,
"did:plc:abc",
"abc123",
Some(&prev_cid.to_string()),
Some(&data_cid.to_string()),
);
assert_eq!(commit.did, "did:plc:abc");
assert_eq!(commit.rev, "abc123");
assert_eq!(commit.prev, Some(prev_cid));
assert_eq!(commit.data, Some(data_cid));
assert!(commit.verify(&sk.verifying_key()).is_ok());
}
#[test]
fn tampered_signed_bytes_fail_verify() {
let sk = SigningKey::from(SecretKey::from_slice(&[15u8; 32]).unwrap());
let commit = make_test_commit(&sk, "did:plc:abc", "0", None, None);
// Flip a bit in `sig` (last bytes of the CBOR object). The simplest
// way to land inside `sig` (a hex string of ~128 chars) is to flip a
// byte near the tail of the payload.
let mut tampered_bytes = commit.signed_bytes.clone();
let len = tampered_bytes.len();
tampered_bytes[len - 4] ^= 0x01;
tampered_bytes[len - 3] ^= 0x01;
let res = match Commit::from_signed_bytes(tampered_bytes) {
Ok(c) => c.verify(&sk.verifying_key()),
Err(e) => Err(e),
};
assert!(
res.is_err(),
"tampered commit must not verify; got Ok"
);
}
#[test]
fn tampered_field_fails_verify() {
let sk = SigningKey::from(SecretKey::from_slice(&[16u8; 32]).unwrap());
let commit = make_test_commit(&sk, "did:plc:abc", "0", None, None);
// Parse the signed CBOR object, swap the `did`, and re-encode. The
// resulting CID is different, but the signature over the unsigned
// payload is now stale — verification must fail.
let mut value: Value = ciborium::from_reader(commit.signed_bytes.as_slice()).unwrap();
{
let obj = value.as_object_mut().unwrap();
obj.insert("did".into(), Value::String("did:plc:imposter".into()));
}
let mut new_bytes = Vec::new();
ciborium::into_writer(&value, &mut new_bytes).unwrap();
let res = match Commit::from_signed_bytes(new_bytes) {
Ok(c) => c.verify(&sk.verifying_key()),
Err(e) => Err(e),
};
assert!(res.is_err(), "did swap must invalidate signature");
}
}