mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 07:21:32 +02:00
fix(nip05): always serve the well-known directory with CORS * (federation)
The NIP-05 directory (/.well-known/nostr.json) is public, read-only, unauth'd data that MUST be cross-origin fetchable — any Nostr client on any domain resolves `user@this-instance` by fetching it. It was covered by the single global CORS layer, so it only got `Access-Control-Allow-Origin: *` when CORS_ALLOWED_ORIGINS was empty (the default). An operator who restricts CORS to their own frontend would silently break NIP-05 resolution for every other domain/wallet — federation dies for that instance with no error. Split CORS into two route groups: the public surface (well-known + health + landing) always serves `*`; the /api/v1 surface keeps the operator's configurable policy. The API's CORS behavior is unchanged (it still gets the same `cors` layer); only the public directory is now unconditionally cross-origin. cargo check clean.
This commit is contained in:
+19
-5
@@ -117,19 +117,33 @@ pub fn build_router(state: Arc<AppState>) -> Router {
|
|||||||
.layer(normal),
|
.layer(normal),
|
||||||
);
|
);
|
||||||
|
|
||||||
Router::new()
|
// The NIP-05 directory (+ health / landing) is PUBLIC, read-only, unauth'd
|
||||||
|
// data that MUST be cross-origin fetchable for federation to work — any Nostr
|
||||||
|
// client on any domain resolves `user@this-instance` by fetching the
|
||||||
|
// well-known. So it ALWAYS serves `Access-Control-Allow-Origin: *`, regardless
|
||||||
|
// of the operator's API CORS restriction (`CORS_ALLOWED_ORIGINS`). We apply the
|
||||||
|
// two policies to two route groups rather than one outer layer, so a restrictive
|
||||||
|
// API policy can't silently break NIP-05 resolution.
|
||||||
|
let public_cors = CorsLayer::new()
|
||||||
|
.allow_methods([Method::GET])
|
||||||
|
.allow_origin(Any);
|
||||||
|
let public = Router::new()
|
||||||
.route("/health", get(api::health::health))
|
.route("/health", get(api::health::health))
|
||||||
.route("/", get(api::landing::root))
|
.route("/", get(api::landing::root))
|
||||||
.merge(api::nip05::routes())
|
.merge(api::nip05::routes())
|
||||||
.nest("/api/v1", api_v1)
|
.layer(public_cors);
|
||||||
|
|
||||||
|
Router::new()
|
||||||
|
.merge(public)
|
||||||
|
.nest("/api/v1", api_v1.layer(cors))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
// Layers apply outside-in (last added = outermost). Body cap + timeout
|
// Layers apply outside-in (last added = outermost). Body cap + timeout
|
||||||
// sit closest to the handlers; CORS + compression wrap them; tracing is
|
// sit closest to the handlers; compression wraps them; tracing is
|
||||||
// outermost so it observes every request (incl. rejected ones).
|
// outermost so it observes every request (incl. rejected ones). CORS is
|
||||||
|
// applied PER GROUP above (public → `*`, API → operator policy).
|
||||||
// Per-IP rate limiting is layered separately (it needs ConnectInfo).
|
// Per-IP rate limiting is layered separately (it needs ConnectInfo).
|
||||||
.layer(DefaultBodyLimit::max(MAX_BODY_BYTES))
|
.layer(DefaultBodyLimit::max(MAX_BODY_BYTES))
|
||||||
.layer(TimeoutLayer::new(REQUEST_TIMEOUT))
|
.layer(TimeoutLayer::new(REQUEST_TIMEOUT))
|
||||||
.layer(cors)
|
|
||||||
.layer(CompressionLayer::new())
|
.layer(CompressionLayer::new())
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user