feat(workout): record runtime asset integrity
Write and verify asset-integrity.tsv for the Workout mobile release kit using the actual hemx runtime path and SHA-256 digest, so mobile shell review has inspectable runtime integrity evidence. req: examples/006
This commit is contained in:
Generated
+3
@@ -630,6 +630,9 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "hemx-xtask"
|
name = "hemx-xtask"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"hemx-js",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "html5ever"
|
name = "html5ever"
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ The generated manifest records:
|
|||||||
- app identity and version;
|
- app identity and version;
|
||||||
- the production HTTPS origin used by Android and iOS shells;
|
- the production HTTPS origin used by Android and iOS shells;
|
||||||
- `target/release/hemx-workout-example` as the server artifact;
|
- `target/release/hemx-workout-example` as the server artifact;
|
||||||
- runtime assets served by the same release through `hemx_axum::runtime_js_path()`;
|
- the exact runtime asset path and SHA-256 digest served by the same release;
|
||||||
|
- `asset-integrity.tsv` as a plain-text integrity receipt for mobile shell review;
|
||||||
- cache policy: release-scoped HTML/CSS/runtime assets only;
|
- cache policy: release-scoped HTML/CSS/runtime assets only;
|
||||||
- offline truth policy: app-owned command/event/projection records, never DOM
|
- offline truth policy: app-owned command/event/projection records, never DOM
|
||||||
patches or UI effect payloads;
|
patches or UI effect payloads;
|
||||||
|
|||||||
@@ -7,3 +7,6 @@ publish = false
|
|||||||
[[bin]]
|
[[bin]]
|
||||||
name = "hemx-ci"
|
name = "hemx-ci"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
hemx-js = { path = "../hemx-js" }
|
||||||
|
|||||||
+35
-4
@@ -373,6 +373,10 @@ fn write_workout_mobile_release(
|
|||||||
config.out_dir.join("release-manifest.json"),
|
config.out_dir.join("release-manifest.json"),
|
||||||
workout_mobile_manifest(config, blockers),
|
workout_mobile_manifest(config, blockers),
|
||||||
)?;
|
)?;
|
||||||
|
fs::write(
|
||||||
|
config.out_dir.join("asset-integrity.tsv"),
|
||||||
|
workout_asset_integrity(),
|
||||||
|
)?;
|
||||||
fs::write(
|
fs::write(
|
||||||
config.out_dir.join("BLOCKERS.md"),
|
config.out_dir.join("BLOCKERS.md"),
|
||||||
workout_mobile_blockers_md(blockers),
|
workout_mobile_blockers_md(blockers),
|
||||||
@@ -409,7 +413,14 @@ fn verify_workout_mobile_release(
|
|||||||
let android_path = config.out_dir.join("android/twa-release.json");
|
let android_path = config.out_dir.join("android/twa-release.json");
|
||||||
let ios_path = config.out_dir.join("ios/webview-release.json");
|
let ios_path = config.out_dir.join("ios/webview-release.json");
|
||||||
let blockers_path = config.out_dir.join("BLOCKERS.md");
|
let blockers_path = config.out_dir.join("BLOCKERS.md");
|
||||||
for path in [&manifest_path, &android_path, &ios_path, &blockers_path] {
|
let integrity_path = config.out_dir.join("asset-integrity.tsv");
|
||||||
|
for path in [
|
||||||
|
&manifest_path,
|
||||||
|
&android_path,
|
||||||
|
&ios_path,
|
||||||
|
&blockers_path,
|
||||||
|
&integrity_path,
|
||||||
|
] {
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
failures.push(format!("{} is missing", path.display()));
|
failures.push(format!("{} is missing", path.display()));
|
||||||
}
|
}
|
||||||
@@ -421,7 +432,9 @@ fn verify_workout_mobile_release(
|
|||||||
&config.app_id,
|
&config.app_id,
|
||||||
&config.version,
|
&config.version,
|
||||||
&config.origin,
|
&config.origin,
|
||||||
"hemx_axum::runtime_js_path()",
|
hemx_js::RUNTIME_JS_PATH,
|
||||||
|
hemx_js::RUNTIME_JS_HASH,
|
||||||
|
"asset-integrity.tsv",
|
||||||
"app-owned command/event/projection records",
|
"app-owned command/event/projection records",
|
||||||
"secrets and signing credentials stay outside the repo",
|
"secrets and signing credentials stay outside the repo",
|
||||||
"rollback",
|
"rollback",
|
||||||
@@ -466,6 +479,12 @@ fn verify_workout_mobile_release(
|
|||||||
&mut failures,
|
&mut failures,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
check_file_contains(
|
||||||
|
&integrity_path,
|
||||||
|
&[hemx_js::RUNTIME_JS_PATH, hemx_js::RUNTIME_JS_HASH, "sha256"],
|
||||||
|
&mut failures,
|
||||||
|
);
|
||||||
|
|
||||||
for blocker in mobile_external_blockers(config) {
|
for blocker in mobile_external_blockers(config) {
|
||||||
check_file_contains(&manifest_path, &[&blocker], &mut failures);
|
check_file_contains(&manifest_path, &[&blocker], &mut failures);
|
||||||
check_file_contains(&blockers_path, &[&blocker], &mut failures);
|
check_file_contains(&blockers_path, &[&blocker], &mut failures);
|
||||||
@@ -496,13 +515,23 @@ fn check_file_contains(path: &Path, needles: &[&str], failures: &mut Vec<String>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn workout_asset_integrity() -> String {
|
||||||
|
format!(
|
||||||
|
"path\talgorithm\tdigest\n{}\tsha256\t{}\n",
|
||||||
|
hemx_js::RUNTIME_JS_PATH,
|
||||||
|
hemx_js::RUNTIME_JS_HASH
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn workout_mobile_manifest(config: &WorkoutMobileConfig, blockers: &[String]) -> String {
|
fn workout_mobile_manifest(config: &WorkoutMobileConfig, blockers: &[String]) -> String {
|
||||||
format!(
|
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 \"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",
|
"{{\n \"app_id\": \"{}\",\n \"name\": \"{}\",\n \"version\": \"{}\",\n \"origin\": \"{}\",\n \"server_binary\": \"target/release/hemx-workout-example\",\n \"runtime_asset_path\": \"{}\",\n \"runtime_asset_sha256\": \"{}\",\n \"asset_integrity\": \"asset-integrity.tsv\",\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_id),
|
||||||
json_escape(&config.app_name),
|
json_escape(&config.app_name),
|
||||||
json_escape(&config.version),
|
json_escape(&config.version),
|
||||||
json_escape(&config.origin),
|
json_escape(&config.origin),
|
||||||
|
json_escape(hemx_js::RUNTIME_JS_PATH),
|
||||||
|
json_escape(hemx_js::RUNTIME_JS_HASH),
|
||||||
json_string_list(blockers),
|
json_string_list(blockers),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -936,7 +965,9 @@ mod tests {
|
|||||||
let manifest = workout_mobile_manifest(&config, &blockers);
|
let manifest = workout_mobile_manifest(&config, &blockers);
|
||||||
|
|
||||||
assert!(manifest.contains("target/release/hemx-workout-example"));
|
assert!(manifest.contains("target/release/hemx-workout-example"));
|
||||||
assert!(manifest.contains("hemx_axum::runtime_js_path()"));
|
assert!(manifest.contains(hemx_js::RUNTIME_JS_PATH));
|
||||||
|
assert!(manifest.contains(hemx_js::RUNTIME_JS_HASH));
|
||||||
|
assert!(manifest.contains("asset-integrity.tsv"));
|
||||||
assert!(manifest.contains("app-owned command/event/projection records"));
|
assert!(manifest.contains("app-owned command/event/projection records"));
|
||||||
assert!(manifest.contains(
|
assert!(manifest.contains(
|
||||||
"\"host_result_kinds\": [\"denied\", \"timeout\", \"unavailable\", \"error\"]"
|
"\"host_result_kinds\": [\"denied\", \"timeout\", \"unavailable\", \"error\"]"
|
||||||
|
|||||||
Reference in New Issue
Block a user