···182182 devShells = forAllSystems (system: let
183183 pkgs = nixpkgsFor.${system};
184184 packages' = self.packages.${system};
185185- staticShell = pkgs.mkShell.override {
185185+ staticShell = args: (pkgs.mkShell.override {
186186 stdenv = pkgs.pkgsStatic.stdenv;
187187- };
187187+ }) (args // {
188188+ nativeBuildInputs = args.nativeBuildInputs
189189+ ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [
190190+ pkgs.darwin.cctools
191191+ ];
192192+ });
188193 in {
189194 default = staticShell {
190195 nativeBuildInputs = [
···220225 cp -fr --no-preserve=ownership,mode ${packages'.appview-static-files}/* appview/pages/static
221226 export TANGLED_OAUTH_CLIENT_KID="$(date +%s)"
222227 export TANGLED_OAUTH_CLIENT_SECRET="$(${packages'.goat}/bin/goat key generate -t P-256 | grep -A1 "Secret Key" | tail -n1 | awk '{print $1}')"
228228+ # Make xcrun (Nix stub) able to find ld from the system Command Line Tools.
229229+ # Without this, worker-build/cargo fails with "error: tool 'ld' not found".
230230+ if [ -d /Library/Developer/CommandLineTools/usr/bin ]; then
231231+ export PATH=/Library/Developer/CommandLineTools/usr/bin:$PATH
232232+ fi
223233 '';
224234 env.CGO_ENABLED = 1;
225235 };
+64-20
sites/src/lib.rs
···77///
88/// Example KV entry:
99/// key: "foo.example.com"
1010-/// value: {"did": "did:plc:...", "repos": {"my_repo": true, "other_repo": false}}
1010+/// value: {"did": "did:plc:...",
1111+/// "repos": {"my_repo": {"rkey": "3lk...", "is_index": true},
1212+/// "other_repo": {"rkey": "3ll...", "is_index": false}}}
1113///
1212-/// The boolean on each repo indicates whether it is the index site for the
1313-/// domain (true) or a sub-path site (false). At most one repo may be true.
1414+/// The is_index flag on each entry indicates whether it is the index site
1515+/// for the domain (true) or a sub-path site (false). At most one repo may
1616+/// be true. The rkey identifies the {did}/{rkey}/ prefix in R2 where the
1717+/// site's objects live.
1418#[derive(Deserialize)]
1519struct DomainMapping {
2020+ #[serde(default)]
1621 did: String,
1717- /// repo name → is_index
1818- repos: HashMap<String, bool>,
2222+ /// repo name → entry
2323+ #[serde(default)]
2424+ repos: HashMap<String, RepoEntry>,
2525+}
2626+2727+/// Deserialises from either {"rkey": "...", "is_index": bool} (new shape)
2828+/// or a bare bool (old shape, where the map key itself was the rkey).
2929+#[derive(Deserialize)]
3030+#[serde(untagged)]
3131+enum RepoEntry {
3232+ New {
3333+ rkey: String,
3434+ #[serde(default)]
3535+ is_index: bool,
3636+ },
3737+ Legacy(bool),
3838+}
3939+4040+impl RepoEntry {
4141+ fn is_index(&self) -> bool {
4242+ match self {
4343+ RepoEntry::New { is_index, .. } => *is_index,
4444+ RepoEntry::Legacy(b) => *b,
4545+ }
4646+ }
4747+4848+ /// Returns the rkey, falling back to the map key (name) for the legacy
4949+ /// shape where the key itself was the rkey.
5050+ fn rkey<'a>(&'a self, name: &'a str) -> &'a str {
5151+ match self {
5252+ RepoEntry::New { rkey, .. } => rkey.as_str(),
5353+ RepoEntry::Legacy(_) => name,
5454+ }
5555+ }
1956}
20572158impl DomainMapping {
2222- /// Returns the repo that is marked as the index site, if any.
2323- fn index_repo(&self) -> Option<&str> {
2424- self.repos
2525- .iter()
2626- .find_map(|(name, &is_index)| if is_index { Some(name.as_str()) } else { None })
5959+ /// Returns the (name, entry) pair for the index site, if any.
6060+ fn index_repo(&self) -> Option<(&str, &RepoEntry)> {
6161+ self.repos.iter().find_map(|(name, entry)| {
6262+ if entry.is_index() {
6363+ Some((name.as_str(), entry))
6464+ } else {
6565+ None
6666+ }
6767+ })
2768 }
2869}
29703030-/// Build the R2 object key for a given did/repo and intra-site path.
7171+/// Build the R2 object key for a given did/rkey and intra-site path.
3172/// `site_path` should start with a `/` or be empty.
3232-fn r2_key(did: &str, repo: &str, site_path: &str) -> String {
3333- let base = format!("{}/{}/", did, repo);
7373+fn r2_key(did: &str, rkey: &str, site_path: &str) -> String {
7474+ let base = format!("{}/{}/", did, rkey);
3475 if site_path.is_empty() || site_path == "/" {
3576 format!("{}index.html", base)
3677 } else {
···68109 .content_type
69110 .unwrap_or_else(|| "application/octet-stream".to_string());
701117171- let body = obj.body().ok_or_else(|| Error::RustError("empty R2 body".into()))?;
112112+ let body = obj
113113+ .body()
114114+ .ok_or_else(|| Error::RustError("empty R2 body".into()))?;
72115 let mut resp = Response::from_body(body.response_body()?)?;
73116 resp.headers_mut().set("Content-Type", &content_type)?;
7474- resp.headers_mut().set("Cache-Control", "public, max-age=60")?;
117117+ resp.headers_mut()
118118+ .set("Cache-Control", "public, max-age=60")?;
75119 Ok(resp)
76120}
77121···122166 // 1. sub-path site
123167 // If the first path segment matches a non-index repo, serve from it.
124168 if !first_segment.is_empty() {
125125- if let Some(&is_index) = mapping.repos.get(&first_segment) {
126126- if !is_index {
169169+ if let Some(entry) = mapping.repos.get(&first_segment) {
170170+ if !entry.is_index() {
127171 // Strip the leading "/{first_segment}" to get the intra-site path.
128172 let site_path = path
129173 .trim_start_matches('/')
130174 .trim_start_matches(&first_segment)
131175 .to_string();
132176133133- let key = r2_key(&mapping.did, &first_segment, &site_path);
177177+ let key = r2_key(&mapping.did, entry.rkey(&first_segment), &site_path);
134178 return match fetch_from_r2(&bucket, &key).await? {
135179 Some(obj) => response_from_object(obj),
136180 None => Response::error("Not Found", 404),
···141185142186 // 2. index site
143187 // Fall back to the repo marked as the index site, serving the full path.
144144- if let Some(index_repo) = mapping.index_repo() {
145145- let key = r2_key(&mapping.did, index_repo, path);
188188+ if let Some((name, entry)) = mapping.index_repo() {
189189+ let key = r2_key(&mapping.did, entry.rkey(name), path);
146190 return match fetch_from_r2(&bucket, &key).await? {
147191 Some(obj) => response_from_object(obj),
148192 None => Response::error("Not Found", 404),