feat(mesh): /api/share-to-mesh iframe intent endpoint (Phase 3c)
Marketplace app iframes (Penpot, Gitea, IndeedHub, ...) can POST a file to /api/share-to-mesh and postMessage the returned CID to the parent window. The endpoint mirrors /api/blob's body format but adds CORS for the requesting app origin (any port on host_ip) so proxied apps can reach it with credentials:'include'. Session cookie is still the primary auth; the origin check is a sanity guard. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
019144903c
commit
f94f5da6ee
@@ -147,6 +147,36 @@ impl ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Permissive origin check for the share-to-mesh iframe intent: any scheme
|
||||
/// http(s):// followed by the configured host_ip, optionally `:port`. Apps
|
||||
/// proxied under other ports (APP_PORTS) call this from within the same
|
||||
/// node, so they share host_ip but not port. The session cookie still has
|
||||
/// to be valid — this is a sanity check, not the primary auth.
|
||||
fn validate_app_origin(&self, headers: &hyper::HeaderMap) -> Option<String> {
|
||||
let origin = headers.get("origin").and_then(|v| v.to_str().ok())?;
|
||||
// Allow localhost dev server too so the Vite frontend can exercise it.
|
||||
if self.config.dev_mode && origin == "http://localhost:8100" {
|
||||
return Some(origin.to_string());
|
||||
}
|
||||
let host_ip = &self.config.host_ip;
|
||||
let matches = |scheme: &str| -> bool {
|
||||
let prefix = format!("{}{}", scheme, host_ip);
|
||||
if origin == prefix {
|
||||
return true;
|
||||
}
|
||||
let with_port = format!("{}:", prefix);
|
||||
origin.starts_with(&with_port)
|
||||
&& origin[with_port.len()..]
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_digit())
|
||||
};
|
||||
if matches("http://") || matches("https://") {
|
||||
Some(origin.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_request(
|
||||
&self,
|
||||
req: Request<hyper::Body>,
|
||||
@@ -252,6 +282,38 @@ impl ApiHandler {
|
||||
.await
|
||||
}
|
||||
|
||||
// Share-to-mesh intent — marketplace app iframes POST a file here
|
||||
// to stage it as a mesh attachment. Same body format as /api/blob
|
||||
// (raw bytes + X-Blob-Mime/X-Blob-Filename headers). The app is
|
||||
// expected to postMessage `{type:'share-to-mesh', cid, ...}` to
|
||||
// its parent window afterwards so the Mesh view can pick it up.
|
||||
// Authenticated by session cookie + a relaxed Origin check (any
|
||||
// port on the archipelago host is allowed, so proxied apps on
|
||||
// their own ports can reach it with credentials:'include').
|
||||
(Method::POST, "/api/share-to-mesh") => {
|
||||
if !self.is_authenticated(&headers).await {
|
||||
return Ok(Self::unauthorized());
|
||||
}
|
||||
let origin = match self.validate_app_origin(&headers) {
|
||||
Some(o) => o,
|
||||
None => {
|
||||
return Ok(build_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
"text/plain",
|
||||
hyper::Body::from("origin not allowed"),
|
||||
))
|
||||
}
|
||||
};
|
||||
Self::handle_share_to_mesh(
|
||||
&self.blob_store,
|
||||
&self.self_pubkey_hex,
|
||||
&headers,
|
||||
body_bytes,
|
||||
&origin,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
// Blob download — peer-facing. No session required; authenticated
|
||||
// by HMAC capability token signed when the blob ref was shared.
|
||||
(Method::GET, p) if p.starts_with("/blob/") => {
|
||||
|
||||
Reference in New Issue
Block a user