use serde::Deserialize;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderMode {
Phase1ClientMount,
Phase2SsrReconcile,
}
#[derive(Debug)]
pub struct PageConfig<'a> {
pub title: &'a str,
pub route_pattern: &'a str,
pub manifest: &'a AssetManifest,
#[deprecated(
since = "0.2.2",
note = "writes handler text into a nonced script; pass data through \
`config_json` and move code into the bundle. Removed in 3.0.0"
)]
pub config_script: Option<&'a str>,
pub config_json: Option<&'a serde_json::Value>,
pub body_class: Option<&'a str>,
pub personality_css: Option<&'a str>,
pub body_prefix: Option<&'a str>,
pub render_mode: RenderMode,
pub ir_module: Option<&'a forma_ir::parser::IrModule>,
pub slots: Option<&'a forma_ir::slot::SlotData>,
}
#[non_exhaustive]
#[derive(Debug)]
pub struct PageOutput {
#[deprecated(
since = "0.2.2",
note = "send the page as a `PageResponse` (`into_page_response`), which carries \
`no-store` and the policy with it. Private in 3.0.0"
)]
pub html: String,
#[deprecated(
since = "0.2.2",
note = "the policy is typed now: extend it with `PolicyExtensions` and send a \
`PageResponse` rather than editing this text. Private in 3.0.0"
)]
pub csp: String,
#[deprecated(
since = "0.2.2",
note = "render with `try_render` (0.2.2), which draws the nonce and writes it into \
the page and its policy; a handler that must read it takes \
`PageResponse::into_parts_unchecked().nonce`. Private in 3.0.0, which gives \
no caller a nonce"
)]
pub nonce: crate::csp::CspNonce,
pub(crate) counts: crate::response::RenderCounts,
}
#[allow(deprecated)] impl PageOutput {
#[deprecated(
since = "0.2.2",
note = "use `into_page_response`: a `PageResponse` writes these headers itself and \
fails rather than dropping an invalid policy. Removed in 3.0.0"
)]
pub fn headers(&self) -> Vec<(axum::http::HeaderName, axum::http::HeaderValue)> {
let mut headers = vec![
(
axum::http::header::CONTENT_TYPE,
axum::http::HeaderValue::from_static("text/html; charset=utf-8"),
),
(
axum::http::header::CACHE_CONTROL,
axum::http::HeaderValue::from_static("no-store"),
),
];
if let Ok(value) = axum::http::HeaderValue::from_str(&self.csp) {
headers.push((axum::http::header::CONTENT_SECURITY_POLICY, value));
}
if let Some(Ok(value)) =
crate::csp::report_only_policy().map(|text| axum::http::HeaderValue::from_str(&text))
{
headers.push((
axum::http::header::CONTENT_SECURITY_POLICY_REPORT_ONLY,
value,
));
}
headers.extend_from_slice(document_security_headers());
headers
}
pub fn into_page_response(
self,
extensions: &crate::csp::PolicyExtensions,
) -> Result<crate::PageResponse, crate::PolicyError> {
if self.csp != crate::csp::build_csp_header(&self.nonce) {
return Err(crate::csp::PolicyError::edited_as_text(self.csp));
}
crate::PageResponse::new(self.html, self.nonce, extensions, self.counts)
}
}
pub const NOSNIFF: (&str, &str) = ("x-content-type-options", "nosniff");
pub fn document_security_headers() -> &'static [(axum::http::HeaderName, axum::http::HeaderValue)] {
use axum::http::{HeaderName, HeaderValue};
static HEADERS: std::sync::OnceLock<Vec<(HeaderName, HeaderValue)>> =
std::sync::OnceLock::new();
HEADERS.get_or_init(|| {
vec![
(
HeaderName::from_static("x-content-type-options"),
HeaderValue::from_static("nosniff"),
),
(
HeaderName::from_static("referrer-policy"),
HeaderValue::from_static("strict-origin-when-cross-origin"),
),
(
HeaderName::from_static("permissions-policy"),
HeaderValue::from_static("geolocation=(), camera=(), microphone=()"),
),
(
HeaderName::from_static("cross-origin-opener-policy"),
HeaderValue::from_static("same-origin"),
),
]
})
}
pub fn asset_security_headers() -> &'static [(axum::http::HeaderName, axum::http::HeaderValue)] {
use axum::http::{HeaderName, HeaderValue};
static HEADERS: std::sync::OnceLock<Vec<(HeaderName, HeaderValue)>> =
std::sync::OnceLock::new();
HEADERS.get_or_init(|| {
vec![
(
HeaderName::from_static("x-content-type-options"),
HeaderValue::from_static("nosniff"),
),
(
HeaderName::from_static("cross-origin-resource-policy"),
HeaderValue::from_static("same-origin"),
),
]
})
}
#[allow(deprecated)]
#[derive(Debug, Clone, Deserialize)]
pub struct AssetManifest {
pub version: u32,
pub build_hash: String,
pub assets: HashMap<String, String>,
pub routes: HashMap<String, RouteAssets>,
#[deprecated(
since = "0.2.2",
note = "the WASM walker is deprecated: @getforma/core/wasm, forma-ir's `wasm` feature \
and the build's `wasm` key go in 3.0.0, and with them this field and the \
`window.__FORMA_WASM__` script it adds to the page. Islands re-render with \
@getforma/core's `h()`. Removed in 3.0.0"
)]
#[serde(default)]
pub wasm: Option<WasmAssets>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RouteAssets {
pub js: Vec<String>,
pub css: Vec<String>,
pub fonts: Vec<String>,
pub total_size_br: u64,
pub budget_warn_threshold: u64,
#[serde(default)]
pub ir: Option<String>,
}
#[deprecated(
since = "0.2.2",
note = "the WASM walker is deprecated: @getforma/core/wasm, forma-ir's `wasm` feature \
and the build's `wasm` key go in 3.0.0, and with them `AssetManifest::wasm` and \
this type. Islands re-render with @getforma/core's `h()`. Removed in 3.0.0"
)]
#[derive(Debug, Clone, Deserialize)]
pub struct WasmAssets {
pub loader: String,
pub binary: String,
}
impl AssetManifest {
pub fn resolve<'a>(&'a self, logical_name: &'a str) -> &'a str {
self.assets
.get(logical_name)
.map(|s| s.as_str())
.unwrap_or(logical_name)
}
pub fn route(&self, pattern: &str) -> Option<&RouteAssets> {
self.routes.get(pattern)
}
}