mod acl;
mod admin;
#[cfg(feature = "admin-ui")]
mod admin_ui;
mod audit;
mod auth;
mod backup;
mod ceremonies;
mod community;
mod config;
pub(crate) mod did_log;
mod directory;
mod endorsement_types;
mod endorsements;
mod health;
pub(crate) mod install;
pub mod join_requests;
pub(crate) mod members;
pub(crate) mod policies;
pub mod recognise;
mod relationships;
mod schemas;
pub(crate) mod status_lists;
#[cfg(feature = "website")]
mod website;
use std::sync::Arc;
use axum::Router;
use axum::extract::DefaultBodyLimit;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::routing::{any, get, post};
use tower_governor::GovernorLayer;
use tower_governor::governor::GovernorConfigBuilder;
use tower_governor::key_extractor::SmartIpKeyExtractor;
use utoipa::OpenApi;
use utoipa_axum::router::OpenApiRouter;
use utoipa_axum::routes;
use vti_common::trust_task::{TrustTask, task_layer, task_routes};
use crate::config::RoutingConfig;
use crate::server::AppState;
#[derive(OpenApi)]
#[openapi(
info(
title = "Verifiable Trust Community (VTC) API",
description = "Community lifecycle, ACL, audit, policy, credentials, \
endorsements, and cross-community recognition REST surface \
of a Verifiable Trust Community.",
version = env!("CARGO_PKG_VERSION"),
),
modifiers(&SecurityAddon),
)]
pub struct ApiDoc;
struct SecurityAddon;
impl utoipa::Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
let components = openapi.components.get_or_insert_with(Default::default);
components.add_security_scheme(
"bearer_jwt",
SecurityScheme::Http(
HttpBuilder::new()
.scheme(HttpAuthScheme::Bearer)
.bearer_format("JWT")
.build(),
),
);
}
}
async fn serve_openapi(api: utoipa::openapi::OpenApi) -> axum::Json<utoipa::openapi::OpenApi> {
axum::Json(api)
}
pub fn openapi_spec() -> utoipa::openapi::OpenApi {
OpenApiRouter::<AppState>::with_openapi(ApiDoc::openapi())
.nest("/v1", build_api_chain(&RoutingConfig::default(), false))
.split_for_parts()
.1
}
pub const MAX_BODY_SIZE: usize = 1024 * 1024;
pub const UNAUTH_BODY_SIZE: usize = 64 * 1024;
fn tt(
routes: utoipa_axum::router::UtoipaMethodRouter<AppState>,
url: &'static str,
) -> utoipa_axum::router::UtoipaMethodRouter<AppState> {
task_routes(routes, TrustTask::new(url).expect("static Trust-Task URL"))
}
fn ttl(
method_router: axum::routing::MethodRouter<AppState>,
url: &'static str,
) -> axum::routing::MethodRouter<AppState> {
task_layer(
method_router,
TrustTask::new(url).expect("static Trust-Task URL"),
)
}
pub fn router() -> Router<AppState> {
#[cfg(feature = "website")]
{
router_with(&RoutingConfig::default(), None)
}
#[cfg(not(feature = "website"))]
{
router_with(&RoutingConfig::default())
}
}
#[cfg(feature = "website")]
pub fn router_with(
routing: &RoutingConfig,
website_state: Option<crate::website::WebsiteState>,
) -> Router<AppState> {
router_with_inner(routing, website_state, false)
}
#[cfg(feature = "website")]
pub fn router_with_xff(
routing: &RoutingConfig,
website_state: Option<crate::website::WebsiteState>,
trust_xff: bool,
) -> Router<AppState> {
router_with_inner(routing, website_state, trust_xff)
}
#[cfg(not(feature = "website"))]
pub fn router_with(routing: &RoutingConfig) -> Router<AppState> {
router_with_inner(routing, false)
}
#[cfg(not(feature = "website"))]
pub fn router_with_xff(routing: &RoutingConfig, trust_xff: bool) -> Router<AppState> {
router_with_inner(routing, trust_xff)
}
#[cfg(not(feature = "website"))]
fn router_with_inner(routing: &RoutingConfig, trust_xff: bool) -> Router<AppState> {
let api_chain = build_api_chain(routing, trust_xff).split_for_parts().0;
with_csrf(assemble(routing, api_chain))
}
#[cfg(feature = "website")]
fn router_with_inner(
routing: &RoutingConfig,
website_state: Option<crate::website::WebsiteState>,
trust_xff: bool,
) -> Router<AppState> {
let api_chain = build_api_chain(routing, trust_xff).split_for_parts().0;
with_csrf(assemble_with_website(routing, api_chain, website_state))
}
fn with_csrf(app: Router<AppState>) -> Router<AppState> {
app.layer(axum::middleware::from_fn(crate::routing::csrf::enforce))
}
fn build_api_chain(_routing: &RoutingConfig, trust_xff: bool) -> OpenApiRouter<AppState> {
let api = OpenApiRouter::<AppState>::new()
.routes(tt(
routes!(health::diagnostics),
"https://trusttasks.org/openvtc/vtc/health/diagnostics/1.0",
))
.routes(routes!(status_lists::show))
.routes(tt(
routes!(auth::session_list, auth::revoke_sessions_by_did),
"https://trusttasks.org/spec/auth/sessions/list/0.1",
))
.routes(tt(
routes!(auth::revoke_session),
"https://trusttasks.org/spec/auth/revoke-session/0.1",
))
.routes(tt(
routes!(auth::whoami),
"https://trusttasks.org/spec/auth/whoami/0.1",
))
.routes(tt(
routes!(auth::sign_out),
"https://trusttasks.org/spec/auth/revoke-session/0.1",
))
.routes(tt(
routes!(audit::list_audit),
"https://trusttasks.org/openvtc/vtc/audit/list/1.0",
))
.routes(tt(
routes!(config::get_config, config::update_config),
"https://trusttasks.org/openvtc/vtc/config/legacy/manage/1.0",
))
.routes(tt(
routes!(acl::list_acl, acl::create_acl),
"https://trusttasks.org/openvtc/vtc/acl/legacy/manage/1.0",
))
.routes(tt(
routes!(acl::get_acl, acl::update_acl, acl::delete_acl),
"https://trusttasks.org/openvtc/vtc/acl/legacy/entry/1.0",
))
.routes(tt(
routes!(
community::profile::get_profile,
community::profile::put_profile
),
"https://trusttasks.org/openvtc/vtc/community/profile/manage/1.0",
))
.routes(routes!(community::profile::get_public_profile))
.routes(tt(
routes!(admin::config::get_config, admin::config::patch_config),
"https://trusttasks.org/openvtc/vtc/admin/config/manage/1.0",
))
.routes(tt(
routes!(admin::config::reload_config),
"https://trusttasks.org/openvtc/vtc/admin/config/reload/1.0",
))
.routes(tt(
routes!(admin::config::restart_config),
"https://trusttasks.org/openvtc/vtc/admin/config/restart/1.0",
))
.routes(tt(
routes!(admin::config::export_config),
"https://trusttasks.org/openvtc/vtc/admin/config/export/1.0",
))
.routes(tt(
routes!(admin::config::import_config),
"https://trusttasks.org/openvtc/vtc/admin/config/import/1.0",
))
.routes(tt(
routes!(admin::bootstrap::bootstrap),
"https://trusttasks.org/openvtc/vtc/admin/bootstrap/1.0",
))
.routes(tt(
routes!(admin::passkeys::list),
"https://trusttasks.org/openvtc/vtc/admin/passkeys/list/1.0",
))
.routes(tt(
routes!(admin::passkeys::register_start),
"https://trusttasks.org/openvtc/vtc/admin/passkeys/register/1.0",
))
.routes(tt(
routes!(admin::passkeys::register_finish),
"https://trusttasks.org/openvtc/vtc/admin/passkeys/register/1.0",
))
.routes(tt(
routes!(admin::passkeys::revoke_start),
"https://trusttasks.org/openvtc/vtc/admin/passkeys/revoke/1.0",
))
.routes(tt(
routes!(admin::passkeys::revoke_finish),
"https://trusttasks.org/openvtc/vtc/admin/passkeys/revoke/1.0",
))
.routes(tt(
routes!(admin::invites::list_invites, admin::invites::create_invite),
"https://trusttasks.org/openvtc/vtc/admin/invites/manage/1.0",
))
.routes(tt(
routes!(admin::invites::revoke_invite),
"https://trusttasks.org/openvtc/vtc/admin/invites/revoke/1.0",
))
.routes(tt(
routes!(directory::query),
"https://trusttasks.org/openvtc/vtc/directory/query/1.0",
))
.routes(tt(
routes!(ceremonies::list),
"https://trusttasks.org/openvtc/vtc/ceremonies/list/1.0",
))
.routes(tt(
routes!(members::read::list_members),
"https://trusttasks.org/openvtc/vtc/members/list/1.0",
))
.routes(tt(
routes!(members::remove::self_remove),
"https://trusttasks.org/openvtc/vtc/members/self-remove/1.0",
))
.routes(tt(
routes!(members::renew::renew),
"https://trusttasks.org/openvtc/vtc/members/renew/1.0",
))
.routes(tt(
routes!(members::rotate::challenge),
"https://trusttasks.org/openvtc/vtc/members/rotate-challenge/1.0",
))
.routes(tt(
routes!(members::rotate::rotate),
"https://trusttasks.org/openvtc/vtc/members/rotate/1.0",
))
.routes(tt(
routes!(members::personhood::challenge),
"https://trusttasks.org/openvtc/vtc/members/personhood/challenge/1.0",
))
.routes(tt(
routes!(members::personhood::assert, members::personhood::revoke), "https://trusttasks.org/openvtc/vtc/members/personhood/assert/1.0",
))
.routes(tt(
routes!(members::relationships::list),
"https://trusttasks.org/openvtc/vtc/relationships/list/1.0",
))
.routes(tt(
routes!(relationships::publish),
"https://trusttasks.org/openvtc/vtc/relationships/publish/1.0",
))
.routes(tt(
routes!(relationships::revoke),
"https://trusttasks.org/openvtc/vtc/relationships/revoke/1.0",
))
.routes(tt(
routes!(endorsement_types::register, endorsement_types::list), "https://trusttasks.org/openvtc/vtc/endorsement-types/register/1.0",
))
.routes(tt(
routes!(endorsement_types::delete),
"https://trusttasks.org/openvtc/vtc/endorsement-types/delete/1.0",
))
.routes(routes!(
schemas::register_accepts,
schemas::list_accepts_route
))
.routes(routes!(
schemas::get_accepts_route,
schemas::delete_accepts_route
))
.routes(routes!(schemas::register, schemas::list))
.routes(routes!(schemas::get_one, schemas::delete_one))
.routes(tt(
routes!(endorsements::issue, endorsements::list), "https://trusttasks.org/openvtc/vtc/credentials/endorsements/issue/1.0",
))
.routes(tt(
routes!(endorsements::show, endorsements::revoke), "https://trusttasks.org/openvtc/vtc/credentials/endorsements/show/1.0",
))
.routes(tt(
routes!(
members::read::show_member,
members::update::update_member,
members::remove::admin_remove
), "https://trusttasks.org/openvtc/vtc/members/show/1.0",
))
.routes(tt(
routes!(members::promote::promote_start),
"https://trusttasks.org/openvtc/vtc/members/promote-to-admin/1.0",
))
.routes(tt(
routes!(members::promote::promote_finish),
"https://trusttasks.org/openvtc/vtc/members/promote-to-admin/1.0",
))
.routes(tt(
routes!(join_requests::read::list_join_requests),
"https://trusttasks.org/openvtc/vtc/join-requests/submit/1.0",
))
.routes(tt(
routes!(join_requests::read::show_join_request),
"https://trusttasks.org/openvtc/vtc/join-requests/show/1.0",
))
.routes(tt(
routes!(join_requests::decide::approve),
"https://trusttasks.org/openvtc/vtc/join-requests/approve/1.0",
))
.routes(tt(
routes!(join_requests::decide::reject),
"https://trusttasks.org/openvtc/vtc/join-requests/reject/1.0",
))
.routes(tt(
routes!(join_requests::manifest::manifest),
"https://trusttasks.org/openvtc/vtc/join-requests/manifest/1.0",
))
.routes(routes!(join_requests::present::send_query))
.routes(tt(
routes!(policies::read::list_policies, policies::admin::upload),
"https://trusttasks.org/openvtc/vtc/policies/upload/1.0",
))
.routes(tt(
routes!(policies::read::show_policy), "https://trusttasks.org/openvtc/vtc/policies/upload/1.0",
))
.routes(tt(
routes!(policies::admin::activate),
"https://trusttasks.org/openvtc/vtc/policies/activate/1.0",
))
.routes(tt(
routes!(policies::admin::test),
"https://trusttasks.org/openvtc/vtc/policies/test/1.0",
));
#[cfg(feature = "website")]
let api = {
use axum::extract::DefaultBodyLimit;
const WEBSITE_ROUTE_CAP: usize = 64 * 1024 * 1024;
api.route(
"/website/files",
ttl(
get(website::files::list),
"https://trusttasks.org/openvtc/vtc/website/files/list/1.0",
),
)
.route(
"/website/files/{*path}",
ttl(
get(website::files::show)
.put(website::files::write)
.delete(website::files::delete)
.layer(DefaultBodyLimit::max(WEBSITE_ROUTE_CAP)), "https://trusttasks.org/openvtc/vtc/website/files/show/1.0",
),
)
.route(
"/website/deploy",
ttl(
post(website::deploy::deploy).layer(DefaultBodyLimit::max(WEBSITE_ROUTE_CAP)),
"https://trusttasks.org/openvtc/vtc/website/deploy/1.0",
),
)
.route(
"/website/generations",
ttl(
get(website::generations::list),
"https://trusttasks.org/openvtc/vtc/website/generations/list/1.0",
),
)
.route(
"/website/rollback/{gen_num}",
ttl(
post(website::generations::rollback),
"https://trusttasks.org/openvtc/vtc/website/rollback/1.0",
),
)
};
const BACKUP_IMPORT_CAP: usize = 64 * 1024 * 1024;
let api = api
.route(
"/backup/export",
ttl(
post(backup::export),
"https://trusttasks.org/openvtc/vtc/backup/export/1.0",
),
)
.route(
"/backup/import",
ttl(
post(backup::import).layer(DefaultBodyLimit::max(BACKUP_IMPORT_CAP)),
"https://trusttasks.org/openvtc/vtc/backup/import/1.0",
),
);
let api = api
.layer(DefaultBodyLimit::max(MAX_BODY_SIZE));
let unauth = build_unauth_routes(trust_xff);
api.merge(unauth)
}
fn build_unauth_routes(trust_xff: bool) -> OpenApiRouter<AppState> {
let _ = trust_xff;
let synth_connect_info = axum::middleware::from_fn(insert_default_connect_info_if_missing);
let unauth_router = OpenApiRouter::<AppState>::new()
.routes(tt(
routes!(auth::challenge),
"https://trusttasks.org/spec/auth/challenge/0.1",
))
.routes(tt(
routes!(auth::authenticate),
"https://trusttasks.org/spec/auth/authenticate/0.1",
))
.route("/wallet/auth/challenge", post(auth::challenge))
.route("/wallet/auth/", post(auth::authenticate))
.routes(tt(
routes!(auth::refresh),
"https://trusttasks.org/spec/auth/refresh/0.1",
))
.routes(tt(
routes!(auth::admin_login),
"https://trusttasks.org/openvtc/vtc/auth/admin-login/1.0",
))
.routes(tt(
routes!(auth::admin_session),
"https://trusttasks.org/openvtc/vtc/auth/admin-session/1.0",
))
.routes(tt(
routes!(auth::passkey_login_start),
"https://trusttasks.org/spec/auth/passkey/login/start/0.1",
))
.routes(tt(
routes!(auth::passkey_login_finish),
"https://trusttasks.org/spec/auth/passkey/login/finish/0.1",
))
.routes(tt(
routes!(install::claim_start),
"https://trusttasks.org/openvtc/vtc/install/claim/start/1.0",
))
.routes(tt(
routes!(install::claim_finish),
"https://trusttasks.org/openvtc/vtc/install/claim/finish/1.0",
))
.routes(tt(
routes!(recognise::recognise_challenge),
"https://trusttasks.org/openvtc/vtc/auth/recognise/challenge/1.0",
))
.routes(tt(
routes!(recognise::recognise),
"https://trusttasks.org/openvtc/vtc/auth/recognise/1.0",
))
.routes(tt(
routes!(join_requests::submit::submit),
"https://trusttasks.org/openvtc/vtc/join-requests/submit/1.0",
))
.routes(tt(
routes!(join_requests::accept::accept),
"https://trusttasks.org/openvtc/vtc/join-requests/accept/1.0",
))
.routes(tt(
routes!(join_requests::status::status),
"https://trusttasks.org/openvtc/vtc/join-requests/status/1.0",
))
.layer(DefaultBodyLimit::max(UNAUTH_BODY_SIZE));
let unauth_router = if trust_xff {
let cfg = Arc::new(
GovernorConfigBuilder::default()
.per_second(5)
.burst_size(10)
.key_extractor(SmartIpKeyExtractor)
.finish()
.expect("governor config values are static and non-zero"),
);
unauth_router.layer(GovernorLayer::new(cfg))
} else {
let cfg = Arc::new(
GovernorConfigBuilder::default()
.per_second(5)
.burst_size(10)
.key_extractor(tower_governor::key_extractor::PeerIpKeyExtractor)
.finish()
.expect("governor config values are static and non-zero"),
);
unauth_router.layer(GovernorLayer::new(cfg))
};
unauth_router.layer(synth_connect_info)
}
async fn insert_default_connect_info_if_missing(
mut request: axum::extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use axum::extract::ConnectInfo;
if request
.extensions()
.get::<ConnectInfo<SocketAddr>>()
.is_none()
{
let synthetic =
ConnectInfo::<SocketAddr>(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0));
request.extensions_mut().insert(synthetic);
}
next.run(request).await
}
#[cfg_attr(feature = "website", allow(dead_code))]
fn assemble(routing: &RoutingConfig, api: Router<AppState>) -> Router<AppState> {
use axum::middleware::from_fn;
use crate::routing::security_headers::security_headers;
let admin_placeholder: Router<AppState> = Router::new()
.fallback(any(placeholder_503))
.layer(from_fn(security_headers));
let website_placeholder: Router<AppState> = Router::new()
.fallback(any(placeholder_503))
.layer(from_fn(security_headers));
let spec = openapi_spec();
let mut app: Router<AppState> = Router::new()
.route("/health", get(health::health))
.route("/openapi.json", get(move || serve_openapi(spec.clone())))
.route("/.well-known/did.jsonl", get(did_log::did_log))
.nest(&routing.api.mount, api);
app = app.nest(&routing.admin_ui.mount, admin_placeholder);
if routing.website.mount == "/" {
app = app.merge(website_placeholder);
} else {
app = app.nest(&routing.website.mount, website_placeholder);
}
app
}
#[cfg(feature = "website")]
pub fn assemble_with_website(
routing: &RoutingConfig,
api: Router<AppState>,
website_state: Option<crate::website::WebsiteState>,
) -> Router<AppState> {
use axum::middleware::from_fn;
use crate::routing::security_headers::security_headers;
#[cfg(feature = "admin-ui")]
let admin: Router<AppState> = Router::new()
.route("/build-info.json", get(admin_ui::build_info))
.route("/plugins.json", get(admin_ui::plugins_manifest))
.route("/plugins/{id}/{*rel_path}", get(admin_ui::plugin_asset))
.route("/", get(admin_ui::serve_spa))
.route("/{*path}", get(admin_ui::serve_spa))
.layer(from_fn(security_headers));
#[cfg(not(feature = "admin-ui"))]
let admin: Router<AppState> = Router::new()
.route("/", any(placeholder_503))
.route("/{*path}", any(placeholder_503))
.layer(from_fn(security_headers));
let website: axum::Router<()> = match website_state {
Some(state) => Router::new()
.route("/", get(crate::website::serve))
.route("/{*path}", get(crate::website::serve))
.layer(from_fn(security_headers))
.with_state(state),
None => Router::new()
.route("/", get(crate::website::default_site::serve))
.route("/{*path}", get(crate::website::default_site::serve))
.layer(from_fn(security_headers)),
};
let spec = openapi_spec();
let mut app: Router<AppState> = Router::new()
.route("/health", get(health::health))
.route("/openapi.json", get(move || serve_openapi(spec.clone())))
.route("/.well-known/did.jsonl", get(did_log::did_log))
.nest(&routing.api.mount, api);
app = app.nest(&routing.admin_ui.mount, admin);
let admin_slash = format!("{}/", routing.admin_ui.mount.trim_end_matches('/'));
#[cfg(feature = "admin-ui")]
{
app = app.route(admin_slash.as_str(), get(admin_ui::serve_spa));
}
#[cfg(not(feature = "admin-ui"))]
{
app = app.route(admin_slash.as_str(), any(placeholder_503));
}
if routing.website.mount == "/" {
app = app.fallback_service(website);
} else {
app = app.nest_service(&routing.website.mount, website);
}
app
}
#[cfg_attr(all(feature = "website", feature = "admin-ui"), allow(dead_code))]
async fn placeholder_503() -> impl IntoResponse {
(
StatusCode::SERVICE_UNAVAILABLE,
"surface not yet implemented",
)
}
#[cfg(test)]
mod openapi_tests {
use super::*;
#[test]
fn openapi_spec_documents_the_migrated_route_and_security_scheme() {
let spec = openapi_spec();
assert_eq!(spec.info.title, "Verifiable Trust Community (VTC) API");
let diag = spec
.paths
.paths
.get("/v1/health/diagnostics")
.expect("/v1/health/diagnostics must be documented");
assert!(diag.get.is_some(), "diagnostics documents a GET operation");
let components = spec.components.as_ref().expect("components present");
assert!(components.security_schemes.contains_key("bearer_jwt"));
assert!(
components.schemas.contains_key("DiagnosticsResponse"),
"DiagnosticsResponse schema must be emitted"
);
}
#[test]
fn openapi_spec_covers_the_route_groups() {
let spec = openapi_spec();
let paths = &spec.paths.paths;
for p in [
"/v1/acl",
"/v1/acl/{did}",
"/v1/audit",
"/v1/config",
"/v1/auth/challenge",
"/v1/auth/sessions",
"/v1/admin/config",
"/v1/admin/invites",
"/v1/admin/passkeys",
"/v1/members",
"/v1/members/{did}",
"/v1/community/profile",
"/v1/join-requests",
"/v1/policies",
"/v1/credentials/endorsements",
"/v1/endorsement-types",
"/v1/schemas",
"/v1/relationships",
"/v1/directory/{did}",
"/v1/install/claim/start",
] {
assert!(paths.contains_key(p), "spec missing documented path {p}");
}
assert!(
paths.len() >= 55,
"expected the documented surface to be >= 55 paths, got {}",
paths.len()
);
}
const GOVERNED_UNAUTH: &[(&str, &str)] = &[
("POST", "/v1/auth/challenge"),
("POST", "/v1/auth/"),
("POST", "/v1/auth/refresh"),
("POST", "/v1/auth/admin-login"),
("POST", "/v1/auth/admin-session"),
("POST", "/v1/auth/passkey-login/start"),
("POST", "/v1/auth/passkey-login/finish"),
("POST", "/v1/auth/recognise/challenge"),
("POST", "/v1/auth/recognise"),
("POST", "/v1/install/claim/start"),
("POST", "/v1/install/claim/finish"),
("POST", "/v1/join-requests"),
("POST", "/v1/join-requests/{id}/accept"),
("POST", "/v1/join-requests/{id}/status"),
];
const PUBLIC_UNGOVERNED: &[(&str, &str)] = &[
("GET", "/v1/community/public-profile"),
("GET", "/v1/join-requests/manifest"),
("GET", "/v1/status-lists/{purpose}"),
("POST", "/v1/admin/bootstrap"),
];
fn documented_ops() -> Vec<(&'static str, String, bool)> {
let spec = openapi_spec();
let mut ops = Vec::new();
for (path, item) in &spec.paths.paths {
for (method, op) in [
("GET", &item.get),
("POST", &item.post),
("PATCH", &item.patch),
("DELETE", &item.delete),
("PUT", &item.put),
] {
if let Some(op) = op {
let secured = op.security.as_ref().map(|s| !s.is_empty()).unwrap_or(false);
ops.push((method, path.clone(), secured));
}
}
}
ops
}
fn in_allowlist(list: &[(&str, &str)], method: &str, path: &str) -> bool {
list.iter().any(|(m, p)| *m == method && *p == path)
}
#[test]
fn every_unauthenticated_route_is_classified() {
for (method, path, secured) in documented_ops() {
let governed = in_allowlist(GOVERNED_UNAUTH, method, &path);
let public = in_allowlist(PUBLIC_UNGOVERNED, method, &path);
if secured {
assert!(
!governed,
"{method} {path} requires a bearer JWT but is listed on the unauthenticated \
governed chain — an authenticated route must not sit in GOVERNED_UNAUTH"
);
} else {
assert!(
governed || public,
"{method} {path} is UNAUTHENTICATED but unclassified — add it to the governed \
unauth chain (GOVERNED_UNAUTH) or, if it is a deliberate public endpoint, to \
PUBLIC_UNGOVERNED. (This is the P0.5 backstop: an unauth route must never \
silently land on the 1 MiB no-limiter main chain.)"
);
assert!(
!(governed && public),
"{method} {path} is in both GOVERNED_UNAUTH and PUBLIC_UNGOVERNED — pick one"
);
}
}
}
#[test]
fn posture_allowlists_have_no_stale_entries() {
let ops = documented_ops();
let is_unauth_op = |method: &str, path: &str| {
ops.iter()
.any(|(m, p, secured)| *m == method && p == path && !secured)
};
for (method, path) in GOVERNED_UNAUTH.iter().chain(PUBLIC_UNGOVERNED) {
assert!(
is_unauth_op(method, path),
"posture allowlist entry {method} {path} is not a documented unauthenticated \
operation — remove it or fix the path/method"
);
}
}
}