- 1
//! The public website, served at `/` (docs/design/48-web-client.md §4.6). - 2
//! - 3
//! Static pages built from one source by `site/build.py` and embedded - 4
//! here, the same way `/admin` and `/app` embed their bundles. It replaced a - 5
//! single hand-written `assets/landing.html` when the front door grew past - 6
//! one page: a shared rail, footer and design system copied four times drift - 7
//! the moment anyone edits three of them. - 8
//! - 9
//! Two properties this module has to preserve. - 10
//! - 11
//! **It renders before anything else is up.** Each page carries its own CSS - 12
//! and its own script inline; the only sub-resource is the shared, - 13
//! content-hashed `motion.js`, which is deferred and which the site works - 14
//! entirely without. A front door that needs a second round trip to show the - 15
//! product is not a front door. - 16
//! - 17
//! **It is auth-exempt, so it must not disclose anything.** Every route here - 18
//! answers an unauthenticated stranger. The pages show the product and, via - 19
//! `/version`, this build's version and commit — which that endpoint already - 20
//! publishes. They must never carry bind address, permission mode, workspace - 21
//! names, or session counts; `/health` reports several of those and is - 22
//! deliberately not what these pages read. - 23
- 24
use axum::routing::get; - 25
use include_dir::{Dir, include_dir}; - 26
- 27
use crate::embedded_ui::{register_files, serve_file}; - 28
- 29
static SITE: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/site/dist"); - 30
- 31
/// The routes the site owns, and the file each is served from. - 32
/// - 33
/// A table rather than a filesystem walk on purpose: these are the paths - 34
/// this server promises, and a page appearing under `site/dist` should not - 35
/// silently become a public URL without someone writing it down here. The - 36
/// `auth_exempt_path` list in `lib.rs` is checked against this, so the two - 37
/// cannot drift apart. - 38
pub(crate) const ROUTES: &[(&str, &str)] = &[ - 39
("/", "index.html"), - 40
("/outcomes", "outcomes/index.html"), - 41
("/vak", "vak/index.html"), - 42
("/surfaces", "surfaces/index.html"), - 43
("/tour", "tour/index.html"), - 44
("/security", "security/index.html"), - 45
("/install", "install/index.html"), - 46
("/wallpapers", "wallpapers/index.html"), - 47
("/terms", "terms/index.html"), - 48
("/privacy", "privacy/index.html"), - 49
]; - 50
- 51
fn page(file: &'static str) -> axum::response::Response { - 52
serve_file(&SITE, file) - 53
} - 54
- 55
pub(crate) fn routes() -> axum::Router<crate::AppState> { - 56
let mut router = axum::Router::new(); - 57
for (uri, file) in ROUTES { - 58
router = router.route(uri, get(move || async move { page(file) })); - 59
// `/surfaces/` and `/surfaces` are the same page. A trailing slash - 60
// is the single most common way a hand-typed URL misses. - 61
if *uri != "/" { - 62
router = router.route(&format!("{uri}/"), get(move || async move { page(file) })); - 63
} - 64
} - 65
// The shared vendored script, under its content-hashed name. - 66
if let Some(assets) = SITE.get_dir("site") { - 67
router = register_files(router, assets, &SITE, ""); - 68
} - 69
router - 70
} - 71
- 72
#[cfg(test)] - 73
#[allow(clippy::unwrap_used, clippy::expect_used)] - 74
mod tests { - 75
use super::*; - 76
use http_body_util::BodyExt as _; - 77
use tower::ServiceExt as _; - 78
- 79
async fn get_path(path: &str) -> (axum::http::StatusCode, String) { - 80
let router = routes().with_state(crate::test_support::state()); - 81
let response = router - 82
.oneshot( - 83
axum::http::Request::builder() - 84
.uri(path) - 85
.body(axum::body::Body::empty()) - 86
.unwrap(), - 87
) - 88
.await - 89
.unwrap(); - 90
let status = response.status(); - 91
let body = response.into_body().collect().await.unwrap().to_bytes(); - 92
(status, String::from_utf8_lossy(&body).into_owned()) - 93
} - 94
- 95
#[tokio::test] - 96
async fn every_declared_route_serves_its_page() { - 97
for (uri, _file) in ROUTES { - 98
let (status, body) = get_path(uri).await; - 99
assert_eq!(status, axum::http::StatusCode::OK, "{uri}"); - 100
assert!( - 101
body.contains("<title>"), - 102
"{uri} served something that is not a page" - 103
); - 104
assert!( - 105
body.contains("vak"), - 106
"{uri} served a page that does not mention the product" - 107
); - 108
} - 109
} - 110
- 111
/// The failure `/admin` and `/app` each shipped: the page loads, and - 112
/// every asset it references 404s from the router. - 113
#[tokio::test] - 114
async fn every_script_the_pages_reference_is_routable() { - 115
let (_, home) = get_path("/").await; - 116
let mut referenced: Vec<String> = home - 117
.split(['"', '\'']) - 118
.filter(|s| s.starts_with("/site/")) - 119
.map(String::from) - 120
.collect(); - 121
referenced.sort(); - 122
referenced.dedup(); - 123
assert!( - 124
!referenced.is_empty(), - 125
"the home page references no /site/ asset — the layout no longer loads motion, \ - 126
or this extraction no longer matches the markup" - 127
); - 128
for path in referenced { - 129
let (status, _) = get_path(&path).await; - 130
assert_eq!( - 131
status, - 132
axum::http::StatusCode::OK, - 133
"{path} is referenced but not routable" - 134
); - 135
} - 136
} - 137
- 138
#[tokio::test] - 139
async fn all_wallpaper_downloads_serve_jpegs() { - 140
let (_, page) = get_path("/wallpapers").await; - 141
let downloads: Vec<_> = page - 142
.split('"') - 143
.filter(|path| path.starts_with("/site/wallpapers/") && path.ends_with(".jpg")) - 144
.collect(); - 145
assert_eq!(downloads.len(), 16); - 146
for path in downloads { - 147
let response = routes() - 148
.with_state(crate::test_support::state()) - 149
.oneshot( - 150
axum::http::Request::builder() - 151
.uri(path) - 152
.body(axum::body::Body::empty()) - 153
.unwrap(), - 154
) - 155
.await - 156
.unwrap(); - 157
assert_eq!(response.status(), axum::http::StatusCode::OK, "{path}"); - 158
assert_eq!(response.headers()["content-type"], "image/jpeg", "{path}"); - 159
let bytes = response.into_body().collect().await.unwrap().to_bytes(); - 160
assert!(bytes.starts_with(&[0xff, 0xd8, 0xff]), "{path}"); - 161
} - 162
} - 163
- 164
/// These pages answer an unauthenticated stranger, so the only server - 165
/// data they may pull is what `/version` already publishes. - 166
/// - 167
/// Checking the *text* for machine-specific words does not work and was - 168
/// tried first: `/security` legitimately names `0.0.0.0` while - 169
/// explaining that the server refuses to bind it. The property that - 170
/// actually holds is about fetches — these are static files, so nothing - 171
/// machine-specific can appear unless a script goes and gets it. - 172
#[tokio::test] - 173
async fn the_pages_fetch_nothing_but_the_build_stamp() { - 174
for (uri, _file) in ROUTES { - 175
let (_, body) = get_path(uri).await; - 176
let mut fetched: Vec<&str> = Vec::new(); - 177
for tail in body.split("fetch(\"").skip(1) { - 178
fetched.push(tail.split('"').next().unwrap_or("")); - 179
} - 180
assert!( - 181
!fetched.is_empty(), - 182
"{uri} fetches nothing at all — the build stamp script is gone, or this \ - 183
extraction no longer matches how it is written" - 184
); - 185
for url in fetched { - 186
assert_eq!( - 187
url, "/version", - 188
"{uri} fetches {url}; an auth-exempt page may only read /version, which \ - 189
publishes the version and commit and nothing else. /health reports \ - 190
provider, model, sandbox and permission mode, and is not for strangers." - 191
); - 192
} - 193
} - 194
} - 195
- 196
/// A trailing slash is the commonest way a hand-typed URL misses. - 197
#[tokio::test] - 198
async fn sub_pages_answer_with_and_without_a_trailing_slash() { - 199
for (uri, _file) in ROUTES.iter().filter(|(u, _)| *u != "/") { - 200
let (status, _) = get_path(&format!("{uri}/")).await; - 201
assert_eq!(status, axum::http::StatusCode::OK, "{uri}/"); - 202
} - 203
} - 204
} - 205
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.