Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions crates/buzz-relay/src/api/dkg_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ use super::{api_error, bridge, internal_error, not_found};

/// Maximum public request body accepted by `/api/dkg/query`.
pub(crate) const MAX_REQUEST_BYTES: usize = 16 * 1024;
pub(super) const MAX_RESPONSE_BYTES: usize = 2 * 1024 * 1024;
// A channel graph may contain up to 10,000 bounded triples. Keep the relay
// ceiling finite, but large enough for that documented response contract.
pub(super) const MAX_RESPONSE_BYTES: usize = 8 * 1024 * 1024;
const MAX_NAME_BYTES: usize = 256;
const MAX_URI_BYTES: usize = 2048;
const MAX_SPARQL_BYTES: usize = 8 * 1024;
Expand Down Expand Up @@ -61,6 +63,7 @@ enum Operation {
ReputationSummary,
SubgraphGraph,
SubgraphTriples,
ChannelTriples,
Evidence,
SemanticQuery,
}
Expand Down Expand Up @@ -231,7 +234,7 @@ fn parse_and_sanitize_request(
}

let arguments = match request.operation {
Operation::ChannelMemory => {
Operation::ChannelMemory | Operation::ChannelTriples => {
let arguments: EmptyArguments = parse_arguments(request.arguments)?;
serde_json::to_value(arguments)
}
Expand Down Expand Up @@ -513,6 +516,7 @@ mod tests {
let requester = requester();
for (operation, arguments) in [
("channel_memory", serde_json::json!({})),
("channel_triples", serde_json::json!({})),
("trust_network", serde_json::json!({})),
(
"reputation_summary",
Expand Down
4 changes: 4 additions & 0 deletions crates/buzz-relay/src/nip11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ fn dkg_memory_descriptor(trust_enabled: bool) -> serde_json::Value {
"decision_trace",
"subgraph_graph",
"subgraph_triples",
"channel_triples",
"evidence",
"semantic_query",
];
Expand Down Expand Up @@ -469,6 +470,9 @@ mod tests {
assert!(descriptor["query_operations"]
.as_array()
.is_some_and(|operations| operations.contains(&serde_json::json!("semantic_query"))));
assert!(descriptor["query_operations"]
.as_array()
.is_some_and(|operations| operations.contains(&serde_json::json!("channel_triples"))));
assert!(descriptor["query_operations"].as_array().is_some_and(
|operations| operations.contains(&serde_json::json!("reputation_summary"))
));
Expand Down
45 changes: 42 additions & 3 deletions crates/buzz-relay/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use axum::{
};
use serde_json::json;
use tower::ServiceExt;
use tower_http::cors::{AllowOrigin, CorsLayer};
use tower_http::cors::{AllowHeaders, AllowOrigin, CorsLayer};
use tower_http::limit::RequestBodyLimitLayer;
use tower_http::services::ServeDir;
use tower_http::trace::{HttpMakeClassifier, TraceLayer};
Expand Down Expand Up @@ -443,7 +443,10 @@ async fn mesh_status_handler(State(state): State<Arc<AppState>>) -> impl IntoRes
/// Build a CORS layer from the configured origins list.
fn build_cors_layer(cors_origins: &[String]) -> CorsLayer {
if cors_origins.is_empty() {
return CorsLayer::permissive();
return CorsLayer::new()
.allow_origin(tower_http::cors::Any)
.allow_methods(tower_http::cors::Any)
.allow_headers(AllowHeaders::mirror_request());
}

let origins: Vec<axum::http::HeaderValue> = cors_origins
Expand All @@ -463,7 +466,10 @@ fn build_cors_layer(cors_origins: &[String]) -> CorsLayer {
CorsLayer::new()
.allow_origin(AllowOrigin::list(origins))
.allow_methods(tower_http::cors::Any)
.allow_headers(tower_http::cors::Any)
// `Authorization` is a CORS non-wildcard request header. WebKit rejects
// `Access-Control-Allow-Headers: *` for NIP-98 requests, so echo the
// browser's requested header list explicitly.
.allow_headers(AllowHeaders::mirror_request())
}

#[cfg(test)]
Expand Down Expand Up @@ -511,6 +517,39 @@ mod tests {
assert!(!should_serve_spa("/arbitrary", true));
}

#[tokio::test]
async fn cors_preflight_explicitly_allows_nip98_authorization() {
let app = Router::new()
.route("/api/dkg/query", axum::routing::post(|| async {}))
.layer(build_cors_layer(&[]));

let response = app
.oneshot(
Request::builder()
.method("OPTIONS")
.uri("/api/dkg/query")
.header(axum::http::header::ORIGIN, "tauri://localhost")
.header(axum::http::header::ACCESS_CONTROL_REQUEST_METHOD, "POST")
.header(
axum::http::header::ACCESS_CONTROL_REQUEST_HEADERS,
"authorization,content-type",
)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();

assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get(axum::http::header::ACCESS_CONTROL_ALLOW_HEADERS)
.unwrap(),
"authorization,content-type"
);
}

#[tokio::test(flavor = "current_thread")]
async fn http_and_datastore_spans_are_exported_in_the_same_trace() {
let exporter = InMemorySpanExporter::default();
Expand Down
3 changes: 2 additions & 1 deletion desktop/src/features/dkg-memory/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,8 @@ export async function runDkgDiagnostics(
const query = await timedDiagnostic(() =>
fetchSemanticQuery(
channelId,
`PREFIX schema: <http://schema.org/>\nASK WHERE { GRAPH ?g { ?entity schema:name ?name . } }`,
`PREFIX schema: <http://schema.org/>\nSELECT ?entity WHERE { GRAPH ?g { ?entity schema:name ?name . } } LIMIT 1`,
"shared",
),
);
checks.push({
Expand Down
11 changes: 11 additions & 0 deletions desktop/src/features/dkg-memory/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { getRelayHttpUrl, signRelayEvent } from "@/shared/api/tauri";
const LOCAL_EXPLORER = "http://127.0.0.1:9295";
const LOCAL_PROBE_TIMEOUT_MS = 1_500;
const QUERY_TIMEOUT_MS = 25_000;
const CHANNEL_MEMORY_TIMEOUT_MS = 90_000;
const CHANNEL_GRAPH_TIMEOUT_MS = 115_000;
const NIP98_KIND = 27235;

export type ExplorerSource = "local" | "gateway";
Expand All @@ -16,6 +18,7 @@ export type DkgQueryOperation =
| "reputation_summary"
| "subgraph_graph"
| "subgraph_triples"
| "channel_triples"
| "evidence"
| "semantic_query";

Expand All @@ -36,6 +39,7 @@ type DkgQueryArguments = {
reputation_summary: { pubkey: string };
subgraph_graph: { name: string };
subgraph_triples: { name: string };
channel_triples: Record<string, never>;
evidence: { uri: string };
semantic_query: {
sparql: string;
Expand Down Expand Up @@ -264,6 +268,7 @@ function adaptCommunityResult<Operation extends DkgQueryOperation>(
case "reputation_summary":
case "subgraph_graph":
case "subgraph_triples":
case "channel_triples":
case "semantic_query":
return { ...envelope.result, gate: "ok", cg: envelope.cg };
case "evidence":
Expand All @@ -288,6 +293,12 @@ async function communityGatewayQuery<
const { result } = await postAuthenticatedDkgJson<unknown>({
path: "/api/dkg/query",
body,
timeoutMs:
query.operation === "channel_triples"
? CHANNEL_GRAPH_TIMEOUT_MS
: query.operation === "channel_memory"
? CHANNEL_MEMORY_TIMEOUT_MS
: QUERY_TIMEOUT_MS,
});
const envelope = validateEnvelope(result, query);
return adaptCommunityResult(envelope) as Result;
Expand Down
20 changes: 12 additions & 8 deletions desktop/src/features/dkg-memory/topology/TopologyView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const NODE_UI_GRAPH_OPTIONS = {
edgeWidth: 0.9,
},
hexagon: { baseSize: 4, minSize: 3, maxSize: 6, scaleWithDegree: true },
focus: { maxNodes: 3000, hops: 999 },
focus: { maxNodes: 20_000, hops: 999 },
};

export interface TopologyNeighbor {
Expand Down Expand Up @@ -113,16 +113,20 @@ export function TopologyView({
const shaped = useMemo(() => {
const triples = query.data?.triples ?? [];
const bounded = applyHeaviestSubjectsCap(triples);
const { canvasTriples, singletonItems } =
splitGraphTriplesForShelf(bounded);
const shelf = splitGraphTriplesForShelf(bounded);
// The channel action is explicitly the complete Context Graph view: keep
// every returned triple in the renderer. A named subgraph retains the
// compact singleton shelf used by the focused inspection UI.
const canvasTriples = channelWide ? bounded : shelf.canvasTriples;
const singletonItems = channelWide ? [] : shelf.singletonItems;
return {
canvasTriples,
singletonItems,
nodeColors: attributionNodeColors(bounded),
legend: attributionLegend(bounded),
dropped: triples.length - bounded.length,
};
}, [query.data]);
}, [channelWide, query.data]);

const graphData = useMemo(
() =>
Expand Down Expand Up @@ -219,14 +223,14 @@ export function TopologyView({
</span>
))}
<span className="ml-auto text-2xs text-muted-foreground">
{summary.entities} connected entities · {summary.relationships}{" "}
relationships ·{" "}
{summary.entities} entities · {summary.relationships} relationships ·{" "}
{channelWide
? "bounded channel view · use search to narrow further"
? `complete channel view · up to ${query.data?.limit?.toLocaleString() ?? "10,000"} triples`
: colorMode === "attribution"
? "colors = recorded attribution, not verification"
: "colors = entity types, as in your DKG node"}
{shaped.dropped > 0 && ` · ${shaped.dropped} triples beyond cap`}
{(query.data?.truncated || shaped.dropped > 0) &&
" · graph reached the safety cap"}
</span>
</div>

Expand Down
Loading
Loading