fix(cli): allow generic file uploads

Co-authored-by: npub1x4hk035p3p9q39a3fcrd2fe30lpkrhr5dwe0cqzzjphxyyh8m0gsq4vqap <356f67c681884a0897b14e06d527317fc361dc746bb2fc0042906e6212e7dbd1@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1x4hk035p3p9q39a3fcrd2fe30lpkrhr5dwe0cqzzjphxyyh8m0gsq4vqap <356f67c681884a0897b14e06d527317fc361dc746bb2fc0042906e6212e7dbd1@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1x4hk035p3p9q39a3fcrd2fe30lpkrhr5dwe0cqzzjphxyyh8m0gsq4vqap <356f67c681884a0897b14e06d527317fc361dc746bb2fc0042906e6212e7dbd1@buzz.block.builderlab.xyz>
This commit is contained in:
npub1x4hk035p3p9q39a3fcrd2fe30lpkrhr5dwe0cqzzjphxyyh8m0gsq4vqap
2026-07-30 11:26:48 -07:00
parent cca8839034
commit e2819b9ac4
+49 -11
View File
@@ -75,6 +75,29 @@ const MAX_IMAGE_BYTES: u64 = 50 * 1024 * 1024;
/// Maximum file size for video uploads (500 MB).
const MAX_VIDEO_BYTES: u64 = 500 * 1024 * 1024;
/// Maximum file size for generic attachment uploads (100 MB).
const MAX_FILE_BYTES: u64 = 100 * 1024 * 1024;
/// Return the client-side size cap for an upload MIME type.
///
/// Known media must use an explicitly supported image/video format. Other
/// content is a generic attachment and is validated again by the relay.
fn upload_size_limit(mime: &str) -> Result<u64, CliError> {
let is_media =
mime.starts_with("image/") || mime.starts_with("video/") || mime.starts_with("audio/");
if is_media && !ALLOWED_MIMES.contains(&mime) {
return Err(CliError::Usage(format!("unsupported file type: {mime}")));
}
if mime.starts_with("video/") {
Ok(MAX_VIDEO_BYTES)
} else if mime.starts_with("image/") {
Ok(MAX_IMAGE_BYTES)
} else {
Ok(MAX_FILE_BYTES)
}
}
/// Sign a NIP-98 HTTP auth event (kind:27235) and return the Authorization header value.
///
/// The event includes:
@@ -1113,16 +1136,9 @@ impl BuzzClient {
.map(|t| t.mime_type().to_string())
.unwrap_or_else(|| "application/octet-stream".to_string());
if !ALLOWED_MIMES.contains(&mime.as_str()) {
return Err(CliError::Usage(format!("unsupported file type: {mime}")));
}
// 3. Size check
let max = if mime.starts_with("video/") {
MAX_VIDEO_BYTES
} else {
MAX_IMAGE_BYTES
};
// 3. Enforce media allowlist and the matching client-side size cap.
// Generic attachments are deny-listed and re-sniffed by the relay.
let max = upload_size_limit(&mime)?;
if bytes.len() as u64 > max {
return Err(CliError::Usage(format!(
"file too large: {} bytes (max {})",
@@ -2297,10 +2313,32 @@ mod retry_policy_tests {
#[cfg(test)]
mod tests {
use super::{
advance_query_cursor, create_response_with_id, extract_relay_response_field, BuzzClient,
advance_query_cursor, create_response_with_id, extract_relay_response_field,
upload_size_limit, BuzzClient, MAX_FILE_BYTES, MAX_IMAGE_BYTES, MAX_VIDEO_BYTES,
};
use nostr::{EventBuilder, Keys, Kind, Tag};
#[test]
fn upload_size_limit_accepts_plain_text_as_generic_attachment() {
let mime = infer::get(b"# Markdown attachment\n")
.map(|kind| kind.mime_type())
.unwrap_or("application/octet-stream");
assert_eq!(mime, "application/octet-stream");
assert_eq!(upload_size_limit(mime).unwrap(), MAX_FILE_BYTES);
assert_eq!(
upload_size_limit("application/pdf").unwrap(),
MAX_FILE_BYTES
);
}
#[test]
fn upload_size_limit_preserves_media_allowlist_and_caps() {
assert_eq!(upload_size_limit("image/png").unwrap(), MAX_IMAGE_BYTES);
assert_eq!(upload_size_limit("video/mp4").unwrap(), MAX_VIDEO_BYTES);
assert!(upload_size_limit("image/svg+xml").is_err());
assert!(upload_size_limit("audio/mpeg").is_err());
}
#[test]
fn query_cursor_uses_last_events_composite_sort_key() {
let mut filter = serde_json::json!({"kinds": [39000], "limit": 500});