feat(blobs): HTTP upload+download routes and UI round-trip widget

Plumbs the BlobStore from blobs.rs into ApiHandler. The HMAC capability
key is derived from the node's Ed25519 signing key via a domain-separated
SHA-256 — rotating the identity rotates every outstanding cap (intentional
so a replaced node cannot honour old tokens).

New routes (added to nginx config in both server blocks):
- POST /api/blob — session-authenticated raw upload, returns
  {cid, size, mime, filename, self_test_url}. The self_test_url is a
  pre-signed cap pointing at the local node so the UI can verify the
  round-trip without needing a peer pubkey.
- GET /blob/<cid>?cap=<hex>&exp=<epoch>&peer=<pubkey> — peer-facing,
  HMAC-verified in constant time, expiry-checked, then streams bytes.

Mesh.vue gets a minimal "Attachment test (blob store)" section: file
picker → upload → cid display → "Verify round-trip" and "Open in new
tab" buttons. This validates Phase 3a end-to-end before we layer the
ContentRef typed envelope variant on top.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-13 08:48:48 -04:00
parent 77eb1b907b
commit e8a729a4c7
4 changed files with 333 additions and 4 deletions

View File

@@ -210,6 +210,37 @@ server {
error_page 504 = @backend_timeout;
}
# Blob store — peer-facing download (HMAC capability auth, no session)
location /blob/ {
limit_req zone=peer burst=20 nodelay;
client_max_body_size 64m;
proxy_connect_timeout 30s;
proxy_read_timeout 120s;
proxy_send_timeout 60s;
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
error_page 502 503 = @backend_unavailable;
error_page 504 = @backend_timeout;
}
# Blob store — local upload (session-authenticated, raw body)
location /api/blob {
client_max_body_size 64m;
proxy_connect_timeout 30s;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
proxy_request_buffering off;
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Cookie $http_cookie;
error_page 502 503 = @backend_unavailable;
error_page 504 = @backend_timeout;
}
# DWN endpoints — peer access over Tor (no auth)
location /dwn {
limit_req zone=peer burst=20 nodelay;
@@ -478,17 +509,17 @@ server {
sub_filter '</head>' '<script src="/nostr-provider.js"></script><script>window.addEventListener("message",function(e){var d=e.data;if(d&&d.type==="arcade-input"&&d.key){var t=d.action==="up"?"keyup":"keydown";document.dispatchEvent(new KeyboardEvent(t,{key:d.key,bubbles:true}))}})</script></head>';
}
location /app/gitea/ {
# Gitea runs on 3001, nginx proxies 3000 stripping X-Frame-Options for iframe
proxy_pass http://127.0.0.1:3001/;
proxy_http_version 1.1;
proxy_pass http://127.0.0.1:3000/;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 1G;
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
# Override parent add_header to allow iframe embedding
add_header X-Content-Type-Options nosniff always;
client_max_body_size 1G;
add_header Referrer-Policy strict-origin-when-cross-origin always;
}
location /app/lnd/ {
proxy_pass http://127.0.0.1:8081/;
@@ -967,6 +998,37 @@ server {
error_page 504 = @backend_timeout;
}
# Blob store — peer-facing download (HMAC capability auth, no session)
location /blob/ {
limit_req zone=peer burst=20 nodelay;
client_max_body_size 64m;
proxy_connect_timeout 30s;
proxy_read_timeout 120s;
proxy_send_timeout 60s;
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
error_page 502 503 = @backend_unavailable;
error_page 504 = @backend_timeout;
}
# Blob store — local upload (session-authenticated, raw body)
location /api/blob {
client_max_body_size 64m;
proxy_connect_timeout 30s;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
proxy_request_buffering off;
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Cookie $http_cookie;
error_page 502 503 = @backend_unavailable;
error_page 504 = @backend_timeout;
}
# DWN endpoints — peer access over Tor (no auth)
location /dwn {
limit_req zone=peer burst=20 nodelay;