Compare commits

...
Author SHA1 Message Date
Hampus d514501d57 fix(app-proxy): tell the web client its release channel (#3448) 2026-10-13 15:15:11 +02:00
3 changed files with 81 additions and 1 deletions

No files matched your search

+11 -1
View File
@@ -24,6 +24,16 @@ if (!parsedBuildInfo.success) {
}
const buildInfo = parsedBuildInfo.output;
const SERVED_RELEASE_CHANNEL_META_SELECTOR = 'meta[name="fluxer-release-channel"]';
function servedReleaseChannel(): 'stable' | 'canary' | null {
if (typeof document === 'undefined') {
return null;
}
const value = document.querySelector<HTMLMetaElement>(SERVED_RELEASE_CHANNEL_META_SELECTOR)?.content;
return value === 'stable' || value === 'canary' ? value : null;
}
function clientReleaseChannel(value: (typeof buildInfo)['PUBLIC_RELEASE_CHANNEL']): 'stable' | 'canary' {
switch (value) {
case 'stable':
@@ -36,5 +46,5 @@ function clientReleaseChannel(value: (typeof buildInfo)['PUBLIC_RELEASE_CHANNEL'
export default {
PUBLIC_BUILD_VERSION: buildInfo.PUBLIC_BUILD_VERSION,
PUBLIC_RELEASE_CHANNEL: clientReleaseChannel(buildInfo.PUBLIC_RELEASE_CHANNEL),
PUBLIC_RELEASE_CHANNEL: servedReleaseChannel() ?? clientReleaseChannel(buildInfo.PUBLIC_RELEASE_CHANNEL),
} as const;
+36
View File
@@ -362,6 +362,7 @@ pub struct AppProxyConfig {
pub csp: CspConfig,
pub same_origin_hosts: Vec<String>,
pub manifest_scope_extensions: Vec<String>,
pub release_channel: Option<&'static str>,
pub self_hosted: bool,
}
@@ -477,11 +478,31 @@ impl AppProxyConfig {
"FLUXER_APP_PROXY_MANIFEST_SCOPE_EXTENSIONS",
&cfg::read_env("FLUXER_APP_PROXY_MANIFEST_SCOPE_EXTENSIONS", ""),
),
release_channel: parse_release_channel(
"RELEASE_CHANNEL",
&cfg::read_env("RELEASE_CHANNEL", ""),
),
self_hosted: cfg::read_bool_env("FLUXER_SELF_HOSTED", false),
}
}
}
fn parse_release_channel(name: &'static str, raw: &str) -> Option<&'static str> {
match raw.trim().to_ascii_lowercase().as_str() {
"" => None,
"stable" => Some("stable"),
"canary" => Some("canary"),
_ => {
warn_invalid(&InvalidAppProxyEnvironmentError::new(
name,
raw,
"stable or canary",
));
None
}
}
}
fn parse_manifest_scope_extensions(name: &'static str, raw: &str) -> Vec<String> {
let mut origins: Vec<String> = Vec::new();
for value in raw
@@ -591,6 +612,21 @@ mod tests {
);
}
#[test]
fn release_channel_accepts_only_stable_or_canary() {
assert_eq!(
parse_release_channel("TEST_CHANNEL", " Stable "),
Some("stable")
);
assert_eq!(
parse_release_channel("TEST_CHANNEL", "canary"),
Some("canary")
);
assert_eq!(parse_release_channel("TEST_CHANNEL", ""), None);
assert_eq!(parse_release_channel("TEST_CHANNEL", "development"), None);
assert_eq!(parse_release_channel("TEST_CHANNEL", "\"><script>"), None);
}
#[test]
fn manifest_scope_extensions_are_normalised_and_deduplicated() {
assert_eq!(
+34
View File
@@ -27,6 +27,7 @@ const SHARED_SHELL_CACHE_CONTROL: &str = "public, max-age=0, s-maxage=1";
const CSP_NONCE_PLACEHOLDER_ATTRIBUTE: &str = r#" nonce="{{CSP_NONCE_PLACEHOLDER}}""#;
const MEDIA_PRECONNECT_TAG: &str = r#"<link rel="preconnect" href="{{MEDIA_ENDPOINT}}">"#;
const SAME_ORIGIN_API_META_TAG: &str = r#"<meta name="fluxer-api-origin" content="self">"#;
const RELEASE_CHANNEL_META_NAME: &str = "fluxer-release-channel";
const HEAD_CLOSE_TAG: &str = "</head>";
const TITLE_START: &str = "<title";
const TITLE_END: &str = "</title>";
@@ -204,6 +205,10 @@ async fn serve_spa_index(state: &AppState, headers: &HeaderMap, request_path: &s
} else {
raw_html
};
let raw_html = match state.config.release_channel {
Some(channel) => mark_release_channel(&raw_html, channel),
None => raw_html,
};
let dev_buster = should_bust_dev_assets.then(current_dev_asset_cache_buster);
let html = match render_spa_document(
@@ -260,6 +265,17 @@ fn mark_same_origin_api(html: &str) -> String {
)
}
fn mark_release_channel(html: &str, channel: &str) -> String {
html.replacen(
HEAD_CLOSE_TAG,
&format!(
r#"<meta name="{RELEASE_CHANNEL_META_NAME}" content="{channel}">
{HEAD_CLOSE_TAG}"#
),
1,
)
}
#[derive(Debug)]
struct SpaDocumentSizeLimitError {
attempted_bytes: usize,
@@ -1166,6 +1182,24 @@ mod tests {
assert!(!served.contains(SAME_ORIGIN_API_META_TAG));
}
#[tokio::test]
async fn the_configured_release_channel_is_marked_in_the_document() {
let mut state = spa_state_serving(SHIPPED_APP_SHELL);
let served = read_document(serve_spa_index(&state, &HeaderMap::new(), "/").await).await;
assert!(!served.contains(RELEASE_CHANNEL_META_NAME));
for channel in ["stable", "canary"] {
Arc::make_mut(&mut state.config).release_channel = Some(channel);
let response = serve_spa_index(&state, &HeaderMap::new(), "/").await;
let policy = policy_of(&response);
let served = read_document(response).await;
let tag = format!(r#"<meta name="fluxer-release-channel" content="{channel}">"#);
assert_eq!(served.matches(&tag).count(), 1);
assert!(served.find(&tag).unwrap() < served.find("</head>").unwrap());
assert_every_inline_script_is_granted(&served, &policy);
}
}
#[tokio::test]
async fn the_configured_media_endpoint_is_preconnected() {
let mut state = assemble_spa_state(