use super::catalog::{browser_ui_assets, browser_ui_v1_html};
use super::*;
use bucketwarden_browser_ui_shell::{
browser_ui_accessibility_checks, browser_ui_action_filter_controls,
browser_ui_api_client_contracts, browser_ui_api_endpoints, browser_ui_mobile_layout_rules,
browser_ui_page_header_regions, browser_ui_responsive_breakpoints, browser_ui_routes,
browser_ui_secondary_inspector_regions, browser_ui_security_policy, browser_ui_sidebar_order,
browser_ui_state_contracts, browser_ui_topbar_context_order,
};
const FOUNDATION_BOUNDARY_ID: &str = "bnd:bucketwarden.browser-ui.foundation-slice";
const BUCKETWARDEN_MONOGRAM_PNG: &[u8] =
include_bytes!("../../assets/bucketwarden-monogram.png");
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct BrowserUiAssetResponse {
pub status: u16,
pub path: String,
pub content_type: String,
pub headers: BTreeMap<String, String>,
pub body: Vec<u8>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct BrowserUiFoundationReport {
pub boundary_id: String,
pub claim_tier: String,
pub implementation_status: String,
pub generated_at_epoch_seconds: u64,
pub routes: Vec<BrowserUiRoute>,
pub sidebar_order: Vec<String>,
pub topbar_context_order: Vec<String>,
pub page_header_regions: Vec<String>,
pub action_filter_controls: Vec<String>,
pub secondary_inspector_regions: Vec<String>,
pub mobile_layout_rules: Vec<String>,
pub api_endpoints: Vec<String>,
pub assets: Vec<BrowserUiAsset>,
pub api_client_contracts: Vec<String>,
pub state_contracts: Vec<String>,
pub accessibility_checks: Vec<String>,
pub responsive_breakpoints: Vec<String>,
pub security: BrowserUiSecurityPolicy,
}
impl BucketWarden {
pub fn browser_ui_foundation_report(
&mut self,
principal: &str,
) -> Result<BrowserUiFoundationReport, RuntimeError> {
self.require_operator_action(
principal,
OperatorAction::ReadDiagnostics,
"*",
"ui:GetBrowserFoundationReport",
)?;
let report = BrowserUiFoundationReport {
boundary_id: FOUNDATION_BOUNDARY_ID.to_string(),
claim_tier: BROWSER_UI_CLAIM_TIER.to_string(),
implementation_status: "implemented".to_string(),
generated_at_epoch_seconds: self.clock_epoch_seconds,
routes: browser_ui_routes(),
sidebar_order: browser_ui_sidebar_order(),
topbar_context_order: browser_ui_topbar_context_order(),
page_header_regions: browser_ui_page_header_regions(),
action_filter_controls: browser_ui_action_filter_controls(),
secondary_inspector_regions: browser_ui_secondary_inspector_regions(),
mobile_layout_rules: browser_ui_mobile_layout_rules(),
api_endpoints: browser_ui_api_endpoints(),
assets: browser_ui_assets(),
api_client_contracts: browser_ui_api_client_contracts(),
state_contracts: browser_ui_state_contracts(),
accessibility_checks: browser_ui_accessibility_checks(),
responsive_breakpoints: browser_ui_responsive_breakpoints(),
security: browser_ui_security_policy(),
};
self.audit.append(
principal,
"ui:GetBrowserFoundationReport",
"*",
AuditOutcome::Allowed,
Some(format!(
"routes={},assets={},api_endpoints={}",
report.routes.len(),
report.assets.len(),
report.api_endpoints.len()
)),
);
Ok(report)
}
pub fn browser_ui_asset_response(
&self,
path: &str,
) -> Result<BrowserUiAssetResponse, RuntimeError> {
let normalized = normalize_ui_path(path)?;
let canonical = canonical_ui_path(&normalized);
let (content_type, body) = match canonical.as_str() {
"/ui" | "/ui/" => ("text/html; charset=utf-8", browser_ui_html().into_bytes()),
"/ui/v1" | "/ui/v1/" => (
"text/html; charset=utf-8",
browser_ui_v1_html().into_bytes(),
),
"/ui/logout" => ("text/html; charset=utf-8", browser_ui_html().into_bytes()),
"/ui/v1/logout" => (
"text/html; charset=utf-8",
browser_ui_v1_html().into_bytes(),
),
"/ui/assets/app.css" => (
"text/css; charset=utf-8",
browser_ui_css().as_bytes().to_vec(),
),
"/ui/assets/app.js" => (
"application/javascript; charset=utf-8",
browser_ui_js().as_bytes().to_vec(),
),
"/ui/assets/bucketwarden-monogram.png" => {
("image/png", BUCKETWARDEN_MONOGRAM_PNG.to_vec())
}
"/ui/assets/favicon.png" => ("image/png", BUCKETWARDEN_MONOGRAM_PNG.to_vec()),
"/ui/manifest.json" => (
"application/json; charset=utf-8",
browser_ui_manifest_json().into_bytes(),
),
route
if route.starts_with("/ui/v1")
&& browser_ui_routes()
.iter()
.any(|entry| entry.route.replacen("/ui", "/ui/v1", 1) == route) =>
{
(
"text/html; charset=utf-8",
browser_ui_v1_html().into_bytes(),
)
}
route if is_versioned_bucket_workspace_route(route) => (
"text/html; charset=utf-8",
browser_ui_v1_html().into_bytes(),
),
route if is_legacy_bucket_workspace_route(route) => {
("text/html; charset=utf-8", browser_ui_html().into_bytes())
}
route if browser_ui_routes().iter().any(|entry| entry.route == route) => {
("text/html; charset=utf-8", browser_ui_html().into_bytes())
}
_ => return Err(RuntimeError::NoSuchKey(normalized)),
};
Ok(BrowserUiAssetResponse {
status: 200,
path: normalized,
content_type: content_type.to_string(),
headers: browser_ui_security_headers(),
body,
})
}
}
pub fn browser_ui_security_headers() -> BTreeMap<String, String> {
BTreeMap::from([
(
"content-security-policy".to_string(),
browser_ui_security_policy().content_security_policy,
),
("x-content-type-options".to_string(), "nosniff".to_string()),
("referrer-policy".to_string(), "same-origin".to_string()),
("cache-control".to_string(), "no-store".to_string()),
])
}
fn normalize_ui_path(path: &str) -> Result<String, RuntimeError> {
if !path.starts_with("/ui") || path.contains('\\') || path.split('/').any(|part| part == "..") {
return Err(RuntimeError::InvalidObjectKey(path.to_string()));
}
Ok(path.trim_end_matches('/').to_string())
}
fn canonical_ui_path(path: &str) -> String {
for suffix in [
"/assets/app.css",
"/assets/app.js",
"/assets/bucketwarden-monogram.png",
"/assets/favicon.png",
"/manifest.json",
] {
if path == format!("/ui/v1{suffix}") {
return format!("/ui{suffix}");
}
}
path.to_string()
}
fn is_versioned_bucket_workspace_route(route: &str) -> bool {
route
.strip_prefix("/ui/v1/buckets/")
.is_some_and(is_bucket_workspace_tail)
}
fn is_legacy_bucket_workspace_route(route: &str) -> bool {
route
.strip_prefix("/ui/buckets/")
.is_some_and(is_bucket_workspace_tail)
}
fn is_bucket_workspace_tail(tail: &str) -> bool {
if tail.is_empty() || tail.contains("..") || tail.contains('\\') {
return false;
}
let parts: Vec<&str> = tail.split('/').collect();
parts.len() == 1 || (parts.len() >= 3 && parts[1] == "objects")
}