//! App scoping: top-level isolation boundary for scripts, routes, //! domains, and (forward) data. Every script and route belongs to //! exactly one app; cross-app references are not allowed. //! //! See blueprint §11.5. The orchestrator dispatches via two-phase //! lookup: `Host → app_id → route trie`. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::AppId; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct App { pub id: AppId, /// URL-safe identifier; appears in dashboard paths. Mutable via the /// slug-rename flow which preserves the old slug as a permanent 301 /// in `app_slug_history`. pub slug: String, pub name: String, pub description: Option, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum DomainShape { /// Exact host: `app.example.com`. Exact, /// Wildcard suffix: `*.example.com` matches any subdomain. Wildcard, /// Parameterized wildcard: `{tenant}.example.com`. Same shape as /// `Wildcard` for collision purposes; the binding name surfaces in /// request context (future). Parameterized, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppDomain { pub id: Uuid, pub app_id: AppId, /// As the user typed it: `app.example.com`, `*.example.com`, or /// `{tenant}.example.com`. pub pattern: String, pub shape: DomainShape, /// Normalized collision key. `exact:` for exact; `wildcard:` /// for both wildcard and parameterized (parameter name is a binding, /// not a discriminator — per blueprint §11.5). pub shape_key: String, pub created_at: DateTime, }