···77 extract::Query,88 http::{HeaderName, Request, Response},99};1010-use futures_util::StreamExt;1010+use futures_util::StreamExt as _;1111+use identity::Resolver;1112use jetstream::client_config::JetstreamConfig;1213use knot::{1313- model::{Knot, KnotState},1414+ model::{Knot, KnotState, config::KnotConfiguration},1415 services::database::DataStore,1516};1617use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};1718use std::{1919+ borrow::Cow,1820 collections::BTreeMap,1921 env,2022 ffi::OsStr,···4139#[cfg(all(not(target_env = "msvc"), feature = "jemalloc"))]4240#[global_allocator]4341static GLOBAL: Jemalloc = Jemalloc;4242+4343+const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));44444545fn main() -> anyhow::Result<()> {4646 tracing_subscriber::registry()···122118 }123119 )?);124120125125- let pool = {126126- let connect_options = SqliteConnectOptions::new()127127- .filename(&arguments.db)128128- .create_if_missing(true)129129- .foreign_keys(true)130130- .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal);121121+ let database = {122122+ let pool = {123123+ let connect_options = SqliteConnectOptions::new()124124+ .filename(&arguments.db)125125+ .create_if_missing(true)126126+ .foreign_keys(true)127127+ .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal);131128132132- SqlitePoolOptions::new()133133- .connect_with(connect_options)134134- .await?129129+ SqlitePoolOptions::new()130130+ .connect_with(connect_options)131131+ .await?132132+ };133133+134134+ sqlx::migrate!().run(&pool).await?;135135+ DataStore::new(pool)135136 };136136-137137- // Run database migrations.138138- sqlx::migrate!().run(&pool).await?;139139-140140- let db = DataStore::new(pool.clone());141141-142142- let (resolver, public_http, config) = arguments.build().await?;143137144138 // Ensure the instance owner is also a member.145139 //146146- // This uses an invalid, empty rkey, and the maximum TID value as a revision ID to147147- // ensure this entry cannot be overwritten by an event from the firehose.148148- let needs_initial_fetch = db140140+ // Use an invalid, empty rkey, and the maximum TID value as a revision ID to141141+ // ensure this entry does not get overwritten by an event from the firehose.142142+ let needs_initial_fetch = database149143 .upsert_knot_member(150144 "",151145 &Tid::MAX.to_string(),152146 "",153153- &config.owner_did,147147+ &arguments.owner,154148 &arguments.name,155149 OffsetDateTime::now_utc(),156150 )157151 .await?;158152159159- let jetstream_config = {160160- let cursor = db161161- .get_jetstream_cursor()153153+ let public_http = reqwest::ClientBuilder::new()154154+ .timeout(Duration::from_secs(2))155155+ .user_agent(USER_AGENT)156156+ .http2_keep_alive_while_idle(true)157157+ .https_only(true)158158+ .build()159159+ .context("Failed to build public HTTP client")?;160160+161161+ let resolver = Resolver::builder()162162+ .plc_directory(arguments.plc_directory.clone())163163+ .build_with(public_http.clone());164164+165165+ // Bind listeners for the public API.166166+ let mut public_listeners = Vec::with_capacity(arguments.bind.len());167167+ for addr in &arguments.bind {168168+ for socket in addr.to_socket_addrs()? {169169+ let listener = TcpListener::bind(socket).await?;170170+ public_listeners.push(listener);171171+ }172172+ }173173+174174+ // Bind listeners for the private API.175175+ let mut private_listeners = Vec::with_capacity(2);176176+ for socket in "localhost:0".to_socket_addrs()? {177177+ let listener = TcpListener::bind(socket).await?;178178+ private_listeners.push(listener);179179+ }180180+181181+ // The knot needs to know the sockets we've bound the private API.182182+ let private_addrs = private_listeners183183+ .iter()184184+ .map(|listener| listener.local_addr())185185+ .collect::<Result<Vec<_>, std::io::Error>>()?;186186+187187+ tracing::info!(?private_addrs, "bound internal API");188188+189189+ let jetstream_instances: Vec<_> = arguments190190+ .jetstream191191+ .iter()192192+ .map(|url| Cow::Owned(url.to_string()))193193+ .collect();194194+195195+ let config: KnotConfiguration = arguments.try_into()?;196196+ let knot_state = KnotState::new(config, resolver, public_http, database, &private_addrs)?;197197+ let knot = Knot::from(knot_state);198198+199199+ let mut service = JoinSet::new();200200+201201+ // Spawn the internal API.202202+ service.spawn(knot::serve_all(203203+ knot::private::router().with_state(knot.clone()),204204+ private_listeners,205205+ ));206206+207207+ if needs_initial_fetch {208208+ tracing::info!("Appears to be first run, fetching records");209209+ knot.backfill_public_keys().await.unwrap();210210+ knot.backfill_repositories(&knot.config.owner)162211 .await163163- .context("Failed to query last jetstream cursor from datastore")?212212+ .unwrap();213213+ }214214+215215+ // Spawn the jetstream consumer.216216+ {217217+ let cursor = knot218218+ .database()219219+ .get_jetstream_cursor()220220+ .await?164221 .map(|odt| (odt, (odt.unix_timestamp_nanos() / 1000).unsigned_abs()));165222166223 if let Some((cursor, cursor_us)) = &cursor {···229164 }230165231166 let mut config = JetstreamConfig::default()167167+ .with_instances(jetstream_instances)232168 .with_cursor(cursor.map(|(_, us)| us))233169 .with_collections([234170 "sh.tangled.knot.member",···238172 "sh.tangled.repo.collaborator",239173 ]);240174241241- let mut member_dids = db.members();175175+ let mut member_dids = knot.database().members();242176 while let Some(did) = member_dids.next().await {243177 config244178 .subscriber_options···246180 .expect("knot members shouldn't exceed maximum DID filters");247181 }248182249249- config183183+ let (jetstream, jetstream_rx, jetstream_task) = config.connect();184184+ let knot = knot.clone();185185+ service.spawn(async move {186186+ tokio::join!(knot::services::jetstream::jetstream_task(187187+ jetstream,188188+ knot.clone(),189189+ jetstream_rx,190190+ ),);191191+192192+ panic!("jetstream consumer/task completed");193193+ });194194+195195+ service.spawn(async move {196196+ jetstream_task.await;197197+ Ok(())198198+ });250199 };251200252252- let mut service = JoinSet::new();253253-254254- let (knot_state, knot_tasks) =255255- KnotState::new(config, resolver, public_http, jetstream_config, db)?;256256- let knot: Knot = knot_state.into();257257-258258- if needs_initial_fetch {259259- tracing::info!("Appears to be first run, fetching records");260260- knot.backfill_public_keys().await.unwrap();261261- knot.backfill_repositories(&knot.config.owner_did)262262- .await263263- .unwrap();264264- }265265-201201+ // Build the public API.266202 let router = knot::public::router()267203 .layer(SetRequestIdLayer::new(268204 X_REQUEST_ID,···303235 )304236 .with_state(knot);305237306306- let mut public_listeners = Vec::with_capacity(arguments.bind.len());307307- for addr in &arguments.bind {308308- for socket in addr.to_socket_addrs()? {309309- let listener = TcpListener::bind(socket).await?;310310- public_listeners.push(listener);311311- }312312- }313313-314238 service.spawn(knot::serve_all(router, public_listeners));315315-316316- service.spawn(async move {317317- tracing::debug!("starting knot tasks");318318- for _task in knot_tasks.join_all().await {319319- //320320- }321321- Ok(())322322- });323239324240 for task in service.join_all().await {325241 if let Err(error) = task {
+17-17
crates/knot/src/model/config.rs
···11//! Knot configuration.22use atproto::{Did, did::OwnedDid};33use gix::bstr::BString;44+use rustc_hash::FxHashSet;45use std::{55- collections::HashSet,66 path::{Path, PathBuf},77 time::Duration,88};···27272828#[derive(Debug)]2929pub struct KnotConfiguration {3030- pub owner_did: OwnedDid,3131- pub instance_name: Box<str>,3232- pub instance_audience: OwnedDid,3030+ pub owner: OwnedDid,3131+ pub instance: OwnedDid,3332 pub repo_path: PathBuf,3433 pub hook_path: PathBuf,3534 pub git_config: PathBuf,3636- pub readmes: HashSet<BString>,3535+ pub readmes: FxHashSet<BString>,3736 pub repo_cache: RepoCacheConfig,3837}3938···4445}45464647impl KnotConfiguration {4747- /// The FQDN of the knot-server.4848 #[inline]4949- pub const fn instance_name(&self) -> &str {5050- &self.instance_name4949+ pub fn instance_ident(&self) -> &str {5050+ self.instance().ident()5151 }52525353+ /// The DID of the knot.5354 #[inline]5454- pub fn instance_audience(&self) -> &Did {5555- &self.instance_audience5555+ pub fn instance(&self) -> &Did {5656+ assert_eq!(self.instance.method(), "web");5757+ &self.instance5658 }57595858- /// The DID of the knot-server owner.6060+ /// The DID of the knot owner.5961 #[inline]6060- pub fn owner_did(&self) -> &Did {6161- &self.owner_did6262+ pub fn owner(&self) -> &Did {6363+ &self.owner6264 }63656464- /// Base path to the knot-server's repositories.6666+ /// Base path to the knot's repositories.6567 #[inline]6666- pub fn repository_path(&self) -> &Path {6868+ pub fn repository_base(&self) -> &Path {6769 self.repo_path.as_path()6870 }69717070- /// Path to the knot-server's global git hooks directory.7272+ /// Path to the knot's global git hooks directory.7173 #[inline]7274 pub fn hook_path(&self) -> &Path {7375 self.hook_path.as_path()···7979 self.git_config.as_path()8080 }81818282- pub fn readmes(&self) -> &HashSet<BString> {8282+ pub fn readmes(&self) -> &FxHashSet<BString> {8383 &self.readmes8484 }8585}
+34-70
crates/knot/src/model/knot_state.rs
···11use std::{22 collections::HashMap,33 io,44- net::{TcpListener, ToSocketAddrs as _},44+ net::SocketAddr,55 ops,66 sync::{Arc, Mutex},77 time::Duration,···1111use bytes::Bytes;1212use futures_util::{FutureExt, future::BoxFuture};1313use identity::{HttpClient, Resolver};1414-use jetstream::{JetstreamClient, client_config::JetstreamConfig};1514use lexicon::sh::tangled::{git::RefUpdate, repo::Repo};1615use moka::future::{Cache, CacheBuilder};1716use rayon::{ThreadPool, ThreadPoolBuilder};1817use serde::Serialize;1918use time::OffsetDateTime;2020-use tokio::task::JoinSet;2119use tokio_stream::StreamExt as _;22202321use crate::{···5860 /// Reqwest client.5961 ///6062 // @TODO Wrap this so prevent requests to sensitive/private endpoints.6161- public_http: HttpClient,6363+ http: HttpClient,62646363- /// Jetstream client.6464- pub jetstream: JetstreamClient,6565-6666- store: DataStore,6565+ database: DataStore,67666867 /// Thread pool for running synchronous tasks.6968 pool: ThreadPool,···7782}78837984impl KnotState {8080- pub fn new(8585+ pub fn new<'a>(8186 config: KnotConfiguration,8287 resolver: Resolver,8383- public_http: HttpClient,8484- jetstream_config: JetstreamConfig,8888+ http: HttpClient,8589 database: DataStore,8686- ) -> io::Result<(Arc<Self>, JoinSet<io::Result<()>>)> {9090+ private_binds: impl IntoIterator<Item = &'a SocketAddr>,9191+ ) -> io::Result<Arc<Self>> {8792 let pool = ThreadPoolBuilder::new()8893 .build()8994 .expect("Failed to build thread pool");90959196 let (events, _) = tokio::sync::broadcast::channel(16);92979393- let (jetstream, jetstream_rx, jetstream_task) = jetstream_config.connect();9494-9595- // Setup the private API.9696- let private_listeners = "localhost:0"9797- .to_socket_addrs()?9898- .map(|socket| {9999- let bound = TcpListener::bind(socket)?;100100- bound.set_nonblocking(true)?;101101- tokio::net::TcpListener::from_std(bound)102102- })103103- .collect::<Result<Vec<_>, io::Error>>()?;104104-105105- let private_addrs = private_listeners106106- .iter()107107- .map(|listener| Ok(format!("http://{}/", listener.local_addr()?)))108108- .collect::<Result<Vec<_>, io::Error>>()?9898+ let private_addrs = private_binds9999+ .into_iter()100100+ .map(|socket| format!("http://{socket}/"))101101+ .collect::<Vec<_>>()109102 .join(" ");110110-111111- tracing::info!(?private_addrs, "bound internal API");112103113104 let repo_cache = CacheBuilder::new(config.repo_cache.size)114105 .name("repository_cache")···105124106125 let inner = Arc::new(Self {107126 config,108108- public_http,127127+ http,109128 resolver,110110- jetstream,111111- store: database,129129+ database,112130 pool,113131 events,114132 repo_key_cache: CacheBuilder::new(REPO_KEY_CACHE_SIZE)···119139 private_addrs,120140 });121141122122- let mut tasks = JoinSet::new();123123-124124- let _ = {125125- let state = Arc::clone(&inner);126126- tasks.spawn(async move {127127- tokio::join!(128128- crate::services::jetstream::jetstream_task(Arc::clone(&state), jetstream_rx,),129129- jetstream_task130130- );131131-132132- panic!("jetstream consumer/task completed");133133- });134134- let internal = crate::private::router().with_state(Arc::clone(&inner).into());135135- tasks.spawn(crate::serve_all(internal, private_listeners))136136- };137137-138138- Ok((inner, tasks))142142+ Ok(inner)139143 }140144141145 /// Return a reference to the identity resolver.···146182 }147183 }148184149149- /// Return a reference to the database shim.185185+ /// Get a reference to the database.150186 #[inline]151151- pub fn store(&self) -> &DataStore {152152- &self.store187187+ pub fn database(&self) -> &DataStore {188188+ &self.database153189 }154190155191 pub async fn fetch_public_keys(&self, did: &Did) -> anyhow::Result<()> {156156- let db = self.store().clone();192192+ let db = self.database().clone();157193 let did = did.to_owned();158194 let rev = Tid::MIN.to_string();159195160196 atrepo::fetch_collection::<_, anyhow::Error>(161197 self.resolver(),162162- &self.public_http,198198+ &self.http,163199 &did.clone(),164200 "sh.tangled.publicKey",165201 async move |records| {···191227192228 /// Fetch public keys for all knot members.193229 pub async fn backfill_public_keys(&self) -> anyhow::Result<()> {194194- let mut knot_members = self.store().knot_members();230230+ let mut knot_members = self.database().knot_members();195231 while let Some(member) = knot_members.next().await {196232 let member = member?;197233 if let Err(error) = self.fetch_public_keys(&member).await {···206242 }207243208244 pub async fn backfill_repositories(&self, did: &Did) -> anyhow::Result<()> {209209- let base = self.config.repository_path().to_path_buf();210210- let db = self.store().clone();211211- let instance = self.instance_name().to_owned();245245+ let base = self.config.repository_base().to_path_buf();246246+ let db = self.database().clone();247247+ let instance = self.instance_ident().to_owned();212248 let did = did.to_owned();213249 let rev = Tid::MIN.to_string();214250215251 atrepo::fetch_collection::<_, anyhow::Error>(216252 self.resolver(),217217- &self.public_http,253253+ &self.http,218254 &did.clone(),219255 "sh.tangled.repo",220256 async move |records| {···283319 .service_endpoint;284320285321 let response = self286286- .public_http322322+ .http287323 .get(get_record_url(pds.clone(), did, collection, rkey))288324 .send()289325 .await?···319355 };320356321357 let (rkey, _) = self322322- .store()358358+ .database()323359 .resolve_repository(&owner, repo_path.name())324360 .await?325361 .ok_or(RepositoryResolveError::NotFound)?;···347383 .repo_cache348384 .try_get_with_by_ref(repo_key, async {349385 let path = self350350- .repository_path()386386+ .repository_base()351387 .join(repo_key.owner_str())352388 .join(repo_key.rkey());353389···374410 }375411376412 pub fn create_repo(&self, did: &Did, rkey: &str, name: &str) -> anyhow::Result<()> {377377- let path = self.repository_path().join(did.as_str()).join(rkey);413413+ let path = self.repository_base().join(did.as_str()).join(rkey);378414 let repo = gix::init_bare(&path)?;379415 tracing::info!(?repo, "created repository");380416381417 // Create a symlink to map the repository name -> rkey.382382- let symlink_path = self.repository_path().join(did.as_str()).join(name);418418+ let symlink_path = self.repository_base().join(did.as_str()).join(name);383419 let _ = std::fs::remove_file(&symlink_path);384420 std::os::unix::fs::symlink(rkey, &symlink_path)?;385421···387423 }388424389425 pub fn delete_repo(&self, did: &Did, rkey: &str) -> anyhow::Result<()> {390390- let path = self.repository_path().join(did.as_str()).join(rkey);426426+ let path = self.repository_base().join(did.as_str()).join(rkey);391427392392- let mut delete_path = self.repository_path().join("deleted");428428+ let mut delete_path = self.repository_base().join("deleted");393429 let _ = std::fs::create_dir_all(&delete_path);394430395431 delete_path.push(format!("{}-{}", did, rkey));···440476 now: i64,441477 ) -> BoxFuture<'b, Result<Option<auth::jwt::Claims>, AuthorizationClaimsStoreError>> {442478 async move {443443- let claims = self.store().get_claims(jti, now).await.ok().flatten();479479+ let claims = self.database().get_claims(jti, now).await.ok().flatten();444480445481 // If the claims have expired, remove them.446482 if matches!(&claims, Some(claims) if claims.exp < now) {447447- self.store()483483+ self.database()448484 .delete_claims(jti)449485 .await450486 .map_err(|error| AuthorizationClaimsStoreError(error.into()))?;···463499 now: i64,464500 ) -> BoxFuture<'_, Result<(), AuthorizationClaimsStoreError>> {465501 async move {466466- self.store()502502+ self.database()467503 .store_claims(claims, now)468504 .await469505 .map_err(|error| AuthorizationClaimsStoreError(error.into()))?;
···246246 .map_err(|error| (StatusCode::BAD_REQUEST, error.to_string()))?;247247248248 for public_key in state249249- .store()249249+ .database()250250 .public_keys_for_did(&user_did)251251 .await252252 .map_err(|error| (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()))?···292292 // Our hook refers to the repository using DID and rkey, but Tangled needs DID and name, so we293293 // need to lookup the repository's name in the database.294294 let (_, repo_name) = state295295- .store()295295+ .database()296296 .resolve_repository(&repo_key.owner, repo_key.rkey())297297 .await298298 .map_err(errors::RepoNotFound)?···334334 let ts = OffsetDateTime::now_utc();335335 let event = Event::RefUpdate(Arc::new(ref_update));336336 let id = state337337- .store()337337+ .database()338338 .insert_event(339339 ts,340340 &repo_key.owner,
+1-1
crates/knot/src/public/events.rs
···66666767 let (mut sender, mut receiver) = socket.split();68686969- let mut past_events = state.store().get_events(&start_ts);6969+ let mut past_events = state.database().get_events(&start_ts);7070 while let Some(Ok(event)) = past_events.next().await {7171 cursor = event.id;7272 let wrapper = EventWrapper {
+3-3
crates/knot/src/public/git/authorization.rs
···5050 // Before performing a relatively expensive DID look-up, ensure the token5151 // claims are valid.5252 let unverified_claims = unverified_token.claims;5353- GitVerification::verify(&knot, now, knot.instance_audience(), &unverified_claims)5353+ GitVerification::verify(&knot, now, knot.instance(), &unverified_claims)5454 .await5555 .map_err(|error| match error {5656 // Git re-uses the token from the credential helper for each request in a single push.···8282 // with claimed issuer.8383 // let public_keys = knot.public_keys(&unverified_claims.iss).await;8484 let public_keys = knot8585- .store()8585+ .database()8686 .public_keys_for_did(&unverified_claims.iss)8787 .await8888 .unwrap_or_default()···9696 let claims = token.claims;97979898 // Re-verify the claims for the sake of paranoia.9999- GitVerification::verify(&knot, now, knot.instance_audience(), &claims)9999+ GitVerification::verify(&knot, now, knot.instance(), &claims)100100 .await101101 .map_err(|error| Error::forbidden(&knot, error.to_string()))?;102102
+1-1
crates/knot/src/public/git/error.rs
···4646 fn authenticate_header(knot: &Knot) -> HeaderValue {4747 // @TODO We should probably generate a challenge code for the client to sign.4848 // Inform the client which knot it needs to authenticate with.4949- HeaderValue::from_str(&format!("knot={}", knot.instance_name()))4949+ HeaderValue::from_str(&format!("knot={}", knot.instance_ident()))5050 .expect("Header should be valid")5151 }5252}
···116116 // Use the minimum rev value so *any* firehose-derived entry will have priority.117117 let rev = Tid::MIN.to_string();118118 let is_new = knot119119- .store()119119+ .database()120120 .insert_repository(&claims.iss, ¶ms.rkey, &rev, "", &repo)121121 .await122122 .map_err(errors::RepoError)?;123123124124- if is_new && repo.knot == knot.instance_name() {124124+ if is_new && repo.knot == knot.instance_ident() {125125 knot.create_repo(&claims.iss, ¶ms.rkey, &repo.name)126126 .map_err(errors::RepoError)?;127127 }···176176 };177177178178 if let Some(record) = knot179179- .store()179179+ .database()180180 .delete_repository(&delete)181181 .await182182 .map_err(errors::RepoError)?
+2-2
crates/knot/src/services/authorization.rs
···163163 // Before performing a relatively expensive DID look-up, ensure the token164164 // claims are valid.165165 let unverified_claims = unverified_token.claims;166166- V::verify(&knot, now, knot.instance_audience(), &unverified_claims)166166+ V::verify(&knot, now, knot.instance(), &unverified_claims)167167 .await168168 .map_err(errors::Forbidden)?;169169···192192 let claims = token.claims;193193194194 // Re-verify the claims for the sake of paranoia.195195- V::verify(&knot, now, knot.instance_audience(), &claims)195195+ V::verify(&knot, now, knot.instance(), &claims)196196 .await197197 .map_err(errors::Forbidden)?;198198
+54-43
crates/knot/src/services/jetstream.rs
···11use crate::{22- model::KnotState,22+ model::{Knot, KnotState},33 services::rbac::{44 Action, AddCollaboratorPolicy, AddMemberPolicy, Policy, PolicyResult::*,55 RemoveCollaboratorPolicy, RemoveMemberPolicy, RepositoryCreatePolicy,66 RepositoryDeletePolicy, RepositoryRef,77 },88};99-use jetstream::{CommitEvent, Event};99+use jetstream::{CommitEvent, Event, JetstreamClient};1010use lexicon::Lexicon;1111-use std::{sync::Arc, time::Duration};1111+use std::time::Duration;1212use tokio::time::Instant;13131414-pub async fn jetstream_task(state: Arc<KnotState>, jetstream_rx: jetstream::JetstreamReceiver) {1414+pub async fn jetstream_task(1515+ client: JetstreamClient,1616+ knot: Knot,1717+ jetstream_rx: jetstream::JetstreamReceiver,1818+) {1519 let mut last_jetstream_sync: Option<Instant> = None;16201721 while let Some(event) = jetstream_rx.recv_async().await {···3329 match &event {3430 Event::Commit(commit) => match commit.collection() {3531 "sh.tangled.knot.member" => {3636- if let Err(error) = process_knot_member(&state, commit).await {3232+ if let Err(error) = process_knot_member(&client, &knot, commit).await {3733 tracing::error!(?error, "failed to process 'sh.tangled.knot_member' record")3834 }3935 }4036 "sh.tangled.publicKey" => {4141- if let Err(error) = process_public_key(&state, commit).await {3737+ if let Err(error) = process_public_key(&client, &knot, commit).await {4238 tracing::error!(?error, "failed to process 'sh.tangled.publicKey' record")4339 }4440 }4541 "sh.tangled.repo" => {4646- if let Err(error) = process_repo(&state, commit).await {4242+ if let Err(error) = process_repo(&client, &knot, commit).await {4743 tracing::error!(?error, "failed to process 'sh.tangled.repo' record")4844 }4945 }5046 "sh.tangled.repo.collaborator" => {5151- if let Err(error) = process_repo_collaborator(&state, commit).await {4747+ if let Err(error) = process_repo_collaborator(&client, &knot, commit).await {5248 tracing::error!(5349 ?error,5450 "failed to process 'sh.tangled.repo.collaborator' record"···5854 collection => unimplemented!("no handler implemented for '{collection}' commits"),5955 },6056 Event::Account(account) => {6161- state.resolver().invalidate_did(account.did).await;5757+ knot.resolver().invalidate_did(account.did).await;6258 }6359 Event::Identity(identity) => {6464- state.resolver().invalidate_did(identity.did).await;6060+ knot.resolver().invalidate_did(identity.did).await;6561 }6662 }67636864 if last_jetstream_sync.is_none_or(|value| value.elapsed() > Duration::from_secs(1)) {6969- match state.store().store_jetstream_cursor(event.ts()).await {6565+ match knot.database().store_jetstream_cursor(event.ts()).await {7066 Ok(()) => last_jetstream_sync = Some(Instant::now()),7167 Err(error) => tracing::error!(?error, "failed to log jetstream event"),7268 };···7773}78747975async fn process_public_key<'db, 'c, 'k>(8080- state: &KnotState,7676+ _client: &JetstreamClient,7777+ knot: &KnotState,8178 event: &'c CommitEvent<'c>,8279) -> anyhow::Result<()>8380where···9186 return Err(anyhow::anyhow!("expected a 'sh.tangled.publicKey' record"));9287 };93889494- if !state.store().is_knot_member(event.did()).await? {8989+ if !knot.database().is_knot_member(event.did()).await? {9590 return Ok(());9691 }97929898- state9999- .store()9393+ knot.database()10094 .upsert_public_key(commit.did, commit.rkey, commit.rev, commit.cid, &key)10195 .await?;10296 }10397 CommitEvent::Delete(delete) => {10498 assert_eq!(delete.collection, "sh.tangled.publicKey");105105- state106106- .store()9999+ knot.database()107100 .delete_public_key(delete.did, delete.rkey, delete.rev)108101 .await?;109102 }···110107 Ok(())111108}112109113113-#[tracing::instrument(skip(state))]114114-async fn process_repo(state: &KnotState, event: &CommitEvent<'_>) -> anyhow::Result<()> {110110+#[tracing::instrument(skip(knot))]111111+async fn process_repo(112112+ client: &JetstreamClient,113113+ knot: &KnotState,114114+ event: &CommitEvent<'_>,115115+) -> anyhow::Result<()> {115116 match event {116117 CommitEvent::Create(commit) => {117118 let Lexicon::Repo(repository) = serde_json::from_str(commit.record.get())? else {118119 return Err(anyhow::anyhow!("expected a 'sh.tangled.repo' record"));119120 };120121121121- if repository.knot != state.instance_name() {122122+ if repository.knot != knot.instance_ident() {122123 tracing::debug!(123124 did = %event.did(),124125 rkey = %event.rkey(),···134127135128 let policy = RepositoryCreatePolicy;136129 let can_create = policy137137- .evaluate_access(&commit.did, &Action::RepositoryCreate, state, state)130130+ .evaluate_access(&commit.did, &Action::RepositoryCreate, knot, knot)138131 .await;139132140133 if !matches!(can_create, Granted) {···142135 return Ok(());143136 }144137145145- let is_new = state146146- .store()138138+ let is_new = knot139139+ .database()147140 .insert_repository(commit.did, commit.rkey, commit.rev, commit.cid, &repository)148141 .await?;149142···157150 return Ok(());158151 }159152160160- state.create_repo(commit.did, commit.rkey, &repository.name)?;153153+ knot.create_repo(commit.did, commit.rkey, &repository.name)?;161154 }162155 CommitEvent::Update(commit) => {163156 let Lexicon::Repo(repository) = serde_json::from_str(commit.record.get())? else {164157 return Err(anyhow::anyhow!("expected a 'sh.tangled.repo' record"));165158 };166159167167- if repository.knot != state.instance_name() {160160+ if repository.knot != knot.instance_ident() {168161 tracing::debug!(169162 did = %event.did(),170163 rkey = %event.rkey(),···176169177170 // @TODO Does this need auth?178171179179- state180180- .store()172172+ knot.database()181173 .update_repository(commit.did, commit.rkey, commit.rev, commit.cid, &repository)182174 .await?;183175 }184176 CommitEvent::Delete(commit) => {185177 // First determine whether the repository exists on this knot186186- if state187187- .store()178178+ if knot179179+ .database()188180 .resolve_repository(commit.did, commit.rkey)189181 .await?190182 .is_none()···199193 let policy = RepositoryDeletePolicy;200194 let repository = RepositoryRef::new(commit.did, commit.rkey);201195 let can_create = policy202202- .evaluate_access(&commit.did, &Action::RepositoryDelete, &repository, state)196196+ .evaluate_access(&commit.did, &Action::RepositoryDelete, &repository, knot)203197 .await;204198205199 if !matches!(can_create, Granted) {···207201 return Ok(());208202 }209203210210- if let Some(record) = state.store().delete_repository(commit).await? {211211- state.delete_repo(&record.did, &record.rkey)?;204204+ if let Some(record) = knot.database().delete_repository(commit).await? {205205+ knot.delete_repo(&record.did, &record.rkey)?;212206 }213207 }214208 }···216210 Ok(())217211}218212219219-async fn process_knot_member(state: &KnotState, event: &CommitEvent<'_>) -> anyhow::Result<()> {213213+async fn process_knot_member(214214+ client: &JetstreamClient,215215+ state: &KnotState,216216+ event: &CommitEvent<'_>,217217+) -> anyhow::Result<()> {220218 match event {221219 CommitEvent::Create(commit) | CommitEvent::Update(commit) => {222220 let policy = AddMemberPolicy;···239229 ));240230 };241231242242- if member.domain != state.instance_name() {232232+ if member.domain != state.instance_ident() {243233 return Ok(());244234 }245235246236 if state247247- .store()237237+ .database()248238 .upsert_knot_member(249239 commit.rkey,250240 commit.rev,···256246 .await?257247 {258248 // Start tracking the DID of the new member.259259- state.jetstream.add_did(member.subject).await?;249249+ client.add_did(member.subject).await?;260250 state.fetch_public_keys(member.subject).await?;261251 }262252···274264 }275265276266 if let Some(did) = state277277- .store()267267+ .database()278268 .delete_knot_member(delete.rkey, delete.rev)279269 .await?280270 {281271 // Stop tracking the DID of the new member.282282- state.jetstream.remove_did(did).await?;272272+ client.remove_did(did).await?;283273 }284274285275 Ok(())···288278}289279290280async fn process_repo_collaborator(281281+ client: &JetstreamClient,291282 state: &KnotState,292283 event: &CommitEvent<'_>,293284) -> anyhow::Result<()> {···320309 }321310322311 if state323323- .store()312312+ .database()324313 .upsert_repository_collaborator(325314 commit.did,326315 commit.rkey,···333322 )334323 .await?335324 {336336- state.jetstream.add_did(coll.subject).await?;325325+ client.add_did(coll.subject).await?;337326 state.fetch_public_keys(coll.subject).await?;338327 }339328···341330 }342331 CommitEvent::Delete(delete) => {343332 let (repo_did, repo_rkey, _) = state344344- .store()333333+ .database()345334 .get_repository_collaborator_record(delete.did, delete.rkey)346335 .await?;347336···362351 }363352364353 if let Some(did) = state365365- .store()354354+ .database()366355 .delete_repository_collaborator(delete.did, delete.rkey, delete.rev)367356 .await?368357 {369369- state.jetstream.remove_did(did).await?;358358+ client.remove_did(did).await?;370359 }371360372361 Ok(())