tauri-app: 8a review fixes

- fetchBlob cache keyed by (did, cid), not just cid.
  Security: future per-DID access control on getBlob would
  otherwise leak the first responder's bytes to subsequent
  viewers.
- EmbedImage: pass did to releaseBlob, release previous cid
  on cid change (no leaked URLs).
- ComposeBox: releaseBlob called with both did and cid.
- pds-server: rename test
  get_blob_after_upload_with_different_did ->
  get_blob_returns_404_for_cross_did_cid_lookup. The
  docstring was misleading — the test only verifies the
  (did,cid) PK on the PDS row, not auth. The renamed name
  matches what the test actually checks.
- vitest: update releaseBlob call sites to the new
  (did, cid) signature.
This commit is contained in:
tomdebone
2026-07-06 18:53:09 +02:00
parent 226cfdac5c
commit b912132a05
8 changed files with 795 additions and 17 deletions
@@ -322,4 +322,66 @@ impl PdsHttpClient {
let bytes = resp.bytes().await?;
Ok(bytes.to_vec())
}
/// `POST /xrpc/com.atproto.uploadBlob`
///
/// Authenticated; the server derives the DID from the JWT `sub`
/// claim and writes the block to `(did, sha256(cid))` in
/// `repo_blocks`. The body is the raw blob bytes and the
/// `Content-Type` header is mandatory — the PDS uses it as the
/// authoritative MIME type for the row.
///
/// Returns the parsed `com.atproto.uploadBlob` response verbatim:
/// `{ blob: { $type, ref: { $link }, mimeType, size } }`. The
/// caller is expected to forward this to the Svelte UI so it can
/// drop the blob ref straight into a record's `embed.images[]`.
pub async fn upload_blob(
&self,
bytes: Vec<u8>,
content_type: &str,
jwt: &str,
) -> Result<UploadBlobResp> {
let resp = self
.client
.post(format!("{}/xrpc/com.atproto.uploadBlob", self.base_url))
.bearer_auth(jwt)
.header(reqwest::header::CONTENT_TYPE, content_type)
.body(bytes)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
anyhow::bail!("uploadBlob failed: {} {}", status, body);
}
Ok(resp.json::<UploadBlobResp>().await?)
}
}
/// `com.atproto.uploadBlob` response. Spec'd at
/// <https://atproto.com/specs/blob>. The server returns a `blob`
/// object that mirrors what an `app.bsky.embed.images#image` entry
/// expects on the wire — the Tauri command forwards this verbatim so
/// the UI can drop it into the post record with no further
/// transformation.
#[derive(Debug, Serialize, Deserialize)]
pub struct UploadBlobResp {
pub blob: UploadedBlob,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UploadedBlob {
#[serde(rename = "$type")]
pub ty: String,
#[serde(rename = "ref")]
pub blob_ref: UploadedBlobRef,
#[serde(rename = "mimeType")]
pub mime_type: String,
pub size: u64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UploadedBlobRef {
#[serde(rename = "$link")]
pub link: String,
}