max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
9 files changed,
+197 insertions,
-57 deletions
| @@ -4817,7 +4817,7 @@ | |||
| 4817 | 4817 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4818 | 4818 | checksum = "82cb6a9f675da968c63b6208c641b9dca58fc0133ae53375736b1767b0cab8bd" | |
| 4819 | 4819 | dependencies = [ | |
| 4820 | - | "windows-sys 0.59.0", | |
| 4820 | + | "windows-sys 0.61.2", | |
| 4821 | 4821 | ] | |
| 4822 | 4822 | ||
| 4823 | 4823 | [[package]] | |
| @@ -5712,7 +5712,7 @@ | |||
| 5712 | 5712 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5713 | 5713 | checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" | |
| 5714 | 5714 | dependencies = [ | |
| 5715 | - | "windows-sys 0.59.0", | |
| 5715 | + | "windows-sys 0.61.2", | |
| 5716 | 5716 | ] | |
| 5717 | 5717 | ||
| 5718 | 5718 | [[package]] | |
| @@ -6632,7 +6632,7 @@ | |||
| 6632 | 6632 | ||
| 6633 | 6633 | [[package]] | |
| 6634 | 6634 | name = "quasi-axum" | |
| 6635 | - | version = "0.29.1" | |
| 6635 | + | version = "0.30.0" | |
| 6636 | 6636 | dependencies = [ | |
| 6637 | 6637 | "axum", | |
| 6638 | 6638 | "http 1.4.2", | |
| @@ -6643,7 +6643,7 @@ | |||
| 6643 | 6643 | ||
| 6644 | 6644 | [[package]] | |
| 6645 | 6645 | name = "quasi-basics" | |
| 6646 | - | version = "0.29.1" | |
| 6646 | + | version = "0.30.0" | |
| 6647 | 6647 | dependencies = [ | |
| 6648 | 6648 | "makeover-layout", | |
| 6649 | 6649 | "quasi-router", | |
| @@ -6651,7 +6651,7 @@ | |||
| 6651 | 6651 | ||
| 6652 | 6652 | [[package]] | |
| 6653 | 6653 | name = "quasi-http" | |
| 6654 | - | version = "0.29.1" | |
| 6654 | + | version = "0.30.0" | |
| 6655 | 6655 | dependencies = [ | |
| 6656 | 6656 | "form_urlencoded", | |
| 6657 | 6657 | "http 1.4.2", | |
| @@ -6660,7 +6660,7 @@ | |||
| 6660 | 6660 | ||
| 6661 | 6661 | [[package]] | |
| 6662 | 6662 | name = "quasi-router" | |
| 6663 | - | version = "0.29.1" | |
| 6663 | + | version = "0.30.0" | |
| 6664 | 6664 | dependencies = [ | |
| 6665 | 6665 | "makeover-layout", | |
| 6666 | 6666 | ] | |
| @@ -6683,7 +6683,7 @@ | |||
| 6683 | 6683 | ||
| 6684 | 6684 | [[package]] | |
| 6685 | 6685 | name = "quasi-webview" | |
| 6686 | - | version = "0.29.1" | |
| 6686 | + | version = "0.30.0" | |
| 6687 | 6687 | dependencies = [ | |
| 6688 | 6688 | "docengine", | |
| 6689 | 6689 | "makeover-layout", | |
| @@ -6759,7 +6759,7 @@ | |||
| 6759 | 6759 | "once_cell", | |
| 6760 | 6760 | "socket2", | |
| 6761 | 6761 | "tracing", | |
| 6762 | - | "windows-sys 0.59.0", | |
| 6762 | + | "windows-sys 0.52.0", | |
| 6763 | 6763 | ] | |
| 6764 | 6764 | ||
| 6765 | 6765 | [[package]] | |
| @@ -7322,7 +7322,7 @@ | |||
| 7322 | 7322 | "errno", | |
| 7323 | 7323 | "libc", | |
| 7324 | 7324 | "linux-raw-sys 0.4.15", | |
| 7325 | - | "windows-sys 0.59.0", | |
| 7325 | + | "windows-sys 0.52.0", | |
| 7326 | 7326 | ] | |
| 7327 | 7327 | ||
| 7328 | 7328 | [[package]] | |
| @@ -10768,6 +10768,30 @@ | |||
| 10768 | 10768 | "pkg-config", | |
| 10769 | 10769 | ] | |
| 10770 | 10770 | ||
| 10771 | + | [[patch.unused]] | |
| 10772 | + | name = "makeover-immediate" | |
| 10773 | + | version = "0.30.0" | |
| 10774 | + | ||
| 10775 | + | [[patch.unused]] | |
| 10776 | + | name = "makeover-tui" | |
| 10777 | + | version = "0.30.0" | |
| 10778 | + | ||
| 10779 | + | [[patch.unused]] | |
| 10780 | + | name = "quasi-immediate" | |
| 10781 | + | version = "0.30.0" | |
| 10782 | + | ||
| 10783 | + | [[patch.unused]] | |
| 10784 | + | name = "quasi-notifs" | |
| 10785 | + | version = "0.30.0" | |
| 10786 | + | ||
| 10787 | + | [[patch.unused]] | |
| 10788 | + | name = "quasi-store" | |
| 10789 | + | version = "0.1.0" | |
| 10790 | + | ||
| 10791 | + | [[patch.unused]] | |
| 10792 | + | name = "quasi-tauri" | |
| 10793 | + | version = "0.30.0" | |
| 10794 | + | ||
| 10771 | 10795 | [[patch.unused]] | |
| 10772 | 10796 | name = "synckit-client" | |
| 10773 | 10797 | version = "0.8.0" | |
| @@ -10787,23 +10811,3 @@ | |||
| 10787 | 10811 | [[patch.unused]] | |
| 10788 | 10812 | name = "painhours" | |
| 10789 | 10813 | version = "0.1.0" | |
| 10790 | - | ||
| 10791 | - | [[patch.unused]] | |
| 10792 | - | name = "makeover-immediate" | |
| 10793 | - | version = "0.30.0" | |
| 10794 | - | ||
| 10795 | - | [[patch.unused]] | |
| 10796 | - | name = "makeover-tui" | |
| 10797 | - | version = "0.30.0" | |
| 10798 | - | ||
| 10799 | - | [[patch.unused]] | |
| 10800 | - | name = "quasi-immediate" | |
| 10801 | - | version = "0.29.1" | |
| 10802 | - | ||
| 10803 | - | [[patch.unused]] | |
| 10804 | - | name = "quasi-store" | |
| 10805 | - | version = "0.1.0" | |
| 10806 | - | ||
| 10807 | - | [[patch.unused]] | |
| 10808 | - | name = "quasi-tauri" | |
| 10809 | - | version = "0.29.1" |
| @@ -158,7 +158,7 @@ | |||
| 158 | 158 | # through quasi beside its Askama equivalent, so the cost of describing a screen | |
| 159 | 159 | # here is measured rather than argued. Not load-bearing for any shipped route. | |
| 160 | 160 | # See wiki look-wave-2, tier G. | |
| 161 | - | quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.29" } | |
| 161 | + | quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.30" } | |
| 162 | 162 | # The description vocabulary quasi's screen types are built from. Pinned here | |
| 163 | 163 | # rather than reached through quasi-router's re-export because a described | |
| 164 | 164 | # screen names FieldKind and Tone directly; it has to track what quasi-router | |
| @@ -167,13 +167,13 @@ | |||
| 167 | 167 | # For the request head the per-viewer state factory reads. axum re-exports it, | |
| 168 | 168 | # but the factory's signature is quasi-axum's and names `http::request::Parts`. | |
| 169 | 169 | http = "1.3.1" | |
| 170 | - | quasi-axum = { git = "https://makenot.work/git/max/quasi.git", version = "0.29" } | |
| 171 | - | quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.29" } | |
| 170 | + | quasi-axum = { git = "https://makenot.work/git/max/quasi.git", version = "0.30" } | |
| 171 | + | quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.30" } | |
| 172 | 172 | # The first-party widget set. The carousel was proved here against three pages | |
| 173 | 173 | # and then moved there; what is left in `quasi/widgets/carousel.rs` is the | |
| 174 | 174 | # Askama glue. Not `makeover-basics`: a widget is an assembly of `Node`s, so it | |
| 175 | 175 | # sits above quasi-router rather than inside the description suite. | |
| 176 | - | quasi-basics = { git = "https://makenot.work/git/max/quasi.git", version = "0.29" } | |
| 176 | + | quasi-basics = { git = "https://makenot.work/git/max/quasi.git", version = "0.30" } | |
| 177 | 177 | ||
| 178 | 178 | # Git source browser | |
| 179 | 179 | gix = { version = "0.86", default-features = false, features = ["blame", "mailmap", "revision", "sha1", "max-performance-safe"] } |
| @@ -38,7 +38,7 @@ | |||
| 38 | 38 | use makeover_layout as layout; | |
| 39 | 39 | use quasi_router::screen::{Cell, Cells, Column}; | |
| 40 | 40 | use quasi_router::{Action, Node, RegionKind, Request, Response, RouteError, Slot}; | |
| 41 | - | use quasi_webview::{Shell, Webview}; | |
| 41 | + | use quasi_webview::Webview; | |
| 42 | 42 | ||
| 43 | 43 | use super::Viewer; | |
| 44 | 44 | use crate::db; | |
| @@ -173,8 +173,8 @@ | |||
| 173 | 173 | } | |
| 174 | 174 | ||
| 175 | 175 | /// The renderer this screen is drawn with. | |
| 176 | - | pub fn renderer(_viewer: &Viewer) -> Webview { | |
| 177 | - | Webview::new().with_shell(Shell::under("/static").layered(["base", "components", "responsive"])) | |
| 176 | + | pub fn renderer(viewer: &Viewer) -> Webview { | |
| 177 | + | Webview::new().with_shell(viewer.shell()) | |
| 178 | 178 | } | |
| 179 | 179 | ||
| 180 | 180 | #[cfg(test)] |
| @@ -34,7 +34,7 @@ | |||
| 34 | 34 | use makeover_layout as layout; | |
| 35 | 35 | use quasi_router::screen::{Act, Cell, Cells, Column, Tag}; | |
| 36 | 36 | use quasi_router::{Action, Node, RegionKind, Request, Response, RouteError, Slot}; | |
| 37 | - | use quasi_webview::{Shell, Webview}; | |
| 37 | + | use quasi_webview::Webview; | |
| 38 | 38 | ||
| 39 | 39 | use super::Viewer; | |
| 40 | 40 | ||
| @@ -245,8 +245,8 @@ | |||
| 245 | 245 | } | |
| 246 | 246 | ||
| 247 | 247 | /// The renderer both screens are drawn with. | |
| 248 | - | pub fn renderer(_viewer: &Viewer) -> Webview { | |
| 249 | - | Webview::new().with_shell(Shell::under("/static").layered(["base", "components", "responsive"])) | |
| 248 | + | pub fn renderer(viewer: &Viewer) -> Webview { | |
| 249 | + | Webview::new().with_shell(viewer.shell()) | |
| 250 | 250 | } | |
| 251 | 251 | ||
| 252 | 252 | #[cfg(test)] |
| @@ -30,7 +30,7 @@ | |||
| 30 | 30 | use makeover_layout as layout; | |
| 31 | 31 | use quasi_router::screen::{Act, Cell, Cells, Column}; | |
| 32 | 32 | use quasi_router::{Action, Method, Node, RegionKind, Request, Response, RouteError, Slot}; | |
| 33 | - | use quasi_webview::{Shell, Webview}; | |
| 33 | + | use quasi_webview::Webview; | |
| 34 | 34 | ||
| 35 | 35 | use super::Viewer; | |
| 36 | 36 | use crate::db; | |
| @@ -266,8 +266,8 @@ | |||
| 266 | 266 | } | |
| 267 | 267 | ||
| 268 | 268 | /// The renderer this screen is drawn with. | |
| 269 | - | pub fn renderer(_viewer: &Viewer) -> Webview { | |
| 270 | - | Webview::new().with_shell(Shell::under("/static").layered(["base", "components", "responsive"])) | |
| 269 | + | pub fn renderer(viewer: &Viewer) -> Webview { | |
| 270 | + | Webview::new().with_shell(viewer.shell()) | |
| 271 | 271 | } | |
| 272 | 272 | ||
| 273 | 273 | #[cfg(test)] |
| @@ -55,6 +55,14 @@ | |||
| 55 | 55 | /// handler: `Handle::current` works on a blocking thread today, and | |
| 56 | 56 | /// depending on that is depending on where quasi-axum happens to dispatch. | |
| 57 | 57 | pub runtime: Handle, | |
| 58 | + | /// This session's CSRF token, for the shell to hand to the document. | |
| 59 | + | /// | |
| 60 | + | /// Resolved in the factory rather than in a renderer because minting one is | |
| 61 | + | /// an async session write and a renderer is sync. It is the same token the | |
| 62 | + | /// Askama pages carry: [`crate::csrf::get_or_create_token`] is | |
| 63 | + | /// get-or-create, so a described page and a templated one in the same | |
| 64 | + | /// session agree, and validation is one comparison either way. | |
| 65 | + | pub csrf: String, | |
| 58 | 66 | /// Markup for the bespoke regions this request's screen describes. | |
| 59 | 67 | /// | |
| 60 | 68 | /// The seam between a handler and its renderer, and the reason it has to be | |
| @@ -95,6 +103,27 @@ | |||
| 95 | 103 | } | |
| 96 | 104 | } | |
| 97 | 105 | ||
| 106 | + | /// The shell every described screen is drawn in. | |
| 107 | + | /// | |
| 108 | + | /// Here rather than in each screen's `renderer` because of what it carries: | |
| 109 | + | /// a described page whose shell does not declare the session token has | |
| 110 | + | /// every write on it refused, and the five screens that wrote | |
| 111 | + | /// `Shell::under("/static")` out by hand were five chances to forget. A | |
| 112 | + | /// screen that wants more says so on top of this; a screen that says | |
| 113 | + | /// nothing gets the token anyway. | |
| 114 | + | /// | |
| 115 | + | /// The layer order matches `crate::shell`, which is the Askama half of the | |
| 116 | + | /// same document: `makeover` is prepended by the renderer, and the site's | |
| 117 | + | /// own sheets live in `components`. | |
| 118 | + | #[must_use] | |
| 119 | + | pub fn shell(&self) -> quasi_webview::Shell { | |
| 120 | + | quasi_webview::Shell::under("/static") | |
| 121 | + | .layered(["base", "components", "responsive"]) | |
| 122 | + | // Every described write is an htmx request, and htmx inherits this | |
| 123 | + | // from the body, so one declaration covers the whole document. | |
| 124 | + | .sending("X-CSRF-Token", &self.csrf) | |
| 125 | + | } | |
| 126 | + | ||
| 98 | 127 | /// Everything the handler drew, for the renderer to mount. | |
| 99 | 128 | pub fn drawn(&self) -> std::collections::HashMap<String, String> { | |
| 100 | 129 | self.fills.lock().map(|f| f.clone()).unwrap_or_default() | |
| @@ -123,12 +152,21 @@ | |||
| 123 | 152 | return Err(quasi_router::RouteError::internal("no session layer")); | |
| 124 | 153 | }; | |
| 125 | 154 | match crate::auth::authenticate(&session, &app).await { | |
| 126 | - | Ok(user) => Ok(Viewer { | |
| 127 | - | app, | |
| 128 | - | user, | |
| 129 | - | runtime, | |
| 130 | - | fills: std::sync::Mutex::default(), | |
| 131 | - | }), | |
| 155 | + | Ok(user) => { | |
| 156 | + | // Before the handler runs, because the write that mints a | |
| 157 | + | // token has to finish on the session this request holds. | |
| 158 | + | // A failure here is the session store, not the reader. | |
| 159 | + | let csrf = crate::csrf::get_or_create_token(&session) | |
| 160 | + | .await | |
| 161 | + | .map_err(|_| quasi_router::RouteError::internal("csrf token"))?; | |
| 162 | + | Ok(Viewer { | |
| 163 | + | app, | |
| 164 | + | user, | |
| 165 | + | runtime, | |
| 166 | + | csrf, | |
| 167 | + | fills: std::sync::Mutex::default(), | |
| 168 | + | }) | |
| 169 | + | } | |
| 132 | 170 | Err(_) => Err(quasi_router::RouteError::denied("sign in to continue")), | |
| 133 | 171 | } | |
| 134 | 172 | }) |
| @@ -43,7 +43,7 @@ | |||
| 43 | 43 | use makeover_layout as layout; | |
| 44 | 44 | use quasi_router::screen::{Act, Cell, Cells, Choice, Column, Field}; | |
| 45 | 45 | use quasi_router::{Action, Method, Node, RegionKind, Request, Response, RouteError, Slot}; | |
| 46 | - | use quasi_webview::{Shell, Webview}; | |
| 46 | + | use quasi_webview::Webview; | |
| 47 | 47 | ||
| 48 | 48 | use super::Viewer; | |
| 49 | 49 | use crate::db; | |
| @@ -399,11 +399,11 @@ | |||
| 399 | 399 | /// Per request because `Adapter::per_viewer` builds one per request, and this | |
| 400 | 400 | /// screen has nothing viewer-specific to say to it yet. It will when S2 puts the | |
| 401 | 401 | /// site chrome here. | |
| 402 | - | pub fn renderer(_viewer: &Viewer) -> Webview { | |
| 402 | + | pub fn renderer(viewer: &Viewer) -> Webview { | |
| 403 | 403 | // The fragment path never emits a document, so the shell's asset paths do | |
| 404 | 404 | // not arise here. It is still the one the rest of the site uses, so a screen | |
| 405 | 405 | // that later answers as a whole page cannot disagree with `crate::shell`. | |
| 406 | - | Webview::new().with_shell(Shell::under("/static").layered(["base", "components", "responsive"])) | |
| 406 | + | Webview::new().with_shell(viewer.shell()) | |
| 407 | 407 | } | |
| 408 | 408 | ||
| 409 | 409 | #[cfg(test)] |
| @@ -47,7 +47,7 @@ | |||
| 47 | 47 | use makeover_layout as layout; | |
| 48 | 48 | use quasi_router::screen::{Cell, Cells, Column, Figure, Row, Tag}; | |
| 49 | 49 | use quasi_router::{Action, Node, RegionKind, Request, Response, RouteError, Slot}; | |
| 50 | - | use quasi_webview::{Shell, Webview}; | |
| 50 | + | use quasi_webview::Webview; | |
| 51 | 51 | ||
| 52 | 52 | use super::Viewer; | |
| 53 | 53 | use crate::db; | |
| @@ -446,11 +446,7 @@ | |||
| 446 | 446 | /// Mounts whatever the handler drew for its bespoke regions. The two see one | |
| 447 | 447 | /// `Viewer`; see [`super::Viewer::fills`]. | |
| 448 | 448 | pub fn renderer(viewer: &Viewer) -> Webview { | |
| 449 | - | let mut webview = Webview::new().with_shell(Shell::under("/static").layered([ | |
| 450 | - | "base", | |
| 451 | - | "components", | |
| 452 | - | "responsive", | |
| 453 | - | ])); | |
| 449 | + | let mut webview = Webview::new().with_shell(viewer.shell()); | |
| 454 | 450 | for (slot, markup) in viewer.drawn() { | |
| 455 | 451 | webview = webview.with_fill(slot, markup); | |
| 456 | 452 | } |
| @@ -349,3 +349,105 @@ | |||
| 349 | 349 | resp.status | |
| 350 | 350 | ); | |
| 351 | 351 | } | |
| 352 | + | ||
| 353 | + | /// A described write SUCCEEDS with the token the document around it carries. | |
| 354 | + | /// | |
| 355 | + | /// The other half, and the one that was missing. The test above asserts the | |
| 356 | + | /// enforcement works; nothing asserted a described write can be completed at | |
| 357 | + | /// all, so a surface where every write answered 403 would have read as covered. | |
| 358 | + | /// | |
| 359 | + | /// # Where the token comes from, measured 2026-08-18 | |
| 360 | + | /// | |
| 361 | + | /// Every described screen answers a `Response::fragment`, so it is the inside of | |
| 362 | + | /// an element in an Askama document, and that document is what carries the | |
| 363 | + | /// token: `base.html` emits `<meta name="csrf-token">` and the core module | |
| 364 | + | /// attaches it to every htmx request on the page. So this walks the browser's | |
| 365 | + | /// path exactly -- read the page, take the meta, use it on the write -- with the | |
| 366 | + | /// test doing by hand what `htmx-glue.ts` does on `htmx:configRequest`. | |
| 367 | + | /// | |
| 368 | + | /// The document-owning case is the other half and no screen exercises it yet: | |
| 369 | + | /// see `a_described_screen_never_builds_its_own_shell` below and | |
| 370 | + | /// `Viewer::shell`. | |
| 371 | + | #[tokio::test] | |
| 372 | + | async fn a_described_write_accepts_the_token_the_page_around_it_carries() { | |
| 373 | + | let mut h = TestHarness::build(crate::harness::BuildOptions { | |
| 374 | + | quasi_screens: makenotwork::config::QuasiScreens::parse("library_contacts"), | |
| 375 | + | ..Default::default() | |
| 376 | + | }) | |
| 377 | + | .await; | |
| 378 | + | h.signup("creator", "creator@example.com", "password123") | |
| 379 | + | .await; | |
| 380 | + | h.login("creator", "password123").await; | |
| 381 | + | ||
| 382 | + | // The document the tab lives in, not the tab: the tab is a fragment and a | |
| 383 | + | // fragment carries no head. | |
| 384 | + | let page = h.client.get("/library").await; | |
| 385 | + | assert_eq!(page.status, 200, "the library page did not render"); | |
| 386 | + | let token = meta_csrf_token(&page.text) | |
| 387 | + | .expect("the page around a described screen carries no csrf-token meta"); | |
| 388 | + | ||
| 389 | + | let resp = h | |
| 390 | + | .client | |
| 391 | + | .request_with_headers( | |
| 392 | + | "DELETE", | |
| 393 | + | "/library/tabs/contacts/revoke/00000000-0000-0000-0000-000000000000", | |
| 394 | + | None, | |
| 395 | + | &[("X-CSRF-Token", token.as_str()), ("HX-Request", "true")], | |
| 396 | + | ) | |
| 397 | + | .await; | |
| 398 | + | ||
| 399 | + | // What the write answers about a contact that does not exist is the | |
| 400 | + | // handler's business. This asserts only that it reached the handler. | |
| 401 | + | assert_ne!( | |
| 402 | + | resp.status, 403, | |
| 403 | + | "a described write refused the token its own document supplied" | |
| 404 | + | ); | |
| 405 | + | } | |
| 406 | + | ||
| 407 | + | /// The token out of `<meta name="csrf-token">`, the way the glue reads it. | |
| 408 | + | fn meta_csrf_token(html: &str) -> Option<String> { | |
| 409 | + | let at = html.split("name=\"csrf-token\"").nth(1)?; | |
| 410 | + | let at = at.split("content=\"").nth(1)?; | |
| 411 | + | Some(at.split('"').next()?.to_owned()) | |
| 412 | + | } | |
| 413 | + | ||
| 414 | + | /// No described screen builds its own shell. | |
| 415 | + | /// | |
| 416 | + | /// The failure this forbids is silent and one line long: a screen that writes | |
| 417 | + | /// `Shell::under("/static")` by hand gets a document with no session token in | |
| 418 | + | /// it, and every write on that screen answers 403 the moment the screen owns | |
| 419 | + | /// its document rather than answering into an Askama page. Five screens wrote | |
| 420 | + | /// that line before `Viewer::shell` existed, which was five chances to forget. | |
| 421 | + | /// | |
| 422 | + | /// Source-level rather than behavioural because there is nothing to observe | |
| 423 | + | /// yet: every described screen answers a fragment today, and a fragment has no | |
| 424 | + | /// `<body>` to carry anything. The test that would catch it is the one that | |
| 425 | + | /// cannot be written until the mistake ships. | |
| 426 | + | #[test] | |
| 427 | + | fn a_described_screen_never_builds_its_own_shell() { | |
| 428 | + | for (name, source) in [ | |
| 429 | + | ("ssh_keys", include_str!("../../src/quasi/ssh_keys.rs")), | |
| 430 | + | ( | |
| 431 | + | "library_contacts", | |
| 432 | + | include_str!("../../src/quasi/library_contacts.rs"), | |
| 433 | + | ), | |
| 434 | + | ( | |
| 435 | + | "buyer_contacts", | |
| 436 | + | include_str!("../../src/quasi/buyer_contacts.rs"), | |
| 437 | + | ), | |
| 438 | + | ( | |
| 439 | + | "user_analytics", | |
| 440 | + | include_str!("../../src/quasi/user_analytics.rs"), | |
| 441 | + | ), | |
| 442 | + | ( | |
| 443 | + | "forum_memberships", | |
| 444 | + | include_str!("../../src/quasi/forum_memberships.rs"), | |
| 445 | + | ), | |
| 446 | + | ] { | |
| 447 | + | assert!( | |
| 448 | + | !source.contains("Shell::under"), | |
| 449 | + | "{name} builds its own shell: use `viewer.shell()`, which carries \ | |
| 450 | + | the session token every write on a described document needs" | |
| 451 | + | ); | |
| 452 | + | } | |
| 453 | + | } |