Something went wrong. Try again.
atproto git client
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403//! The pages the browser lands on when a login finishes.//!//! `atgc auth login` runs a loopback server for exactly one request. What it//! serves back is the only part of atgc a person sees rendered rather than//! printed, and it is HTML and SVG rather than Rust.//!//! Everything interpolated into a page goes through [`super::escape`] first.//! The success page carries a handle, a display name and an avatar URL that//! came from a profile record somebody else controls.//!//! Serving these is not here — see [`super`] on where the line falls.
use super::brand::{self, FIELD_CSS, HEADER_CSS, Scheme};use super::escape;use crate::clients::atproto::did::Identity;
/// The page the browser lands on once the grant is in: who you signed in as,/// on the same field of DNA `atgc about` draws, prerendered by/// [`super::field::field`] and hung behind the card.////// The art is a `<pre>` of some nineteen thousand cells, which is most of/// this page's fifty-odd kilobytes and all of it goes over loopback once,/// to a browser that has already been opened. Everything animated is a/// property the compositor can handle on its own — an opacity breath on the/// field and a light band drifting along the axis of the helices — so nothing/// here relayouts, and `prefers-reduced-motion` turns both off.pub(crate) fn success_page(who: &Identity, did: &str) -> String { let field = brand::field_markup(); let favicon = brand::favicon_link(); let theme = brand::theme_css(Scheme::System); let avatar = who .avatar .as_deref() // The avatar is fetched by the browser — from Bluesky's CDN, or from // the account's own PDS for the accounts that appview has never // indexed — and this page has to look right whether or not that host // answers: a profile whose picture 404s should lose the picture, not // gain a broken-image glyph in the middle of the card. .map(|url| { format!( r#"<img class="avatar" src="{}" alt="" onerror="this.remove()">"#, escape(url) ) }) .unwrap_or_default(); let name = escape( who.display_name .as_deref() .unwrap_or_else(|| who.handle.as_deref().unwrap_or(did)), ); // Linked to the account's profile rather than left as inert text — the // one piece of this card most worth clicking through to, and bsky.app // resolves a profile by handle or DID for any atproto account, not just // ones with a Bluesky-specific presence. let handle = format!( r#"<a href="https://bsky.app/profile/{}" target="_blank" rel="noopener noreferrer">{}</a>"#, escape(who.handle.as_deref().unwrap_or(did)), escape(&who.display()) ); let did = escape(did); // The scheme is dropped for display; a PDS the browser doesn't already // trust doesn't gain anything from looking clickable, and "via // bsky.social" reads as the confirmation it is rather than a URL. let pds = who .pds .as_deref() .map(|p| { p.trim_start_matches("https://") .trim_start_matches("http://") }) .map(|p| format!(r#"<div class="pds">via {}</div>"#, escape(p))) .unwrap_or_default(); let header = brand::header_markup(); format!( r#"<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>atgc: logged in</title>{favicon}<style>{theme}{FIELD_CSS}{HEADER_CSS} body {{ font-family: system-ui, sans-serif; display: flex; min-height: 100vh; margin: 0; align-items: center; justify-content: center; background: var(--bg); color: var(--fg); }}
.card {{ position: relative; z-index: 1; background: var(--card); border-radius: 14px; padding: 2.5rem 3rem; text-align: center; box-shadow: 0 4px 24px rgba(0,0,0,.10), 0 0 0 1px var(--edge), 0 0 90px 40px var(--halo); }} .brand {{ justify-content: center; margin-bottom: 1.5rem; padding-bottom: 1.25rem; }} .avatar {{ width: 72px; height: 72px; border-radius: 50%; }} h1 {{ font-size: 1.2rem; margin: .75rem 0 .25rem; }} .handle {{ display: block; font-weight: 600; }} .handle a, .credit a {{ color: inherit; text-decoration: none; }} .handle a:hover, .credit a:hover {{ text-decoration: underline; }} .did {{ color: var(--muted); font-size: .8rem; font-family: monospace; }} .pds {{ color: var(--muted); font-size: .75rem; margin-top: .3rem; }} p {{ margin-top: 1.5rem; }} .credit {{ margin-top: .5rem; font-size: .72rem; color: var(--muted); }} @media (max-width: 480px) {{ .card {{ padding: 2rem 1.5rem; }} }}</style></head><body> {field} <div class="card"> {header} {avatar} <h1>{name}</h1> <div class="handle">{handle}</div> <div class="did">{did}</div> {pds} <p>You're logged in to <strong>atgc</strong>. You can close this tab.</p> {CREDIT_LINE} </div></body></html>"# )}
/// Static, unlike everything above it on the card: the account signing in/// is never the account that built atgc, so this has no business coming/// from [`Identity`]. `permadeath.com` is already the identity every commit/// on this branch is attributed to (see `.git/config`), so naming it again/// here is not a new disclosure.const CREDIT_LINE: &str = r#"<p class="credit">Built by <a href="https://bsky.app/profile/permadeath.com" target="_blank" rel="noopener noreferrer">@permadeath.com</a> · <a href="https://tangled.org/permadeath.com/atgc" target="_blank" rel="noopener noreferrer">source on Tangled</a></p>"#;
/// The page the browser lands on when the login did not finish: what went/// wrong, in the window the person is looking at.////// `auth login` is the one command whose output is split across two screens,/// and the browser holds the attention at the moment it ends. This page used/// to be a fixed "Login failed — return to the terminal for details", which/// is a second lookup for something atgc already knows, and it was worse than/// that in the case that rewrote it: a panic inside the token exchange took/// the process down before anything could be written, so the terminal held a/// Rust backtrace and the browser held a connection error.////// `detail` is a failure as Rust worded it — atgc's own error chain, or a/// panic's location and payload — so it is prose from a dependency at worst/// and goes through [`super::escape`] like every other interpolation here./// Nothing about the session reaches this page: there is no session, which/// is the point.pub(crate) fn failure_page(detail: &str) -> String { let detail = escape(detail); let favicon = brand::favicon_link(); let theme = brand::theme_css(Scheme::System); let header = brand::header_markup(); format!( r#"<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>atgc: login failed</title>{favicon}<style>{theme}{HEADER_CSS} body {{ font-family: system-ui, sans-serif; display: flex; min-height: 100vh; margin: 0; align-items: center; justify-content: center; padding: 1.5rem; box-sizing: border-box; background: var(--bg); color: var(--fg); }} .card {{ background: var(--card); border-radius: 14px; padding: 2rem 2.25rem; max-width: 46rem; box-shadow: 0 4px 24px rgba(0,0,0,.10), 0 0 0 1px var(--edge); }} .brand {{ margin-bottom: 1.25rem; padding-bottom: 1rem; }} h1 {{ font-size: 1.2rem; margin: 0 0 .85rem; }} /* The error is the page. It wraps rather than scrolls sideways, because a panic's payload is one long line and half of it off-screen is the half with the reason in it. */ pre {{ margin: 0; padding: .85rem 1rem; border-radius: 8px; background: var(--bg); border: 1px solid var(--edge); font-size: .82rem; line-height: 1.5; font-family: ui-monospace, SFMono-Regular, Menlo, "DejaVu Sans Mono", monospace; white-space: pre-wrap; overflow-wrap: anywhere; }} p {{ color: var(--muted); font-size: .85rem; margin: 1rem 0 0; }} /* The same credit the success page carries, styled the same way: a link that reads as text until it is hovered. On this page it is also where to take a bug, which is why it is worth keeping on the bad day too. */ .credit {{ font-size: .72rem; margin-top: .5rem; }} .credit a {{ color: inherit; text-decoration: none; }} .credit a:hover {{ text-decoration: underline; }} @media (max-width: 480px) {{ .card {{ padding: 1.5rem 1.25rem; }} }}</style></head><body> <div class="card"> {header} <h1>Login failed</h1> <pre>{detail}</pre> <p>No session was saved. atgc printed the same thing and exited 1: you can close this tab.</p> {CREDIT_LINE} </div></body></html>"# )}
#[cfg(test)]mod tests { use super::{CREDIT_LINE, failure_page, success_page}; use crate::clients::atproto::did::Identity; use crate::html::brand::{MARK_SVG, SITE};
fn someone() -> Identity { Identity { handle: Some("permadeath.com".into()), display_name: Some("permadeath".into()), avatar: Some("https://cdn.bsky.app/img/avatar/plain/did:plc:abc/x@jpeg".into()), pds: Some("https://bsky.social".into()), } } /// Who you signed in as, on the field of DNA, with the avatar the profile /// gave. The field is the prerendering: it has to arrive in the page as /// `about` drew it, not as something the page rebuilds. #[test] fn the_page_shows_the_account_on_the_field() { let page = success_page(&someone(), "did:plc:abc"); assert!(page.contains(&crate::html::field::field()), "no field"); assert!(page.contains("@permadeath.com") && page.contains("did:plc:abc")); assert!( page.contains(r#"<img class="avatar" src="https://cdn.bsky.app/"#), "the avatar was dropped" ); // The scheme is dropped: "via bsky.social" reads as a confirmation, // not a link to click. assert!(page.contains("via bsky.social"), "the PDS was dropped"); assert!(!page.contains("via https://"), "the scheme leaked through"); // The handle is the one thing on this card actually worth clicking // through on, linked by handle rather than DID since that's what // resolves to a readable bsky.app profile. assert!( page.contains(r#"<a href="https://bsky.app/profile/permadeath.com" target="_blank" rel="noopener noreferrer">@permadeath.com</a>"#), "the handle was not linked" ); // Both animations are declared, and both are given up under // `prefers-reduced-motion` rather than only slowed down. assert!(page.contains("@keyframes breathe") && page.contains("@keyframes drift")); assert!(page.contains("prefers-reduced-motion")); } /// The card leads with the mark from `brand/` and the running version, /// above the account it already showed — a header, not a redesign, so /// this only ever adds an assertion to `the_page_shows_the_account_on_the_field` /// rather than replacing one. Drawn from the page's own `--a`/`--t`/`--g`/ /// `--c`/`--bond` custom properties rather than a second, hardcoded /// palette that could drift from the one `:root` and its dark-scheme /// override already declare. #[test] fn the_card_leads_with_the_brand_mark_and_version() { let page = success_page(&someone(), "did:plc:abc"); assert!(page.contains(MARK_SVG), "no mark"); for var in [ "var(--a)", "var(--t)", "var(--g)", "var(--c)", "var(--bond)", ] { assert!(MARK_SVG.contains(var), "mark does not use {var}"); } assert!(!MARK_SVG.contains('#'), "mark hardcodes a color"); assert!( page.contains(&format!("v{}", env!("CARGO_PKG_VERSION"))), "no version" ); }
/// The mark and the wordmark are one link to the project's page, on both /// pages, and the version is left outside it: a person who has just met /// atgc through a browser window their own command opened is exactly who /// the site is for, and the header is where anybody already looks for it. /// It opens in a new tab, since closing this one is what both pages say /// to do next. #[test] fn the_header_links_to_the_project_s_page() { let link = format!( r#"<a class="brand-link" href="{SITE}" target="_blank" rel="noopener noreferrer">"# ); for page in [ success_page(&someone(), "did:plc:abc"), failure_page("something went wrong"), ] { assert!(page.contains(&link), "the header is not a link to {SITE}"); assert!( page.contains("https://atgc.codes"), "the link does not go to the site" ); // Inside the link, the mark and the name; outside it, the // version, which is a fact about this binary and not the mark. let (linked, rest) = page.split_once("</a>").expect("the header link closes"); let (_, linked) = linked.split_once(&link).expect("the link opens"); assert!(linked.contains(MARK_SVG), "the mark is outside the link"); assert!( linked.contains(r#"<span class="brand-name">atgc</span>"#), "the wordmark is outside the link" ); assert!( rest.contains(&format!( r#"<span class="brand-version">v{}</span>"#, env!("CARGO_PKG_VERSION") )), "the version is inside the link" ); } } /// Static credit, unlike everything else on the card: it names who /// built atgc, not who is signed into it, so it does not move when the /// account does. #[test] fn the_card_credits_its_author_and_the_repo() { let page = success_page(&someone(), "did:plc:abc"); assert!(page.contains(CREDIT_LINE), "no credit line"); assert!( CREDIT_LINE.contains(r#"href="https://bsky.app/profile/permadeath.com""#), "author not linked to bsky" ); assert!( CREDIT_LINE.contains(r#"href="https://tangled.org/permadeath.com/atgc""#), "repo not linked to tangled" ); } /// A handle can be unresolvable — a DID document with no `alsoKnownAs` /// entry the Bluesky profile lookup also missed — and the card still /// says "(unknown handle)" per [`Identity::display`]. The link has to /// go somewhere that isn't a 404, so it falls back to the DID, which /// bsky.app resolves the same as a handle. #[test] fn an_unresolved_handle_links_by_did_instead() { let anonymous = Identity { handle: None, display_name: None, avatar: None, pds: None, }; let page = success_page(&anonymous, "did:plc:abc"); assert!( page.contains(r#"<a href="https://bsky.app/profile/did:plc:abc" target="_blank" rel="noopener noreferrer">(unknown handle)</a>"#), "no DID fallback link" ); } /// A display name is whatever the account typed into its Bluesky profile, /// so it reaches this page as somebody else's markup unless it is escaped. /// The page is served on loopback by a process holding fresh tokens; a /// `<script>` in a profile has no business running there. #[test] fn a_profile_cannot_smuggle_markup_into_the_page() { let hostile = Identity { handle: Some("permadeath.com".into()), display_name: Some("<script>alert(1)</script>".into()), avatar: Some(r#"https://x/" onload="alert(1)"#.into()), pds: Some("https://<script>alert(2)</script>".into()), }; let page = success_page(&hostile, "did:plc:abc"); assert!(!page.contains("<script>"), "an element got through"); assert!(page.contains("<script>alert(1)</script>")); assert!(page.contains("<script>alert(2)</script>")); // The quote that would have ended the src attribute is escaped, so // the handler never becomes one. assert!(!page.contains(r#"" onload=""#), "attribute breakout"); assert!(page.contains("" onload="")); // What `escape` does to each character on its own is // `super::super`'s test now that the function lives there. What is // asserted here is the part that is this page's: that every field // interpolated into it went through that function. }
/// The failure the terminal got is the failure the browser gets. The /// page's whole reason to exist is that this text is on it, so it is /// asserted verbatim, panic location included — that line is the one /// worth quoting in a bug report. #[test] fn the_failure_page_carries_the_failure() { let page = failure_page( "panicked at vendor/jacquard-oauth/src/client.rs:352:26:\n\ Failed to parse scopes from token response", ); assert!(page.contains("vendor/jacquard-oauth/src/client.rs:352:26")); assert!(page.contains("Failed to parse scopes from token response")); // Same header as the page it stands in for, and the same exit status // named as the terminal's. assert!(page.contains(MARK_SVG), "no mark"); assert!(page.contains(&format!("v{}", env!("CARGO_PKG_VERSION")))); assert!(page.contains("exited 1")); // It used to send people back to a terminal that, in the case this // page was rewritten for, had a backtrace on it and nothing else. assert!(!page.contains("Return to the terminal for details")); }
/// A failure's text is a Rust error message, which can carry a scope /// string, a URL or a server's own prose — all of it somebody else's /// bytes on a page served over loopback by a process that was moments /// ago holding tokens. #[test] fn a_failure_message_cannot_smuggle_markup_into_the_page() { let page = failure_page("invalid scope <script>alert(1)</script>"); assert!(!page.contains("<script>"), "an element got through"); assert!(page.contains("<script>alert(1)</script>")); }}