Compare commits

...
Author SHA1 Message Date
Brandon Thomas 7a88118349 fold cdn configs in 2026-09-27 22:41:01 -04:00
Brandon Thomas e07bf35cd0 static config for first party domains 2026-09-27 22:40:14 -04:00
Brandon Thomas a1147a98d2 Merge pull request #40 from storytold/bt-desktop-site
add desktop marketing website
2026-09-27 22:12:11 -04:00
25 changed files with 784 additions and 72 deletions

No files matched your search

Generated
+9
View File
@@ -313,6 +313,7 @@ dependencies = [
"actix-http",
"actix-rt",
"actix-web",
"domains",
"log",
"server_environment",
"speculoos",
@@ -2688,6 +2689,13 @@ dependencies = [
"litrs",
]
[[package]]
name = "domains"
version = "0.0.1"
dependencies = [
"url",
]
[[package]]
name = "dotenv"
version = "0.15.0"
@@ -8715,6 +8723,7 @@ dependencies = [
"datetimes",
"derive_more 0.99.17",
"diesel",
"domains",
"easyenv",
"elasticsearch",
"elasticsearch_schema",
+2
View File
@@ -67,6 +67,7 @@ members = [
"crates/lib/files/zip_archives",
"crates/lib/http_headers",
"crates/lib/jobs_common",
"crates/lib/domains",
"crates/lib/jwt_light",
"crates/lib/opaque_cursors",
"crates/lib/jwt_signer",
@@ -252,6 +253,7 @@ grok_api_client = { path = "crates/api_clients/grok_api_client" }
rootly_client = { path = "crates/api_clients/rootly_client" }
rootly_config = { path = "crates/schema/config/rootly_config" }
shared_env_var_config = { path = "crates/schema/config/shared_env_var_config" }
domains = { path = "crates/lib/domains" }
http_headers = { path = "crates/lib/http_headers" }
images = { path = "crates/lib/files/images" }
jobs_common = { path = "crates/lib/jobs_common" }
+1
View File
@@ -20,6 +20,7 @@ normal = ["workspace-build-acceleration"]
workspace-build-acceleration = { workspace = true }
# Internal
domains.workspace = true
server_environment.workspace = true
# External
@@ -1,11 +1,13 @@
use actix_cors::Cors;
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
use domains::artcraft::ARTCRAFT_2D;
use domains::storyteller::STORYTELLER_2D;
pub fn add_artcraft_2d(cors: Cors, _is_production: bool) -> Cors {
cors
// Hypothetical domains
.allowed_origin("https://2d.storyteller.ai")
.allowed_origin("https://2d.getartcraft.com")
.allowed_origin(STORYTELLER_2D.https_link_without_path())
.allowed_origin(ARTCRAFT_2D.https_link_without_path())
// Netlify project
.allowed_origin_fn(|origin, _req_head| {
netlify_branch_domain_matches(origin, "storyteller-2d.netlify.app")
@@ -1,11 +1,13 @@
use actix_cors::Cors;
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
use domains::artcraft::ARTCRAFT_3D;
use domains::storyteller::STORYTELLER_3D;
pub fn add_artcraft_3d(cors: Cors, _is_production: bool) -> Cors {
cors
// Hypothetical domains
.allowed_origin("https://3d.storyteller.ai")
.allowed_origin("https://3d.getartcraft.com")
.allowed_origin(STORYTELLER_3D.https_link_without_path())
.allowed_origin(ARTCRAFT_3D.https_link_without_path())
// Netlify project
.allowed_origin_fn(|origin, _req_head| {
netlify_branch_domain_matches(origin, "storyteller-3d.netlify.app")
@@ -1,9 +1,10 @@
use actix_cors::Cors;
use domains::artcraft::ARTCRAFT_DESKTOP;
pub fn add_artcraft_desktop(cors: Cors, _is_production: bool) -> Cors {
cors
// Stable identity sent by the Artcraft native HTTP bridge (dev and packaged apps).
.allowed_origin("https://desktop.getartcraft.com")
.allowed_origin(ARTCRAFT_DESKTOP.https_link_without_path())
}
#[cfg(test)]
@@ -1,10 +1,11 @@
use actix_cors::Cors;
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
use domains::artcraft::ARTCRAFT_WEBAPP;
pub fn add_artcraft_webapp(cors: Cors, _is_production: bool) -> Cors {
cors
// Actual domains
.allowed_origin("https://app.getartcraft.com")
.allowed_origin(ARTCRAFT_WEBAPP.https_link_without_path())
// Netlify project
.allowed_origin_fn(|origin, _req_head| {
netlify_branch_domain_matches(origin, "artcraft-webapp.netlify.app")
@@ -1,14 +1,18 @@
use actix_cors::Cors;
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
use domains::artcraft::ARTCRAFT_AI;
use domains::artcraft::ARTCRAFT_AI_WWW;
use domains::artcraft::ARTCRAFT_WEBSITE;
use domains::artcraft::ARTCRAFT_WEBSITE_WWW;
pub fn add_artcraft_website(cors: Cors, _is_production: bool) -> Cors {
cors
// Actual domains
.allowed_origin("https://getartcraft.com")
.allowed_origin("https://www.getartcraft.com")
.allowed_origin(ARTCRAFT_WEBSITE.https_link_without_path())
.allowed_origin(ARTCRAFT_WEBSITE_WWW.https_link_without_path())
// Hypothetical domains
.allowed_origin("https://artcraft.ai")
.allowed_origin("https://www.artcraft.ai")
.allowed_origin(ARTCRAFT_AI.https_link_without_path())
.allowed_origin(ARTCRAFT_AI_WWW.https_link_without_path())
// Development against production
.allowed_origin("http://localhost:4200")
.allowed_origin("http://localhost:4201")
@@ -1,17 +1,21 @@
use actix_cors::Cors;
use log::warn;
use url::{Host, Url};
use domains::fakeyou::FAKEYOU;
use domains::fakeyou::FAKEYOU_API;
use domains::fakeyou::FAKEYOU_ENGINE;
use domains::fakeyou::FAKEYOU_STAGING;
pub fn add_fakeyou(cors: Cors, is_production: bool) -> Cors {
if is_production {
cors
// Storyteller Engine (Production)
.allowed_origin("https://engine.fakeyou.com")
.allowed_origin(FAKEYOU_ENGINE.https_link_without_path())
// FakeYou (Production)
.allowed_origin("https://api.fakeyou.com")
.allowed_origin("https://fakeyou.com")
.allowed_origin(FAKEYOU_API.https_link_without_path())
.allowed_origin(FAKEYOU.https_link_without_path())
// FakeYou (Staging)
.allowed_origin("https://staging.fakeyou.com")
.allowed_origin(FAKEYOU_STAGING.https_link_without_path())
// Allow Netlify domains within "fakeyou" project.
.allowed_origin_fn(|origin, _req_head| {
let maybe_url = origin.to_str()
@@ -50,7 +54,7 @@ pub fn add_fakeyou(cors: Cors, is_production: bool) -> Cors {
.allowed_origin("https://dev.fakeyou.com:7000") // Yarn default port
.allowed_origin("https://dev.fakeyou.com:7001") // NB: Mac frontend
// Storyteller Engine (Development)
.allowed_origin("https://engine.fakeyou.com") // NB: We use prod for integration testing
.allowed_origin(FAKEYOU_ENGINE.https_link_without_path()) // NB: We use prod for integration testing
}
}
@@ -1,19 +1,24 @@
use actix_cors::Cors;
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
use domains::storyteller::STORYTELLER;
use domains::storyteller::STORYTELLER_API;
use domains::storyteller::STORYTELLER_ENGINE;
use domains::storyteller::STORYTELLER_STAGING;
use domains::storyteller::STORYTELLER_STUDIO;
pub fn add_storyteller(cors: Cors, is_production: bool) -> Cors {
if is_production {
cors
// Storyteller Engine (Production)
.allowed_origin("https://engine.storyteller.ai")
.allowed_origin(STORYTELLER_ENGINE.https_link_without_path())
// Storyteller Studio (Production)
.allowed_origin("https://studio.storyteller.ai")
.allowed_origin(STORYTELLER_STUDIO.https_link_without_path())
// Storyteller.ai (Production)
.allowed_origin("https://api.storyteller.ai")
.allowed_origin("https://storyteller.ai")
.allowed_origin(STORYTELLER_API.https_link_without_path())
.allowed_origin(STORYTELLER.https_link_without_path())
// Storyteller.ai (Staging)
.allowed_origin("https://staging.storyteller.ai")
.allowed_origin(STORYTELLER_STAGING.https_link_without_path())
// Allow Netlify domains within "storyteller-ai" project.
.allowed_origin_fn(|origin, _req_head| {
netlify_branch_domain_matches(origin, "storyteller-ai.netlify.app")
@@ -1,13 +1,14 @@
use actix_cors::Cors;
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
use domains::storyteller::STORYTELLER_BOARD;
pub fn add_storyteller_board(cors: Cors, _is_production: bool) -> Cors {
cors
// Hypothetical domains
.allowed_origin("https://memeboard.ai")
.allowed_origin("https://dingboard.ai")
.allowed_origin("https://board.storyteller.ai")
.allowed_origin(STORYTELLER_BOARD.https_link_without_path())
// Netlify project
.allowed_origin_fn(|origin, _req_head| {
netlify_branch_domain_matches(origin, "storyteller-board.netlify.app")
@@ -1,11 +1,12 @@
use actix_cors::Cors;
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
use domains::storyteller::STORYTELLER_RENDER;
pub fn add_storyteller_render(cors: Cors, _is_production: bool) -> Cors {
cors
// Hypothetical domains
.allowed_origin("https://render.storyteller.ai")
.allowed_origin(STORYTELLER_RENDER.https_link_without_path())
// Netlify project
.allowed_origin_fn(|origin, _req_head| {
netlify_branch_domain_matches(origin, "storyteller-render.netlify.app")
@@ -1,11 +1,15 @@
use actix_cors::Cors;
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
use domains::storyteller::STORYTELLER_ANIMATE;
use domains::storyteller::STORYTELLER_STUDIO;
use domains::storyteller::STORYTELLER_STUDIO_STAGING;
use domains::storyteller::STORYTELLER_STUDIO_TESTING;
pub fn add_storyteller_studio(cors: Cors, _is_production: bool) -> Cors {
cors.allowed_origin("https://studio.storyteller.ai")
.allowed_origin("https://studio-staging.studio.storyteller.ai")
.allowed_origin("https://studio-testing.studio.storyteller.ai")
cors.allowed_origin(STORYTELLER_STUDIO.https_link_without_path())
.allowed_origin(STORYTELLER_STUDIO_STAGING.https_link_without_path())
.allowed_origin(STORYTELLER_STUDIO_TESTING.https_link_without_path())
.allowed_origin_fn(|origin, _req_head| {
netlify_branch_domain_matches(origin, "storytellerstudio.netlify.app")
})
@@ -13,7 +17,7 @@ pub fn add_storyteller_studio(cors: Cors, _is_production: bool) -> Cors {
netlify_branch_domain_matches(origin, "pipeline-gottagofast.netlify.app")
})
.allowed_origin("http://localhost:5173")
.allowed_origin("https://animate.storyteller.ai") // NB: Gen2 Studio
.allowed_origin(STORYTELLER_ANIMATE.https_link_without_path()) // NB: Gen2 Studio
}
#[cfg(test)]
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "domains"
edition = "2024"
version = "0.0.1"
publish = false
[lib]
name = "domains"
path = "src/lib.rs"
[features]
default = []
# Adds `Domain::https_url()`, returning a cached `&'static url::Url`.
url = ["dep:url"]
# No dependencies by default: the domain constants and their string forms are
# built entirely at compile time.
[dependencies]
url = { workspace = true, optional = true }
+69
View File
@@ -0,0 +1,69 @@
//! ArtCraft domains.
use crate::{Domain, domain};
/// Public marketing website (Next.js).
pub const ARTCRAFT_WEBSITE: Domain = domain!("getartcraft.com");
pub const ARTCRAFT_WEBSITE_WWW: Domain = domain!("www.getartcraft.com");
/// Browser web application.
pub const ARTCRAFT_WEBAPP: Domain = domain!("app.getartcraft.com");
/// Desktop app landing page.
pub const ARTCRAFT_DESKTOP: Domain = domain!("desktop.getartcraft.com");
/// HTTP API.
pub const ARTCRAFT_API: Domain = domain!("api.getartcraft.com");
pub const ARTCRAFT_2D: Domain = domain!("2d.getartcraft.com");
pub const ARTCRAFT_3D: Domain = domain!("3d.getartcraft.com");
/// Alternate marketing domain.
pub const ARTCRAFT_AI: Domain = domain!("artcraft.ai");
pub const ARTCRAFT_AI_WWW: Domain = domain!("www.artcraft.ai");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn website() {
assert_eq!(ARTCRAFT_WEBSITE.https_link_without_path(), "https://getartcraft.com");
assert_eq!(ARTCRAFT_WEBSITE_WWW.https_link_without_path(), "https://www.getartcraft.com");
}
#[test]
fn webapp() {
assert_eq!(ARTCRAFT_WEBAPP.https_link_without_path(), "https://app.getartcraft.com");
}
#[test]
fn desktop() {
assert_eq!(ARTCRAFT_DESKTOP.bare_domain(), "desktop.getartcraft.com");
assert_eq!(ARTCRAFT_DESKTOP.https_link_without_path(), "https://desktop.getartcraft.com");
assert_eq!(ARTCRAFT_DESKTOP.https_link_with_root_path(), "https://desktop.getartcraft.com/");
}
#[test]
fn api() {
assert_eq!(ARTCRAFT_API.https_link_without_path(), "https://api.getartcraft.com");
}
#[test]
fn two_d_and_three_d() {
assert_eq!(ARTCRAFT_2D.https_link_without_path(), "https://2d.getartcraft.com");
assert_eq!(ARTCRAFT_3D.https_link_without_path(), "https://3d.getartcraft.com");
}
#[test]
fn artcraft_ai() {
assert_eq!(ARTCRAFT_AI.https_link_without_path(), "https://artcraft.ai");
assert_eq!(ARTCRAFT_AI_WWW.https_link_without_path(), "https://www.artcraft.ai");
}
#[cfg(feature = "url")]
#[test]
fn desktop_url() {
assert_eq!(ARTCRAFT_DESKTOP.https_url().as_str(), "https://desktop.getartcraft.com/");
}
}
+163
View File
@@ -0,0 +1,163 @@
/// A validated domain name, with its `https://` link forms precomputed at
/// compile time. Build one with the [`domain!`](crate::domain) macro:
///
/// ```
/// use domains::{domain, Domain};
///
/// const EXAMPLE: Domain = domain!("desktop.example.com");
///
/// assert_eq!(EXAMPLE.bare_domain(), "desktop.example.com");
/// assert_eq!(EXAMPLE.https_link_without_path(), "https://desktop.example.com");
/// assert_eq!(EXAMPLE.https_link_with_root_path(), "https://desktop.example.com/");
/// ```
///
/// An invalid literal fails to compile:
///
/// ```compile_fail
/// const BAD: domains::Domain = domains::domain!("https://example.com");
/// ```
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct Domain {
bare: &'static str,
https_without_path: &'static str,
https_with_root_path: &'static str,
}
/// Declares a [`Domain`] from a string literal. Validation runs at compile
/// time: an invalid domain is a build error, never a runtime panic.
#[macro_export]
macro_rules! domain {
($domain:literal) => {
$crate::Domain::__from_macro(
$domain,
::core::concat!("https://", $domain),
::core::concat!("https://", $domain, "/"),
)
};
}
impl Domain {
/// Only for the `domain!` macro, which guarantees the three strings agree.
/// Panics (a compile error in const context) on an invalid domain.
#[doc(hidden)]
pub const fn __from_macro(
bare: &'static str,
https_without_path: &'static str,
https_with_root_path: &'static str,
) -> Self {
crate::validate::assert_valid_domain(bare);
Self { bare, https_without_path, https_with_root_path }
}
/// eg. `desktop.getartcraft.com`
pub const fn bare_domain(&self) -> &'static str {
self.bare
}
/// eg. `https://desktop.getartcraft.com`. This is also the browser `Origin`
/// header value, so use it for CORS and origin allowlists.
pub const fn https_link_without_path(&self) -> &'static str {
self.https_without_path
}
/// eg. `https://desktop.getartcraft.com/`
pub const fn https_link_with_root_path(&self) -> &'static str {
self.https_with_root_path
}
/// eg. `https://desktop.getartcraft.com/` as a `Url`.
///
/// Parsed on first use per domain, then served from a process-wide cache,
/// so every call returns the same `&'static Url`.
#[cfg(feature = "url")]
pub fn https_url(&self) -> &'static url::Url {
crate::url_cache::https_url(self.bare, self.https_with_root_path)
}
}
impl std::fmt::Debug for Domain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Domain({})", self.bare)
}
}
impl std::fmt::Display for Domain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.bare)
}
}
#[cfg(test)]
mod tests {
use crate::Domain;
const TEST_DOMAIN: Domain = domain!("desktop.example.com");
#[test]
fn bare_domain() {
assert_eq!(TEST_DOMAIN.bare_domain(), "desktop.example.com");
}
#[test]
fn https_links() {
assert_eq!(TEST_DOMAIN.https_link_without_path(), "https://desktop.example.com");
assert_eq!(TEST_DOMAIN.https_link_with_root_path(), "https://desktop.example.com/");
}
#[test]
fn usable_in_const_context() {
const ORIGINS: &[&str] = &[TEST_DOMAIN.https_link_without_path()];
assert_eq!(ORIGINS, &["https://desktop.example.com"]);
}
#[test]
fn display_and_debug() {
assert_eq!(TEST_DOMAIN.to_string(), "desktop.example.com");
assert_eq!(format!("{TEST_DOMAIN:?}"), "Domain(desktop.example.com)");
}
#[cfg(feature = "url")]
mod url_tests {
use super::*;
#[test]
fn https_url_has_root_path() {
let url = TEST_DOMAIN.https_url();
assert_eq!(url.as_str(), "https://desktop.example.com/");
assert_eq!(url.scheme(), "https");
assert_eq!(url.host_str(), Some("desktop.example.com"));
assert_eq!(url.path(), "/");
}
#[test]
fn https_url_is_parsed_once_and_shared() {
let first = TEST_DOMAIN.https_url();
let second = TEST_DOMAIN.https_url();
assert!(std::ptr::eq(first, second));
}
#[test]
fn https_url_is_shared_across_threads() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Domain>();
assert_send_sync::<&'static url::Url>();
// Borrowed into 'static threads with no Arc or cloning; every thread
// sees the same parsed instance.
let main_thread_url = TEST_DOMAIN.https_url();
let handles: Vec<_> = (0..8)
.map(|_| std::thread::spawn(|| TEST_DOMAIN.https_url()))
.collect();
for handle in handles {
assert!(std::ptr::eq(handle.join().unwrap(), main_thread_url));
}
}
#[test]
fn https_url_matches_string_forms() {
let url = TEST_DOMAIN.https_url();
assert_eq!(url.as_str(), TEST_DOMAIN.https_link_with_root_path());
assert_eq!(url.origin().ascii_serialization(), TEST_DOMAIN.https_link_without_path());
}
}
}
+77
View File
@@ -0,0 +1,77 @@
//! FakeYou domains.
use crate::{Domain, domain};
/// Main website.
pub const FAKEYOU: Domain = domain!("fakeyou.com");
/// HTTP API.
pub const FAKEYOU_API: Domain = domain!("api.fakeyou.com");
/// Engine frontend (also used against production for integration testing).
pub const FAKEYOU_ENGINE: Domain = domain!("engine.fakeyou.com");
pub const FAKEYOU_STAGING: Domain = domain!("staging.fakeyou.com");
/// Local development hosts (resolve to a developer machine).
pub const FAKEYOU_DEV: Domain = domain!("dev.fakeyou.com");
pub const FAKEYOU_DEV_PROXY: Domain = domain!("devproxy.fakeyou.com");
/// Production media CDN. Serves both FakeYou and Storyteller media.
pub const FAKEYOU_CDN: Domain = domain!("cdn-2.fakeyou.com");
/// Development media CDN (a Cloudflare R2 public bucket). Serves both FakeYou
/// and Storyteller development media, like `FAKEYOU_CDN` does in production.
pub const FAKEYOU_DEV_CDN: Domain = domain!("pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn website() {
assert_eq!(FAKEYOU.bare_domain(), "fakeyou.com");
assert_eq!(FAKEYOU.https_link_without_path(), "https://fakeyou.com");
assert_eq!(FAKEYOU.https_link_with_root_path(), "https://fakeyou.com/");
}
#[test]
fn api() {
assert_eq!(FAKEYOU_API.https_link_without_path(), "https://api.fakeyou.com");
}
#[test]
fn engine_and_staging() {
assert_eq!(FAKEYOU_ENGINE.https_link_without_path(), "https://engine.fakeyou.com");
assert_eq!(FAKEYOU_STAGING.https_link_without_path(), "https://staging.fakeyou.com");
}
#[test]
fn development() {
assert_eq!(FAKEYOU_DEV.bare_domain(), "dev.fakeyou.com");
assert_eq!(FAKEYOU_DEV_PROXY.bare_domain(), "devproxy.fakeyou.com");
}
mod cdn {
use super::*;
#[test]
fn production() {
assert_eq!(FAKEYOU_CDN.bare_domain(), "cdn-2.fakeyou.com");
assert_eq!(FAKEYOU_CDN.https_link_without_path(), "https://cdn-2.fakeyou.com");
assert_eq!(FAKEYOU_CDN.https_link_with_root_path(), "https://cdn-2.fakeyou.com/");
}
#[test]
fn development() {
assert_eq!(FAKEYOU_DEV_CDN.https_link_without_path(), "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev");
}
#[cfg(feature = "url")]
#[test]
fn urls() {
assert_eq!(FAKEYOU_CDN.https_url().as_str(), "https://cdn-2.fakeyou.com/");
assert_eq!(FAKEYOU_DEV_CDN.https_url().as_str(), "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/");
}
}
}
+21
View File
@@ -0,0 +1,21 @@
//! First-party domain names, validated at compile time.
//!
//! ```
//! use domains::artcraft::ARTCRAFT_DESKTOP;
//!
//! assert_eq!(ARTCRAFT_DESKTOP.https_link_without_path(), "https://desktop.getartcraft.com");
//! ```
//!
//! Enable the `url` feature for `Domain::https_url()`, a cached `&'static url::Url`.
pub mod artcraft;
pub mod fakeyou;
pub mod storyteller;
mod domain;
mod validate;
#[cfg(feature = "url")]
mod url_cache;
pub use domain::Domain;
+77
View File
@@ -0,0 +1,77 @@
//! Storyteller domains. Storyteller media is served from `fakeyou::FAKEYOU_CDN`.
use crate::{Domain, domain};
/// Main website.
pub const STORYTELLER: Domain = domain!("storyteller.ai");
/// HTTP API.
pub const STORYTELLER_API: Domain = domain!("api.storyteller.ai");
pub const STORYTELLER_ENGINE: Domain = domain!("engine.storyteller.ai");
pub const STORYTELLER_STAGING: Domain = domain!("staging.storyteller.ai");
/// Storyteller Studio and its deploy environments.
pub const STORYTELLER_STUDIO: Domain = domain!("studio.storyteller.ai");
pub const STORYTELLER_STUDIO_STAGING: Domain = domain!("studio-staging.studio.storyteller.ai");
pub const STORYTELLER_STUDIO_TESTING: Domain = domain!("studio-testing.studio.storyteller.ai");
/// Gen2 Studio.
pub const STORYTELLER_ANIMATE: Domain = domain!("animate.storyteller.ai");
pub const STORYTELLER_BOARD: Domain = domain!("board.storyteller.ai");
pub const STORYTELLER_RENDER: Domain = domain!("render.storyteller.ai");
/// Legacy hosts for the ArtCraft 2D and 3D apps.
pub const STORYTELLER_2D: Domain = domain!("2d.storyteller.ai");
pub const STORYTELLER_3D: Domain = domain!("3d.storyteller.ai");
/// Local development hosts (resolve to a developer machine).
pub const STORYTELLER_DEV: Domain = domain!("dev.storyteller.ai");
pub const STORYTELLER_DEV_PROXY: Domain = domain!("devproxy.storyteller.ai");
/// Storyteller Studio static assets.
pub const STORYTELLER_CDN: Domain = domain!("cdn.storyteller.ai");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn website() {
assert_eq!(STORYTELLER.bare_domain(), "storyteller.ai");
assert_eq!(STORYTELLER.https_link_without_path(), "https://storyteller.ai");
assert_eq!(STORYTELLER.https_link_with_root_path(), "https://storyteller.ai/");
}
#[test]
fn api() {
assert_eq!(STORYTELLER_API.https_link_without_path(), "https://api.storyteller.ai");
}
#[test]
fn studio() {
assert_eq!(STORYTELLER_STUDIO.https_link_without_path(), "https://studio.storyteller.ai");
assert_eq!(STORYTELLER_STUDIO_STAGING.https_link_without_path(), "https://studio-staging.studio.storyteller.ai");
assert_eq!(STORYTELLER_STUDIO_TESTING.https_link_without_path(), "https://studio-testing.studio.storyteller.ai");
}
#[test]
fn apps() {
assert_eq!(STORYTELLER_BOARD.https_link_without_path(), "https://board.storyteller.ai");
assert_eq!(STORYTELLER_RENDER.https_link_without_path(), "https://render.storyteller.ai");
assert_eq!(STORYTELLER_2D.https_link_without_path(), "https://2d.storyteller.ai");
assert_eq!(STORYTELLER_3D.https_link_without_path(), "https://3d.storyteller.ai");
}
#[test]
fn development() {
assert_eq!(STORYTELLER_DEV.bare_domain(), "dev.storyteller.ai");
assert_eq!(STORYTELLER_DEV_PROXY.bare_domain(), "devproxy.storyteller.ai");
}
#[test]
fn cdn() {
assert_eq!(STORYTELLER_CDN.https_link_without_path(), "https://cdn.storyteller.ai");
}
}
+25
View File
@@ -0,0 +1,25 @@
//! Process-wide cache backing `Domain::https_url()`.
//!
//! `Url::parse` can't run at compile time, and `Domain` stays free of
//! interior mutability so its constants remain usable in `const` contexts.
//! So each domain's `Url` is parsed on first use and leaked into this map.
//! Domains only come from `domain!` literals, so the set (and the leak) is
//! bounded by the constants in the source code.
use std::collections::BTreeMap;
use std::sync::Mutex;
use url::Url;
static URLS: Mutex<BTreeMap<&'static str, &'static Url>> = Mutex::new(BTreeMap::new());
pub(crate) fn https_url(bare: &'static str, https_with_root_path: &'static str) -> &'static Url {
// The map is only ever inserted into, so a poisoned lock still holds
// valid entries.
let mut urls = URLS.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
urls.entry(bare).or_insert_with(|| {
// The domain was validated at compile time, so this parse can't fail.
let url = Url::parse(https_with_root_path).expect("compile-time validated domain should parse");
Box::leak(Box::new(url))
})
}
+217
View File
@@ -0,0 +1,217 @@
//! Compile-time domain name validation.
//!
//! `assert_valid_domain` is a `const fn` that panics on an invalid name. The
//! `domain!` macro calls it while evaluating a `const`/`static`, where a panic
//! is a compile error, so a bad domain literal never builds.
const MAX_DOMAIN_LEN: usize = 253;
const MAX_LABEL_LEN: usize = 63;
/// Panics (a compile error in const context) unless `domain` is a canonical,
/// fully-qualified DNS name: lowercase `a-z`, `0-9` and `-` labels separated
/// by single dots, at least two labels, no scheme, port, path or trailing dot,
/// and a non-numeric top-level label (so IP addresses are rejected).
pub const fn assert_valid_domain(domain: &str) {
match check_domain(domain) {
Ok(()) => {}
Err(DomainError::Empty) => panic!("invalid domain: empty"),
Err(DomainError::TooLong) => panic!("invalid domain: longer than 253 bytes"),
Err(DomainError::EmptyLabel) => panic!("invalid domain: empty label (leading, trailing or doubled dot)"),
Err(DomainError::LabelTooLong) => panic!("invalid domain: label longer than 63 bytes"),
Err(DomainError::InvalidCharacter) => panic!("invalid domain: only lowercase a-z, 0-9, '-' and '.' are allowed"),
Err(DomainError::HyphenAtLabelEdge) => panic!("invalid domain: label starts or ends with '-'"),
Err(DomainError::SingleLabel) => panic!("invalid domain: needs at least two labels (eg. example.com)"),
Err(DomainError::NumericTopLevelLabel) => panic!("invalid domain: top-level label is numeric (IP addresses are not domains)"),
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum DomainError {
Empty,
TooLong,
EmptyLabel,
LabelTooLong,
InvalidCharacter,
HyphenAtLabelEdge,
SingleLabel,
NumericTopLevelLabel,
}
pub(crate) const fn check_domain(domain: &str) -> Result<(), DomainError> {
let bytes = domain.as_bytes();
if bytes.is_empty() {
return Err(DomainError::Empty);
}
if bytes.len() > MAX_DOMAIN_LEN {
return Err(DomainError::TooLong);
}
let mut labels = 0;
let mut label_start = 0;
let mut last_label_start = 0;
let mut i = 0;
// Walk one past the end so the final label is checked like the others.
while i <= bytes.len() {
if i == bytes.len() || bytes[i] == b'.' {
match check_label(bytes, label_start, i) {
Ok(()) => {}
Err(err) => return Err(err),
}
labels += 1;
last_label_start = label_start;
label_start = i + 1;
}
i += 1;
}
if labels < 2 {
return Err(DomainError::SingleLabel);
}
if is_all_digits(bytes, last_label_start, bytes.len()) {
return Err(DomainError::NumericTopLevelLabel);
}
Ok(())
}
/// Checks `bytes[start..end]` as a single label.
const fn check_label(bytes: &[u8], start: usize, end: usize) -> Result<(), DomainError> {
let len = end - start;
if len == 0 {
return Err(DomainError::EmptyLabel);
}
if len > MAX_LABEL_LEN {
return Err(DomainError::LabelTooLong);
}
let mut i = start;
while i < end {
match bytes[i] {
b'a'..=b'z' | b'0'..=b'9' | b'-' => {}
_ => return Err(DomainError::InvalidCharacter),
}
i += 1;
}
if bytes[start] == b'-' || bytes[end - 1] == b'-' {
return Err(DomainError::HyphenAtLabelEdge);
}
Ok(())
}
const fn is_all_digits(bytes: &[u8], start: usize, end: usize) -> bool {
let mut i = start;
while i < end {
if !bytes[i].is_ascii_digit() {
return false;
}
i += 1;
}
true
}
#[cfg(test)]
mod tests {
use super::DomainError::*;
use super::check_domain;
mod valid {
use super::*;
#[test]
fn apex_domain() {
assert_eq!(check_domain("getartcraft.com"), Ok(()));
}
#[test]
fn subdomains() {
assert_eq!(check_domain("desktop.getartcraft.com"), Ok(()));
assert_eq!(check_domain("studio-staging.studio.storyteller.ai"), Ok(()));
}
#[test]
fn digits_and_hyphens_inside_labels() {
assert_eq!(check_domain("cdn-2.fakeyou.com"), Ok(()));
assert_eq!(check_domain("3d.getartcraft.com"), Ok(()));
assert_eq!(check_domain("pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev"), Ok(()));
}
#[test]
fn max_lengths() {
let label_63 = "a".repeat(63);
assert_eq!(check_domain(&format!("{label_63}.com")), Ok(()));
// 63 + 1 + 63 + 1 + 63 + 1 + 61 = 253
let domain_253 = format!("{label_63}.{label_63}.{label_63}.{}", "a".repeat(61));
assert_eq!(domain_253.len(), 253);
assert_eq!(check_domain(&domain_253), Ok(()));
}
}
mod invalid {
use super::*;
#[test]
fn empty() {
assert_eq!(check_domain(""), Err(Empty));
}
#[test]
fn too_long() {
let label_63 = "a".repeat(63);
let domain_254 = format!("{label_63}.{label_63}.{label_63}.{}", "a".repeat(62));
assert_eq!(check_domain(&domain_254), Err(TooLong));
}
#[test]
fn empty_labels() {
assert_eq!(check_domain(".getartcraft.com"), Err(EmptyLabel));
assert_eq!(check_domain("getartcraft.com."), Err(EmptyLabel));
assert_eq!(check_domain("desktop..getartcraft.com"), Err(EmptyLabel));
assert_eq!(check_domain("."), Err(EmptyLabel));
}
#[test]
fn label_too_long() {
assert_eq!(check_domain(&format!("{}.com", "a".repeat(64))), Err(LabelTooLong));
}
#[test]
fn uppercase() {
assert_eq!(check_domain("GetArtCraft.com"), Err(InvalidCharacter));
}
#[test]
fn scheme_port_path_and_whitespace() {
assert_eq!(check_domain("https://getartcraft.com"), Err(InvalidCharacter));
assert_eq!(check_domain("getartcraft.com:443"), Err(InvalidCharacter));
assert_eq!(check_domain("getartcraft.com/"), Err(InvalidCharacter));
assert_eq!(check_domain(" getartcraft.com"), Err(InvalidCharacter));
}
#[test]
fn non_ascii() {
assert_eq!(check_domain("artcräft.com"), Err(InvalidCharacter));
}
#[test]
fn underscore() {
assert_eq!(check_domain("dev_proxy.fakeyou.com"), Err(InvalidCharacter));
}
#[test]
fn hyphen_at_label_edge() {
assert_eq!(check_domain("-desktop.getartcraft.com"), Err(HyphenAtLabelEdge));
assert_eq!(check_domain("desktop-.getartcraft.com"), Err(HyphenAtLabelEdge));
assert_eq!(check_domain("getartcraft.com-"), Err(HyphenAtLabelEdge));
}
#[test]
fn single_label() {
assert_eq!(check_domain("localhost"), Err(SingleLabel));
}
#[test]
fn ip_addresses() {
assert_eq!(check_domain("127.0.0.1"), Err(NumericTopLevelLabel));
assert_eq!(check_domain("example.123"), Err(NumericTopLevelLabel));
}
}
}
@@ -34,6 +34,7 @@ workspace-build-acceleration.workspace = true
# Internal
actix_artcraft.workspace = true
actix_cors_configs = { path = "../../../lib/actix_cors_configs" }
domains = { workspace = true, features = ["url"] }
actix_helpers.workspace = true
artcraft_api_defs.workspace = true
artcraft_api_keys.workspace = true
@@ -1,43 +1,56 @@
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use once_cell::sync::Lazy;
use domains::Domain;
use domains::fakeyou::{FAKEYOU_CDN, FAKEYOU_DEV_CDN};
use server_environment::ServerEnvironment;
use url::Url;
const FAKEYOU_CDN_STR: &str = "https://cdn-2.fakeyou.com";
const STORYTELLER_CDN_STR: &str = "https://cdn-2.fakeyou.com";
const FAKEYOU_DEVELOPMENT_CDN_STR: &str = "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev";
const STORYTELLER_DEVELOPMENT_CDN_STR: &str = "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev";
const FAKEYOU_CDN: Lazy<Url> = Lazy::new(|| Url::parse(FAKEYOU_CDN_STR)
.expect("should never fail"));
const STORYTELLER_CDN: Lazy<Url> = Lazy::new(|| Url::parse(STORYTELLER_CDN_STR)
.expect("should never fail"));
const FAKEYOU_DEVELOPMENT_CDN: Lazy<Url> = Lazy::new(|| Url::parse(FAKEYOU_DEVELOPMENT_CDN_STR)
.expect("should never fail"));
const STORYTELLER_DEVELOPMENT_CDN: Lazy<Url> = Lazy::new(|| Url::parse(STORYTELLER_DEVELOPMENT_CDN_STR)
.expect("should never fail"));
// NB: Storyteller media is served from the FakeYou CDN too.
const FAKEYOU_PRODUCTION: Domain = FAKEYOU_CDN;
const STORYTELLER_PRODUCTION: Domain = FAKEYOU_CDN;
const FAKEYOU_DEVELOPMENT: Domain = FAKEYOU_DEV_CDN;
const STORYTELLER_DEVELOPMENT: Domain = FAKEYOU_DEV_CDN;
// TODO(bt,2025-01-31): Perhaps this should be config driven and configurable at runtime instead of hardcoded.
pub fn get_cdn_host(media_domain: MediaDomain, server_environment: ServerEnvironment) -> &'static str {
match (media_domain, server_environment) {
(MediaDomain::FakeYou, ServerEnvironment::Development) => FAKEYOU_DEVELOPMENT_CDN_STR,
(MediaDomain::FakeYou, ServerEnvironment::Production) => FAKEYOU_CDN_STR,
(MediaDomain::Storyteller, ServerEnvironment::Development) => STORYTELLER_DEVELOPMENT_CDN_STR,
(MediaDomain::Storyteller, ServerEnvironment::Production) => STORYTELLER_CDN_STR,
}
cdn_domain(media_domain, server_environment).https_link_without_path()
}
pub fn new_cdn_url(media_domain: MediaDomain, server_environment: ServerEnvironment) -> Url {
cdn_domain(media_domain, server_environment).https_url().clone()
}
fn cdn_domain(media_domain: MediaDomain, server_environment: ServerEnvironment) -> Domain {
match (media_domain, server_environment) {
(MediaDomain::FakeYou, ServerEnvironment::Development) => FAKEYOU_DEVELOPMENT_CDN.clone(),
(MediaDomain::FakeYou, ServerEnvironment::Production) => FAKEYOU_CDN.clone(),
(MediaDomain::Storyteller, ServerEnvironment::Development) => STORYTELLER_DEVELOPMENT_CDN.clone(),
(MediaDomain::Storyteller, ServerEnvironment::Production) => STORYTELLER_CDN.clone(),
(MediaDomain::FakeYou, ServerEnvironment::Development) => FAKEYOU_DEVELOPMENT,
(MediaDomain::FakeYou, ServerEnvironment::Production) => FAKEYOU_PRODUCTION,
(MediaDomain::Storyteller, ServerEnvironment::Development) => STORYTELLER_DEVELOPMENT,
(MediaDomain::Storyteller, ServerEnvironment::Production) => STORYTELLER_PRODUCTION,
}
}
#[cfg(test)]
mod tests {
use crate::http_server::common_responses::media::cdn_link::{get_cdn_host, new_cdn_url};
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use server_environment::ServerEnvironment;
const PROD_CDN: &str = "https://cdn-2.fakeyou.com";
const DEV_CDN: &str = "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev";
#[test]
fn cdn_hosts() {
assert_eq!(get_cdn_host(MediaDomain::FakeYou, ServerEnvironment::Production), PROD_CDN);
assert_eq!(get_cdn_host(MediaDomain::Storyteller, ServerEnvironment::Production), PROD_CDN);
assert_eq!(get_cdn_host(MediaDomain::FakeYou, ServerEnvironment::Development), DEV_CDN);
assert_eq!(get_cdn_host(MediaDomain::Storyteller, ServerEnvironment::Development), DEV_CDN);
}
#[test]
fn cdn_urls() {
assert_eq!(new_cdn_url(MediaDomain::FakeYou, ServerEnvironment::Production).as_str(), "https://cdn-2.fakeyou.com/");
assert_eq!(new_cdn_url(MediaDomain::Storyteller, ServerEnvironment::Production).as_str(), "https://cdn-2.fakeyou.com/");
assert_eq!(new_cdn_url(MediaDomain::FakeYou, ServerEnvironment::Development).as_str(), "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/");
assert_eq!(new_cdn_url(MediaDomain::Storyteller, ServerEnvironment::Development).as_str(), "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/");
}
}
@@ -1,17 +1,6 @@
use once_cell::sync::Lazy;
use domains::fakeyou::FAKEYOU_CDN;
use url::Url;
//const FAKEYOU_CDN_STR: &str = "https://storage.googleapis.com/dev-vocodes-public";
const FAKEYOU_CDN_STR: &str = "https://cdn-2.fakeyou.com";
const STORYTELLER_CDN_STR: &str = "https://cdn-2.fakeyou.com";
const FAKEYOU_CDN: Lazy<Url> = Lazy::new(|| Url::parse(FAKEYOU_CDN_STR)
.expect("should never fail"));
const STORYTELLER_CDN: Lazy<Url> = Lazy::new(|| Url::parse(STORYTELLER_CDN_STR)
.expect("should never fail"));
/// Which domain to generate CDN, etc. links for.
#[derive(Copy, Clone, Debug)]
pub enum MediaDomain {
@@ -22,14 +11,13 @@ pub enum MediaDomain {
impl MediaDomain {
pub fn new_cdn_url(&self) -> Url {
match self {
MediaDomain::FakeYou => FAKEYOU_CDN.clone(),
MediaDomain::Storyteller => STORYTELLER_CDN.clone(),
// NB: Storyteller media is served from the FakeYou CDN too.
MediaDomain::FakeYou | MediaDomain::Storyteller => FAKEYOU_CDN.https_url().clone(),
}
}
pub fn cdn_url_str(&self) -> &'static str {
match self {
MediaDomain::FakeYou => FAKEYOU_CDN_STR,
MediaDomain::Storyteller => STORYTELLER_CDN_STR,
MediaDomain::FakeYou | MediaDomain::Storyteller => FAKEYOU_CDN.https_link_without_path(),
}
}
}
@@ -6,6 +6,7 @@ use std::time::{Duration, Instant};
use actix_web::HttpRequest;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use domains::artcraft::{ARTCRAFT_WEBAPP, ARTCRAFT_WEBSITE, ARTCRAFT_WEBSITE_WWW};
use http_server_common::request::get_request_ip::get_request_ip;
use rand::{
rngs::{OsRng, StdRng},
@@ -16,7 +17,11 @@ use sha2::{Digest, Sha256};
use crate::http_server::common_responses::common_web_error::CommonWebError;
const CODE_ALPHABET: &[u8] = b"BCDFGHJKLMNPQRSTVWXZ";
const APPROVAL_ORIGINS: &[&str] = &["https://app.getartcraft.com", "https://getartcraft.com", "https://www.getartcraft.com"];
const APPROVAL_ORIGINS: &[&str] = &[
ARTCRAFT_WEBAPP.https_link_without_path(),
ARTCRAFT_WEBSITE.https_link_without_path(),
ARTCRAFT_WEBSITE_WWW.https_link_without_path(),
];
const WINDOW: Duration = Duration::from_secs(60);
const MAX_BUCKETS: usize = 10_000;