···11+DROP TABLE jetstream_cursor;12DROP TABLE public_key;23DROP TABLE repository;33--- DROP TABLE jetstream_log;44--- DROP TYPE jetstream_action;55-DROP FUNCTION updated_at_trigger;44+DROP TABLE knot_member;55+DROP TABLE repository_member;66+DROP TABLE event;
+48-59
crates/knot/migrations/20251103141538_init.up.sql
···11-CREATE OR REPLACE FUNCTION updated_at_trigger() RETURNS TRIGGER22- LANGUAGE plpgsql AS33-$$BEGIN44- NEW.updated_at := current_timestamp;55- RETURN NEW;66-END;$$;11+CREATE TABLE jetstream_cursor (22+ id integer NOT NULL,33+ cursor integer NOT NULL,7488-DO $$ BEGIN99- CREATE TYPE jetstream_action AS ENUM (1010- 'create',1111- 'update',1212- 'delete'1313- );1414-EXCEPTION1515- WHEN duplicate_object THEN null;1616-END $$;55+ PRIMARY KEY (id)66+);1771818--- Commit updates from Jetstream (create, update, and delete).1919-CREATE TABLE IF NOT EXISTS jetstream_log (2020- ts timestamp WITH TIME ZONE NOT NULL,2121- did text NOT NULL,2222- collection text NOT NULL,2323- rkey text NOT NULL,2424- rev text NOT NULL,2525- action jetstream_action NOT NULL,2626- cid text,2727- payload json,88+CREATE TABLE knot_member (99+ did text NOT NULL,28102929- PRIMARY KEY (did, collection, rkey, rev)1111+ PRIMARY KEY (did)3012);31133214-- Public keys, derived from 'sh.tangled.publicKey' records.3333-CREATE TABLE IF NOT EXISTS public_key (3434- did text NOT NULL,3535- rkey text NOT NULL,3636- cid text NOT NULL,1515+CREATE TABLE public_key (1616+ did text NOT NULL,1717+ rkey text NOT NULL,1818+ rev text NOT NULL,1919+ cid text NOT NULL,37203821 -- 'sh.tangled.publicKey' fields3922 -- <https://tangled.org/@tangled.org/core/blob/master/lexicons/publicKey.json>4040- name text NOT NULL,4141- key text NOT NULL,4242- created_at timestamp WITH TIME ZONE NOT NULL,2323+ name text NOT NULL,2424+ key text NOT NULL,2525+ created_at datetime NOT NULL,43264444- updated_at timestamp WITH TIME ZONE NOT NULL DEFAULT current_timestamp,2727+ PRIMARY KEY (did, rkey),2828+ FOREIGN KEY (did) REFERENCES knot_member (did) ON DELETE CASCADE45294646- PRIMARY KEY (did, rkey)4730);48314949-CREATE OR REPLACE TRIGGER public_key_update_trigger5050- BEFORE UPDATE ON public_key5151- FOR EACH ROW EXECUTE PROCEDURE updated_at_trigger();5252-5332-- Repositories, derived from 'sh.tangled.repo' records.5454-CREATE TABLE IF NOT EXISTS repository (5555- did text NOT NULL,5656- rkey text NOT NULL,5757- cid text NOT NULL,3333+CREATE TABLE repository (3434+ did text NOT NULL,3535+ rkey text NOT NULL,3636+ rev text NOT NULL,3737+ cid text NOT NULL,58385939 -- 'sh.tangled.repo' fields6040 -- <https://tangled.org/@tangled.org/core/blob/master/lexicons/repo/repo.json>6161- name text NOT NULL,6262- knot text NOT NULL,6363- spindle text,6464- description text,6565- website text,6666- topics text[],6767- source text,6868- labels text[],6969- created_at timestamp WITH TIME ZONE NOT NULL,7070-7171- xrpc_create_at timestamp WITH TIME ZONE,7272- jetstream_at timestamp WITH TIME ZONE, 7373- updated_at timestamp WITH TIME ZONE NOT NULL DEFAULT current_timestamp,4141+ name text NOT NULL,4242+ knot text NOT NULL,4343+ spindle text,4444+ source text,4545+ created_at datetime NOT NULL,74467547 PRIMARY KEY (did, rkey)4848+ FOREIGN KEY (did) REFERENCES knot_member (did) ON DELETE CASCADE7649);77507878-CREATE OR REPLACE TRIGGER repository_update_trigger7979- BEFORE UPDATE ON repository8080- FOR EACH ROW EXECUTE PROCEDURE updated_at_trigger();5151+CREATE TABLE repository_member (5252+ repo_did text NOT NULL,5353+ repo_rkey text NOT NULL,5454+ did text NOT NULL, 5555+5656+ PRIMARY KEY (repo_did, repo_rkey, did),5757+ FOREIGN KEY (repo_did, repo_rkey) REFERENCES repository (did, rkey) ON DELETE CASCADE5858+);5959+6060+CREATE TABLE event (6161+ id integer PRIMARY KEY AUTOINCREMENT NOT NULL,6262+ ts datetime NOT NULL,6363+ repo_did text NOT NULL,6464+ repo_rkey text NOT NULL,6565+ collection text NOT NULL,6666+ record jsonb NOT NULL,6767+6868+ FOREIGN KEY (repo_did, repo_rkey) REFERENCES repository (did, rkey) ON DELETE CASCADE6969+);
···11-CREATE TABLE repository_member (22- repo_did text NOT NULL,33- repo_rkey text NOT NULL,44- member_did text NOT NULL, 55-66- PRIMARY KEY (repo_did, repo_rkey, member_did),77- FOREIGN KEY (repo_did, repo_rkey) REFERENCES repository (did, rkey) ON DELETE CASCADE88-);99-1010-CREATE TABLE knot_member (1111- instance_name text NOT NULL,1212- member_did text NOT NULL,1313-1414- PRIMARY KEY (instance_name, member_did)1515-);
···11-CREATE TABLE events (22- id serial PRIMARY KEY,33- collection text NOT NULL,44- rkey text NOT NULL,55- event jsonb NOT NULL,66-77- UNIQUE (collection, rkey),88- UNIQUE (event)99-);
+8-3
crates/knot/src/cli.rs
···22use core::fmt;33use identity::{Did, ResolveError, Resolver};44use std::{path::PathBuf, str::FromStr};55-use url::Url;6576#[derive(Parser)]87#[command(about, author, version)]···3233 )]3334 pub addr: Vec<String>,34353535- #[arg(long, short = 'D', env = "KNOT_SERVER_DATABASE_URL")]3636- pub db: Url,3636+ /// Database filename3737+ #[arg(3838+ long,3939+ short = 'D',4040+ env = "KNOT_SERVER_DATABASE_PATH",4141+ default_value = "knot.db"4242+ )]4343+ pub db: PathBuf,37443845 /// Port number of the real knot-server.3946 #[arg(long, short = 'U', env = "KNOT_SERVER_UPSTREAM")]
+55-24
crates/knot/src/main.rs
···99use identity::Resolver;1010use knot::{1111 model::{Knot, KnotState, config::KnotConfiguration},1212- services::database::{DataStore, PgDatabase},1212+ services::database::DataStore,1313};1414use reqwest::ClientBuilder;1515-use sqlx::postgres::PgPoolOptions;1515+use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};1616use std::{1717 env,1818 ffi::OsString,···139139 .success()140140 );141141142142- let pool = PgPoolOptions::new()143143- .max_connections(5)144144- .connect(arguments.db.as_str())145145- .await?;142142+ let pool = {143143+ let connect_options = SqliteConnectOptions::new()144144+ .filename("knot.db")145145+ .create_if_missing(true)146146+ .foreign_keys(true);146147147147- let row: (String,) = sqlx::query_as("SELECT version()").fetch_one(&pool).await?;148148- tracing::info!(version = %row.0, "connected to db");148148+ SqlitePoolOptions::new()149149+ .connect_with(connect_options)150150+ .await?151151+ };152152+153153+ // Run database migrations.149154 sqlx::migrate!().run(&pool).await?;155155+156156+ let db = DataStore::new(pool.clone());150157151158 let rate_limit: &'static _ = Box::leak(Box::new(152159 ServiceBuilder::new()···187180 .without_v07_checks()188181 .merge(knot::public::router());189182190190- let store = DataStore::new(PgDatabase::new(pool));191191- let jetstream_cursor = store192192- .jetstream_cursor()193193- .await194194- .context("Failed to query last jetstream cursor from datastore")?195195- .map(|odt| (odt, (odt.unix_timestamp_nanos() / 1000).unsigned_abs()));183183+ let jetstream = {184184+ let cursor = db185185+ .get_jetstream_cursor()186186+ .await187187+ .context("Failed to query last jetstream cursor from datastore")?188188+ .map(|odt| (odt, (odt.unix_timestamp_nanos() / 1000).unsigned_abs()));196189197197- if let Some((cursor, cursor_us)) = &jetstream_cursor {198198- tracing::info!(?cursor, ?cursor_us, "found jetstream cursor");199199- }190190+ if let Some((cursor, cursor_us)) = &cursor {191191+ tracing::info!(?cursor, ?cursor_us, "found jetstream cursor");192192+ }200193201201- let jetstream = jetstream::builder()202202- .collection("sh.tangled.*".try_into()?)203203- .cursor(jetstream_cursor.map(|(_, ts)| ts))204204- .build(Url::parse(jetstream::PUBLIC_JETSTREAM_INSTANCES[0])?);194194+ jetstream::builder()195195+ .collection("sh.tangled.publicKey".try_into()?)196196+ .collection("sh.tangled.repo".try_into()?)197197+ .cursor(cursor.map(|(_, ts)| ts))198198+ .build(Url::parse(jetstream::PUBLIC_JETSTREAM_INSTANCES[0])?)199199+ };205200206201 let mut service = JoinSet::new();207202 let mut private_sockets = Vec::new();···217208 .map(|listener| listener.local_addr().unwrap())218209 .collect();219210211211+ tracing::info!(?private_addrs, "bound internal API");212212+220213 let config = KnotConfiguration::builder()221214 .instance_name(&arguments.name)222215 .owner_did(&resolved_owner)···228217 .private_sockets(&private_addrs)229218 .build()?;230219231231- let knot: Knot = KnotState::new(config, resolver, public_http, jetstream, store).into();220220+ let knot: Knot = KnotState::new(config, resolver, public_http, jetstream, db).into();221221+222222+ {223223+ let knot = knot.clone();224224+ tokio::spawn(async move {225225+ let owner = resolved_owner.as_str();226226+ match sqlx::query!(227227+ "INSERT INTO knot_member (did) VALUES (?) ON CONFLICT (did) DO NOTHING RETURNING did",228228+ owner229229+ )230230+ .fetch_optional(&pool)231231+ .await {232232+ Ok(Some(_)) => {233233+ knot.backfill_public_keys().await.unwrap();234234+ knot.backfill_repositories(&resolved_owner).await.unwrap();235235+ },236236+ Ok(None) => tracing::debug!("skipping public key backfill"),237237+ Err(error) => tracing::error!(?error)238238+ }239239+ });240240+ }232241233242 let router = router234243 .layer(SetRequestIdLayer::new(···264233 let uri = request.uri();265234 let path = uri.path();266235267267- let span = tracing::trace_span!("public", id = Empty, ?method, ?path);236236+ let span = tracing::info_span!("public", id = Empty, ?method, ?path);268237 if let Some(request_id) = extract_request_id(request) {269238 span.record("id", request_id);270239 }···273242 })274243 .on_request(|_: &Request<_>, _: &Span| {})275244 .on_response(|response: &Response<_>, latency: Duration, _: &Span| {276276- tracing::trace!(?latency, status = ?response.status());245245+ tracing::info!(?latency, status = ?response.status());277246 }),278247 )279248 .with_state(knot.clone());
···66};7788use atproto::{did::Did, tid::Tid};99-use auth::{jwt, public_key};99+use auth::jwt;1010use bytes::Bytes;1111use identity::{HttpClient, Resolver};1212use jetstream::JetstreamClient;1313-use lexicon::{com::atproto::repo::list_records, sh::tangled::git::RefUpdate};1313+use lexicon::sh::tangled::{git::RefUpdate, repo::Repo};1414use rayon::{ThreadPool, ThreadPoolBuilder};1515use serde::Serialize;1616use time::OffsetDateTime;···18181919use crate::{2020 services::{2121+ atrepo,2122 authorization::AuthorizationClaimsStore,2223 database::{DataStore, DataStoreError},2324 },···6564 /// Thread pool for running synchronous tasks.6665 pool: ThreadPool,67666868- events: tokio::sync::broadcast::Sender<(i32, Tid, Event)>,6767+ events: tokio::sync::broadcast::Sender<(i64, OffsetDateTime, Event)>,69687069 /// Stores JWT claims to prevent re-use.7170 jwt_claims: Mutex<HashMap<Box<str>, jwt::Claims>>,···153152 &self.pool154153 }155154156156- pub(crate) fn subscribe_events(&self) -> tokio::sync::broadcast::Receiver<(i32, Tid, Event)> {155155+ pub(crate) fn subscribe_events(156156+ &self,157157+ ) -> tokio::sync::broadcast::Receiver<(i64, OffsetDateTime, Event)> {157158 self.events.subscribe()158159 }159160160160- pub(crate) async fn send_event(&self, id: i32, rkey: Tid, event: Event) {161161- if self.events.send((id, rkey, event)).is_err() {161161+ pub(crate) async fn send_event(&self, id: i64, ts: OffsetDateTime, event: Event) {162162+ if self.events.send((id, ts, event)).is_err() {162163 tracing::warn!("no external listeners to consume events");163164 }164165 }···176173 self.jwt_claims.lock().unwrap()177174 }178175179179- /// Get public keys the knot holds for the specified DID.180180- ///181181- /// If the knot has no public keys stored, they will be fetched from the PDS associated with182182- /// the DID.183183- pub async fn public_keys(&self, did: &Did) -> Vec<public_key::PublicKey> {184184- let keys: Vec<_> = self185185- .store186186- .public_keys_for_did(did)187187- .await188188- .inspect_err(|error| {189189- tracing::error!(?error, "failed to query public keys from database")190190- })191191- .unwrap_or_default()192192- .into_iter()193193- .filter_map(|pk| {194194- public_key::PublicKey::from_openssh(&pk.key)195195- .inspect_err(|error| {196196- tracing::error!(?error, ?pk, "failed to parse public key from db")197197- })198198- .ok()199199- })200200- .collect();176176+ pub async fn fetch_public_keys(&self, did: &Did) -> anyhow::Result<()> {177177+ let db = self.store().clone();178178+ let did = did.to_owned();179179+ let rev = Tid::MIN.to_string();201180202202- match keys.is_empty() {203203- true => {204204- tracing::info!(?did, "fetching sh.tangled.publicKeys for DID");205205- self.fetch_public_keys(did).await.unwrap_or_default()206206- }207207- false => keys,208208- }181181+ atrepo::fetch_collection::<_, anyhow::Error>(182182+ self.resolver(),183183+ &self.public_http,184184+ &did.clone(),185185+ "sh.tangled.publicKey",186186+ async move |records| {187187+ for record in records {188188+ let rkey = record189189+ .uri190190+ .rkey191191+ .ok_or(anyhow::anyhow!("record uri missing rkey"))?;192192+193193+ let Ok(public_key) = serde_json::from_str(record.value.get()) else {194194+ continue;195195+ };196196+197197+ db.upsert_public_key(&did, &rkey, &rev, &record.cid, &public_key)198198+ .await?;199199+ }200200+201201+ Ok(())202202+ },203203+ )204204+ .await?;205205+206206+ Ok(())209207 }210208211211- pub async fn fetch_public_keys(&self, did: &Did) -> anyhow::Result<Vec<public_key::PublicKey>> {212212- use lexicon::sh::tangled::PublicKey as LexiconPublicKey;213213- use url::Url;209209+ pub async fn backfill_knot_members(&self) -> anyhow::Result<()> {210210+ Ok(())211211+ }214212215215- fn list_records_url(mut pds: Url, collection: &str, repo: &Did) -> Url {216216- pds.set_path("/xrpc/com.atproto.repo.listRecords");217217-218218- let mut query = pds.query_pairs_mut();219219- query.append_pair("repo", repo.as_str());220220- query.append_pair("collection", collection);221221- drop(query);222222-223223- pds213213+ /// Fetch public keys for all knot members.214214+ pub async fn backfill_public_keys(&self) -> anyhow::Result<()> {215215+ let mut knot_members = self.store().knot_members();216216+ while let Some(member) = knot_members.next().await {217217+ let member = member?;218218+ if let Err(error) = self.fetch_public_keys(&member).await {219219+ tracing::error!(220220+ ?error,221221+ ?member,222222+ "failed to backfill knot member's public keys"223223+ );224224+ }224225 }226226+ Ok(())227227+ }225228226226- let (_, doc) = self.resolver.resolve(did.as_str()).await?;227227- let pds = &doc228228- .atproto_pds()229229- .ok_or(anyhow::anyhow!("DID document does not declare a pds"))?230230- .service_endpoint;229229+ pub async fn backfill_repositories(&self, did: &Did) -> anyhow::Result<()> {230230+ let base = self.config.repository_path().to_path_buf();231231+ let db = self.store().clone();232232+ let instance = self.instance_name().to_owned();233233+ let did = did.to_owned();234234+ let rev = Tid::MIN.to_string();231235232232- let response = self233233- .public_http234234- .get(list_records_url(pds.clone(), "sh.tangled.publicKey", did))235235- .send()236236- .await?237237- .error_for_status()?238238- .bytes()239239- .await?;236236+ atrepo::fetch_collection::<_, anyhow::Error>(237237+ self.resolver(),238238+ &self.public_http,239239+ &did.clone(),240240+ "sh.tangled.repo",241241+ async move |records| {242242+ for record in records {243243+ let rkey = record244244+ .uri245245+ .rkey246246+ .ok_or(anyhow::anyhow!("record uri missing rkey"))?;240247241241- let public_keys: list_records::Output<LexiconPublicKey> =242242- serde_json::from_slice(&response)?;248248+ let Ok(repo) = serde_json::from_str::<Repo>(record.value.get()) else {249249+ tracing::error!(?record, "error parsing record");250250+ continue;251251+ };243252244244- for record in public_keys.records() {245245- self.store.upsert_public_key_from_record(record).await?;246246- }253253+ if repo.knot != instance {254254+ continue;255255+ }247256248248- Ok(public_keys249249- .records()250250- .filter_map(|rec| {251251- public_key::PublicKey::from_openssh(&rec.value.key)252252- .inspect_err(|e| {253253- tracing::error!(?e, ?did, "failed to parse public key from collection")254254- })255255- .ok()256256- })257257- .collect())257257+ if let Ok(true) = db258258+ .insert_repository(&did, &rkey, &rev, &record.cid, &repo)259259+ .await260260+ {261261+ let path = base.join(did.as_str()).join(rkey);262262+ let Ok(new_repo) = gix::init_bare(&path) else {263263+ continue;264264+ };265265+266266+ tracing::info!(?new_repo, "created repository");267267+268268+ // Create a symlink to map the repository name -> rkey.269269+ let symlink_path = base.join(did.as_str()).join(repo.name.as_ref());270270+ let _ = std::fs::remove_file(&symlink_path);271271+ let _ = std::os::unix::fs::symlink(rkey, &symlink_path);272272+ }273273+ }274274+275275+ Ok(())276276+ },277277+ )278278+ .await?;279279+280280+ Ok(())258281 }259282260283 pub async fn fetch_pds_record(
+14-14
crates/knot/src/private.rs
···11use core::fmt;22use std::{borrow::Cow, sync::Arc};3344-use atproto::{Did, tid::TidClock};44+use atproto::Did;55use axum::{66 extract::{FromRequestParts, Path, State},77 http::{HeaderMap, StatusCode, request::Parts},···99};1010use lexicon::sh::tangled::git::{Meta, RefUpdate};1111use serde::{Deserialize, Serialize};1212+use time::OffsetDateTime;12131314/// Environment variable containing one or more whitespace separated URLs for the internal API.1415///···3534pub const ENV_HEADER_PREFIX: &str = "X-Gordian";36353736use crate::{3838- model::{Knot, errors},3737+ model::{Knot, errors, knot_state::Event},3938 public::xrpc::XrpcError,4039 types::{push_certificate::PushCertificate, repository_key::RepositoryKey},4140};4242-4343-static TID_CLOCK: TidClock = TidClock::with_id(0);44414542/// Build a new router for the internal API.4643#[rustfmt::skip]···304305 r#ref: Cow::Owned(refname.into()),305306 committer_did: Cow::Owned(user_did.as_ref().into()),306307 repo_did: Cow::Owned(repo.owner.as_ref().into()),307307- repo_name: Cow::Owned(repo_name.as_ref().into()),308308+ repo_name: Cow::Owned(repo_name.clone()),308309 old_sha: Cow::Owned(old_sha.into()),309310 new_sha: Cow::Owned(new_sha.into()),310311 meta: Meta {···313314 },314315 };315316316316- let rkey = TID_CLOCK.next();317317+ let ts = OffsetDateTime::now_utc();318318+ let event = Event::RefUpdate(Arc::new(ref_update));317319 let id = state318320 .store()319319- .insert_event("sh.tangled.git.refUpdate", &rkey, &ref_update)321321+ .insert_event(322322+ ts,323323+ &repo.owner,324324+ &repo.rkey,325325+ "sh.tangled.git.refUpdate",326326+ &event,327327+ )320328 .await321329 .unwrap();322330323331 tracing::info!(?id);324324- state325325- .send_event(326326- id,327327- rkey,328328- crate::model::knot_state::Event::RefUpdate(Arc::new(ref_update)),329329- )330330- .await;332332+ state.send_event(id, ts, event).await;331333 }332334333335 Ok(StatusCode::NO_CONTENT)
+14-12
crates/knot/src/public/events.rs
···11use std::time::Duration;2233+use atproto::tid::Tid;34use axum::{45 extract::{56 Query, State, WebSocketUpgrade,···66656766 let (mut sender, mut receiver) = socket.split();68676969- let mut past_events = state.store().get_events(start_ts);7070- while let Some(Ok(db_event)) = past_events.next().await {7171- cursor = db_event.id;6868+ let mut past_events = state.store().get_events(&start_ts);6969+ while let Some(Ok(event)) = past_events.next().await {7070+ cursor = event.id;7271 let wrapper = EventWrapper {7373- nsid: &db_event.collection,7474- rkey: &db_event.rkey.to_string(),7575- event: &db_event.event,7272+ nsid: &event.collection,7373+ rkey: &Tid::from_datetime(event.ts, event.id.rem_euclid(1023).try_into().unwrap())7474+ .to_string(),7575+ event: &event.record,7676 };77777878 let serialized = serde_json::to_string(&wrapper).unwrap();···8482 }85838684 loop {8787- let (event_id, rkey, event) = tokio::select! {8585+ let (id, ts, event) = tokio::select! {8886 now = keep_alive.tick() => {8987 let bytes = (now.duration_since(start)).as_secs().to_string().into();9088 if let Err(error) = sender.send(Message::Ping(bytes)).await {···9492 continue;9593 }9694 Ok(Some(message)) = receiver.try_next() => {9797- tracing::debug!(?message);9595+ tracing::trace!(?message);9896 continue;9997 }10098 Ok(event) = events.recv() => event,10199 else => break,102100 };103101104104- if event_id < cursor {105105- tracing::debug!(?event_id, "skipping event, client has already seen");102102+ if id < cursor {103103+ tracing::debug!(?id, "skipping event, client has already seen");106104 continue;107105 }108106109107 let wrapper = EventWrapper {110108 nsid: event.collection(),111111- rkey: &rkey.to_string(),109109+ rkey: &Tid::from_datetime(ts, id.rem_euclid(1023).try_into().unwrap()).to_string(),112110 event: &event,113111 };114112···118116 return;119117 }120118121121- cursor = event_id;119119+ cursor = id;122120 }123121}
+10-3
crates/knot/src/public/git/authorization.rs
···80808181 // Read the 'sh.tangled.publicKey' records the knot has associated8282 // with claimed issuer.8383- let public_keys = knot.public_keys(&unverified_claims.iss).await;8383+ // let public_keys = knot.public_keys(&unverified_claims.iss).await;8484+ let public_keys = knot8585+ .store()8686+ .public_keys_for_did(&unverified_claims.iss)8787+ .await8888+ .unwrap_or_default()8989+ .into_iter()9090+ .filter_map(|public_key| PublicKey::from_openssh(&public_key.key).ok());84918592 // Try to decode and verify the JWT using any one of the public keys8693 // we have for the DID.8787- for verification_key in verification_keys.iter().chain(public_keys.iter()) {8888- if let Ok(token) = decode::<Claims>(credential, verification_key) {9494+ for verification_key in verification_keys.into_iter().chain(public_keys) {9595+ if let Ok(token) = decode::<Claims>(credential, &verification_key) {8996 let claims = token.claims;90979198 // Re-verify the claims for the sake of paranoia.
+42-13
crates/knot/src/public/xrpc/sh/tangled/repo.rs
···11-use axum::{Json, extract::State};22-use lexicon::sh::tangled::repo::{create, delete, get_default_branch, languages, tree};11+use atproto::tid::Tid;22+use axum::{Json, extract::State, http::StatusCode};33+use lexicon::{44+ com::atproto::repo::list_records::Record,55+ sh::tangled::repo::{Repo, create, delete, get_default_branch, languages, tree},66+};77+use time::OffsetDateTime;38use tokio_rayon::AsyncThreadPool as _;49510use crate::{611 model::{Knot, errors, repository::GixRepository},77- public::xrpc::{XrpcQuery, XrpcResult},1212+ public::xrpc::{XrpcError, XrpcQuery, XrpcResult},813 services::authorization::{Authorization, Verification},914 types::sh::tangled::repo::{blob, branches, diff, log, tags},1015};···7570 .await7671 .map_err(errors::RepoError)?;77727878- let record = serde_json::from_slice(&response).map_err(errors::RepoError)?;7373+ let record = serde_json::from_slice::<Record>(&response)7474+ .inspect_err(|error| tracing::error!(?error))7575+ .map_err(errors::RepoError)?;79767777+ let repo: Repo = serde_json::from_str(record.value.get()).map_err(|error| {7878+ XrpcError::new(7979+ StatusCode::INTERNAL_SERVER_ERROR,8080+ "LexiconError",8181+ error.to_string(),8282+ )8383+ })?;8484+8585+ // Use the minimum rev value so *any* firehose-derived entry will have priority.8686+ let rev = Tid::MIN.to_string();8087 let is_new = knot8188 .store()8282- .insert_repository_from_record(&record)8989+ .insert_repository(&claims.iss, ¶ms.rkey, &rev, "", &repo)8390 .await8491 .map_err(errors::RepoError)?;85928686- if is_new && record.value.knot == knot.instance_name() {8787- knot.create_repo(&claims.iss, ¶ms.rkey, &record.value.name)9393+ if is_new && repo.knot == knot.instance_name() {9494+ knot.create_repo(&claims.iss, ¶ms.rkey, &repo.name)8895 .map_err(errors::RepoError)?;8996 }9097···139122 )))?;140123 }141124142142- knot.store()143143- .delete_repository(¶ms.did, ¶ms.rkey)144144- .await145145- .map_err(errors::RepoError)?;125125+ // Sythesize a jetstream delete commit.126126+ let ts = OffsetDateTime::now_utc();127127+ let delete = jetstream::Delete {128128+ ts,129129+ did: ¶ms.did,130130+ collection: "sh.tangled.repo",131131+ rkey: ¶ms.rkey,132132+ rev: &Tid::from_datetime(ts, 0).to_string(),133133+ };146134147147- knot.delete_repo(¶ms.did, ¶ms.rkey)148148- .map_err(errors::RepoError)?;135135+ if let Some(record) = knot136136+ .store()137137+ .delete_repository(&delete)138138+ .await139139+ .map_err(errors::RepoError)?140140+ {141141+ knot.delete_repo(&record.did, &record.rkey)142142+ .map_err(errors::RepoError)?;143143+ }149144150145 Ok(().into())151146}
+1
crates/knot/src/services.rs
···11+pub mod atrepo;12pub mod authorization;23pub mod database;34pub mod jetstream;