Skip to main content

qualia_core_db/services/
webizen_server.rs

1use axum::{
2    body::{Body, Bytes},
3    extract::{ws::Message, Query, State, WebSocketUpgrade},
4    http::{header, HeaderMap, HeaderName, HeaderValue, Method, StatusCode},
5    response::{IntoResponse, Response},
6    routing::{get, post},
7    Json, Router,
8};
9use futures_util::StreamExt;
10use serde::{Deserialize, Serialize};
11use serde_json::json;
12use std::net::SocketAddr;
13use std::sync::{Arc, Mutex};
14use tokio::sync::broadcast;
15use tokio_stream::wrappers::UnboundedReceiverStream;
16use tower_http::{cors::CorsLayer, services::ServeDir, set_header::SetResponseHeaderLayer};
17
18use crate::{
19    daemon_query::{self, QueryExecError},
20    q_hash,
21    wal::append_mutation,
22    NQuin,
23};
24
25const OFFICIAL_WEB_HUB_ORIGIN: &str = "https://mediaprophet.github.io";
26const QUERY_PAYLOAD_LIMIT_BYTES: u64 = 64 * 1024;
27const PROXY_FETCH_MAX_BYTES: usize = 64 * 1024 * 1024;
28
29/// Block loopback / RFC1918 targets for the browser CORS relay (`GET /proxy/fetch`).
30fn proxy_target_allowed(url: &reqwest::Url) -> bool {
31    match url.scheme() {
32        "http" | "https" => {}
33        _ => return false,
34    }
35
36    let host = match url.host_str() {
37        Some(h) => h.to_ascii_lowercase(),
38        None => return false,
39    };
40
41    if host == "localhost" || host.ends_with(".localhost") || host == "127.0.0.1" {
42        return false;
43    }
44    if host.starts_with("127.") || host == "::1" || host == "[::1]" {
45        return false;
46    }
47    if host.starts_with("10.")
48        || host.starts_with("192.168.")
49        || host.starts_with("169.254.")
50        || host.starts_with("fe80:")
51    {
52        return false;
53    }
54    if let Some(stripped) = host.strip_prefix('[').and_then(|h| h.strip_suffix(']')) {
55        if let Ok(ip) = stripped.parse::<std::net::IpAddr>() {
56            return !ip_is_restricted(ip);
57        }
58    }
59    if let Ok(ip) = host.parse::<std::net::IpAddr>() {
60        return !ip_is_restricted(ip);
61    }
62
63    true
64}
65
66fn ip_is_restricted(ip: std::net::IpAddr) -> bool {
67    match ip {
68        std::net::IpAddr::V4(v4) => v4.is_loopback() || v4.is_private() || v4.is_link_local(),
69        std::net::IpAddr::V6(v6) => {
70            v6.is_loopback() || v6.is_unicast_link_local() || v6.is_unique_local()
71        }
72    }
73}
74
75/// Shared state for the Webizen loopback server
76#[derive(Clone)]
77pub struct WebizenState {
78    pub telemetry_tx: broadcast::Sender<Vec<u8>>,
79    pub type_index: Arc<[(u64, u64)]>,
80    pub dev: bool,
81    pub token: Option<String>,
82    pub vault: Arc<Mutex<crate::key_vault::KeyVault>>,
83    pub storage_path: String,
84    pub port: u16,
85    pub in_sanctuary_mode: Arc<std::sync::atomic::AtomicBool>,
86}
87
88#[derive(Deserialize)]
89struct NativeQueryRequest {
90    query: String,
91    format: Option<String>,
92}
93
94enum OutputFormat {
95    JsonLd,
96    NTriples,
97    RawQ42,
98}
99
100const SUPPORTED_QUERY_FORMATS: &str = "application/ld+json, application/n-triples";
101
102fn not_acceptable_format_response() -> (StatusCode, Json<serde_json::Value>) {
103    (
104        StatusCode::NOT_ACCEPTABLE,
105        Json(json!({
106            "code": "not_acceptable",
107            "status": "error",
108            "message": format!("Supported formats: {SUPPORTED_QUERY_FORMATS}")
109        })),
110    )
111}
112
113fn negotiate_format(
114    payload_format: Option<&str>,
115    accept: Option<&str>,
116) -> Result<OutputFormat, ()> {
117    if let Some(fmt) = payload_format {
118        return match fmt {
119            "json-ld" | "application/ld+json" => Ok(OutputFormat::JsonLd),
120            "n-triples" | "application/n-triples" => Ok(OutputFormat::NTriples),
121            "q42" | "application/x-qualia-q42" => Ok(OutputFormat::RawQ42),
122            _ => Err(()),
123        };
124    }
125    if let Some(accept) = accept {
126        if accept.contains("application/x-qualia-q42") {
127            return Ok(OutputFormat::RawQ42);
128        }
129        if accept.contains("application/n-triples") {
130            return Ok(OutputFormat::NTriples);
131        }
132        if accept.contains("application/ld+json")
133            || accept.contains("application/json")
134            || accept.contains("*/*")
135        {
136            return Ok(OutputFormat::JsonLd);
137        }
138        return Err(());
139    }
140    Ok(OutputFormat::JsonLd)
141}
142
143fn ws_query_error_json(id: u64, err: QueryExecError) -> serde_json::Value {
144    match err {
145        QueryExecError::EmptyQuery => json!({ "type": "error", "id": id, "code": "empty_query" }),
146        QueryExecError::ParseError(msg) => {
147            json!({ "type": "error", "id": id, "code": "parse_error", "message": msg })
148        }
149        QueryExecError::OutputBufferFull => {
150            json!({ "type": "error", "id": id, "code": "result_set_too_large" })
151        }
152        QueryExecError::InvalidProgram => json!({ "type": "error", "id": id, "code": "vm_error" }),
153        QueryExecError::ClassifiedEgress => {
154            json!({ "type": "error", "id": id, "code": "restricted_data_access" })
155        }
156    }
157}
158
159fn decode_bench_load_b64(b64: &str) -> Result<Vec<u8>, &'static str> {
160    let cleaned: String = b64.chars().filter(|c| !c.is_whitespace()).collect();
161    let padded = match cleaned.len() % 4 {
162        0 => cleaned,
163        n => format!("{cleaned}{}", "=".repeat(4 - n)),
164    };
165    let mut out = Vec::with_capacity(padded.len() * 3 / 4);
166    let table = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
167    let mut buf = 0u32;
168    let mut bits = 0u32;
169    for ch in padded.bytes() {
170        if ch == b'=' {
171            break;
172        }
173        let val = table
174            .iter()
175            .position(|&t| t == ch)
176            .ok_or("invalid base64")? as u32;
177        buf = (buf << 6) | val;
178        bits += 6;
179        if bits >= 8 {
180            bits -= 8;
181            out.push((buf >> bits) as u8);
182            buf &= (1u32 << bits) - 1;
183        }
184    }
185    Ok(out)
186}
187
188pub fn spawn_loopback_server(
189    port: u16,
190    dev: bool,
191    vault: Arc<Mutex<crate::key_vault::KeyVault>>,
192    token: Option<String>,
193) -> Arc<WebizenState> {
194    let mut flat_index = Vec::new();
195    let index_predicate = q_hash("q42:TypeIndex");
196    if let Ok(mut file) = std::fs::File::open("qualia_global.wal") {
197        use std::io::Read;
198        let mut buf = [0u8; 48];
199        while file.read_exact(&mut buf).is_ok() {
200            let quin: NQuin = bytemuck::cast(buf);
201            if quin.predicate == index_predicate {
202                flat_index.push((quin.subject, quin.object));
203            }
204        }
205    }
206    flat_index.sort_unstable_by_key(|&(k, _)| k);
207
208    let (telemetry_tx, _) = broadcast::channel(100);
209
210    let storage_path = std::env::var("QUALIA_STORAGE_PATH").unwrap_or_else(|_| {
211        std::env::var("HOME")
212            .or_else(|_| std::env::var("USERPROFILE"))
213            .map(|h| format!("{h}/.qualia"))
214            .unwrap_or_else(|_| ".qualia".to_string())
215    });
216
217    let state = Arc::new(WebizenState {
218        telemetry_tx: telemetry_tx.clone(),
219        type_index: flat_index.into(),
220        dev,
221        token: token.or_else(|| {
222            std::env::var("QUALIA_TOKEN")
223                .ok()
224                .or_else(|| std::env::var("QUALIA_DEV_TOKEN").ok())
225        }),
226        vault: vault.clone(),
227        storage_path: storage_path.clone(),
228        port,
229        in_sanctuary_mode: Arc::new(std::sync::atomic::AtomicBool::new(false)),
230    });
231
232    let server_state = state.clone();
233
234    std::thread::Builder::new()
235        .name("Webizen-Axum-Core3".into())
236        .spawn(move || {
237            if let Some(core_ids) = core_affinity::get_core_ids() {
238                if let Some(core3) = core_ids.get(3).or(core_ids.last()) {
239                    core_affinity::set_for_current(*core3);
240                }
241            }
242
243            let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
244
245            rt.block_on(async move {
246                let allowed_origins: Vec<HeaderValue> = if server_state.dev {
247                    vec![
248                        "http://localhost:8080".parse().unwrap(),
249                        "http://127.0.0.1:8080".parse().unwrap(),
250                        "http://localhost:8788".parse().unwrap(),
251                        "http://127.0.0.1:8788".parse().unwrap(),
252                        "http://localhost:5173".parse().unwrap(),
253                        "http://127.0.0.1:5173".parse().unwrap(),
254                        "http://localhost:4173".parse().unwrap(),
255                        "http://127.0.0.1:4173".parse().unwrap(),
256                        OFFICIAL_WEB_HUB_ORIGIN.parse().unwrap(),
257                    ]
258                } else {
259                    vec![OFFICIAL_WEB_HUB_ORIGIN.parse().unwrap()]
260                };
261
262                let cors = if server_state.dev {
263                    CorsLayer::permissive()
264                } else {
265                    CorsLayer::new()
266                        .allow_origin(allowed_origins)
267                        .allow_methods(vec![Method::GET, Method::POST, Method::OPTIONS])
268                        .allow_headers(vec![
269                            header::CONTENT_TYPE,
270                            header::ACCEPT,
271                            HeaderName::from_static("x-qualia-token"),
272                            HeaderName::from_static("x-qualia-standpoint-class"),
273                            HeaderName::from_static("x-qualia-t-slice"),
274                            HeaderName::from_static("x-qualia-t-window"),
275                            HeaderName::from_static("x-qualia-session-nonce"),
276                            HeaderName::from_static("x-qualia-identifier-did"),
277                            HeaderName::from_static("x-qualia-signature"),
278                            HeaderName::from_static("x-qualia-lane"),
279                            HeaderName::from_static("access-control-request-private-network"),
280                        ])
281                        .expose_headers(vec![
282                            HeaderName::from_static("x-qualia-compute-cost"),
283                            HeaderName::from_static("x-qualia-tensor-nodes"),
284                            HeaderName::from_static("x-qualia-tensor-bytes"),
285                        ])
286                };
287
288                let csp_layer = SetResponseHeaderLayer::overriding(
289                    header::CONTENT_SECURITY_POLICY,
290                    HeaderValue::from_static(
291                        "default-src 'self'; connect-src 'self' ws://127.0.0.1:4242; script-src 'self' 'wasm-unsafe-eval'; style-src 'self';"
292                    )
293                );
294
295                let ui_static_dir = std::env::var("QUALIA_UI_DIR").unwrap_or_else(|_| "crates/webizen-studio/dist".to_string());
296
297                let app = Router::new()
298                    // UI & Static
299                    .fallback_service(ServeDir::new(ui_static_dir).precompressed_gzip())
300
301                    // WebSockets
302                    .route("/qualia-bridge", get(bridge_handler))
303                    .route("/telemetry", get(telemetry_handler))
304                    // External WASM telemetry ingestion — external devices
305                    // (phones, browser tabs, other machines) POST their
306                    // SystemTelemetry here for local processing.
307                    .route("/telemetry/ingest", post(telemetry_ingest_handler).options(preflight_handler))
308
309                    // REST
310                    .route("/health", get(health_handler).options(preflight_handler))
311                    .route(
312                        "/tensor/slice",
313                        get(tensor_slice_handler).options(preflight_handler),
314                    )
315                    .route(
316                        "/tensor/events",
317                        get(tensor_events_handler).options(preflight_handler),
318                    )
319                    .route(
320                        "/tensor/dev-signing-key",
321                        get(tensor_dev_signing_key_handler).options(preflight_handler),
322                    )
323                    .route("/query", post(query_handler).options(preflight_handler))
324                    .route("/update", post(update_handler).options(preflight_handler))
325                    .route("/cache", post(cache_handler))
326                    .route("/proxy/fetch", get(proxy_fetch_handler).options(preflight_handler))
327                    .route("/api/v1/system/storage/selfhood", get(storage_selfhood_handler))
328                    .route("/api/v1/system/storage/commons", get(storage_commons_handler))
329                    .route("/api/v1/permissions/compile", post(permissions_compile_handler))
330                    .route("/api/v1/webizen/rpc", post(webizen_rpc_handler))
331
332                    // Manifest
333                    .route("/manifest", post(manifest_handler))
334                    .route("/manifest/current", get(current_manifest_handler))
335
336                    // Extensions
337                    .route("/extensions/list", get(list_extensions_handler))
338                    .route("/extensions/query/{interface}", get(query_extensions_handler))
339                    .route("/extensions/register", post(register_extension_handler))
340
341                    // Mobile
342                    .route("/mobile/qr", get(mobile_qr_handler))
343                    .route("/mobile/stream", get(mobile_ws_handler))
344                    .route("/generate_pane", post(mobile_generate_pane_handler))
345                    .nest_service("/mobile/app", tower_http::services::ServeDir::new("bootstrap_gateway/mobile"))
346
347                    // External Routers
348                    .with_state(server_state.clone())
349                    .merge(crate::chat_relay_daemon::chat_relay_routes(server_state.storage_path.clone(), server_state.vault.clone()))
350                    .nest("/torrent", crate::webtorrent_routes::webtorrent_routes(server_state.port))
351                    .layer(csp_layer)
352                    .layer(cors)
353                    .layer(axum::middleware::from_fn(pna_middleware));
354
355                let addr = SocketAddr::from(([0, 0, 0, 0], server_state.port));
356                let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
357                axum::serve(listener, app).await.unwrap();
358            });
359        })
360        .expect("Failed to spawn Webizen server thread");
361
362    state
363}
364
365async fn pna_middleware(req: axum::extract::Request, next: axum::middleware::Next) -> Response {
366    let mut res = next.run(req).await;
367    res.headers_mut().insert(
368        HeaderName::from_static("access-control-allow-private-network"),
369        HeaderValue::from_static("true"),
370    );
371    res
372}
373
374async fn preflight_handler() -> impl IntoResponse {
375    (StatusCode::OK, Json(json!({ "status": "ok" })))
376}
377
378#[derive(Deserialize)]
379struct TensorSliceQuery {
380    max_nodes: Option<u32>,
381    t_slice: Option<f32>,
382    t_window: Option<f32>,
383    lane: Option<String>,
384}
385
386fn header_parse_f32(headers: &HeaderMap, name: &str) -> Option<f32> {
387    headers
388        .get(name)
389        .and_then(|v| v.to_str().ok())
390        .and_then(|s| s.parse().ok())
391}
392
393fn header_parse_u32(headers: &HeaderMap, name: &str) -> Option<u32> {
394    headers
395        .get(name)
396        .and_then(|v| v.to_str().ok())
397        .and_then(|s| s.parse().ok())
398}
399
400fn header_str(headers: &HeaderMap, name: &str) -> Option<String> {
401    headers
402        .get(name)
403        .and_then(|v| v.to_str().ok())
404        .map(|s| s.to_string())
405}
406
407async fn tensor_slice_handler(
408    State(state): State<Arc<WebizenState>>,
409    Query(q): Query<TensorSliceQuery>,
410    headers: HeaderMap,
411) -> Response {
412    use crate::daemon_tensor::{
413        build_tensor_slice_bytes, verify_tensor_slice_signature, TensorSliceAuthError,
414        TensorSliceError, TensorSliceLane, TensorSliceRequest, DEFAULT_SLICE_MAX_NODES,
415    };
416    use crate::render::telemetry::{STANDPOINT_DID, STANDPOINT_VAULT};
417    use crate::tensor::buffer_export::tensor_node_count;
418
419    let max_nodes = header_parse_u32(&headers, "x-qualia-max-nodes")
420        .or(q.max_nodes)
421        .map(|n| n as usize)
422        .unwrap_or(DEFAULT_SLICE_MAX_NODES);
423
424    let t_slice = header_parse_f32(&headers, "x-qualia-t-slice")
425        .or(q.t_slice)
426        .unwrap_or(0.5);
427
428    let t_window = header_parse_f32(&headers, "x-qualia-t-window")
429        .or(q.t_window)
430        .unwrap_or(1.0);
431
432    let standpoint_class = header_parse_u32(&headers, "x-qualia-standpoint-class").unwrap_or(0);
433
434    let identifier_did = header_str(&headers, "x-qualia-identifier-did").unwrap_or_default();
435    let session_nonce = header_str(&headers, "x-qualia-session-nonce").unwrap_or_default();
436    let signature_hex = header_str(&headers, "x-qualia-signature").unwrap_or_default();
437
438    let mut lane = if standpoint_class >= STANDPOINT_VAULT {
439        TensorSliceLane::Identifier
440    } else if standpoint_class >= STANDPOINT_DID {
441        TensorSliceLane::Identifier
442    } else {
443        TensorSliceLane::Commons
444    };
445
446    if standpoint_class >= STANDPOINT_DID {
447        let vault = match state.vault.lock() {
448            Ok(guard) => guard,
449            Err(_) => {
450                return (
451                    StatusCode::INTERNAL_SERVER_ERROR,
452                    Json(json!({ "error": "vault_unavailable" })),
453                )
454                    .into_response();
455            }
456        };
457        if let Err(auth_err) = verify_tensor_slice_signature(
458            &vault,
459            &identifier_did,
460            &session_nonce,
461            standpoint_class,
462            t_slice,
463            t_window,
464            &signature_hex,
465        ) {
466            let code = match auth_err {
467                TensorSliceAuthError::InvalidSignature => "invalid_signature",
468                TensorSliceAuthError::InvalidSignatureEncoding => "invalid_signature_encoding",
469                TensorSliceAuthError::SignatureRequired => "signature_required",
470                TensorSliceAuthError::IdentifierDidRequired => "identifier_did_required",
471                TensorSliceAuthError::SessionNonceRequired => "session_nonce_required",
472            };
473            return (
474                StatusCode::FORBIDDEN,
475                Json(json!({
476                    "error": "tensor_slice_auth_failed",
477                    "code": code,
478                })),
479            )
480                .into_response();
481        }
482        drop(vault);
483    } else {
484        lane = headers
485            .get("x-qualia-lane")
486            .and_then(|v| v.to_str().ok())
487            .or(q.lane.as_deref())
488            .map(TensorSliceLane::from_header)
489            .unwrap_or(TensorSliceLane::Commons);
490    }
491
492    let req = TensorSliceRequest {
493        max_nodes,
494        t_slice,
495        t_window,
496        lane,
497        standpoint_class,
498    };
499
500    let guard = crate::daemon_graph::graph_read_guard();
501    match build_tensor_slice_bytes(guard.as_slice(), &req) {
502        Ok(bytes) => {
503            let node_count = tensor_node_count(&bytes).unwrap_or(0);
504            Response::builder()
505                .status(StatusCode::OK)
506                .header(header::CONTENT_TYPE, "application/octet-stream")
507                .header("X-Qualia-Tensor-Nodes", node_count.to_string())
508                .header("X-Qualia-Tensor-Bytes", bytes.len().to_string())
509                .header(
510                    "X-Qualia-Tensor-Lane",
511                    match lane {
512                        TensorSliceLane::Commons => "commons",
513                        TensorSliceLane::Identifier => "identifier",
514                    },
515                )
516                .body(Body::from(bytes))
517                .unwrap()
518        }
519        Err(TensorSliceError::EmptyGraph) => (
520            StatusCode::NOT_FOUND,
521            Json(json!({
522                "error": "empty_graph",
523                "message": "graph empty or no nodes match temporal window"
524            })),
525        )
526            .into_response(),
527        Err(TensorSliceError::BufferTooSmall) => (
528            StatusCode::INTERNAL_SERVER_ERROR,
529            Json(json!({ "error": "tensor_buffer_error" })),
530        )
531            .into_response(),
532    }
533}
534
535/// Dev-only: export KeyVault-derived Ed25519 seed for `identifier_did` (localhost pairing).
536async fn tensor_dev_signing_key_handler(
537    State(state): State<Arc<WebizenState>>,
538    Query(qs): Query<std::collections::HashMap<String, String>>,
539) -> impl IntoResponse {
540    if !state.dev {
541        return (StatusCode::FORBIDDEN, Json(json!({ "error": "dev_only" }))).into_response();
542    }
543    let identifier_did = qs.get("identifier_did").cloned().unwrap_or_default();
544    if identifier_did.trim().is_empty() {
545        return (
546            StatusCode::BAD_REQUEST,
547            Json(json!({ "error": "identifier_did_required" })),
548        )
549            .into_response();
550    }
551    let vault = match state.vault.lock() {
552        Ok(guard) => guard,
553        Err(_) => {
554            return (
555                StatusCode::INTERNAL_SERVER_ERROR,
556                Json(json!({ "error": "vault_unavailable" })),
557            )
558                .into_response();
559        }
560    };
561    let sk = vault.derive_key(&identifier_did);
562    let pk = vault.public_key_bytes_for_context(&identifier_did);
563    (
564        StatusCode::OK,
565        Json(json!({
566            "identifier_did": identifier_did,
567            "signing_key_hex": hex::encode(sk.to_bytes()),
568            "public_key_hex": hex::encode(pk),
569            "warning": "dev_only — never expose signing keys outside localhost pairing"
570        })),
571    )
572        .into_response()
573}
574
575/// SSE stream of Lamport graph revisions for live viewport sync (`PR-C9c.1`).
576async fn tensor_events_handler() -> Response {
577    use std::convert::Infallible;
578    use std::time::Duration;
579
580    let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<String>();
581    let initial = crate::daemon_graph::graph_revision();
582
583    tokio::spawn(async move {
584        let _ = tx.send(format!("data: {{\"revision\":{initial}}}\n\n"));
585        let mut last = initial;
586        let mut sub = crate::daemon_graph::subscribe_graph_revisions();
587        let mut keepalive = tokio::time::interval(Duration::from_secs(15));
588        keepalive.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
589
590        loop {
591            if tx.is_closed() {
592                break;
593            }
594            tokio::select! {
595                result = sub.recv() => {
596                    match result {
597                        Ok(rev) => {
598                            if rev > last {
599                                last = rev;
600                                let _ = tx.send(format!("data: {{\"revision\":{rev}}}\n\n"));
601                            }
602                        }
603                        Err(broadcast::error::RecvError::Lagged(_)) => {
604                            let current = crate::daemon_graph::graph_revision();
605                            if current > last {
606                                last = current;
607                                let _ = tx.send(format!("data: {{\"revision\":{current}}}\n\n"));
608                            }
609                        }
610                        Err(broadcast::error::RecvError::Closed) => break,
611                    }
612                }
613                _ = keepalive.tick() => {
614                    let _ = tx.send(": keepalive\n\n".to_string());
615                }
616            }
617        }
618    });
619
620    let body_stream =
621        UnboundedReceiverStream::new(rx).map(|chunk| Ok::<Bytes, Infallible>(Bytes::from(chunk)));
622
623    Response::builder()
624        .status(StatusCode::OK)
625        .header(header::CONTENT_TYPE, "text/event-stream")
626        .header(header::CACHE_CONTROL, "no-cache")
627        .header(header::CONNECTION, "keep-alive")
628        .body(Body::from_stream(body_stream))
629        .unwrap()
630}
631
632async fn health_handler(State(state): State<Arc<WebizenState>>) -> impl IntoResponse {
633    (
634        StatusCode::OK,
635        Json(json!({
636            "status": "active",
637            "engine": "qualia-core-db",
638            "version": crate::ENGINE_VERSION,
639            "dev_mode": state.dev,
640            "graph_quin_count": crate::daemon_graph::graph_quin_count(),
641            "graph_revision": crate::daemon_graph::graph_revision(),
642            "webtorrent": crate::webtorrent_seeder::telemetry(),
643            "execution_environment": crate::services::daemon::execution_environment_json(),
644        })),
645    )
646}
647
648async fn proxy_fetch_handler(
649    Query(qs): Query<std::collections::HashMap<String, String>>,
650) -> impl IntoResponse {
651    let target = qs.get("url").cloned().unwrap_or_default();
652    if target.is_empty() {
653        return (
654            StatusCode::BAD_REQUEST,
655            Json(json!({"error": "missing url"})),
656        )
657            .into_response();
658    }
659    let parsed = match reqwest::Url::parse(&target) {
660        Ok(u) => u,
661        Err(_) => {
662            return (
663                StatusCode::BAD_REQUEST,
664                Json(json!({"error": "invalid url"})),
665            )
666                .into_response()
667        }
668    };
669
670    if !proxy_target_allowed(&parsed) {
671        return (
672            StatusCode::FORBIDDEN,
673            Json(json!({"error": "proxy target not allowed"})),
674        )
675            .into_response();
676    }
677
678    let client = reqwest::Client::builder()
679        .timeout(std::time::Duration::from_secs(120))
680        .build()
681        .unwrap();
682    let response = match client.get(parsed).send().await {
683        Ok(r) => r,
684        Err(e) => {
685            return (
686                StatusCode::BAD_GATEWAY,
687                Json(json!({"error": e.to_string()})),
688            )
689                .into_response()
690        }
691    };
692
693    let content_type = response
694        .headers()
695        .get(reqwest::header::CONTENT_TYPE)
696        .and_then(|v| v.to_str().ok())
697        .unwrap_or("application/octet-stream")
698        .to_string();
699    let bytes = response.bytes().await.unwrap_or_default();
700    if bytes.len() > PROXY_FETCH_MAX_BYTES {
701        return (
702            StatusCode::PAYLOAD_TOO_LARGE,
703            Json(json!({"error": "too large"})),
704        )
705            .into_response();
706    }
707
708    ([(header::CONTENT_TYPE, content_type)], bytes.to_vec()).into_response()
709}
710
711async fn cache_handler(
712    Query(qs): Query<std::collections::HashMap<String, String>>,
713    body: axum::body::Bytes,
714) -> impl IntoResponse {
715    let filename = qs
716        .get("filename")
717        .cloned()
718        .unwrap_or_else(|| "dataset_shard.q42".to_string());
719    let mut path = std::path::PathBuf::from(".qualia");
720    path.push("cache");
721    let _ = std::fs::create_dir_all(&path);
722    path.push(&filename);
723    let _ = std::fs::write(&path, body);
724    (
725        StatusCode::OK,
726        Json(json!({ "status": "ok", "saved_to": path.to_str() })),
727    )
728}
729
730async fn storage_selfhood_handler(State(state): State<Arc<WebizenState>>) -> impl IntoResponse {
731    let path = std::path::Path::new(&state.storage_path).join("selfhood");
732    Json(json!({ "path": path.to_str().unwrap_or_default(), "status": "isolated" }))
733}
734
735async fn storage_commons_handler(State(state): State<Arc<WebizenState>>) -> impl IntoResponse {
736    let path = std::path::Path::new(&state.storage_path).join("commons");
737    Json(json!({ "path": path.to_str().unwrap_or_default(), "status": "public" }))
738}
739
740#[derive(Deserialize)]
741pub struct PermissionsCompileRequest {
742    pub payload: String, // yaml-ld-q42
743}
744
745#[derive(Serialize, Deserialize)]
746pub struct CompiledPermission {
747    pub routing_mask: u64,
748    pub semantic_handshake: String,
749    pub is_permissive_commons: bool,
750}
751
752pub async fn permissions_compile_handler(
753    Json(req): Json<PermissionsCompileRequest>,
754) -> impl IntoResponse {
755    // Compile yaml-ld-q42 down to hardware bitmask
756    let mut is_permissive_commons = false;
757    let mut routing_mask = 0u64;
758
759    // Semantic Handshake template creation
760    let semantic_handshake = format!(
761        "Semantic Cryptographic Proof Template: [Payload Length: {}]",
762        req.payload.len()
763    );
764
765    if req.payload.contains("Commercial Micro-Commons") || req.payload.contains("Bilateral") {
766        routing_mask |= 0x02 << 61; // EnforceBilateralMicroCommons
767        routing_mask |= 1 << 50; // MASK_COMMERCIAL_BILLABLE_GATE
768    } else if req.payload.contains("Public Commons") || req.payload.contains("PermissiveCommons") {
769        routing_mask |= 0x01 << 61; // EnforcePermissiveCommons
770        is_permissive_commons = true;
771    } else {
772        routing_mask |= 0x00 << 61; // PassthroughStandard
773    }
774
775    Json(CompiledPermission {
776        routing_mask,
777        semantic_handshake,
778        is_permissive_commons,
779    })
780}
781
782#[derive(Deserialize)]
783pub struct WebizenRpcRequest {
784    pub method: String,
785    pub scopes: Option<Vec<String>>,
786    pub payload: Option<String>,
787}
788
789pub async fn webizen_rpc_handler(
790    State(state): State<Arc<WebizenState>>,
791    Json(req): Json<WebizenRpcRequest>,
792) -> impl IntoResponse {
793    if req.method == "requestAccess" {
794        if let Some(scopes) = req.scopes {
795            if state
796                .in_sanctuary_mode
797                .load(std::sync::atomic::Ordering::Relaxed)
798            {
799                // Fiduciary Supremacy: Reject requests to 'wf:' or the selfhood store completely.
800                // Block both the new `selfhood` name and the legacy `sovereign` name (fail-closed —
801                // never loosen this lock while the rename propagates).
802                if scopes.iter().any(|s| {
803                    s.starts_with("wf:") || s.contains("selfhood") || s.contains("sovereign")
804                }) {
805                    return (
806                        StatusCode::LOCKED,
807                        Json(json!({"error": "Selfhood path locked during Sanctuary mode."})),
808                    )
809                        .into_response();
810                }
811            }
812        }
813        return (StatusCode::OK, Json(json!({"status": "access_granted"}))).into_response();
814    }
815
816    if req.method == "signAndInject" {
817        // Implement pure 48-byte NQuin request and semantic handshake...
818        return (StatusCode::OK, Json(json!({"status": "injected"}))).into_response();
819    }
820
821    if req.method == "resolveNym" {
822        return (StatusCode::OK, Json(json!({"status": "resolved"}))).into_response();
823    }
824
825    (
826        StatusCode::BAD_REQUEST,
827        Json(json!({"error": "unknown method"})),
828    )
829        .into_response()
830}
831
832async fn bridge_handler(
833    ws: WebSocketUpgrade,
834    State(state): State<Arc<WebizenState>>,
835) -> impl IntoResponse {
836    ws.on_upgrade(move |mut socket| async move {
837        let handshake = json!({ "type": "HANDSHAKE_SUCCESS", "payload": { "mode": "NATIVE", "version": crate::ENGINE_VERSION } });
838        let _ = socket.send(Message::Text(handshake.to_string().into())).await;
839
840        let mut pending_bench_id: Option<u64> = None;
841        while let Some(Ok(msg)) = socket.recv().await {
842            match msg {
843                Message::Binary(bytes) => {
844                    if let Some(id) = pending_bench_id.take() {
845                        let reply = match crate::daemon_graph::replace_graph_from_flat_bytes(&bytes) {
846                            Ok(c) => json!({ "type": "bench_loaded", "id": id, "quin_count": c }),
847                            Err(e) => json!({ "type": "error", "id": id, "code": "bench_load_failed", "message": e }),
848                        };
849                        let _ = socket.send(Message::Text(reply.to_string().into())).await;
850                    }
851                }
852                Message::Text(text) => {
853                    if let Ok(frame) = serde_json::from_str::<serde_json::Value>(&text) {
854                        let frame_type = frame.get("type").and_then(|v| v.as_str()).unwrap_or("");
855                        let id = frame.get("id").and_then(|v| v.as_u64()).unwrap_or(0);
856                        let reply = match frame_type {
857                            "query" => {
858                                let q = frame.get("query").and_then(|v| v.as_str()).unwrap_or("");
859                                if q.len() as u64 > QUERY_PAYLOAD_LIMIT_BYTES {
860                                    json!({
861                                        "type": "error",
862                                        "id": id,
863                                        "code": "query_too_large",
864                                        "message": format!("query exceeds {QUERY_PAYLOAD_LIMIT_BYTES} byte limit")
865                                    })
866                                } else {
867                                let graph = crate::daemon_graph::graph_read_guard();
868                                match daemon_query::execute_ntriples_metrics(q, graph.as_slice()) {
869                                    Ok(stats) => json!({
870                                        "type": "result",
871                                        "id": id,
872                                        "match_count": stats.match_count,
873                                        "vm_cycles": stats.vm_cycles
874                                    }),
875                                    Err(err) => ws_query_error_json(id, err),
876                                }
877                                }
878                            }
879                            "bench_load" if state.dev => {
880                                if frame.get("byte_length").is_some() { pending_bench_id = Some(id); json!({ "type": "bench_load_ready", "id": id }) }
881                                else if let Some(b64) = frame.get("db_b64").and_then(|v| v.as_str()) {
882                                    match decode_bench_load_b64(b64) {
883                                        Ok(bytes) => match crate::daemon_graph::replace_graph_from_flat_bytes(&bytes) {
884                                            Ok(c) => json!({ "type": "bench_loaded", "id": id, "quin_count": c }),
885                                            Err(e) => json!({ "type": "error", "id": id, "message": e })
886                                        },
887                                        Err(e) => json!({ "type": "error", "id": id, "message": e })
888                                    }
889                                } else { json!({ "type": "error", "id": id, "message": "requires db_b64" }) }
890                            }
891                            _ => json!({ "type": "error", "id": id, "message": "unsupported" }),
892                        };
893                        let _ = socket.send(Message::Text(reply.to_string().into())).await;
894                    }
895                }
896                _ => {}
897            }
898        }
899    })
900}
901
902#[derive(Deserialize)]
903struct UpdateRequest {
904    update: String,
905}
906
907/// `POST /update` — apply a SPARQL 1.1 Update to the resident graph.
908///
909/// Same token auth as `/query`. The mutation is signed with a **real** key
910/// derived from the daemon's key vault (never a placeholder) and persisted
911/// durably (snapshot + signed WAL audit trail) via
912/// `daemon_graph::apply_sparql_update_durable`. Loopback-only, like `/query`.
913async fn update_handler(
914    State(state): State<Arc<WebizenState>>,
915    headers: HeaderMap,
916    Json(request): Json<UpdateRequest>,
917) -> impl IntoResponse {
918    // Auth: mirror /query (token required unless dev mode).
919    if !state.dev {
920        let token = headers
921            .get("x-qualia-token")
922            .and_then(|v| v.to_str().ok())
923            .map(|s| s.to_string());
924        let ok = token
925            .as_ref()
926            .map(|t| {
927                let vault = state.vault.lock().unwrap();
928                vault.verify_qapp_token(t, "localhost").is_ok() || Some(t) == state.token.as_ref()
929            })
930            .unwrap_or(false);
931        if !ok {
932            return (
933                StatusCode::UNAUTHORIZED,
934                Json(json!({"error": "unauthorized"})),
935            )
936                .into_response();
937        }
938    }
939
940    let src = request.update.trim();
941    if src.is_empty() {
942        return (
943            StatusCode::BAD_REQUEST,
944            Json(json!({"error": "empty update"})),
945        )
946            .into_response();
947    }
948    if src.len() as u64 > QUERY_PAYLOAD_LIMIT_BYTES {
949        return (
950            StatusCode::PAYLOAD_TOO_LARGE,
951            Json(json!({"error": "update too large", "limit_bytes": QUERY_PAYLOAD_LIMIT_BYTES})),
952        )
953            .into_response();
954    }
955    if !crate::sparql_library::sparql_grammar::is_update(src) {
956        return (
957            StatusCode::BAD_REQUEST,
958            Json(json!({"error": "not a SPARQL Update (use /query for reads)"})),
959        )
960            .into_response();
961    }
962
963    let mut ctx = crate::sparql_ast::SparqlQueryContext::new();
964    let prefixes = std::collections::HashMap::new();
965    let op = match crate::sparql_library::sparql_grammar::parse_update(src, &mut ctx, &prefixes) {
966        Ok(op) => op,
967        Err(e) => {
968            return (
969                StatusCode::BAD_REQUEST,
970                Json(json!({"error": format!("update parse error: {e}")})),
971            )
972                .into_response()
973        }
974    };
975
976    // Real signing key from the daemon key vault — never a placeholder.
977    let signing_key = {
978        let vault = match state.vault.lock() {
979            Ok(v) => v,
980            Err(_) => {
981                return (
982                    StatusCode::INTERNAL_SERVER_ERROR,
983                    Json(json!({"error": "vault_unavailable"})),
984                )
985                    .into_response()
986            }
987        };
988        vault.derive_key("sparql-update")
989    };
990    let principal = crate::q_hash("did:qualia:sparql-update-principal");
991    let agent = crate::q_hash("did:qualia:sparql-update-agent");
992    let wal_path = format!("{}/sparql_update.wal", state.storage_path);
993
994    match crate::daemon_graph::apply_sparql_update_durable(
995        &op,
996        &ctx,
997        &signing_key,
998        principal,
999        agent,
1000        &wal_path,
1001        &state.storage_path,
1002    ) {
1003        Ok(outcome) => (
1004            StatusCode::OK,
1005            Json(json!({
1006                "inserted": outcome.inserted,
1007                "deleted": outcome.deleted,
1008                "persisted": outcome.persisted,
1009            })),
1010        )
1011            .into_response(),
1012        Err(e) => (
1013            StatusCode::BAD_REQUEST,
1014            Json(json!({"error": format!("update failed: {e}")})),
1015        )
1016            .into_response(),
1017    }
1018}
1019
1020async fn query_handler(
1021    State(state): State<Arc<WebizenState>>,
1022    headers: HeaderMap,
1023    Json(request): Json<NativeQueryRequest>,
1024) -> impl IntoResponse {
1025    let token = headers
1026        .get("x-qualia-token")
1027        .and_then(|v| v.to_str().ok())
1028        .map(|s| s.to_string());
1029    let accept = headers.get(header::ACCEPT).and_then(|v| v.to_str().ok());
1030
1031    let mut allowed_shapes: Option<Vec<String>> = None;
1032    if !state.dev {
1033        if let Some(t) = token.as_ref() {
1034            let vault = state.vault.lock().unwrap();
1035            match vault.verify_qapp_token(t, "localhost") {
1036                Ok(payload) => {
1037                    allowed_shapes = Some(payload.capabilities);
1038                }
1039                Err(_) => {
1040                    if Some(t) != state.token.as_ref() {
1041                        return (
1042                            StatusCode::UNAUTHORIZED,
1043                            Json(json!({"error": "unauthorized"})),
1044                        )
1045                            .into_response();
1046                    }
1047                }
1048            }
1049        } else {
1050            return (
1051                StatusCode::UNAUTHORIZED,
1052                Json(json!({"error": "missing x-qualia-token"})),
1053            )
1054                .into_response();
1055        }
1056    }
1057
1058    if let Some(shapes) = allowed_shapes {
1059        let q = request.query.to_lowercase();
1060        let mut authorized = false;
1061        for shape in shapes {
1062            let ns = shape.split(':').next().unwrap_or(&shape).to_lowercase();
1063            if q.contains(&ns) {
1064                authorized = true;
1065                break;
1066            }
1067        }
1068        if !authorized && !q.is_empty() {
1069            return (
1070                StatusCode::FORBIDDEN,
1071                Json(json!({"error": "gatekeeper violation"})),
1072            )
1073                .into_response();
1074        }
1075    }
1076
1077    let output_format = match negotiate_format(request.format.as_deref(), accept) {
1078        Ok(f) => f,
1079        Err(_) => return not_acceptable_format_response().into_response(),
1080    };
1081    if matches!(output_format, OutputFormat::RawQ42) {
1082        return (
1083            StatusCode::NOT_IMPLEMENTED,
1084            Json(json!({"error": "raw q42 not implemented - use export tools"})),
1085        )
1086            .into_response();
1087    }
1088
1089    if request.query.trim().is_empty() {
1090        return (
1091            StatusCode::BAD_REQUEST,
1092            Json(json!({"error": "empty query"})),
1093        )
1094            .into_response();
1095    }
1096
1097    if request.query.len() as u64 > QUERY_PAYLOAD_LIMIT_BYTES {
1098        return (
1099            StatusCode::PAYLOAD_TOO_LARGE,
1100            Json(json!({
1101                "error": "query too large",
1102                "limit_bytes": QUERY_PAYLOAD_LIMIT_BYTES
1103            })),
1104        )
1105            .into_response();
1106    }
1107
1108    let graph_guard = crate::daemon_graph::graph_read_guard();
1109    let (stats, final_results) =
1110        match daemon_query::execute_query_on_graph(&request.query, graph_guard.as_slice()) {
1111            Ok(pair) => pair,
1112            Err(_) => {
1113                return (
1114                    StatusCode::BAD_REQUEST,
1115                    Json(json!({"error": "query execution failed"})),
1116                )
1117                    .into_response()
1118            }
1119        };
1120
1121    let mut sanitized_results = Vec::with_capacity(final_results.len());
1122    let mut gatekeeper_halt = false;
1123    for quin in final_results {
1124        let sensitivity = quin.context >> 56;
1125        if sensitivity == 0x02 {
1126            gatekeeper_halt = true;
1127            sanitized_results.clear();
1128            break;
1129        } else {
1130            sanitized_results.push(quin);
1131        }
1132    }
1133    if gatekeeper_halt {
1134        return (
1135            StatusCode::FORBIDDEN,
1136            Json(json!({"error": "classified data egress blocked"})),
1137        )
1138            .into_response();
1139    }
1140
1141    let final_results = sanitized_results;
1142    let match_count = final_results.len();
1143
1144    let mut response_headers = HeaderMap::new();
1145    response_headers.insert(
1146        HeaderName::from_static("x-qualia-compute-cost"),
1147        HeaderValue::from_str(&format!("{}+{}", match_count, stats.vm_cycles)).unwrap(),
1148    );
1149
1150    match output_format {
1151        OutputFormat::NTriples => {
1152            let mut body_buf: Vec<u8> = Vec::with_capacity(match_count.max(1) * 80);
1153            let _ = crate::resolver::format_ntriples_to(&final_results, &mut body_buf);
1154            response_headers.insert(
1155                header::CONTENT_TYPE,
1156                HeaderValue::from_static("application/n-triples"),
1157            );
1158            (
1159                StatusCode::OK,
1160                response_headers,
1161                String::from_utf8(body_buf).unwrap_or_default(),
1162            )
1163                .into_response()
1164        }
1165        OutputFormat::JsonLd => {
1166            let graph: Vec<serde_json::Value> = final_results.iter().map(|q| json!({
1167                "subject": q.subject.to_string(), "predicate": q.predicate.to_string(), "object": q.object.to_string(),
1168                "context": q.context.to_string(), "metadata": q.metadata.to_string(), "parity": q.parity.to_string()
1169            })).collect();
1170            response_headers.insert(
1171                header::CONTENT_TYPE,
1172                HeaderValue::from_static("application/ld+json"),
1173            );
1174            let res = json!({ "@context": { "@vocab": "https://webizen.org/vocab#" }, "@graph": graph, "match_count": match_count });
1175            (StatusCode::OK, response_headers, res.to_string()).into_response()
1176        }
1177        OutputFormat::RawQ42 => unreachable!(),
1178    }
1179}
1180
1181async fn telemetry_handler(
1182    ws: WebSocketUpgrade,
1183    State(state): State<Arc<WebizenState>>,
1184) -> impl IntoResponse {
1185    ws.on_upgrade(|mut socket| async move {
1186        let mut rx = state.telemetry_tx.subscribe();
1187        while let Ok(msg) = rx.recv().await {
1188            if socket.send(Message::Binary(msg.into())).await.is_err() {
1189                break;
1190            }
1191        }
1192    })
1193}
1194
1195/// External WASM telemetry ingestion endpoint.
1196///
1197/// External devices (phones, browser tabs, other machines on the LAN) can
1198/// POST their `SystemTelemetry` data here for local processing. The desktop
1199/// app receives it, processes it locally (GPU visualisation, graph storage,
1200/// anomaly detection), and broadcasts it to all connected telemetry
1201/// subscribers (including the ambient visualisation bridge).
1202///
1203/// POST /telemetry/ingest
1204/// Body: { "source": "device-id", "telemetry": { ...SystemTelemetry fields } }
1205/// Response: 200 OK { "status": "ingested", "subscribers": N }
1206async fn telemetry_ingest_handler(
1207    State(state): State<Arc<WebizenState>>,
1208    Json(payload): Json<TelemetryIngestRequest>,
1209) -> impl IntoResponse {
1210    // Serialize the SystemTelemetry to bytes and broadcast to all subscribers
1211    let bytes = bytemuck::bytes_of(&payload.telemetry);
1212    let subscriber_count = state.telemetry_tx.receiver_count();
1213
1214    // Broadcast the telemetry to all connected subscribers (ambient viz,
1215    // render bridge, etc.)
1216    let _ = state.telemetry_tx.send(bytes.to_vec());
1217
1218    // Also store the telemetry source for provenance tracking
1219    // (the source device ID is logged for audit purposes)
1220    eprintln!(
1221        "[telemetry] Ingested external telemetry from {} ({} subscribers)",
1222        payload.source, subscriber_count
1223    );
1224
1225    (
1226        StatusCode::OK,
1227        Json(json!({
1228            "status": "ingested",
1229            "source": payload.source,
1230            "subscribers": subscriber_count,
1231        })),
1232    )
1233}
1234
1235/// Request body for external telemetry ingestion
1236#[derive(Deserialize)]
1237struct TelemetryIngestRequest {
1238    /// Source device identifier (e.g. "phone-abc123", "browser-tab-1")
1239    source: String,
1240    /// The telemetry data from the external device
1241    telemetry: crate::render::telemetry::SystemTelemetry,
1242}
1243
1244async fn manifest_handler(mut body: Body) -> impl IntoResponse {
1245    use http_body_util::BodyExt;
1246    let mut payload_bytes = Vec::new();
1247    while let Some(Ok(frame)) = body.frame().await {
1248        if let Some(chunk) = frame.data_ref() {
1249            payload_bytes.extend_from_slice(chunk);
1250        }
1251    }
1252    let lamport_clock: u64 = match crate::wal::WriteAheadLog::open("qualia_global.wal") {
1253        Ok(mut wal) => wal.buffered_count().unwrap_or(0) as u64 + 1,
1254        Err(_) => 1,
1255    };
1256    match crate::yaml_ld_q42::compile_yaml_ld_to_quins(&payload_bytes, 0, lamport_clock) {
1257        Ok(quins) => {
1258            for quin in quins {
1259                let _ = append_mutation(&quin);
1260            }
1261            axum::http::StatusCode::OK
1262        }
1263        Err(_) => axum::http::StatusCode::BAD_REQUEST,
1264    }
1265}
1266
1267async fn current_manifest_handler() -> impl IntoResponse {
1268    let workspace = json!({ "pages": [] });
1269    Json(workspace)
1270}
1271
1272use crate::extension_bus::ExtensionBus;
1273use std::sync::OnceLock;
1274static EXTENSION_BUS: OnceLock<ExtensionBus> = OnceLock::new();
1275fn get_extension_bus() -> &'static ExtensionBus {
1276    EXTENSION_BUS.get_or_init(|| {
1277        let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
1278        ExtensionBus::new(
1279            std::path::PathBuf::from(home)
1280                .join(".qualia")
1281                .join("extensions")
1282                .join("pool"),
1283        )
1284    })
1285}
1286async fn list_extensions_handler() -> impl IntoResponse {
1287    Json(get_extension_bus().list_extensions())
1288}
1289async fn query_extensions_handler(
1290    axum::extract::Path(interface): axum::extract::Path<String>,
1291) -> impl IntoResponse {
1292    Json(get_extension_bus().query_capability(q_hash(&interface)))
1293}
1294async fn register_extension_handler(mut body: Body) -> impl IntoResponse {
1295    use http_body_util::BodyExt;
1296    let mut payload_bytes = Vec::new();
1297    while let Some(Ok(frame)) = body.frame().await {
1298        if let Some(chunk) = frame.data_ref() {
1299            payload_bytes.extend_from_slice(chunk);
1300        }
1301    }
1302    let bus = get_extension_bus();
1303    let temp_file = std::env::temp_dir().join("temp_manifest.json");
1304    if std::fs::write(&temp_file, payload_bytes).is_ok()
1305        && bus.register_extension_from_path(&temp_file).is_ok()
1306    {
1307        return axum::http::StatusCode::OK;
1308    }
1309    axum::http::StatusCode::BAD_REQUEST
1310}
1311async fn mobile_qr_handler(
1312    axum::extract::State(state): axum::extract::State<Arc<WebizenState>>,
1313) -> impl IntoResponse {
1314    let port = state.port;
1315    let local_ip = std::net::UdpSocket::bind("0.0.0.0:0")
1316        .and_then(|s| {
1317            s.connect("1.1.1.1:80")?;
1318            s.local_addr()
1319        })
1320        .map(|addr| addr.ip().to_string())
1321        .unwrap_or_else(|_| "192.168.1.45".to_string());
1322
1323    let target = format!("http://{}:{}/mobile/app/index.html", local_ip, port);
1324    let url = format!(
1325        "https://mediaprophet.github.io/qualiaDB/bootstrap_gateway/index.html?target={}",
1326        urlencoding::encode(&target)
1327    );
1328
1329    let qr = fast_qr::QRBuilder::new(url).build().unwrap();
1330    let svg = fast_qr::convert::svg::SvgBuilder::default().to_str(&qr);
1331
1332    (
1333        StatusCode::OK,
1334        [(axum::http::header::CONTENT_TYPE, "image/svg+xml")],
1335        svg,
1336    )
1337        .into_response()
1338}
1339async fn mobile_ws_handler() -> impl IntoResponse {
1340    (StatusCode::OK, "WS Mock").into_response()
1341}
1342async fn mobile_generate_pane_handler() -> impl IntoResponse {
1343    (StatusCode::OK, "Pane Mock").into_response()
1344}
1345
1346#[cfg(test)]
1347mod tests {
1348    use super::*;
1349
1350    #[tokio::test]
1351    async fn test_permissions_compile_handler_commercial() {
1352        let req = PermissionsCompileRequest {
1353            payload: "type: Commercial Micro-Commons".to_string(),
1354        };
1355        let response = permissions_compile_handler(Json(req)).await.into_response();
1356        let body = http_body_util::BodyExt::collect(response.into_body())
1357            .await
1358            .unwrap()
1359            .to_bytes();
1360        let compiled: CompiledPermission = serde_json::from_slice(&body).unwrap();
1361
1362        assert_eq!((compiled.routing_mask >> 61) & 0x03, 0x02); // EnforceBilateralMicroCommons
1363        assert_ne!((compiled.routing_mask >> 50) & 0x01, 0); // MASK_COMMERCIAL_BILLABLE_GATE set
1364        assert_eq!(compiled.is_permissive_commons, false);
1365    }
1366
1367    #[test]
1368    fn not_acceptable_payload_matches_native_test_contract() {
1369        let (_, Json(body)) = not_acceptable_format_response();
1370        assert_eq!(body["code"], "not_acceptable");
1371        assert_eq!(body["status"], "error");
1372        let message = body["message"].as_str().expect("message");
1373        assert!(message.contains("application/ld+json"));
1374        assert!(message.contains("application/n-triples"));
1375    }
1376
1377    #[tokio::test]
1378    async fn test_permissions_compile_handler_public() {
1379        let req = PermissionsCompileRequest {
1380            payload: "type: Public Commons".to_string(),
1381        };
1382        let response = permissions_compile_handler(Json(req)).await.into_response();
1383        let body = http_body_util::BodyExt::collect(response.into_body())
1384            .await
1385            .unwrap()
1386            .to_bytes();
1387        let compiled: CompiledPermission = serde_json::from_slice(&body).unwrap();
1388
1389        assert_eq!((compiled.routing_mask >> 61) & 0x03, 0x01); // EnforcePermissiveCommons
1390        assert_eq!(compiled.is_permissive_commons, true);
1391    }
1392
1393    #[tokio::test]
1394    async fn test_webizen_rpc_sanctuary_mode_assertion() {
1395        let (telemetry_tx, _) = tokio::sync::broadcast::channel(10);
1396        let state = Arc::new(WebizenState {
1397            telemetry_tx,
1398            type_index: Arc::new([]),
1399            dev: true,
1400            token: None,
1401            vault: Arc::new(Mutex::new(crate::key_vault::KeyVault::new())),
1402            storage_path: "/tmp".to_string(),
1403            port: 8080,
1404            in_sanctuary_mode: Arc::new(std::sync::atomic::AtomicBool::new(true)),
1405        });
1406
1407        // 1. Requesting 'wf:AgentProposal' should fail with 423 LOCKED
1408        let req1 = WebizenRpcRequest {
1409            method: "requestAccess".to_string(),
1410            scopes: Some(vec!["wf:AgentProposal".to_string()]),
1411            payload: None,
1412        };
1413        let response1 = webizen_rpc_handler(axum::extract::State(state.clone()), Json(req1))
1414            .await
1415            .into_response();
1416        assert_eq!(response1.status(), StatusCode::LOCKED);
1417
1418        // 2. Requesting 'qp:Project' should succeed (Commons access allowed in Sanctuary)
1419        let req2 = WebizenRpcRequest {
1420            method: "requestAccess".to_string(),
1421            scopes: Some(vec!["qp:Project".to_string()]),
1422            payload: None,
1423        };
1424        let response2 = webizen_rpc_handler(axum::extract::State(state.clone()), Json(req2))
1425            .await
1426            .into_response();
1427        assert_eq!(response2.status(), StatusCode::OK);
1428    }
1429}