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 b2a91ff7b3
48 changed files with 9768 additions and 0 deletions

View file

@ -0,0 +1,167 @@
//! Signed, data-only hotfix channel: selector and flag overrides for site markup changes.
//!
//! The body is JSON (`{version, expires, minNative, selectors{}, flags{}}`) with a detached
//! minisign signature from a separate hotfix key. It is verified, schema-checked, required to be
//! newer than the copy already held (monotonic) and unexpired, and then handed to site tabs as
//! frozen data (`window.<global>`). It is never evaluated as code: a leaked hotfix key can at most
//! break selectors. The app owns the public key and channel URL.
use base64::Engine;
use chrono::{DateTime, Utc};
use minisign_verify::{PublicKey, Signature};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const MAX_BODY_BYTES: usize = 64 * 1024;
pub const MAX_SIG_BYTES: usize = 4 * 1024;
const MAX_SELECTORS: usize = 256;
const MAX_FLAGS: usize = 128;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Hotfix {
/// Monotonic; a client never accepts a version at or below the one it holds.
pub version: u64,
/// RFC 3339 timestamp after which the hotfix is ignored (blocks stale replay).
pub expires: String,
/// Lowest native app version this data is written for.
pub min_native: String,
#[serde(default)]
pub selectors: BTreeMap<String, String>,
#[serde(default)]
pub flags: BTreeMap<String, serde_json::Value>,
}
/// Accepts either the raw minisign text or the base64 wrapping that the Tauri signer produces.
pub fn minisign_text(value: &str) -> Result<String, String> {
let trimmed = value.trim();
if trimmed.starts_with("untrusted comment:") {
return Ok(trimmed.to_owned());
}
let bytes = base64::engine::general_purpose::STANDARD
.decode(trimmed)
.map_err(|_| "HOTFIX: key or signature is not minisign text or base64")?;
String::from_utf8(bytes).map_err(|_| "HOTFIX: key or signature is not UTF-8".into())
}
/// Verifies `body` against a detached minisign signature.
pub fn verify(pubkey: &str, body: &[u8], signature: &str) -> Result<(), String> {
if signature.len() > MAX_SIG_BYTES {
return Err("HOTFIX: signature too large".into());
}
let key_text = minisign_text(pubkey)?;
let key_line = key_text
.lines()
.map(str::trim)
.find(|line| !line.is_empty() && !line.starts_with("untrusted comment:"))
.ok_or("HOTFIX: empty public key")?;
let key = PublicKey::from_base64(key_line).map_err(|_| "HOTFIX: invalid public key")?;
let signature = Signature::decode(&minisign_text(signature)?).map_err(|_| "HOTFIX: invalid signature")?;
key.verify(body, &signature, false)
.map_err(|_| "HOTFIX: signature verification failed".into())
}
fn semver_triplet(value: &str) -> Option<(u64, u64, u64, bool)> {
let (core, pre) = match value.split_once('-') {
Some((core, pre)) => (core, !pre.is_empty()),
None => (value, false),
};
let mut parts = core.split('.').map(|p| p.parse::<u64>().ok());
let triplet = (parts.next()??, parts.next()??, parts.next()??, pre);
parts.next().is_none().then_some(triplet)
}
/// `a >= b` for semantic versions (a prerelease sorts before its release).
pub fn version_at_least(a: &str, b: &str) -> Option<bool> {
let (a, b) = (semver_triplet(a)?, semver_triplet(b)?);
Some(match (a.0, a.1, a.2).cmp(&(b.0, b.1, b.2)) {
std::cmp::Ordering::Greater => true,
std::cmp::Ordering::Less => false,
std::cmp::Ordering::Equal => !a.3 || b.3,
})
}
fn valid_key(key: &str) -> bool {
!key.is_empty() && key.len() <= 64 && key.bytes().all(|c| c.is_ascii_alphanumeric() || b"_-.".contains(&c))
}
/// Schema, expiry and native-version checks on an already verified body.
pub fn validate(body: &[u8], now: DateTime<Utc>, native_version: &str) -> Result<Hotfix, String> {
if body.len() > MAX_BODY_BYTES {
return Err("HOTFIX: body too large".into());
}
let hotfix: Hotfix = serde_json::from_slice(body).map_err(|error| format!("HOTFIX: schema: {error}"))?;
let expires = DateTime::parse_from_rfc3339(&hotfix.expires).map_err(|_| "HOTFIX: invalid expires")?;
if expires.with_timezone(&Utc) <= now {
return Err("HOTFIX: expired".into());
}
match version_at_least(native_version, &hotfix.min_native) {
Some(true) => {}
Some(false) => return Err("HOTFIX: requires a newer app".into()),
None => return Err("HOTFIX: invalid minNative".into()),
}
if hotfix.selectors.len() > MAX_SELECTORS
|| hotfix.selectors.iter().any(|(k, v)| !valid_key(k) || v.is_empty() || v.len() > 1024)
{
return Err("HOTFIX: invalid selectors".into());
}
let scalar = |v: &serde_json::Value| match v {
serde_json::Value::Bool(_) | serde_json::Value::Number(_) => true,
serde_json::Value::String(s) => s.len() <= 256,
_ => false,
};
if hotfix.flags.len() > MAX_FLAGS || hotfix.flags.iter().any(|(k, v)| !valid_key(k) || !scalar(v)) {
return Err("HOTFIX: invalid flags".into());
}
Ok(hotfix)
}
#[derive(Debug, PartialEq)]
pub enum Outcome {
/// A newer valid hotfix to store and deliver.
Accepted(Hotfix),
/// Same version as the one already held; nothing to do.
Unchanged,
}
/// Full client-side acceptance: signature first, then schema/expiry/minNative, then monotonicity.
pub fn accept(
pubkey: &str,
body: &[u8],
signature: &str,
now: DateTime<Utc>,
native_version: &str,
held_version: Option<u64>,
) -> Result<Outcome, String> {
verify(pubkey, body, signature)?;
let hotfix = validate(body, now, native_version)?;
match held_version {
Some(held) if hotfix.version == held => Ok(Outcome::Unchanged),
Some(held) if hotfix.version < held => Err("HOTFIX: older than the held version".into()),
_ => Ok(Outcome::Accepted(hotfix)),
}
}
/// Delivers the hotfix to a site page as data: parsed from a JSON string literal, frozen, stored
/// as `window.<global>` and announced with the `event` window event.
pub fn script(hotfix: &Hotfix, global: &str, event: &str) -> String {
let json = serde_json::to_string(hotfix).unwrap_or_else(|_| "null".into());
let literal = serde_json::to_string(&json).unwrap_or_else(|_| "\"null\"".into());
format!(
"(()=>{{const h=JSON.parse({literal});if(h){{Object.freeze(h.selectors);Object.freeze(h.flags);Object.freeze(h);}}window.{global}=h;try{{window.dispatchEvent(new CustomEvent('{event}'));}}catch(_){{}}}})();"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn semver_ordering() {
assert_eq!(version_at_least("0.2.0", "0.2.0"), Some(true));
assert_eq!(version_at_least("0.10.0", "0.9.9"), Some(true));
assert_eq!(version_at_least("0.2.0-beta.1", "0.2.0"), Some(false));
assert_eq!(version_at_least("0.2.0", "0.2.0-beta.1"), Some(true));
assert_eq!(version_at_least("0.2", "0.2.0"), None);
}
}