Monorepo for Tangled tangled.org
8

Configure Feed

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

sites: name-keyed worker after rkey/name split

author
Anirudh Oppiliappan
committer
Tangled
date (May 16, 2026, 11:51 AM +0300) commit ce5aeb02 parent ccb87cd0 change-id nwmnvunl
+76 -22
+12 -2
flake.nix
··· 182 182 devShells = forAllSystems (system: let 183 183 pkgs = nixpkgsFor.${system}; 184 184 packages' = self.packages.${system}; 185 - staticShell = pkgs.mkShell.override { 185 + staticShell = args: (pkgs.mkShell.override { 186 186 stdenv = pkgs.pkgsStatic.stdenv; 187 - }; 187 + }) (args // { 188 + nativeBuildInputs = args.nativeBuildInputs 189 + ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ 190 + pkgs.darwin.cctools 191 + ]; 192 + }); 188 193 in { 189 194 default = staticShell { 190 195 nativeBuildInputs = [ ··· 220 225 cp -fr --no-preserve=ownership,mode ${packages'.appview-static-files}/* appview/pages/static 221 226 export TANGLED_OAUTH_CLIENT_KID="$(date +%s)" 222 227 export TANGLED_OAUTH_CLIENT_SECRET="$(${packages'.goat}/bin/goat key generate -t P-256 | grep -A1 "Secret Key" | tail -n1 | awk '{print $1}')" 228 + # Make xcrun (Nix stub) able to find ld from the system Command Line Tools. 229 + # Without this, worker-build/cargo fails with "error: tool 'ld' not found". 230 + if [ -d /Library/Developer/CommandLineTools/usr/bin ]; then 231 + export PATH=/Library/Developer/CommandLineTools/usr/bin:$PATH 232 + fi 223 233 ''; 224 234 env.CGO_ENABLED = 1; 225 235 };
+64 -20
sites/src/lib.rs
··· 7 7 /// 8 8 /// Example KV entry: 9 9 /// key: "foo.example.com" 10 - /// value: {"did": "did:plc:...", "repos": {"my_repo": true, "other_repo": false}} 10 + /// value: {"did": "did:plc:...", 11 + /// "repos": {"my_repo": {"rkey": "3lk...", "is_index": true}, 12 + /// "other_repo": {"rkey": "3ll...", "is_index": false}}} 11 13 /// 12 - /// The boolean on each repo indicates whether it is the index site for the 13 - /// domain (true) or a sub-path site (false). At most one repo may be true. 14 + /// The is_index flag on each entry indicates whether it is the index site 15 + /// for the domain (true) or a sub-path site (false). At most one repo may 16 + /// be true. The rkey identifies the {did}/{rkey}/ prefix in R2 where the 17 + /// site's objects live. 14 18 #[derive(Deserialize)] 15 19 struct DomainMapping { 20 + #[serde(default)] 16 21 did: String, 17 - /// repo name → is_index 18 - repos: HashMap<String, bool>, 22 + /// repo name → entry 23 + #[serde(default)] 24 + repos: HashMap<String, RepoEntry>, 25 + } 26 + 27 + /// Deserialises from either {"rkey": "...", "is_index": bool} (new shape) 28 + /// or a bare bool (old shape, where the map key itself was the rkey). 29 + #[derive(Deserialize)] 30 + #[serde(untagged)] 31 + enum RepoEntry { 32 + New { 33 + rkey: String, 34 + #[serde(default)] 35 + is_index: bool, 36 + }, 37 + Legacy(bool), 38 + } 39 + 40 + impl RepoEntry { 41 + fn is_index(&self) -> bool { 42 + match self { 43 + RepoEntry::New { is_index, .. } => *is_index, 44 + RepoEntry::Legacy(b) => *b, 45 + } 46 + } 47 + 48 + /// Returns the rkey, falling back to the map key (name) for the legacy 49 + /// shape where the key itself was the rkey. 50 + fn rkey<'a>(&'a self, name: &'a str) -> &'a str { 51 + match self { 52 + RepoEntry::New { rkey, .. } => rkey.as_str(), 53 + RepoEntry::Legacy(_) => name, 54 + } 55 + } 19 56 } 20 57 21 58 impl DomainMapping { 22 - /// Returns the repo that is marked as the index site, if any. 23 - fn index_repo(&self) -> Option<&str> { 24 - self.repos 25 - .iter() 26 - .find_map(|(name, &is_index)| if is_index { Some(name.as_str()) } else { None }) 59 + /// Returns the (name, entry) pair for the index site, if any. 60 + fn index_repo(&self) -> Option<(&str, &RepoEntry)> { 61 + self.repos.iter().find_map(|(name, entry)| { 62 + if entry.is_index() { 63 + Some((name.as_str(), entry)) 64 + } else { 65 + None 66 + } 67 + }) 27 68 } 28 69 } 29 70 30 - /// Build the R2 object key for a given did/repo and intra-site path. 71 + /// Build the R2 object key for a given did/rkey and intra-site path. 31 72 /// `site_path` should start with a `/` or be empty. 32 - fn r2_key(did: &str, repo: &str, site_path: &str) -> String { 33 - let base = format!("{}/{}/", did, repo); 73 + fn r2_key(did: &str, rkey: &str, site_path: &str) -> String { 74 + let base = format!("{}/{}/", did, rkey); 34 75 if site_path.is_empty() || site_path == "/" { 35 76 format!("{}index.html", base) 36 77 } else { ··· 68 109 .content_type 69 110 .unwrap_or_else(|| "application/octet-stream".to_string()); 70 111 71 - let body = obj.body().ok_or_else(|| Error::RustError("empty R2 body".into()))?; 112 + let body = obj 113 + .body() 114 + .ok_or_else(|| Error::RustError("empty R2 body".into()))?; 72 115 let mut resp = Response::from_body(body.response_body()?)?; 73 116 resp.headers_mut().set("Content-Type", &content_type)?; 74 - resp.headers_mut().set("Cache-Control", "public, max-age=60")?; 117 + resp.headers_mut() 118 + .set("Cache-Control", "public, max-age=60")?; 75 119 Ok(resp) 76 120 } 77 121 ··· 122 166 // 1. sub-path site 123 167 // If the first path segment matches a non-index repo, serve from it. 124 168 if !first_segment.is_empty() { 125 - if let Some(&is_index) = mapping.repos.get(&first_segment) { 126 - if !is_index { 169 + if let Some(entry) = mapping.repos.get(&first_segment) { 170 + if !entry.is_index() { 127 171 // Strip the leading "/{first_segment}" to get the intra-site path. 128 172 let site_path = path 129 173 .trim_start_matches('/') 130 174 .trim_start_matches(&first_segment) 131 175 .to_string(); 132 176 133 - let key = r2_key(&mapping.did, &first_segment, &site_path); 177 + let key = r2_key(&mapping.did, entry.rkey(&first_segment), &site_path); 134 178 return match fetch_from_r2(&bucket, &key).await? { 135 179 Some(obj) => response_from_object(obj), 136 180 None => Response::error("Not Found", 404), ··· 141 185 142 186 // 2. index site 143 187 // Fall back to the repo marked as the index site, serving the full path. 144 - if let Some(index_repo) = mapping.index_repo() { 145 - let key = r2_key(&mapping.did, index_repo, path); 188 + if let Some((name, entry)) = mapping.index_repo() { 189 + let key = r2_key(&mapping.did, entry.rkey(name), path); 146 190 return match fetch_from_r2(&bucket, &key).await? { 147 191 Some(obj) => response_from_object(obj), 148 192 None => Response::error("Not Found", 404),