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
+273
View File
@@ -0,0 +1,273 @@
//! `POST /internal/ingest-commit` — used by the PDS to push local commits
//! into the AppView so the user's own actions show up without waiting for
//! the Jetstream round-trip.
//!
//! Wire shape:
//! ```json
//! {
//! "did": "did:plc:abc",
//! "collection": "app.twi.post",
//! "action": "create",
//! "rkey": "3k2...",
//! "cid": "bafy...", // optional
//! "record": { ... }, // optional; required for follow delete
//! "subject_did": "did:plc:..." // required for app.bsky.graph.follow
//! }
//! ```
//!
//! In production this endpoint would be protected with mTLS and a token
//! minted by the PDS; for now it's open inside the cluster.
use crate::indexer;
use crate::state::AppState;
use axum::extract::State;
use axum::http::{HeaderMap, StatusCode};
use axum::Json;
use serde::Deserialize;
use serde_json::Value;
use tracing::{info, warn};
#[derive(Debug, Deserialize)]
pub struct IngestCommitReq {
pub did: String,
pub collection: String,
pub action: String,
pub rkey: String,
#[serde(default)]
pub cid: Option<String>,
#[serde(default)]
pub record: Option<Value>,
/// Required for `app.bsky.graph.follow` because the record value isn't
/// always preserved on delete events.
#[serde(default)]
pub subject_did: Option<String>,
}
/// Authenticate internal ingest requests.
/// - If `APPVIEW_INGEST_SECRET` env var is unset: dev mode, accept anything.
/// - If set: require `X-Ingest-Secret: <value>` header to match.
pub fn check_ingest_secret(
headers: &HeaderMap,
configured: Option<&str>,
) -> Result<(), (StatusCode, Json<Value>)> {
let Some(expected) = configured else {
return Ok(()); // dev mode
};
let provided = headers
.get("x-ingest-secret")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if constant_time_eq(provided.as_bytes(), expected.as_bytes()) {
Ok(())
} else {
Err((
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({
"error": "AuthenticationRequired",
"message": "missing or invalid X-Ingest-Secret",
})),
))
}
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
pub async fn ingest_commit(
State(state): State<AppState>,
headers: HeaderMap,
Json(req): Json<IngestCommitReq>,
) -> Result<Json<Value>, (StatusCode, Json<Value>)> {
check_ingest_secret(&headers, state.cfg.appview_ingest_secret.as_deref())?;
let result = apply(&state, &req).await;
if let Err((status, body)) = &result {
warn!(
status = status.as_u16(),
body = %body.0,
did = %req.did,
collection = %req.collection,
action = %req.action,
"ingest commit failed"
);
} else {
info!(did = %req.did, collection = %req.collection,
action = %req.action, rkey = %req.rkey, "ingested commit");
}
result.map(|applied| {
Json(serde_json::json!({
"ok": true,
"applied": applied,
}))
})
}
async fn apply(
state: &AppState,
req: &IngestCommitReq,
) -> Result<bool, (StatusCode, Json<Value>)> {
match (req.collection.as_str(), req.action.as_str()) {
("app.twi.post", "create") | ("app.bsky.feed.post", "create") => {
let record = req
.record
.clone()
.unwrap_or_else(|| serde_json::json!({"text": "", "createdAt": chrono::Utc::now().to_rfc3339()}));
let cid = req.cid.clone().unwrap_or_default();
let row = indexer::PostRow::from_record(
&req.did,
&req.rkey,
&req.collection,
&cid,
&record,
);
indexer::upsert_post(&state.db, &row).await.map_err(db_err)?;
Ok(true)
}
("app.twi.post", "delete") | ("app.bsky.feed.post", "delete") => {
let uri = format!("at://{}/{}/{}", req.did, req.collection, req.rkey);
indexer::delete_post(&state.db, &uri).await.map_err(db_err)?;
Ok(true)
}
("app.bsky.feed.like", "create") => {
indexer::upsert_like(
&state.db,
&req.did,
&req.rkey,
req.cid.as_deref(),
req.record.as_ref(),
)
.await
.map_err(db_err)?;
Ok(true)
}
("app.bsky.feed.like", "delete") => {
indexer::delete_like(&state.db, &req.did, &req.rkey)
.await
.map_err(db_err)?;
Ok(true)
}
("app.bsky.feed.repost", "create") => {
indexer::upsert_repost(
&state.db,
&req.did,
&req.rkey,
req.cid.as_deref(),
req.record.as_ref(),
)
.await
.map_err(db_err)?;
Ok(true)
}
("app.bsky.feed.repost", "delete") => {
indexer::delete_repost(&state.db, &req.did, &req.rkey)
.await
.map_err(db_err)?;
Ok(true)
}
("app.bsky.graph.follow", "create") => {
let subject = req
.subject_did
.clone()
.or_else(|| {
req.record
.as_ref()
.and_then(|r| r.get("subject"))
.and_then(|s| s.as_str())
.map(str::to_string)
})
.ok_or_else(|| bad_request("follow create requires subject_did or record.subject"))?;
indexer::upsert_follow(
&state.db,
&req.did,
&subject,
req.record.as_ref(),
)
.await
.map_err(db_err)?;
Ok(true)
}
("app.bsky.graph.follow", "delete") => {
let subject = req
.subject_did
.clone()
.or_else(|| {
req.record
.as_ref()
.and_then(|r| r.get("subject"))
.and_then(|s| s.as_str())
.map(str::to_string)
})
.ok_or_else(|| bad_request("follow delete requires subject_did"))?;
indexer::delete_follow(&state.db, &req.did, &subject)
.await
.map_err(db_err)?;
Ok(true)
}
(coll, action) => {
// Unrecognised collection/action — return ok=false so the PDS
// doesn't retry. Future collections should be added above.
tracing::debug!(collection = %coll, action = %action, "ingest: unhandled");
Ok(false)
}
}
}
fn db_err(e: impl std::fmt::Display) -> (StatusCode, Json<Value>) {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "InternalServerError",
"message": e.to_string(),
})),
)
}
fn bad_request(msg: &str) -> (StatusCode, Json<Value>) {
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "InvalidRequest",
"message": msg,
})),
)
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::HeaderValue;
#[test]
fn no_secret_configured_allows_anonymous() {
let h = HeaderMap::new();
assert!(check_ingest_secret(&h, None).is_ok());
}
#[test]
fn secret_required_when_configured() {
let h = HeaderMap::new();
assert!(check_ingest_secret(&h, Some("hunter2")).is_err());
}
#[test]
fn secret_matches() {
let mut h = HeaderMap::new();
h.insert("x-ingest-secret", HeaderValue::from_static("hunter2"));
assert!(check_ingest_secret(&h, Some("hunter2")).is_ok());
}
#[test]
fn secret_mismatched() {
let mut h = HeaderMap::new();
h.insert("x-ingest-secret", HeaderValue::from_static("hunter3"));
assert!(check_ingest_secret(&h, Some("hunter2")).is_err());
}
}