Extract the wayBetter framework core from wayBetter for X

Site-agnostic parts of the X app, behind a small site adapter:

- crates/waybetter-host: webview-label security policy and tab rect
  clamp, account profiles with native-owned accounts (AccountRules),
  plugin store with bundled-plugin upgrades (PluginHost), signed
  data-only hotfix channel, update scheduling and crash handshake, tab
  payloads/link routing/tab-chrome validation, and (feature "tauri")
  webview helpers and the Windows system menu.
- packages/runtime: DOM hub, request hub, plugin API core, OKLab theme
  color model, split-view rail hiding, link routing and account
  reporting, each configured by a SiteAdapter.
- packages/shell: address bar, pane, link-drop, profile, palette and
  update-chip models, and window controls with the Tauri API injected.

Tests moved/adapted from the app: 45 Rust (headless), 40 TypeScript.
This commit is contained in:
CRTkafa 2026-09-28 03:01:33 +07:00
commit 43a9e2e1a7
48 changed files with 9768 additions and 0 deletions

View file

@ -0,0 +1,368 @@
//! Account profiles: each site tab runs in one profile, and every non-default profile has its own
//! WebView data directory (cookies, storage, cache), so several accounts can be signed in side by
//! side without sharing a session. The default profile keeps the WebView's default data directory.
//!
//! The account inside a profile is owned by native code: only a site tab of that very profile can
//! report it ([`ProfileSettings::observe`]), validated with the site's [`AccountRules`], and a save
//! from the shell never changes it ([`ProfileSettings::keep_accounts_from`]).
use crate::site::AccountRules;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
pub const DEFAULT_PROFILE: &str = "default";
pub const PROFILES_DIR: &str = "profiles";
const MAX_PROFILES: usize = 500;
const MAX_NAME_CHARS: usize = 40;
/// The account signed in inside a profile, as the site itself shows it (read by the page runtime
/// in that profile's tabs and reported through `tab_report`). Kept after a sign-out
/// (`signed_in: false`) so the list still says whose login it was.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Account {
/// Without the `@`.
pub handle: String,
pub name: String,
/// On the site's image CDN only (see [`AccountRules::avatar_prefixes`]).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub avatar: Option<String>,
#[serde(default)]
pub signed_in: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Profile {
pub id: String,
pub name: String,
/// `#rrggbb`, shown as the tab's profile dot.
pub color: String,
/// Owned by native code (see `ProfileSettings::observe`); a save from the shell never changes it.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account: Option<Account>,
}
/// What a site tab saw about its own profile's login.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AccountSeen {
SignedIn(Account),
SignedOut,
}
impl Account {
/// Validated and normalized, or None when the handle is not one the site allows.
pub fn validated(mut self, rules: &AccountRules) -> Option<Self> {
let handle = self.handle.trim().trim_start_matches('@').to_owned();
if !(rules.valid_handle)(&handle) {
return None;
}
self.handle = handle;
let name: String = self.name.chars().filter(|ch| !ch.is_control()).collect();
let name: String = name.trim().chars().take(rules.max_name_chars).collect();
self.name = if name.is_empty() { self.handle.clone() } else { name };
self.avatar = self.avatar.filter(|url| rules.avatar_allowed(url));
Some(self)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProfileSettings {
pub profiles: Vec<Profile>,
/// New tabs open in this profile.
pub active: String,
}
/// The first profile's name and color (the shell's first palette color).
pub const DEFAULT_NAME: &str = "Main";
pub const DEFAULT_COLOR: &str = "#1d9bf0";
impl Default for ProfileSettings {
fn default() -> Self {
Self {
profiles: vec![Profile { id: DEFAULT_PROFILE.into(), name: DEFAULT_NAME.into(), color: DEFAULT_COLOR.into(), account: None }],
active: DEFAULT_PROFILE.into(),
}
}
}
/// `default`, or `p` followed by 1-32 lowercase ASCII letters/digits. Ids become directory names,
/// so nothing else (no dots, slashes, separators or reserved names) is accepted.
pub fn valid_profile_id(id: &str) -> bool {
id == DEFAULT_PROFILE
|| (id.len() >= 2
&& id.len() <= 33
&& id.starts_with('p')
&& id.bytes().all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit()))
}
fn valid_color(color: &str) -> bool {
color.len() == 7 && color.starts_with('#') && color[1..].bytes().all(|ch| ch.is_ascii_hexdigit())
}
impl ProfileSettings {
pub fn validated(mut self, rules: &AccountRules) -> Result<Self, String> {
if self.profiles.is_empty() || self.profiles.len() > MAX_PROFILES {
return Err("VALIDATION: invalid profile count".into());
}
let mut seen = HashSet::new();
for profile in &mut self.profiles {
if !valid_profile_id(&profile.id) || !seen.insert(profile.id.clone()) {
return Err("VALIDATION: invalid or duplicate profile id".into());
}
let name = profile.name.trim();
if name.is_empty() || name.chars().count() > MAX_NAME_CHARS || name.chars().any(char::is_control) {
return Err("VALIDATION: profile name must be 1-40 visible characters".into());
}
profile.name = name.to_owned();
if !valid_color(&profile.color) {
return Err("VALIDATION: profile color must be #rrggbb".into());
}
profile.color = profile.color.to_ascii_lowercase();
profile.account = profile.account.take().and_then(|account| account.validated(rules));
}
if !seen.contains(DEFAULT_PROFILE) {
return Err("VALIDATION: the default profile cannot be removed".into());
}
if !seen.contains(&self.active) {
return Err("VALIDATION: active profile does not exist".into());
}
Ok(self)
}
/// Native code owns each profile's account: a save from the shell keeps the one on record.
pub fn keep_accounts_from(mut self, current: &ProfileSettings) -> Self {
for profile in &mut self.profiles {
profile.account = current.profiles.iter().find(|old| old.id == profile.id).and_then(|old| old.account.clone());
}
self
}
/// Records what a tab of `profile` saw. The profile takes the account's display name. Returns
/// whether anything changed, so repeated reports cost no disk write.
pub fn observe(&mut self, profile: &str, seen: AccountSeen, rules: &AccountRules) -> bool {
let Some(entry) = self.profiles.iter_mut().find(|entry| entry.id == profile) else { return false };
let before = entry.clone();
match seen {
AccountSeen::SignedIn(account) => {
// Narrow site layouts may show only the avatar: no name then means "unchanged".
let unnamed = account.name.trim().is_empty();
let Some(mut account) = account.validated(rules) else { return false };
account.signed_in = true;
if let Some(old) = entry.account.as_ref().filter(|old| unnamed && old.handle.eq_ignore_ascii_case(&account.handle)) {
account.name = old.name.clone();
}
// Some layouts show no avatar: keep the one seen before for the same login.
if account.avatar.is_none() {
if let Some(old) = entry.account.as_ref().filter(|old| old.handle.eq_ignore_ascii_case(&account.handle)) {
account.avatar = old.avatar.clone();
}
}
entry.name = account.name.chars().take(MAX_NAME_CHARS).collect();
entry.account = Some(account);
}
AccountSeen::SignedOut => {
if let Some(account) = entry.account.as_mut() {
account.signed_in = false;
}
}
}
*entry != before
}
pub fn contains(&self, id: &str) -> bool {
self.profiles.iter().any(|profile| profile.id == id)
}
/// Profiles present in `self` but not in `next`.
pub fn removed_in(&self, next: &ProfileSettings) -> Vec<String> {
self.profiles
.iter()
.filter(|profile| !next.contains(&profile.id))
.map(|profile| profile.id.clone())
.collect()
}
/// Removed profiles when switching to `next`, or an error if any of them still has an open tab.
pub fn removal<'a>(&self, next: &ProfileSettings, open_tabs: impl IntoIterator<Item = &'a String>) -> Result<Vec<String>, String> {
let removed = self.removed_in(next);
if open_tabs.into_iter().any(|profile| removed.contains(profile)) {
return Err("PROFILE: close this profile's tabs before removing it".into());
}
Ok(removed)
}
}
/// The WebView data directory of a profile under the app-local data dir; `None` for the default
/// profile (it keeps the WebView's existing default directory) and for invalid ids.
pub fn data_dir(local_data: &Path, id: &str) -> Option<PathBuf> {
if id == DEFAULT_PROFILE || !valid_profile_id(id) {
return None;
}
Some(local_data.join(PROFILES_DIR).join(id))
}
/// Folders under `<local_data>/profiles` named like a profile id that `settings` no longer has
/// (e.g. a removal failed while the folder was still locked). Nothing else is ever listed.
pub fn orphan_dirs(local_data: &Path, settings: &ProfileSettings) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(local_data.join(PROFILES_DIR)) else { return Vec::new() };
entries
.flatten()
.filter(|entry| {
let name = entry.file_name().to_string_lossy().into_owned();
valid_profile_id(&name) && !settings.contains(&name) && entry.path().is_dir()
})
.map(|entry| entry.path())
.collect()
}
/// Stable 16-byte WKWebView data-store identifier for a profile (macOS ignores data_directory).
pub fn store_identifier(id: &str) -> [u8; 16] {
let hash = |seed: u64| {
id.bytes().fold(seed, |acc, byte| (acc ^ u64::from(byte)).wrapping_mul(0x0100_0000_01b3))
};
let mut out = [0u8; 16];
out[..8].copy_from_slice(&hash(0xcbf2_9ce4_8422_2325).to_le_bytes());
out[8..].copy_from_slice(&hash(0x6c62_272e_07bb_0142).to_le_bytes());
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::site::testing::SITE;
const RULES: &AccountRules = &SITE.accounts;
fn profile(id: &str) -> Profile {
Profile { id: id.into(), name: format!("Account {id}"), color: "#AABBCC".into(), account: None }
}
fn account(handle: &str) -> Account {
Account { handle: handle.into(), name: "Sample".into(), avatar: Some("https://pbs.twimg.com/profile_images/1/a_normal.jpg".into()), signed_in: false }
}
#[test]
fn a_seen_login_names_the_profile_and_persists_a_sign_out() {
let mut settings = ProfileSettings { profiles: vec![profile("default"), profile("p1")], active: "default".into() };
assert!(settings.observe("p1", AccountSeen::SignedIn(account("@otheraccount")), RULES));
let seen = settings.profiles[1].account.clone().unwrap();
assert_eq!((seen.handle.as_str(), seen.signed_in, settings.profiles[1].name.as_str()), ("otheraccount", true, "Sample"));
// The same report again changes nothing (no disk write).
assert!(!settings.observe("p1", AccountSeen::SignedIn(account("otheraccount")), RULES));
assert!(settings.observe("p1", AccountSeen::SignedOut, RULES));
assert_eq!(settings.profiles[1].account.as_ref().map(|a| (a.handle.as_str(), a.signed_in)), Some(("otheraccount", false)));
assert!(!settings.observe("nope", AccountSeen::SignedOut, RULES));
}
#[test]
fn an_unnamed_or_avatarless_report_keeps_what_was_seen() {
let mut settings = ProfileSettings::default();
assert!(settings.observe("default", AccountSeen::SignedIn(account("fish")), RULES));
let mut bare = account("FISH");
bare.name = " ".into();
bare.avatar = None;
settings.observe("default", AccountSeen::SignedIn(bare), RULES);
let kept = settings.profiles[0].account.clone().unwrap();
assert_eq!((kept.name.as_str(), kept.avatar.is_some()), ("Sample", true));
}
#[test]
fn account_reports_are_validated() {
let mut settings = ProfileSettings::default();
for bad in ["", "has space", "waytoolonghandle_", "a/b"] {
assert!(!settings.observe("default", AccountSeen::SignedIn(account(bad)), RULES), "{bad:?}");
}
let mut foreign = account("ok");
foreign.avatar = Some("https://evil.test/x.png".into());
assert!(settings.observe("default", AccountSeen::SignedIn(foreign), RULES));
assert_eq!(settings.profiles[0].account.as_ref().unwrap().avatar, None);
}
#[test]
fn a_shell_save_keeps_the_native_accounts() {
let mut current = ProfileSettings { profiles: vec![profile("default"), profile("p1")], active: "default".into() };
current.observe("p1", AccountSeen::SignedIn(account("otheraccount")), RULES);
let mut incoming = ProfileSettings { profiles: vec![profile("default"), profile("p1")], active: "p1".into() };
incoming.profiles[0].account = Some(account("forged"));
let kept = incoming.keep_accounts_from(&current);
assert_eq!(kept.profiles[0].account, None);
assert_eq!(kept.profiles[1].account.as_ref().map(|a| a.handle.as_str()), Some("otheraccount"));
}
#[test]
fn ids_cannot_escape_the_profiles_directory() {
for bad in ["", "p", "..", "p/../x", "p\\x", "P1", "p.1", "con", "p-1", "x1", &format!("p{}", "a".repeat(33))] {
assert!(!valid_profile_id(bad), "{bad:?}");
assert_eq!(data_dir(Path::new("/base"), bad), None);
}
assert!(valid_profile_id("p1abc"));
assert_eq!(data_dir(Path::new("/base"), "p1abc"), Some(Path::new("/base").join("profiles").join("p1abc")));
assert_eq!(data_dir(Path::new("/base"), DEFAULT_PROFILE), None);
}
#[test]
fn orphans_are_only_profile_shaped_folders_no_longer_listed() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join(PROFILES_DIR);
for name in ["p1", "p2", "keep.me", "P3"] {
std::fs::create_dir_all(dir.join(name)).unwrap();
}
std::fs::write(dir.join("p4"), "a file").unwrap();
let settings = ProfileSettings { profiles: vec![profile("default"), profile("p1")], active: "default".into() };
assert_eq!(orphan_dirs(root.path(), &settings), vec![dir.join("p2")]);
assert!(orphan_dirs(&root.path().join("missing"), &settings).is_empty());
}
#[test]
fn default_profile_is_required_and_active_must_exist() {
let only_other = ProfileSettings { profiles: vec![profile("p1")], active: "p1".into() };
assert!(only_other.validated(RULES).is_err());
let missing_active = ProfileSettings { profiles: vec![profile("default")], active: "p9".into() };
assert!(missing_active.validated(RULES).is_err());
}
#[test]
fn many_profiles_are_allowed_and_normalized() {
let mut profiles = vec![profile("default")];
profiles.extend((0..20).map(|n| profile(&format!("p{n}"))));
profiles[3].name = " Work ".into();
let settings = ProfileSettings { profiles, active: "p7".into() }.validated(RULES).unwrap();
assert_eq!(settings.profiles.len(), 21);
assert_eq!(settings.profiles[3].name, "Work");
assert_eq!(settings.profiles[0].color, "#aabbcc");
}
#[test]
fn duplicates_bad_names_and_colors_are_rejected() {
let dup = ProfileSettings { profiles: vec![profile("default"), profile("p1"), profile("p1")], active: "default".into() };
assert!(dup.validated(RULES).is_err());
for (name, color) in [(" ", "#000000"), ("a\nb", "#000000"), (&"x".repeat(41)[..], "#000000"), ("ok", "red"), ("ok", "#12345g")] {
let mut bad = profile("default");
bad.name = name.into();
bad.color = color.into();
assert!(ProfileSettings { profiles: vec![bad], active: "default".into() }.validated(RULES).is_err());
}
}
#[test]
fn removed_profiles_and_store_identifiers() {
let before = ProfileSettings { profiles: vec![profile("default"), profile("p1"), profile("p2")], active: "p1".into() };
let after = ProfileSettings { profiles: vec![profile("default"), profile("p2")], active: "default".into() };
assert_eq!(before.removed_in(&after), vec!["p1".to_string()]);
assert_eq!(store_identifier("p1"), store_identifier("p1"));
assert_ne!(store_identifier("p1"), store_identifier("p2"));
}
#[test]
fn a_profile_with_open_tabs_cannot_be_removed() {
let before = ProfileSettings { profiles: vec![profile("default"), profile("p1"), profile("p2")], active: "p1".into() };
let after = ProfileSettings { profiles: vec![profile("default"), profile("p2")], active: "default".into() };
let open = ["p1".to_string()];
assert!(before.removal(&after, &open).is_err());
let open = ["p2".to_string(), "default".to_string()];
assert_eq!(before.removal(&after, &open).unwrap(), vec!["p1".to_string()]);
}
}