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
29fn 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#[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 .fallback_service(ServeDir::new(ui_static_dir).precompressed_gzip())
300
301 .route("/qualia-bridge", get(bridge_handler))
303 .route("/telemetry", get(telemetry_handler))
304 .route("/telemetry/ingest", post(telemetry_ingest_handler).options(preflight_handler))
308
309 .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 .route("/manifest", post(manifest_handler))
334 .route("/manifest/current", get(current_manifest_handler))
335
336 .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 .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 .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
535async 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
575async 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, }
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 let mut is_permissive_commons = false;
757 let mut routing_mask = 0u64;
758
759 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; routing_mask |= 1 << 50; } else if req.payload.contains("Public Commons") || req.payload.contains("PermissiveCommons") {
769 routing_mask |= 0x01 << 61; is_permissive_commons = true;
771 } else {
772 routing_mask |= 0x00 << 61; }
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 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 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
907async fn update_handler(
914 State(state): State<Arc<WebizenState>>,
915 headers: HeaderMap,
916 Json(request): Json<UpdateRequest>,
917) -> impl IntoResponse {
918 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 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
1195async fn telemetry_ingest_handler(
1207 State(state): State<Arc<WebizenState>>,
1208 Json(payload): Json<TelemetryIngestRequest>,
1209) -> impl IntoResponse {
1210 let bytes = bytemuck::bytes_of(&payload.telemetry);
1212 let subscriber_count = state.telemetry_tx.receiver_count();
1213
1214 let _ = state.telemetry_tx.send(bytes.to_vec());
1217
1218 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#[derive(Deserialize)]
1237struct TelemetryIngestRequest {
1238 source: String,
1240 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); assert_ne!((compiled.routing_mask >> 50) & 0x01, 0); 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); 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 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 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}