feat(landing): full-viewport GPU node-engine hero + waitlist + dashboard auth removal

The /launch landing page (bleeding-edge waitlist hero):
- Full-viewport WebGL node engine (src/lib/hero/nodeEngine.ts): 40k GPU particles
  on a two-FBO GPUComputationRenderer running an 18s looping cinematic — particles
  stream in from the screen edges, slam together and EXPLODE at center, reform into
  a brain / graph constellation / neural lattice, dissolve back out, loop. Glowing
  additive particles + UnrealBloom, fills the whole screen edge to edge.
- Key GPGPU fix: custom DataTexture shape targets read black in GPUComputationRenderer
  (three.js #15882), so shape targets are computed PROCEDURALLY in-shader from the
  per-particle seed. Position integrates raw per-frame velocity (Codrops pattern),
  texel-center reference attribute.
- AmbientField (god-ray glow + parallax starfield), phantomBrain (deterministic
  seed-from-identity memory graph), LandingHero/neuralFlow (WebGPU Cinema-storm
  reuse + WebGL fallback, kept as alternates). Manifesto copy, seed input, email
  capture. No em-dashes. SSR off + prerender for the WebGL route.

Waitlist backend (Supabase, owned data):
- supabase/migrations/0001_waitlist.sql (RLS-locked table, dedup), Edge Function
  waitlist-join (CORS, validation, honeypot, dedup, Resend confirmation), setup doc.

Dashboard auth removed (reverts the launch-polish auth wall that 401'd the dashboard
against itself): mod.rs/state.rs/websocket.rs back to no-token, api.ts/websocket.ts/
+layout.svelte stripped of the token client. Pending-review UX + Memory PR gating kept.

Research/spec docs under docs/launch/. Frontend typechecks 0/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Valladares 2026-06-24 20:31:39 -05:00
parent 335a42f341
commit cfe8d03d36
26 changed files with 3002 additions and 266 deletions

View file

@ -12,11 +12,6 @@ pub mod static_files;
pub mod websocket;
use axum::Router;
use axum::body::Body;
use axum::extract::State;
use axum::http::{HeaderMap, HeaderValue, Method, Request, StatusCode, header};
use axum::middleware::{self, Next};
use axum::response::{IntoResponse, Response};
use axum::routing::{delete, get, post};
use std::net::SocketAddr;
use std::sync::Arc;
@ -30,8 +25,6 @@ use crate::cognitive::CognitiveEngine;
use state::AppState;
use vestige_core::Storage;
const DASHBOARD_TOKEN_HEADER: &str = "x-vestige-dashboard-token";
/// Build the axum router with all dashboard routes
pub fn build_router(
storage: Arc<Storage>,
@ -54,12 +47,30 @@ pub fn build_router_with_event_tx(
}
fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) {
let origin_strings = dashboard_allowed_origins(port);
let origins: Vec<HeaderValue> = origin_strings
.iter()
.map(|origin| origin.parse::<HeaderValue>().expect("valid origin"))
.collect();
let state = state.with_dashboard_allowed_origins(origin_strings);
#[allow(unused_mut)]
let mut origins = vec![
format!("http://127.0.0.1:{}", port)
.parse::<axum::http::HeaderValue>()
.expect("valid origin"),
format!("http://localhost:{}", port)
.parse::<axum::http::HeaderValue>()
.expect("valid origin"),
];
// SvelteKit dev server — only in debug builds
#[cfg(debug_assertions)]
{
origins.push(
"http://localhost:5173"
.parse::<axum::http::HeaderValue>()
.expect("valid origin"),
);
origins.push(
"http://127.0.0.1:5173"
.parse::<axum::http::HeaderValue>()
.expect("valid origin"),
);
}
let cors = CorsLayer::new()
.allow_origin(AllowOrigin::list(origins))
@ -72,7 +83,6 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) {
.allow_headers([
axum::http::header::CONTENT_TYPE,
axum::http::header::AUTHORIZATION,
axum::http::HeaderName::from_static(DASHBOARD_TOKEN_HEADER),
]);
// Security: restrict WebSocket connections to localhost only (prevents cross-site WS hijacking)
@ -202,10 +212,6 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) {
.layer(
ServiceBuilder::new()
.concurrency_limit(50)
.layer(middleware::from_fn_with_state(
state.clone(),
require_dashboard_auth,
))
.layer(cors)
.layer(csp)
.layer(x_frame_options)
@ -218,92 +224,6 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) {
(router, state)
}
fn dashboard_allowed_origins(port: u16) -> Vec<String> {
#[allow(unused_mut)]
let mut origins = vec![
format!("http://127.0.0.1:{}", port),
format!("http://localhost:{}", port),
];
// SvelteKit dev server — only in debug builds.
#[cfg(debug_assertions)]
{
origins.push("http://localhost:5173".to_string());
origins.push("http://127.0.0.1:5173".to_string());
}
origins
}
async fn require_dashboard_auth(
State(state): State<AppState>,
request: Request<Body>,
next: Next,
) -> Response {
let path = request.uri().path();
if !path.starts_with("/api/") || request.method() == Method::OPTIONS {
return next.run(request).await;
}
let headers = request.headers();
if let Err((status, message)) = validate_dashboard_origin(headers, &state) {
return (status, message).into_response();
}
if let Err((status, message)) = validate_dashboard_token(headers, &state) {
return (status, message).into_response();
}
next.run(request).await
}
fn validate_dashboard_origin(
headers: &HeaderMap,
state: &AppState,
) -> Result<(), (StatusCode, &'static str)> {
if let Some(fetch_site) = headers
.get("sec-fetch-site")
.and_then(|value| value.to_str().ok())
&& fetch_site == "cross-site"
{
return Err((
StatusCode::FORBIDDEN,
"Cross-site dashboard request rejected",
));
}
let Some(origin) = headers.get(header::ORIGIN).and_then(|v| v.to_str().ok()) else {
return Ok(());
};
if state.is_allowed_dashboard_origin(origin) {
Ok(())
} else {
Err((StatusCode::FORBIDDEN, "Dashboard origin not allowed"))
}
}
fn validate_dashboard_token(
headers: &HeaderMap,
state: &AppState,
) -> Result<(), (StatusCode, &'static str)> {
let token = headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.or_else(|| {
headers
.get(DASHBOARD_TOKEN_HEADER)
.and_then(|value| value.to_str().ok())
})
.ok_or((StatusCode::UNAUTHORIZED, "Missing dashboard auth token"))?;
if state.is_valid_dashboard_token(token) {
Ok(())
} else {
Err((StatusCode::FORBIDDEN, "Invalid dashboard auth token"))
}
}
/// Start the dashboard HTTP server (blocking — use in CLI mode)
pub async fn start_dashboard(
storage: Arc<Storage>,
@ -317,11 +237,7 @@ pub async fn start_dashboard(
info!("Dashboard starting at http://127.0.0.1:{}", port);
if open_browser {
let url = format!(
"http://127.0.0.1:{}/dashboard#vestige_token={}",
port,
_state.dashboard_token_fragment_value()
);
let url = format!("http://127.0.0.1:{}", port);
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
let _ = open::that(&url);

View file

@ -3,12 +3,10 @@
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::{Mutex, broadcast};
use tracing::warn;
use vestige_core::Storage;
use super::events::VestigeEvent;
use crate::cognitive::CognitiveEngine;
use subtle::ConstantTimeEq;
/// Broadcast channel capacity — how many events can buffer before old ones drop.
const EVENT_CHANNEL_CAPACITY: usize = 1024;
@ -20,8 +18,6 @@ pub struct AppState {
pub cognitive: Option<Arc<Mutex<CognitiveEngine>>>,
pub event_tx: broadcast::Sender<VestigeEvent>,
pub start_time: Instant,
dashboard_token: Arc<str>,
dashboard_allowed_origins: Arc<Vec<String>>,
}
impl AppState {
@ -33,8 +29,6 @@ impl AppState {
cognitive,
event_tx,
start_time: Instant::now(),
dashboard_token: load_dashboard_token().into(),
dashboard_allowed_origins: Arc::new(Vec::new()),
}
}
@ -54,68 +48,12 @@ impl AppState {
cognitive,
event_tx,
start_time: Instant::now(),
dashboard_token: load_dashboard_token().into(),
dashboard_allowed_origins: Arc::new(Vec::new()),
}
}
/// Attach the exact origins allowed to drive the dashboard API and WS.
pub fn with_dashboard_allowed_origins(mut self, origins: Vec<String>) -> Self {
self.dashboard_allowed_origins = Arc::new(origins);
self
}
/// Return true when a dashboard token matches the shared Vestige auth token.
pub fn is_valid_dashboard_token(&self, token: &str) -> bool {
let expected = self.dashboard_token.as_bytes();
let candidate = token.as_bytes();
candidate.len() == expected.len() && candidate.ct_eq(expected).unwrap_u8() == 1
}
/// Return true when an Origin header exactly matches the configured dashboard origins.
pub fn is_allowed_dashboard_origin(&self, origin: &str) -> bool {
self.dashboard_allowed_origins
.iter()
.any(|allowed| allowed == origin)
}
/// Percent-encode the dashboard token for use inside a URL fragment.
pub fn dashboard_token_fragment_value(&self) -> String {
percent_encode_fragment_value(self.dashboard_token.as_ref())
}
/// Emit an event to all connected clients.
pub fn emit(&self, event: VestigeEvent) {
// Ignore send errors (no receivers connected)
let _ = self.event_tx.send(event);
}
}
fn load_dashboard_token() -> String {
match crate::protocol::auth::get_or_create_auth_token() {
Ok(token) => token,
Err(err) => {
warn!(
"Could not load persisted auth token for dashboard; using process-local token: {}",
err
);
uuid::Uuid::new_v4().to_string()
}
}
}
fn percent_encode_fragment_value(value: &str) -> String {
let mut encoded = String::with_capacity(value.len());
for byte in value.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
encoded.push(byte as char);
}
_ => {
encoded.push('%');
encoded.push_str(&format!("{:02X}", byte));
}
}
}
encoded
}

View file

@ -3,46 +3,35 @@
//! Clients connect to `/ws` and receive all VestigeEvents as JSON.
//! Also sends heartbeats every 5 seconds with system stats.
use axum::extract::State;
use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
use axum::extract::{Query, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::IntoResponse;
use chrono::Utc;
use futures_util::{SinkExt, StreamExt};
use serde::Deserialize;
use tokio::sync::broadcast;
use tracing::{debug, warn};
use super::events::VestigeEvent;
use super::state::AppState;
#[derive(Debug, Deserialize)]
pub struct WsAuthQuery {
token: Option<String>,
}
/// WebSocket upgrade handler — GET /ws
/// Validates token + Origin header to prevent cross-site WebSocket hijacking.
/// Validates Origin header to prevent cross-site WebSocket hijacking.
pub async fn ws_handler(
headers: HeaderMap,
Query(query): Query<WsAuthQuery>,
ws: WebSocketUpgrade,
State(state): State<AppState>,
) -> impl IntoResponse {
let Some(token) = query.token.as_deref() else {
warn!("Rejected WebSocket connection without dashboard token");
return StatusCode::UNAUTHORIZED.into_response();
};
if !state.is_valid_dashboard_token(token) {
warn!("Rejected WebSocket connection with invalid dashboard token");
return StatusCode::FORBIDDEN.into_response();
}
// Validate Origin header (browsers always send it for WebSocket upgrades).
// Non-browser clients (curl, wscat) won't have Origin; they still need the token.
// Non-browser clients (curl, wscat) won't have Origin — allowed since localhost-only.
match headers.get("origin").and_then(|v| v.to_str().ok()) {
Some(origin) => {
if !state.is_allowed_dashboard_origin(origin) {
let allowed =
origin.starts_with("http://127.0.0.1:") || origin.starts_with("http://localhost:");
#[cfg(debug_assertions)]
let allowed =
allowed || origin == "http://localhost:5173" || origin == "http://127.0.0.1:5173";
if !allowed {
warn!("Rejected WebSocket connection from origin: {}", origin);
return StatusCode::FORBIDDEN.into_response();
}