feat(workout): include host result contract in mobile kit

Record share/haptics and the denied/timeout/unavailable/error host result kinds in Android and iOS release metadata, and make mobile verification require them.

req: examples/006

req: host/002
This commit is contained in:
slhx agent
2026-06-12 11:26:34 +02:00
parent 860a4edf61
commit 32fa1a4952
2 changed files with 29 additions and 4 deletions
+5 -1
View File
@@ -55,6 +55,9 @@ The generated manifest records:
- cache policy: release-scoped HTML/CSS/runtime assets only;
- offline truth policy: app-owned command/event/projection records, never DOM
patches or UI effect payloads;
- host capability policy: Android and iOS shell metadata declare share/haptics and
the same denied, timeout, unavailable, and error result kinds handled by app
code before UI effects;
- environment/secrets boundary: public shell config in the kit, signing secrets
outside the repo;
- rollback: redeploy the previous server binary and rebuild store artifacts from
@@ -76,4 +79,5 @@ reviewed without guessing what is still external. req: examples/006
This is not a `hemx-mobile` framework, sync layer, client database, or native UI
runtime. The mobile shells load the production Workout web app and route host
capabilities such as share/haptics through the typed host boundary before UI
effects are produced. req: host/002 req: local/002
effects are produced; denied, timeout, unavailable, and error cases share the
same host result shape. req: host/002 req: local/002
+24 -3
View File
@@ -291,6 +291,10 @@ fn verify_workout_mobile_release(
"rollback",
"android/twa-release.json",
"ios/webview-release.json",
"denied",
"timeout",
"unavailable",
"error",
],
&mut failures,
);
@@ -301,6 +305,12 @@ fn verify_workout_mobile_release(
&config.app_name,
origin_host(&config.origin),
"external Android keystore",
"share",
"haptics",
"denied",
"timeout",
"unavailable",
"error",
],
&mut failures,
);
@@ -312,6 +322,10 @@ fn verify_workout_mobile_release(
"share",
"haptics",
"external Apple team",
"denied",
"timeout",
"unavailable",
"error",
],
&mut failures,
);
@@ -331,7 +345,7 @@ fn check_file_contains(path: &Path, needles: &[&str], failures: &mut Vec<String>
fn workout_mobile_manifest(config: &WorkoutMobileConfig, blockers: &[String]) -> String {
format!(
"{{\n \"app_id\": \"{}\",\n \"name\": \"{}\",\n \"version\": \"{}\",\n \"origin\": \"{}\",\n \"server_binary\": \"target/release/hemx-workout-example\",\n \"runtime_asset_path\": \"served by hemx_axum::runtime_js_path() from the same release\",\n \"cache_policy\": \"cache only release-scoped HTML/CSS/runtime assets; never store DOM patches or UI effects as truth\",\n \"state_policy\": \"app-owned command/event/projection records\",\n \"environment_boundary\": \"public mobile shell config lives here; secrets and signing credentials stay outside the repo\",\n \"rollback\": \"redeploy the previous server binary and matching mobile shell metadata; rebuild store artifacts with the previous version/signing inputs\",\n \"android\": \"android/twa-release.json\",\n \"ios\": \"ios/webview-release.json\",\n \"external_blockers\": [{}]\n}}\n",
"{{\n \"app_id\": \"{}\",\n \"name\": \"{}\",\n \"version\": \"{}\",\n \"origin\": \"{}\",\n \"server_binary\": \"target/release/hemx-workout-example\",\n \"runtime_asset_path\": \"served by hemx_axum::runtime_js_path() from the same release\",\n \"cache_policy\": \"cache only release-scoped HTML/CSS/runtime assets; never store DOM patches or UI effects as truth\",\n \"state_policy\": \"app-owned command/event/projection records\",\n \"host_result_kinds\": [\"denied\", \"timeout\", \"unavailable\", \"error\"],\n \"environment_boundary\": \"public mobile shell config lives here; secrets and signing credentials stay outside the repo\",\n \"rollback\": \"redeploy the previous server binary and matching mobile shell metadata; rebuild store artifacts with the previous version/signing inputs\",\n \"android\": \"android/twa-release.json\",\n \"ios\": \"ios/webview-release.json\",\n \"external_blockers\": [{}]\n}}\n",
json_escape(&config.app_id),
json_escape(&config.app_name),
json_escape(&config.version),
@@ -342,7 +356,7 @@ fn workout_mobile_manifest(config: &WorkoutMobileConfig, blockers: &[String]) ->
fn android_twa_release_json(config: &WorkoutMobileConfig) -> String {
format!(
"{{\n \"package\": \"{}\",\n \"name\": \"{}\",\n \"start_url\": \"{}/\",\n \"host\": \"{}\",\n \"version\": \"{}\",\n \"signing\": \"external Android keystore; never commit credentials\"\n}}\n",
"{{\n \"package\": \"{}\",\n \"name\": \"{}\",\n \"start_url\": \"{}/\",\n \"host\": \"{}\",\n \"version\": \"{}\",\n \"host_capabilities\": [\"share\", \"haptics\"],\n \"host_result_kinds\": [\"denied\", \"timeout\", \"unavailable\", \"error\"],\n \"signing\": \"external Android keystore; never commit credentials\"\n}}\n",
json_escape(&config.android_package),
json_escape(&config.app_name),
json_escape(config.origin.trim_end_matches('/')),
@@ -353,7 +367,7 @@ fn android_twa_release_json(config: &WorkoutMobileConfig) -> String {
fn ios_webview_release_json(config: &WorkoutMobileConfig) -> String {
format!(
"{{\n \"bundle_id\": \"{}\",\n \"name\": \"{}\",\n \"start_url\": \"{}/\",\n \"version\": \"{}\",\n \"host_capabilities\": [\"share\", \"haptics\"],\n \"signing\": \"external Apple team/provisioning profile; never commit credentials\"\n}}\n",
"{{\n \"bundle_id\": \"{}\",\n \"name\": \"{}\",\n \"start_url\": \"{}/\",\n \"version\": \"{}\",\n \"host_capabilities\": [\"share\", \"haptics\"],\n \"host_result_kinds\": [\"denied\", \"timeout\", \"unavailable\", \"error\"],\n \"signing\": \"external Apple team/provisioning profile; never commit credentials\"\n}}\n",
json_escape(&config.ios_bundle_id),
json_escape(&config.app_name),
json_escape(config.origin.trim_end_matches('/')),
@@ -750,6 +764,9 @@ mod tests {
assert!(manifest.contains("target/release/hemx-workout-example"));
assert!(manifest.contains("hemx_axum::runtime_js_path()"));
assert!(manifest.contains("app-owned command/event/projection records"));
assert!(manifest.contains(
"\"host_result_kinds\": [\"denied\", \"timeout\", \"unavailable\", \"error\"]"
));
assert!(manifest.contains("secrets and signing credentials stay outside the repo"));
assert!(manifest.contains("Android signing key not configured"));
}
@@ -763,6 +780,10 @@ mod tests {
assert_eq!(origin_host(&config.origin), "workout.example.com");
assert!(android.contains("\"start_url\": \"https://workout.example.com/app/\""));
assert!(android.contains("\"host\": \"workout.example.com\""));
assert!(android.contains("\"host_capabilities\": [\"share\", \"haptics\"]"));
assert!(android.contains(
"\"host_result_kinds\": [\"denied\", \"timeout\", \"unavailable\", \"error\"]"
));
}
#[test]