[MIRROR ONLY] A correct and efficient ATProto blob proxy for secure content delivery. codeberg.org/Blooym/porxie
36
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: use xrpc for policy calls

Lyna 17c0fa96 d6c09542

+491 -364
+1
.gitignore
··· 1 1 target/ 2 2 .direnv/ 3 + __pycache__/ 3 4 4 5 test_server.py
+1
Cargo.lock
··· 2803 2803 "multihash-codetable", 2804 2804 "reqwest", 2805 2805 "serde", 2806 + "serde_json", 2806 2807 "subtle", 2807 2808 "sysinfo", 2808 2809 "thiserror 2.0.18",
+2 -2
README.md
··· 70 70 71 71 - [GET] `/{did}/{cid}`: Fetch a blob either from cache or origin. 72 72 - [GET] `/xrpc/dev.blooym.porxie.getBlob?did=<did>&cid=<cid>`: XRPC Compatibility alias for the fetch blob endpoint. 73 - - [POST] `/xrpc/dev.blooym.porxie.clearActorCache?did=<did>`: Clear all cached items relating to an actor DID. 74 - - [POST] `/xrpc/dev.blooym.porxie.clearBlobCache?cid=<cid>`: Clear all cache items relating to a blob CID. 73 + - [POST] `/xrpc/dev.blooym.porxie.cache.purgeActor?did=<did>`: Purge all cached items relating to an actor DID. 74 + - [POST] `/xrpc/dev.blooym.porxie.cache.purgeBlob?cid=<cid>`: Purge all cache items relating to a blob CID. 75 75 76 76 77 77 ## Policy Service
+7 -2
crates/lexgen/lexicons/dev/blooym/porxie/clearActorCache.json crates/lexgen/lexicons/dev/blooym/porxie/cache/purgeActor.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "dev.blooym.porxie.clearActorCache", 3 + "id": "dev.blooym.porxie.cache.purgeActor", 4 4 "defs": { 5 5 "main": { 6 6 "type": "procedure", ··· 26 26 "type": "object", 27 27 "properties": {} 28 28 } 29 - } 29 + }, 30 + "errors": [ 31 + { 32 + "name": "MalformedDid" 33 + } 34 + ] 30 35 } 31 36 } 32 37 }
+7 -2
crates/lexgen/lexicons/dev/blooym/porxie/clearBlobCache.json crates/lexgen/lexicons/dev/blooym/porxie/cache/purgeBlob.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "dev.blooym.porxie.clearBlobCache", 3 + "id": "dev.blooym.porxie.cache.purgeBlob", 4 4 "defs": { 5 5 "main": { 6 6 "type": "procedure", ··· 26 26 "type": "object", 27 27 "properties": {} 28 28 } 29 - } 29 + }, 30 + "errors": [ 31 + { 32 + "name": "MalformedCid" 33 + } 34 + ] 30 35 } 31 36 } 32 37 }
+36 -1
crates/lexgen/lexicons/dev/blooym/porxie/getBlob.json
··· 24 24 }, 25 25 "output": { 26 26 "encoding": "*/*" 27 - } 27 + }, 28 + "errors": [ 29 + { 30 + "name": "BlobCidMismatch" 31 + }, 32 + { 33 + "name": "BlobFetchFailed" 34 + }, 35 + { 36 + "name": "BlobForbiddenType" 37 + }, 38 + { 39 + "name": "BlobNotFound" 40 + }, 41 + { 42 + "name": "BlobTooLarge" 43 + }, 44 + { 45 + "name": "CannotResolve" 46 + }, 47 + { 48 + "name": "CidUnsupported" 49 + }, 50 + { 51 + "name": "InternalServerError" 52 + }, 53 + { 54 + "name": "MalformedCid" 55 + }, 56 + { 57 + "name": "MalformedDid" 58 + }, 59 + { 60 + "name": "PolicyForbidden" 61 + } 62 + ] 28 63 } 29 64 } 30 65 }
+6 -20
crates/lexgen/lexicons/dev/blooym/porxie/getBlobPolicy.json
··· 4 4 "defs": { 5 5 "main": { 6 6 "type": "query", 7 + "description": "Returns the policy status of the given actor + blob combination.", 7 8 "parameters": { 8 9 "type": "params", 9 10 "required": [ ··· 31 32 "properties": { 32 33 "policy": { 33 34 "type": "union", 35 + "closed": true, 34 36 "refs": [ 35 37 "#allowed", 36 - "#restricted", 37 - "#unlisted" 38 + "#forbidden" 38 39 ] 39 40 } 40 41 } ··· 46 47 "description": "Blob is allowed to be served.", 47 48 "properties": {} 48 49 }, 49 - "restricted": { 50 - "type": "object", 51 - "description": "Blob is explicitly restricted. It may have been removed due to moderation reasons.", 52 - "properties": { 53 - "reason": { 54 - "type": "string", 55 - "description": "An optional reason provided for this policy being applied to provide context to the requesting service." 56 - } 57 - } 58 - }, 59 - "unlisted": { 50 + "forbidden": { 60 51 "type": "object", 61 - "description": "Blob is not being served at operator discretion. It may not meet the requirements for the service.", 62 - "properties": { 63 - "reason": { 64 - "type": "string", 65 - "description": "An optional reason provided for this policy being applied to provide context to the requesting service." 66 - } 67 - } 52 + "description": "Blob is not allowed to be served.", 53 + "properties": {} 68 54 } 69 55 } 70 56 }
+1 -2
crates/lexgen/src/dev_blooym/porxie.rs
··· 3 3 // This file was automatically generated from Lexicon schemas. 4 4 // Any manual changes will be overwritten on the next regeneration. 5 5 6 - pub mod clear_actor_cache; 7 - pub mod clear_blob_cache; 6 + pub mod cache; 8 7 pub mod get_blob; 9 8 pub mod get_blob_policy;
+7
crates/lexgen/src/dev_blooym/porxie/cache.rs
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // This file was automatically generated from Lexicon schemas. 4 + // Any manual changes will be overwritten on the next regeneration. 5 + 6 + pub mod purge_actor; 7 + pub mod purge_blob;
+36 -36
crates/lexgen/src/dev_blooym/porxie/clear_actor_cache.rs crates/lexgen/src/dev_blooym/porxie/cache/purge_actor.rs
··· 1 1 // @generated by jacquard-lexicon. DO NOT EDIT. 2 2 // 3 - // Lexicon: dev.blooym.porxie.clearActorCache 3 + // Lexicon: dev.blooym.porxie.cache.purgeActor 4 4 // 5 5 // This file was automatically generated from Lexicon schemas. 6 6 // Any manual changes will be overwritten on the next regeneration. ··· 17 17 #[lexicon] 18 18 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] 19 19 #[serde(rename_all = "camelCase")] 20 - pub struct ClearActorCache<'a> { 20 + pub struct PurgeActor<'a> { 21 21 #[serde(borrow)] 22 22 pub did: Did<'a>, 23 23 } ··· 26 26 #[lexicon] 27 27 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic, Default)] 28 28 #[serde(rename_all = "camelCase")] 29 - pub struct ClearActorCacheOutput<'a> {} 30 - /// Response type for dev.blooym.porxie.clearActorCache 31 - pub struct ClearActorCacheResponse; 32 - impl jacquard_common::xrpc::XrpcResp for ClearActorCacheResponse { 33 - const NSID: &'static str = "dev.blooym.porxie.clearActorCache"; 29 + pub struct PurgeActorOutput<'a> {} 30 + /// Response type for dev.blooym.porxie.cache.purgeActor 31 + pub struct PurgeActorResponse; 32 + impl jacquard_common::xrpc::XrpcResp for PurgeActorResponse { 33 + const NSID: &'static str = "dev.blooym.porxie.cache.purgeActor"; 34 34 const ENCODING: &'static str = "application/json"; 35 - type Output<'de> = ClearActorCacheOutput<'de>; 35 + type Output<'de> = PurgeActorOutput<'de>; 36 36 type Err<'de> = jacquard_common::xrpc::GenericError<'de>; 37 37 } 38 38 39 - impl<'a> jacquard_common::xrpc::XrpcRequest for ClearActorCache<'a> { 40 - const NSID: &'static str = "dev.blooym.porxie.clearActorCache"; 39 + impl<'a> jacquard_common::xrpc::XrpcRequest for PurgeActor<'a> { 40 + const NSID: &'static str = "dev.blooym.porxie.cache.purgeActor"; 41 41 const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( 42 42 "application/json", 43 43 ); 44 - type Response = ClearActorCacheResponse; 44 + type Response = PurgeActorResponse; 45 45 } 46 46 47 - /// Endpoint type for dev.blooym.porxie.clearActorCache 48 - pub struct ClearActorCacheRequest; 49 - impl jacquard_common::xrpc::XrpcEndpoint for ClearActorCacheRequest { 50 - const PATH: &'static str = "/xrpc/dev.blooym.porxie.clearActorCache"; 47 + /// Endpoint type for dev.blooym.porxie.cache.purgeActor 48 + pub struct PurgeActorRequest; 49 + impl jacquard_common::xrpc::XrpcEndpoint for PurgeActorRequest { 50 + const PATH: &'static str = "/xrpc/dev.blooym.porxie.cache.purgeActor"; 51 51 const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( 52 52 "application/json", 53 53 ); 54 - type Request<'de> = ClearActorCache<'de>; 55 - type Response = ClearActorCacheResponse; 54 + type Request<'de> = PurgeActor<'de>; 55 + type Response = PurgeActorResponse; 56 56 } 57 57 58 - pub mod clear_actor_cache_state { 58 + pub mod purge_actor_state { 59 59 60 60 pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 61 61 #[allow(unused)] ··· 88 88 } 89 89 90 90 /// Builder for constructing an instance of this type 91 - pub struct ClearActorCacheBuilder<'a, S: clear_actor_cache_state::State> { 91 + pub struct PurgeActorBuilder<'a, S: purge_actor_state::State> { 92 92 _state: PhantomData<fn() -> S>, 93 93 _fields: (Option<Did<'a>>,), 94 94 _lifetime: PhantomData<&'a ()>, 95 95 } 96 96 97 - impl<'a> ClearActorCache<'a> { 97 + impl<'a> PurgeActor<'a> { 98 98 /// Create a new builder for this type 99 - pub fn new() -> ClearActorCacheBuilder<'a, clear_actor_cache_state::Empty> { 100 - ClearActorCacheBuilder::new() 99 + pub fn new() -> PurgeActorBuilder<'a, purge_actor_state::Empty> { 100 + PurgeActorBuilder::new() 101 101 } 102 102 } 103 103 104 - impl<'a> ClearActorCacheBuilder<'a, clear_actor_cache_state::Empty> { 104 + impl<'a> PurgeActorBuilder<'a, purge_actor_state::Empty> { 105 105 /// Create a new builder with all fields unset 106 106 pub fn new() -> Self { 107 - ClearActorCacheBuilder { 107 + PurgeActorBuilder { 108 108 _state: PhantomData, 109 109 _fields: (None,), 110 110 _lifetime: PhantomData, ··· 112 112 } 113 113 } 114 114 115 - impl<'a, S> ClearActorCacheBuilder<'a, S> 115 + impl<'a, S> PurgeActorBuilder<'a, S> 116 116 where 117 - S: clear_actor_cache_state::State, 118 - S::Did: clear_actor_cache_state::IsUnset, 117 + S: purge_actor_state::State, 118 + S::Did: purge_actor_state::IsUnset, 119 119 { 120 120 /// Set the `did` field (required) 121 121 pub fn did( 122 122 mut self, 123 123 value: impl Into<Did<'a>>, 124 - ) -> ClearActorCacheBuilder<'a, clear_actor_cache_state::SetDid<S>> { 124 + ) -> PurgeActorBuilder<'a, purge_actor_state::SetDid<S>> { 125 125 self._fields.0 = Option::Some(value.into()); 126 - ClearActorCacheBuilder { 126 + PurgeActorBuilder { 127 127 _state: PhantomData, 128 128 _fields: self._fields, 129 129 _lifetime: PhantomData, ··· 131 131 } 132 132 } 133 133 134 - impl<'a, S> ClearActorCacheBuilder<'a, S> 134 + impl<'a, S> PurgeActorBuilder<'a, S> 135 135 where 136 - S: clear_actor_cache_state::State, 137 - S::Did: clear_actor_cache_state::IsSet, 136 + S: purge_actor_state::State, 137 + S::Did: purge_actor_state::IsSet, 138 138 { 139 139 /// Build the final struct 140 - pub fn build(self) -> ClearActorCache<'a> { 141 - ClearActorCache { 140 + pub fn build(self) -> PurgeActor<'a> { 141 + PurgeActor { 142 142 did: self._fields.0.unwrap(), 143 143 extra_data: Default::default(), 144 144 } ··· 150 150 jacquard_common::deps::smol_str::SmolStr, 151 151 jacquard_common::types::value::Data<'a>, 152 152 >, 153 - ) -> ClearActorCache<'a> { 154 - ClearActorCache { 153 + ) -> PurgeActor<'a> { 154 + PurgeActor { 155 155 did: self._fields.0.unwrap(), 156 156 extra_data: Some(extra_data), 157 157 }
+36 -36
crates/lexgen/src/dev_blooym/porxie/clear_blob_cache.rs crates/lexgen/src/dev_blooym/porxie/cache/purge_blob.rs
··· 1 1 // @generated by jacquard-lexicon. DO NOT EDIT. 2 2 // 3 - // Lexicon: dev.blooym.porxie.clearBlobCache 3 + // Lexicon: dev.blooym.porxie.cache.purgeBlob 4 4 // 5 5 // This file was automatically generated from Lexicon schemas. 6 6 // Any manual changes will be overwritten on the next regeneration. ··· 17 17 #[lexicon] 18 18 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] 19 19 #[serde(rename_all = "camelCase")] 20 - pub struct ClearBlobCache<'a> { 20 + pub struct PurgeBlob<'a> { 21 21 #[serde(borrow)] 22 22 pub cid: Cid<'a>, 23 23 } ··· 26 26 #[lexicon] 27 27 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic, Default)] 28 28 #[serde(rename_all = "camelCase")] 29 - pub struct ClearBlobCacheOutput<'a> {} 30 - /// Response type for dev.blooym.porxie.clearBlobCache 31 - pub struct ClearBlobCacheResponse; 32 - impl jacquard_common::xrpc::XrpcResp for ClearBlobCacheResponse { 33 - const NSID: &'static str = "dev.blooym.porxie.clearBlobCache"; 29 + pub struct PurgeBlobOutput<'a> {} 30 + /// Response type for dev.blooym.porxie.cache.purgeBlob 31 + pub struct PurgeBlobResponse; 32 + impl jacquard_common::xrpc::XrpcResp for PurgeBlobResponse { 33 + const NSID: &'static str = "dev.blooym.porxie.cache.purgeBlob"; 34 34 const ENCODING: &'static str = "application/json"; 35 - type Output<'de> = ClearBlobCacheOutput<'de>; 35 + type Output<'de> = PurgeBlobOutput<'de>; 36 36 type Err<'de> = jacquard_common::xrpc::GenericError<'de>; 37 37 } 38 38 39 - impl<'a> jacquard_common::xrpc::XrpcRequest for ClearBlobCache<'a> { 40 - const NSID: &'static str = "dev.blooym.porxie.clearBlobCache"; 39 + impl<'a> jacquard_common::xrpc::XrpcRequest for PurgeBlob<'a> { 40 + const NSID: &'static str = "dev.blooym.porxie.cache.purgeBlob"; 41 41 const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( 42 42 "application/json", 43 43 ); 44 - type Response = ClearBlobCacheResponse; 44 + type Response = PurgeBlobResponse; 45 45 } 46 46 47 - /// Endpoint type for dev.blooym.porxie.clearBlobCache 48 - pub struct ClearBlobCacheRequest; 49 - impl jacquard_common::xrpc::XrpcEndpoint for ClearBlobCacheRequest { 50 - const PATH: &'static str = "/xrpc/dev.blooym.porxie.clearBlobCache"; 47 + /// Endpoint type for dev.blooym.porxie.cache.purgeBlob 48 + pub struct PurgeBlobRequest; 49 + impl jacquard_common::xrpc::XrpcEndpoint for PurgeBlobRequest { 50 + const PATH: &'static str = "/xrpc/dev.blooym.porxie.cache.purgeBlob"; 51 51 const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( 52 52 "application/json", 53 53 ); 54 - type Request<'de> = ClearBlobCache<'de>; 55 - type Response = ClearBlobCacheResponse; 54 + type Request<'de> = PurgeBlob<'de>; 55 + type Response = PurgeBlobResponse; 56 56 } 57 57 58 - pub mod clear_blob_cache_state { 58 + pub mod purge_blob_state { 59 59 60 60 pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 61 61 #[allow(unused)] ··· 88 88 } 89 89 90 90 /// Builder for constructing an instance of this type 91 - pub struct ClearBlobCacheBuilder<'a, S: clear_blob_cache_state::State> { 91 + pub struct PurgeBlobBuilder<'a, S: purge_blob_state::State> { 92 92 _state: PhantomData<fn() -> S>, 93 93 _fields: (Option<Cid<'a>>,), 94 94 _lifetime: PhantomData<&'a ()>, 95 95 } 96 96 97 - impl<'a> ClearBlobCache<'a> { 97 + impl<'a> PurgeBlob<'a> { 98 98 /// Create a new builder for this type 99 - pub fn new() -> ClearBlobCacheBuilder<'a, clear_blob_cache_state::Empty> { 100 - ClearBlobCacheBuilder::new() 99 + pub fn new() -> PurgeBlobBuilder<'a, purge_blob_state::Empty> { 100 + PurgeBlobBuilder::new() 101 101 } 102 102 } 103 103 104 - impl<'a> ClearBlobCacheBuilder<'a, clear_blob_cache_state::Empty> { 104 + impl<'a> PurgeBlobBuilder<'a, purge_blob_state::Empty> { 105 105 /// Create a new builder with all fields unset 106 106 pub fn new() -> Self { 107 - ClearBlobCacheBuilder { 107 + PurgeBlobBuilder { 108 108 _state: PhantomData, 109 109 _fields: (None,), 110 110 _lifetime: PhantomData, ··· 112 112 } 113 113 } 114 114 115 - impl<'a, S> ClearBlobCacheBuilder<'a, S> 115 + impl<'a, S> PurgeBlobBuilder<'a, S> 116 116 where 117 - S: clear_blob_cache_state::State, 118 - S::Cid: clear_blob_cache_state::IsUnset, 117 + S: purge_blob_state::State, 118 + S::Cid: purge_blob_state::IsUnset, 119 119 { 120 120 /// Set the `cid` field (required) 121 121 pub fn cid( 122 122 mut self, 123 123 value: impl Into<Cid<'a>>, 124 - ) -> ClearBlobCacheBuilder<'a, clear_blob_cache_state::SetCid<S>> { 124 + ) -> PurgeBlobBuilder<'a, purge_blob_state::SetCid<S>> { 125 125 self._fields.0 = Option::Some(value.into()); 126 - ClearBlobCacheBuilder { 126 + PurgeBlobBuilder { 127 127 _state: PhantomData, 128 128 _fields: self._fields, 129 129 _lifetime: PhantomData, ··· 131 131 } 132 132 } 133 133 134 - impl<'a, S> ClearBlobCacheBuilder<'a, S> 134 + impl<'a, S> PurgeBlobBuilder<'a, S> 135 135 where 136 - S: clear_blob_cache_state::State, 137 - S::Cid: clear_blob_cache_state::IsSet, 136 + S: purge_blob_state::State, 137 + S::Cid: purge_blob_state::IsSet, 138 138 { 139 139 /// Build the final struct 140 - pub fn build(self) -> ClearBlobCache<'a> { 141 - ClearBlobCache { 140 + pub fn build(self) -> PurgeBlob<'a> { 141 + PurgeBlob { 142 142 cid: self._fields.0.unwrap(), 143 143 extra_data: Default::default(), 144 144 } ··· 150 150 jacquard_common::deps::smol_str::SmolStr, 151 151 jacquard_common::types::value::Data<'a>, 152 152 >, 153 - ) -> ClearBlobCache<'a> { 154 - ClearBlobCache { 153 + ) -> PurgeBlob<'a> { 154 + PurgeBlob { 155 155 cid: self._fields.0.unwrap(), 156 156 extra_data: Some(extra_data), 157 157 }
+129 -2
crates/lexgen/src/dev_blooym/porxie/get_blob.rs
··· 7 7 8 8 #[allow(unused_imports)] 9 9 use core::marker::PhantomData; 10 + use jacquard_common::CowStr; 10 11 use jacquard_common::deps::bytes::Bytes; 11 12 use jacquard_common::types::string::{Did, Cid}; 12 - use jacquard_derive::IntoStatic; 13 + use jacquard_derive::{IntoStatic, open_union}; 13 14 use serde::{Serialize, Deserialize}; 14 15 15 16 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] ··· 28 29 pub body: Bytes, 29 30 } 30 31 32 + 33 + #[open_union] 34 + #[derive( 35 + Serialize, 36 + Deserialize, 37 + Debug, 38 + Clone, 39 + PartialEq, 40 + Eq, 41 + thiserror::Error, 42 + miette::Diagnostic, 43 + IntoStatic 44 + )] 45 + 46 + #[serde(tag = "error", content = "message")] 47 + #[serde(bound(deserialize = "'de: 'a"))] 48 + pub enum GetBlobError<'a> { 49 + #[serde(rename = "MalformedDid")] 50 + MalformedDid(Option<CowStr<'a>>), 51 + #[serde(rename = "MalformedCid")] 52 + MalformedCid(Option<CowStr<'a>>), 53 + #[serde(rename = "PolicyForbidden")] 54 + PolicyForbidden(Option<CowStr<'a>>), 55 + #[serde(rename = "InternalServerError")] 56 + InternalServerError(Option<CowStr<'a>>), 57 + #[serde(rename = "BlobNotFound")] 58 + BlobNotFound(Option<CowStr<'a>>), 59 + #[serde(rename = "BlobTooLarge")] 60 + BlobTooLarge(Option<CowStr<'a>>), 61 + #[serde(rename = "BlobForbiddenType")] 62 + BlobForbiddenType(Option<CowStr<'a>>), 63 + #[serde(rename = "BlobCidMismatch")] 64 + BlobCidMismatch(Option<CowStr<'a>>), 65 + #[serde(rename = "CidUnsupported")] 66 + CidUnsupported(Option<CowStr<'a>>), 67 + #[serde(rename = "CannotResolve")] 68 + CannotResolve(Option<CowStr<'a>>), 69 + #[serde(rename = "BlobFetchFailed")] 70 + BlobFetchFailed(Option<CowStr<'a>>), 71 + } 72 + 73 + impl core::fmt::Display for GetBlobError<'_> { 74 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 75 + match self { 76 + Self::MalformedDid(msg) => { 77 + write!(f, "MalformedDid")?; 78 + if let Some(msg) = msg { 79 + write!(f, ": {}", msg)?; 80 + } 81 + Ok(()) 82 + } 83 + Self::MalformedCid(msg) => { 84 + write!(f, "MalformedCid")?; 85 + if let Some(msg) = msg { 86 + write!(f, ": {}", msg)?; 87 + } 88 + Ok(()) 89 + } 90 + Self::PolicyForbidden(msg) => { 91 + write!(f, "PolicyForbidden")?; 92 + if let Some(msg) = msg { 93 + write!(f, ": {}", msg)?; 94 + } 95 + Ok(()) 96 + } 97 + Self::InternalServerError(msg) => { 98 + write!(f, "InternalServerError")?; 99 + if let Some(msg) = msg { 100 + write!(f, ": {}", msg)?; 101 + } 102 + Ok(()) 103 + } 104 + Self::BlobNotFound(msg) => { 105 + write!(f, "BlobNotFound")?; 106 + if let Some(msg) = msg { 107 + write!(f, ": {}", msg)?; 108 + } 109 + Ok(()) 110 + } 111 + Self::BlobTooLarge(msg) => { 112 + write!(f, "BlobTooLarge")?; 113 + if let Some(msg) = msg { 114 + write!(f, ": {}", msg)?; 115 + } 116 + Ok(()) 117 + } 118 + Self::BlobForbiddenType(msg) => { 119 + write!(f, "BlobForbiddenType")?; 120 + if let Some(msg) = msg { 121 + write!(f, ": {}", msg)?; 122 + } 123 + Ok(()) 124 + } 125 + Self::BlobCidMismatch(msg) => { 126 + write!(f, "BlobCidMismatch")?; 127 + if let Some(msg) = msg { 128 + write!(f, ": {}", msg)?; 129 + } 130 + Ok(()) 131 + } 132 + Self::CidUnsupported(msg) => { 133 + write!(f, "CidUnsupported")?; 134 + if let Some(msg) = msg { 135 + write!(f, ": {}", msg)?; 136 + } 137 + Ok(()) 138 + } 139 + Self::CannotResolve(msg) => { 140 + write!(f, "CannotResolve")?; 141 + if let Some(msg) = msg { 142 + write!(f, ": {}", msg)?; 143 + } 144 + Ok(()) 145 + } 146 + Self::BlobFetchFailed(msg) => { 147 + write!(f, "BlobFetchFailed")?; 148 + if let Some(msg) = msg { 149 + write!(f, ": {}", msg)?; 150 + } 151 + Ok(()) 152 + } 153 + Self::Unknown(err) => write!(f, "Unknown error: {:?}", err), 154 + } 155 + } 156 + } 157 + 31 158 /// Response type for dev.blooym.porxie.getBlob 32 159 pub struct GetBlobResponse; 33 160 impl jacquard_common::xrpc::XrpcResp for GetBlobResponse { 34 161 const NSID: &'static str = "dev.blooym.porxie.getBlob"; 35 162 const ENCODING: &'static str = "*/*"; 36 163 type Output<'de> = GetBlobOutput; 37 - type Err<'de> = jacquard_common::xrpc::GenericError<'de>; 164 + type Err<'de> = GetBlobError<'de>; 38 165 fn encode_output( 39 166 output: &Self::Output<'_>, 40 167 ) -> Result<Vec<u8>, jacquard_common::xrpc::EncodeError> {
+55 -131
crates/lexgen/src/dev_blooym/porxie/get_blob_policy.rs
··· 10 10 11 11 #[allow(unused_imports)] 12 12 use core::marker::PhantomData; 13 - use jacquard_common::CowStr; 14 13 15 14 #[allow(unused_imports)] 16 15 use jacquard_common::deps::codegen::unicode_segmentation::UnicodeSegmentation; 17 16 use jacquard_common::types::string::{Did, Cid}; 18 - use jacquard_derive::{IntoStatic, lexicon, open_union}; 17 + use jacquard_derive::{IntoStatic, lexicon}; 19 18 use jacquard_lexicon::lexicon::LexiconDoc; 20 19 use jacquard_lexicon::schema::LexiconSchema; 21 20 ··· 29 28 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic, Default)] 30 29 #[serde(rename_all = "camelCase")] 31 30 pub struct Allowed<'a> {} 31 + /// Blob is not allowed to be served. 32 + 33 + #[lexicon] 34 + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic, Default)] 35 + #[serde(rename_all = "camelCase")] 36 + pub struct Forbidden<'a> {} 32 37 33 38 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] 34 39 #[serde(rename_all = "camelCase")] ··· 49 54 } 50 55 51 56 52 - #[open_union] 53 57 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] 54 - #[serde(tag = "$type", bound(deserialize = "'de: 'a"))] 58 + #[serde(tag = "$type")] 59 + #[serde(bound(deserialize = "'de: 'a"))] 55 60 pub enum GetBlobPolicyOutputPolicy<'a> { 56 61 #[serde(rename = "dev.blooym.porxie.getBlobPolicy#allowed")] 57 62 Allowed(Box<get_blob_policy::Allowed<'a>>), 58 - #[serde(rename = "dev.blooym.porxie.getBlobPolicy#restricted")] 59 - Restricted(Box<get_blob_policy::Restricted<'a>>), 60 - #[serde(rename = "dev.blooym.porxie.getBlobPolicy#unlisted")] 61 - Unlisted(Box<get_blob_policy::Unlisted<'a>>), 63 + #[serde(rename = "dev.blooym.porxie.getBlobPolicy#forbidden")] 64 + Forbidden(Box<get_blob_policy::Forbidden<'a>>), 62 65 } 63 66 64 - /// Blob is explicitly restricted. It may have been removed due to moderation reasons. 65 - 66 - #[lexicon] 67 - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic, Default)] 68 - #[serde(rename_all = "camelCase")] 69 - pub struct Restricted<'a> { 70 - ///An optional reason provided for this policy being applied to provide context to the requesting service. 71 - #[serde(skip_serializing_if = "Option::is_none")] 72 - #[serde(borrow)] 73 - pub reason: Option<CowStr<'a>>, 67 + impl<'a> LexiconSchema for Allowed<'a> { 68 + fn nsid() -> &'static str { 69 + "dev.blooym.porxie.getBlobPolicy" 70 + } 71 + fn def_name() -> &'static str { 72 + "allowed" 73 + } 74 + fn lexicon_doc() -> LexiconDoc<'static> { 75 + lexicon_doc_dev_blooym_porxie_getBlobPolicy() 76 + } 77 + fn validate(&self) -> Result<(), ConstraintError> { 78 + Ok(()) 79 + } 74 80 } 75 81 76 - /// Blob is not being served at operator discretion. It may not meet the requirements for the service. 77 - 78 - #[lexicon] 79 - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic, Default)] 80 - #[serde(rename_all = "camelCase")] 81 - pub struct Unlisted<'a> { 82 - ///An optional reason provided for this policy being applied to provide context to the requesting service. 83 - #[serde(skip_serializing_if = "Option::is_none")] 84 - #[serde(borrow)] 85 - pub reason: Option<CowStr<'a>>, 86 - } 87 - 88 - impl<'a> LexiconSchema for Allowed<'a> { 82 + impl<'a> LexiconSchema for Forbidden<'a> { 89 83 fn nsid() -> &'static str { 90 84 "dev.blooym.porxie.getBlobPolicy" 91 85 } 92 86 fn def_name() -> &'static str { 93 - "allowed" 87 + "forbidden" 94 88 } 95 89 fn lexicon_doc() -> LexiconDoc<'static> { 96 - lexicon_doc_dev_blooym_porxie_get_blob_policy() 90 + lexicon_doc_dev_blooym_porxie_getBlobPolicy() 97 91 } 98 92 fn validate(&self) -> Result<(), ConstraintError> { 99 93 Ok(()) ··· 124 118 type Response = GetBlobPolicyResponse; 125 119 } 126 120 127 - impl<'a> LexiconSchema for Restricted<'a> { 128 - fn nsid() -> &'static str { 129 - "dev.blooym.porxie.getBlobPolicy" 130 - } 131 - fn def_name() -> &'static str { 132 - "restricted" 133 - } 134 - fn lexicon_doc() -> LexiconDoc<'static> { 135 - lexicon_doc_dev_blooym_porxie_get_blob_policy() 136 - } 137 - fn validate(&self) -> Result<(), ConstraintError> { 138 - Ok(()) 139 - } 140 - } 141 - 142 - impl<'a> LexiconSchema for Unlisted<'a> { 143 - fn nsid() -> &'static str { 144 - "dev.blooym.porxie.getBlobPolicy" 145 - } 146 - fn def_name() -> &'static str { 147 - "unlisted" 148 - } 149 - fn lexicon_doc() -> LexiconDoc<'static> { 150 - lexicon_doc_dev_blooym_porxie_get_blob_policy() 151 - } 152 - fn validate(&self) -> Result<(), ConstraintError> { 153 - Ok(()) 154 - } 155 - } 156 - 157 - fn lexicon_doc_dev_blooym_porxie_get_blob_policy() -> LexiconDoc<'static> { 121 + fn lexicon_doc_dev_blooym_porxie_getBlobPolicy() -> LexiconDoc<'static> { 158 122 #[allow(unused_imports)] 159 123 use jacquard_common::{CowStr, deps::smol_str::SmolStr, types::blob::MimeType}; 160 124 use jacquard_lexicon::lexicon::*; ··· 179 143 }), 180 144 ); 181 145 map.insert( 146 + SmolStr::new_static("forbidden"), 147 + LexUserType::Object(LexObject { 148 + description: Some( 149 + CowStr::new_static("Blob is not allowed to be served."), 150 + ), 151 + properties: { 152 + #[allow(unused_mut)] 153 + let mut map = BTreeMap::new(); 154 + map 155 + }, 156 + ..Default::default() 157 + }), 158 + ); 159 + map.insert( 182 160 SmolStr::new_static("main"), 183 161 LexUserType::XrpcQuery(LexXrpcQuery { 184 162 parameters: Some( ··· 211 189 ..Default::default() 212 190 }), 213 191 ); 214 - map.insert( 215 - SmolStr::new_static("restricted"), 216 - LexUserType::Object(LexObject { 217 - description: Some( 218 - CowStr::new_static( 219 - "Blob is explicitly restricted. It may have been removed due to moderation reasons.", 220 - ), 221 - ), 222 - properties: { 223 - #[allow(unused_mut)] 224 - let mut map = BTreeMap::new(); 225 - map.insert( 226 - SmolStr::new_static("reason"), 227 - LexObjectProperty::String(LexString { 228 - description: Some( 229 - CowStr::new_static( 230 - "An optional reason provided for this policy being applied to provide context to the requesting service.", 231 - ), 232 - ), 233 - ..Default::default() 234 - }), 235 - ); 236 - map 237 - }, 238 - ..Default::default() 239 - }), 240 - ); 241 - map.insert( 242 - SmolStr::new_static("unlisted"), 243 - LexUserType::Object(LexObject { 244 - description: Some( 245 - CowStr::new_static( 246 - "Blob is not being served at operator discretion. It may not meet the requirements for the service.", 247 - ), 248 - ), 249 - properties: { 250 - #[allow(unused_mut)] 251 - let mut map = BTreeMap::new(); 252 - map.insert( 253 - SmolStr::new_static("reason"), 254 - LexObjectProperty::String(LexString { 255 - description: Some( 256 - CowStr::new_static( 257 - "An optional reason provided for this policy being applied to provide context to the requesting service.", 258 - ), 259 - ), 260 - ..Default::default() 261 - }), 262 - ); 263 - map 264 - }, 265 - ..Default::default() 266 - }), 267 - ); 268 192 map 269 193 }, 270 194 ..Default::default() ··· 281 205 } 282 206 /// State trait tracking which required fields have been set 283 207 pub trait State: sealed::Sealed { 284 - type Cid; 285 208 type Did; 209 + type Cid; 286 210 } 287 211 /// Empty state - all required fields are unset 288 212 pub struct Empty(()); 289 213 impl sealed::Sealed for Empty {} 290 214 impl State for Empty { 291 - type Cid = Unset; 292 215 type Did = Unset; 293 - } 294 - ///State transition - sets the `cid` field to Set 295 - pub struct SetCid<S: State = Empty>(PhantomData<fn() -> S>); 296 - impl<S: State> sealed::Sealed for SetCid<S> {} 297 - impl<S: State> State for SetCid<S> { 298 - type Cid = Set<members::cid>; 299 - type Did = S::Did; 216 + type Cid = Unset; 300 217 } 301 218 ///State transition - sets the `did` field to Set 302 219 pub struct SetDid<S: State = Empty>(PhantomData<fn() -> S>); 303 220 impl<S: State> sealed::Sealed for SetDid<S> {} 304 221 impl<S: State> State for SetDid<S> { 305 - type Cid = S::Cid; 306 222 type Did = Set<members::did>; 223 + type Cid = S::Cid; 224 + } 225 + ///State transition - sets the `cid` field to Set 226 + pub struct SetCid<S: State = Empty>(PhantomData<fn() -> S>); 227 + impl<S: State> sealed::Sealed for SetCid<S> {} 228 + impl<S: State> State for SetCid<S> { 229 + type Did = S::Did; 230 + type Cid = Set<members::cid>; 307 231 } 308 232 /// Marker types for field names 309 233 #[allow(non_camel_case_types)] 310 234 pub mod members { 235 + ///Marker type for the `did` field 236 + pub struct did(()); 311 237 ///Marker type for the `cid` field 312 238 pub struct cid(()); 313 - ///Marker type for the `did` field 314 - pub struct did(()); 315 239 } 316 240 } 317 241 ··· 381 305 impl<'a, S> GetBlobPolicyBuilder<'a, S> 382 306 where 383 307 S: get_blob_policy_state::State, 384 - S::Cid: get_blob_policy_state::IsSet, 385 308 S::Did: get_blob_policy_state::IsSet, 309 + S::Cid: get_blob_policy_state::IsSet, 386 310 { 387 311 /// Build the final struct 388 312 pub fn build(self) -> GetBlobPolicy<'a> {
+1
crates/porxie/Cargo.toml
··· 77 77 "derive", 78 78 "std", 79 79 ], default-features = false } 80 + serde_json = "1.0.149" 80 81 subtle = { version = "2.6", default-features = false, features = ["std"] } 81 82 sysinfo = { version = "0.38.4", default-features = false, features = [ 82 83 "system",
+7 -5
crates/porxie/src/blob_service.rs
··· 261 261 .await 262 262 } 263 263 264 - pub async fn invalidate_blob(&self, cid: &BlobCid) { 265 - self.data_cache.invalidate(cid).await 266 - } 267 - 268 264 /// Fetch whether the user owns the given blob either from the cache if available or the upstream source. 269 265 /// 270 266 /// The internal cache will be automatically populated if the blob was previously fetched from the same user. ··· 333 329 .await 334 330 } 335 331 336 - pub fn invalidate_blob_ownership< 332 + /// Invalid a specific blob cache entry. 333 + pub async fn invalidate_blob_cache_entry(&self, cid: &BlobCid) { 334 + self.data_cache.invalidate(cid).await 335 + } 336 + 337 + /// Invalidate blob ownership cache entries if they match the predicate. 338 + pub fn invalidate_blob_ownership_cache_entries< 337 339 F: Fn(&(BlobCid, Did<'static>), &()) -> bool + Send + Sync + 'static, 338 340 >( 339 341 &self,
+13 -10
crates/porxie/src/main.rs
··· 19 19 get_blob_handler, get_index_handler, 20 20 xrpc::{ 21 21 dev_blooym::porxie::{ 22 - clear_actor_cache_handler, clear_blob_cache_handler, get_blob_handler_xrpc_compat, 22 + cache::{xrpc_cache_purge_actor_handler, xrpc_cache_purge_blob_handler}, 23 + xrpc_compat_get_blob_handler, 23 24 }, 24 - get_health_handler, 25 + xrpc_fallback_handler, xrpc_get_health_handler, 25 26 }, 26 27 }, 27 28 }; ··· 33 34 http::{HeaderName, HeaderValue, StatusCode, header}, 34 35 middleware::{self as axum_middleware, Next}, 35 36 response::Response, 36 - routing::{get, post}, 37 + routing::{any, get, post}, 37 38 }; 38 39 use bytesize::ByteSize; 39 40 use clap::{Args, Parser}; ··· 493 494 .nest( 494 495 "/xrpc", 495 496 Router::new() 496 - .route("/_health", get(get_health_handler)) 497 + .route("/_health", get(xrpc_get_health_handler)) 497 498 .route( 498 499 "/dev.blooym.porxie.getBlob", 499 - get(get_blob_handler_xrpc_compat).layer(TimeoutLayer::with_status_code( 500 + get(xrpc_compat_get_blob_handler).layer(TimeoutLayer::with_status_code( 500 501 StatusCode::REQUEST_TIMEOUT, 501 502 args.blob.processing_timeout.into(), 502 503 )), 503 504 ) 504 505 .route( 505 - "/dev.blooym.porxie.clearActorCache", 506 - post(clear_actor_cache_handler), 506 + "/dev.blooym.porxie.cache.purgeActor", 507 + post(xrpc_cache_purge_actor_handler), 507 508 ) 508 509 .route( 509 - "/dev.blooym.porxie.clearBlobCache", 510 - post(clear_blob_cache_handler), 511 - ), 510 + "/dev.blooym.porxie.cache.purgeBlob", 511 + post(xrpc_cache_purge_blob_handler), 512 + ) 513 + // Ensure /xrpc/... routes don't fall through elsewhere. 514 + .route("/{rest}", any(xrpc_fallback_handler)), 512 515 ) 513 516 .layer( 514 517 TraceLayer::new_for_http()
+60 -33
crates/porxie/src/policy_client.rs
··· 1 1 use crate::{http::PORXIE_USER_AGENT, types::blob_cid::BlobCid}; 2 2 use jacquard_common::types::did::Did; 3 + use lexgen::dev_blooym::porxie::get_blob_policy::{GetBlobPolicyOutput, GetBlobPolicyOutputPolicy}; 3 4 use moka::{future::Cache as MokaCache, policy::EvictionPolicy}; 4 5 use reqwest::{ 5 6 StatusCode, Url, ··· 9 10 use thiserror::Error; 10 11 use tracing::instrument; 11 12 12 - #[derive(Debug, Clone)] 13 - pub struct PolicyDecision { 14 - /// Whether the service allows this blob can be served. 15 - pub can_serve: bool, 16 - } 17 - 18 13 #[derive(Debug, Error)] 19 14 #[non_exhaustive] 20 15 pub enum CreatePolicyClientError { ··· 23 18 HttpClient(#[from] reqwest::Error), 24 19 } 25 20 21 + #[derive(Debug, Clone)] 22 + pub enum PolicyDecision { 23 + Allowed, 24 + Forbidden, 25 + } 26 + 27 + impl PolicyDecision { 28 + fn from_service_output(response: &GetBlobPolicyOutput) -> Self { 29 + match response.policy { 30 + GetBlobPolicyOutputPolicy::Allowed(_) => Self::Allowed, 31 + GetBlobPolicyOutputPolicy::Forbidden(_) => Self::Forbidden, 32 + } 33 + } 34 + } 35 + 26 36 #[derive(Debug, Error)] 27 37 #[non_exhaustive] 28 38 pub enum GetBlobPolicyError { 29 - /// Policy service returned an unhandled status code (Not 200 OK or 410 GONE). 30 - #[error("received an unhandled status code from the policy service: {0}")] 31 - UnhandledStatusCode(StatusCode), 39 + /// Policy service returned an unsuccessful status code. 40 + #[error("received an unsuccessful status code from the policy service: {0}")] 41 + StatusCode(StatusCode), 42 + 43 + /// An internal deserialization error occured. 44 + #[error(transparent)] 45 + Deserialize(#[from] serde_json::Error), 46 + 32 47 /// An internal http client error occurred, see [`reqwest::Error`]. 33 48 #[error(transparent)] 34 49 HttpClient(#[from] reqwest::Error), ··· 63 78 tracing::debug!("creating policy service client with options: {options:?}"); 64 79 Ok(Self { 65 80 cache: MokaCache::<(Did<'static>, BlobCid), PolicyDecision>::builder() 66 - .name("blob-policy") 81 + .name("policy") 67 82 .weigher(|key, _value| { 68 83 (key.0.len() + key.1.encoded_len()) 69 84 .try_into() ··· 91 106 }) 92 107 } 93 108 94 - /// Query the policy service for the policy decision of this blob. 109 + /// Query the policy service for any policy decisions applied to this actor/blob. 95 110 /// 96 111 /// Concurrent requests for the same policy are coalesced. 97 112 #[instrument(skip_all, fields(did = %did, cid = %cid))] 98 - pub async fn get_policy_for_blob( 113 + pub async fn get_policy( 99 114 &self, 100 115 did: &Did<'static>, 101 116 cid: BlobCid, ··· 104 119 .try_get_with_by_ref(&(did.clone(), cid), async { 105 120 tracing::debug!("querying policy service for the status"); 106 121 107 - let mut policy_service_url = self.policy_service_url.clone(); 108 - policy_service_url 109 - .path_segments_mut() 110 - .expect("policy service URL should not be cannot-be-a-base") 111 - .push(did.as_str()) 112 - .push(&cid.to_string()); 122 + // Build policy service URL. 123 + let url = { 124 + let mut url = self.policy_service_url.clone(); 125 + url.set_path("/xrpc/dev.blooym.porxie.getBlobPolicy"); 126 + url.query_pairs_mut() 127 + .append_pair("did", did.as_str()) 128 + .append_pair("cid", &cid.to_string()); 129 + url 130 + }; 113 131 114 - let mut request = self.http_client.get(policy_service_url); 132 + // Build request. 133 + let mut request = self.http_client.get(url); 134 + // TODO: Swap this for xrpc admin authentication. 115 135 for (name, value) in &self.policy_service_req_headers { 116 136 request = request.header(name, value); 117 137 } 118 138 139 + // Fetch & deserialize policy data. 119 140 match request.send().await { 120 - Ok(response) => match response.status() { 121 - StatusCode::OK => { 122 - tracing::debug!("policy service allowed blob serving"); 123 - Ok(PolicyDecision { can_serve: true }) 124 - } 125 - StatusCode::GONE => { 126 - tracing::debug!("policy service forbids blob serving"); 127 - Ok(PolicyDecision { can_serve: false }) 141 + Ok(response) => { 142 + let status = response.status(); 143 + if !status.is_success() { 144 + tracing::error!( 145 + "policy service returned unsuccessful status: {status}", 146 + ); 147 + return Err(GetBlobPolicyError::StatusCode(status)); 128 148 } 129 - status => { 130 - tracing::error!("policy service returned unexpected status: {status}"); 131 - Err(GetBlobPolicyError::UnhandledStatusCode(status)) 149 + match serde_json::from_slice::<GetBlobPolicyOutput>( 150 + &response.bytes().await?, 151 + ) { 152 + Ok(output) => Ok(PolicyDecision::from_service_output(&output)), 153 + Err(err) => { 154 + tracing::error!( 155 + "failed to deserialize policy service response: {status}", 156 + ); 157 + Err(GetBlobPolicyError::Deserialize(err)) 158 + } 132 159 } 133 - }, 160 + } 134 161 Err(err) => { 135 162 tracing::error!("error occurred contacting the policy service: {err:?}"); 136 163 Err(GetBlobPolicyError::HttpClient(err)) ··· 140 167 .await 141 168 } 142 169 143 - /// Invalidate cached policy decisions with the given predicate. 144 - pub fn invalidate_policies< 170 + /// Invalidate cached policy entries if they match the predicate. 171 + pub fn invalidate_cache_entries< 145 172 F: Fn(&(Did<'static>, BlobCid), &PolicyDecision) -> bool + Send + Sync + 'static, 146 173 >( 147 174 &self,
+32 -28
crates/porxie/src/routes/blob.rs
··· 1 1 use crate::{ 2 2 AppState, 3 3 blob_service::{BlobDownloadError, BlobOwnershipError, BlobUrlResolver}, 4 - routes::{CACHE_CONTROL_NOCACHE_VALUE, ErrorResponse}, 4 + policy_client::PolicyDecision, 5 + routes::{CACHE_CONTROL_NOCACHE_VALUE, XrpcErrorResponse}, 5 6 types::blob_cid::BlobCid, 6 7 }; 7 8 use axum::{ ··· 23 24 ( 24 25 StatusCode, 25 26 [(HeaderName, &'static str); 1], 26 - Json<ErrorResponse>, 27 + Json<XrpcErrorResponse>, 27 28 ), 28 29 > { 29 30 let (did, cid) = ( ··· 33 34 return Err(( 34 35 StatusCode::UNPROCESSABLE_ENTITY, 35 36 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 36 - Json(ErrorResponse { 37 + Json(XrpcErrorResponse { 37 38 error: "MalformedDid", 38 39 message: Some("Invalid or unprocessable DID"), 39 40 }), ··· 46 47 return Err(( 47 48 StatusCode::UNPROCESSABLE_ENTITY, 48 49 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 49 - Json(ErrorResponse { 50 + Json(XrpcErrorResponse { 50 51 error: "MalformedCid", 51 52 message: Some("Invalid or unprocessable CID"), 52 53 }), ··· 57 58 58 59 // Check the policy status of the blob. 59 60 if let Some(ref policy_client) = state.policy_client { 60 - match policy_client.get_policy_for_blob(&did, cid).await { 61 + match policy_client.get_policy(&did, cid).await { 61 62 Ok(policy) => { 62 - if !policy.can_serve { 63 - return Err(( 64 - StatusCode::GONE, 65 - [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 66 - Json(ErrorResponse { 67 - error: "PolicyForbidden", 68 - message: Some("Requested blob cannot be served by this service"), 69 - }), 70 - )); 71 - } 63 + match policy { 64 + PolicyDecision::Allowed => {} 65 + PolicyDecision::Forbidden => { 66 + return Err(( 67 + StatusCode::GONE, 68 + [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 69 + Json(XrpcErrorResponse { 70 + error: "PolicyForbidden", 71 + message: Some("Requested blob has been forbidden by this service"), 72 + }), 73 + )); 74 + } 75 + }; 72 76 } 73 77 Err(_) => { 74 78 if !state.policy_fail_open { ··· 76 80 return Err(( 77 81 StatusCode::INTERNAL_SERVER_ERROR, 78 82 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 79 - Json(ErrorResponse { 83 + Json(XrpcErrorResponse { 80 84 error: "InternalServerError", 81 85 message: Some("An internal server error occured."), 82 86 }), ··· 106 110 BlobDownloadError::NotFound => ( 107 111 StatusCode::NOT_FOUND, 108 112 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 109 - Json(ErrorResponse { 113 + Json(XrpcErrorResponse { 110 114 error: "BlobNotFound", 111 115 message: Some("Blob not found"), 112 116 }), ··· 114 118 BlobDownloadError::TooLarge => ( 115 119 StatusCode::PAYLOAD_TOO_LARGE, 116 120 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 117 - Json(ErrorResponse { 121 + Json(XrpcErrorResponse { 118 122 error: "BlobTooLarge", 119 123 message: Some("Blob exceeds maximum allowed size"), 120 124 }), ··· 122 126 BlobDownloadError::ForbiddenMimeType => ( 123 127 StatusCode::FORBIDDEN, 124 128 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 125 - Json(ErrorResponse { 129 + Json(XrpcErrorResponse { 126 130 error: "BlobForbiddenType", 127 131 message: Some("Content type is not allowed"), 128 132 }), ··· 130 134 BlobDownloadError::CidMismatch => ( 131 135 StatusCode::BAD_GATEWAY, 132 136 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 133 - Json(ErrorResponse { 137 + Json(XrpcErrorResponse { 134 138 error: "BlobCidMismatch", 135 139 message: Some("Blob content does not match CID"), 136 140 }), ··· 138 142 BlobDownloadError::CidUnsupportedMultihash => ( 139 143 StatusCode::NOT_IMPLEMENTED, 140 144 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 141 - Json(ErrorResponse { 145 + Json(XrpcErrorResponse { 142 146 error: "CidUnsupported", 143 147 message: Some("Unsupported CID multihash"), 144 148 }), ··· 146 150 BlobDownloadError::BlobResolutionFailure => ( 147 151 StatusCode::BAD_GATEWAY, 148 152 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 149 - Json(ErrorResponse { 153 + Json(XrpcErrorResponse { 150 154 error: "CannotResolve", 151 155 message: Some("Failed to resolve source of blob"), 152 156 }), ··· 156 160 | BlobDownloadError::StreamFailed => ( 157 161 StatusCode::BAD_GATEWAY, 158 162 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 159 - Json(ErrorResponse { 163 + Json(XrpcErrorResponse { 160 164 error: "BlobFetchFailed", 161 165 message: Some("Failed to fetch blob from origin"), 162 166 }), ··· 185 189 BlobOwnershipError::NotFound => ( 186 190 StatusCode::NOT_FOUND, 187 191 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 188 - Json(ErrorResponse { 192 + Json(XrpcErrorResponse { 189 193 error: "BlobNotFound", 190 194 message: Some("Blob not found"), 191 195 }), ··· 193 197 BlobOwnershipError::BlobResolutionFailure => ( 194 198 StatusCode::BAD_GATEWAY, 195 199 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 196 - Json(ErrorResponse { 197 - error: "CannotResolvePds", 198 - message: Some("Failed to resolve PDS for DID"), 200 + Json(XrpcErrorResponse { 201 + error: "CannotResolve", 202 + message: Some("Failed to resolve source of blob"), 199 203 }), 200 204 ), 201 205 BlobOwnershipError::ErrorStatusCode | BlobOwnershipError::FetchFailure => ( 202 206 StatusCode::BAD_GATEWAY, 203 207 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 204 - Json(ErrorResponse { 208 + Json(XrpcErrorResponse { 205 209 error: "BlobFetchFailed", 206 210 message: Some("Failed to fetch blob from origin"), 207 211 }),
+3 -2
crates/porxie/src/routes/mod.rs
··· 5 5 pub use blob::get_blob_handler; 6 6 pub use index::get_index_handler; 7 7 8 - /// A header value for [`header::CACHE_CONTROL`] indicating the response cannot be cached at all. 8 + /// Cache-Control header value indicating the response cannot be cached. 9 9 const CACHE_CONTROL_NOCACHE_VALUE: &str = "must-understand, no-store"; 10 10 11 + /// An xrpc-compatiable error response. 11 12 #[derive(serde::Serialize)] 12 - pub struct ErrorResponse { 13 + pub struct XrpcErrorResponse { 13 14 error: &'static str, 14 15 message: Option<&'static str>, 15 16 }
+5
crates/porxie/src/routes/xrpc/dev_blooym/porxie/cache/mod.rs
··· 1 + mod purge_actor; 2 + mod purge_blob; 3 + 4 + pub use purge_actor::xrpc_cache_purge_actor_handler; 5 + pub use purge_blob::xrpc_cache_purge_blob_handler;
+28
crates/porxie/src/routes/xrpc/dev_blooym/porxie/cache/purge_actor.rs
··· 1 + use crate::{AppState, extractors::AdminXrpcAuth}; 2 + use axum::extract::State; 3 + use jacquard_axum::ExtractXrpc; 4 + use lexgen::dev_blooym::porxie::cache::purge_actor::PurgeActorRequest; 5 + use reqwest::StatusCode; 6 + use std::sync::Arc; 7 + 8 + pub async fn xrpc_cache_purge_actor_handler( 9 + _auth: AdminXrpcAuth, 10 + State(state): State<Arc<AppState>>, 11 + ExtractXrpc(request): ExtractXrpc<PurgeActorRequest>, 12 + ) -> StatusCode { 13 + if let Some(ref policy_client) = state.policy_client { 14 + policy_client.invalidate_cache_entries({ 15 + let did = request.did.clone(); 16 + move |k, _v| k.0 == did 17 + }) 18 + } 19 + state 20 + .identity_service 21 + .invalidate_did_cache(&request.did) 22 + .await; 23 + state 24 + .blob_service 25 + .invalidate_blob_ownership_cache_entries(move |k, _v| k.1 == request.did); 26 + 27 + StatusCode::OK 28 + }
-35
crates/porxie/src/routes/xrpc/dev_blooym/porxie/clear_actor_cache.rs
··· 1 - use crate::{AppState, extractors::AdminXrpcAuth, routes::ErrorResponse}; 2 - use axum::{Json, extract::State, http::HeaderName}; 3 - use jacquard_axum::ExtractXrpc; 4 - use lexgen::dev_blooym::porxie::clear_actor_cache::ClearActorCacheRequest; 5 - use reqwest::StatusCode; 6 - use std::sync::Arc; 7 - 8 - pub async fn clear_actor_cache_handler( 9 - _auth: AdminXrpcAuth, 10 - State(state): State<Arc<AppState>>, 11 - ExtractXrpc(request): ExtractXrpc<ClearActorCacheRequest>, 12 - ) -> Result< 13 - StatusCode, 14 - ( 15 - StatusCode, 16 - [(HeaderName, &'static str); 1], 17 - Json<ErrorResponse>, 18 - ), 19 - > { 20 - if let Some(ref policy_client) = state.policy_client { 21 - policy_client.invalidate_policies({ 22 - let did = request.did.clone(); 23 - move |k, _v| k.0 == did 24 - }) 25 - } 26 - state 27 - .identity_service 28 - .invalidate_did_cache(&request.did) 29 - .await; 30 - state 31 - .blob_service 32 - .invalidate_blob_ownership(move |k, _v| k.1 == request.did); 33 - 34 - Ok(StatusCode::OK) 35 - }
+9 -9
crates/porxie/src/routes/xrpc/dev_blooym/porxie/clear_blob_cache.rs crates/porxie/src/routes/xrpc/dev_blooym/porxie/cache/purge_blob.rs
··· 1 1 use crate::{ 2 2 AppState, 3 3 extractors::AdminXrpcAuth, 4 - routes::{CACHE_CONTROL_NOCACHE_VALUE, ErrorResponse}, 4 + routes::{CACHE_CONTROL_NOCACHE_VALUE, XrpcErrorResponse}, 5 5 types::blob_cid::BlobCid, 6 6 }; 7 7 use axum::{ ··· 10 10 http::{HeaderName, header}, 11 11 }; 12 12 use jacquard_axum::ExtractXrpc; 13 - use lexgen::dev_blooym::porxie::clear_blob_cache::ClearBlobCacheRequest; 13 + use lexgen::dev_blooym::porxie::cache::purge_blob::PurgeBlobRequest; 14 14 use reqwest::StatusCode; 15 15 use std::sync::Arc; 16 16 17 - pub async fn clear_blob_cache_handler( 17 + pub async fn xrpc_cache_purge_blob_handler( 18 18 _auth: AdminXrpcAuth, 19 19 State(state): State<Arc<AppState>>, 20 - ExtractXrpc(request): ExtractXrpc<ClearBlobCacheRequest>, 20 + ExtractXrpc(request): ExtractXrpc<PurgeBlobRequest>, 21 21 ) -> Result< 22 22 StatusCode, 23 23 ( 24 24 StatusCode, 25 25 [(HeaderName, &'static str); 1], 26 - Json<ErrorResponse>, 26 + Json<XrpcErrorResponse>, 27 27 ), 28 28 > { 29 29 let cid = BlobCid::try_from(request.cid.as_str()).map_err(|_| { 30 30 ( 31 31 StatusCode::UNPROCESSABLE_ENTITY, 32 32 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 33 - Json(ErrorResponse { 33 + Json(XrpcErrorResponse { 34 34 error: "MalformedCid", 35 35 message: Some("Invalid or unprocessable CID"), 36 36 }), ··· 38 38 })?; 39 39 40 40 if let Some(ref policy_client) = state.policy_client { 41 - policy_client.invalidate_policies(move |k, _v| k.1 == cid) 41 + policy_client.invalidate_cache_entries(move |k, _v| k.1 == cid) 42 42 } 43 - state.blob_service.invalidate_blob(&cid).await; 43 + state.blob_service.invalidate_blob_cache_entry(&cid).await; 44 44 state 45 45 .blob_service 46 - .invalidate_blob_ownership(move |k, _v| k.0 == cid); 46 + .invalidate_blob_ownership_cache_entries(move |k, _v| k.0 == cid); 47 47 48 48 Ok(StatusCode::OK) 49 49 }
+1 -1
crates/porxie/src/routes/xrpc/dev_blooym/porxie/get_blob.rs
··· 10 10 /// Compatibility layer that converts the xrpc call into a 11 11 /// regular get blob request. May become the primary method 12 12 /// in the future. 13 - pub async fn get_blob_handler_xrpc_compat( 13 + pub async fn xrpc_compat_get_blob_handler( 14 14 state: State<Arc<AppState>>, 15 15 ExtractXrpc(request): ExtractXrpc<GetBlobRequest>, 16 16 ) -> impl IntoResponse {
+2 -5
crates/porxie/src/routes/xrpc/dev_blooym/porxie/mod.rs
··· 1 - mod clear_actor_cache; 2 - mod clear_blob_cache; 1 + pub mod cache; 3 2 mod get_blob; 4 3 5 - pub use clear_actor_cache::clear_actor_cache_handler; 6 - pub use clear_blob_cache::clear_blob_cache_handler; 7 - pub use get_blob::get_blob_handler_xrpc_compat; 4 + pub use get_blob::xrpc_compat_get_blob_handler;
+1 -1
crates/porxie/src/routes/xrpc/health.rs
··· 11 11 version: &'static str, 12 12 } 13 13 14 - pub async fn get_health_handler() -> impl IntoResponse { 14 + pub async fn xrpc_get_health_handler() -> impl IntoResponse { 15 15 ( 16 16 StatusCode::OK, 17 17 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)],
+5 -1
crates/porxie/src/routes/xrpc/mod.rs
··· 1 1 pub mod dev_blooym; 2 2 mod health; 3 3 4 - pub use health::get_health_handler; 4 + pub use health::xrpc_get_health_handler; 5 + 6 + pub async fn xrpc_fallback_handler() -> axum::http::StatusCode { 7 + axum::http::StatusCode::NOT_IMPLEMENTED 8 + }