sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit 71d53d141f8d2dd68bf3c049a0e0d88873ab0fb3
parent c2ae351210bb0e9b499922a9f234b86221125b45
Author: triesap <tyson@radroots.org>
Date:   Sun, 28 Jun 2026 04:08:33 +0000

packages: gate npm pack payloads

- run npm pack dry-runs from cargo xtask check
- require package metadata and exported dist artifacts in payloads
- reject source, provenance, ignore, and build-info files
- enforce the expected single wasm artifact per wasm package

Diffstat:
Mtools/xtask/src/check.rs | 257++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 256 insertions(+), 1 deletion(-)

diff --git a/tools/xtask/src/check.rs b/tools/xtask/src/check.rs @@ -1,4 +1,6 @@ -use std::{collections::BTreeSet, fs, path::Path}; +use std::{collections::BTreeSet, fs, path::Path, process::Command}; + +use serde::Deserialize; use crate::{ contracts::validate_sdk_contracts, @@ -19,6 +21,16 @@ const PACKAGE_HOMEPAGE: &str = "https://radroots.org"; const PACKAGE_REPOSITORY_URL: &str = "git+https://github.com/radrootslabs/sdk.git"; const PUBLISH_ACCESS: &str = "public"; +#[derive(Debug, Deserialize)] +struct NpmPackEntry { + files: Vec<NpmPackFile>, +} + +#[derive(Debug, Deserialize)] +struct NpmPackFile { + path: String, +} + pub fn check() -> Result<(), String> { validate_package_matrix()?; let root = workspace_root()?; @@ -47,6 +59,7 @@ pub fn check() -> Result<(), String> { for spec in wasm_package_specs() { check_wasm_package_surface(&root, *spec)?; } + check_npm_pack_payloads(&root)?; let outputs = package_outputs()?; for output in &outputs { check_generated_package_artifact_inventory(&root, output)?; @@ -455,6 +468,163 @@ fn check_workspace_dependencies( Ok(()) } +fn check_npm_pack_payloads(root: &Path) -> Result<(), String> { + for spec in package_specs() { + let package_dir = root.join(spec.package_dir); + let package_json_path = package_dir.join("package.json"); + let json = read_package_json_value(&package_json_path)?; + let required_files = required_npm_payload_files(&json, &package_json_path, None)?; + let payload_files = npm_pack_payload_files(&package_dir, spec.package_name)?; + validate_npm_pack_payload(spec.package_name, &payload_files, &required_files, None)?; + } + for spec in wasm_package_specs() { + let package_dir = root.join(spec.package_dir); + let package_json_path = package_dir.join("package.json"); + let json = read_package_json_value(&package_json_path)?; + let required_files = required_npm_payload_files(&json, &package_json_path, Some(*spec))?; + let payload_files = npm_pack_payload_files(&package_dir, spec.package_name)?; + validate_npm_pack_payload( + spec.package_name, + &payload_files, + &required_files, + Some(&format!("dist/{}_bg.wasm", spec.out_name)), + )?; + } + Ok(()) +} + +fn read_package_json_value(path: &Path) -> Result<serde_json::Value, String> { + let raw = fs::read_to_string(path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + serde_json::from_str::<serde_json::Value>(&raw) + .map_err(|error| format!("failed to parse {}: {error}", path.display())) +} + +fn required_npm_payload_files( + json: &serde_json::Value, + package_json_path: &Path, + wasm_spec: Option<WasmPackageSpec>, +) -> Result<BTreeSet<String>, String> { + let mut required = BTreeSet::from([ + "package.json".to_owned(), + "README.md".to_owned(), + "LICENSE-MIT".to_owned(), + "LICENSE-APACHE".to_owned(), + ]); + for path in package_surface_paths(json, package_json_path)? { + required.insert(normalized_package_path(&path)); + } + if let Some(spec) = wasm_spec { + for path in wasm_runtime_files(spec) { + required.insert(path); + } + } + Ok(required) +} + +fn npm_pack_payload_files( + package_dir: &Path, + package_name: &str, +) -> Result<BTreeSet<String>, String> { + let output = Command::new("npm") + .args(["pack", "--json", "--dry-run", "--ignore-scripts"]) + .current_dir(package_dir) + .output() + .map_err(|error| { + format!( + "failed to run npm pack dry-run for {package_name} in {}: {error}", + package_dir.display() + ) + })?; + if !output.status.success() { + return Err(format!( + "npm pack dry-run failed for {package_name} in {}: {}", + package_dir.display(), + String::from_utf8_lossy(&output.stderr) + )); + } + let entries = serde_json::from_slice::<Vec<NpmPackEntry>>(&output.stdout).map_err(|error| { + format!( + "failed to parse npm pack dry-run output for {package_name}: {error}; stdout: {}; stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ) + })?; + let [entry] = entries.as_slice() else { + return Err(format!( + "npm pack dry-run for {package_name} must return one package entry" + )); + }; + Ok(entry + .files + .iter() + .map(|file| normalized_package_path(&file.path)) + .collect()) +} + +fn validate_npm_pack_payload( + package_name: &str, + payload_files: &BTreeSet<String>, + required_files: &BTreeSet<String>, + expected_wasm_file: Option<&str>, +) -> Result<(), String> { + let missing = required_files + .difference(payload_files) + .cloned() + .collect::<Vec<_>>(); + if !missing.is_empty() { + return Err(format!( + "npm pack payload for {package_name} is missing required files: {missing:?}" + )); + } + for path in payload_files { + if let Some(reason) = forbidden_npm_payload_path(path) { + return Err(format!( + "npm pack payload for {package_name} includes forbidden {reason}: {path}" + )); + } + } + if let Some(expected_wasm_file) = expected_wasm_file { + let wasm_files = payload_files + .iter() + .filter(|path| path.ends_with(".wasm")) + .cloned() + .collect::<Vec<_>>(); + if wasm_files != [expected_wasm_file.to_owned()] { + return Err(format!( + "npm pack payload for {package_name} must include exactly {expected_wasm_file}; found {wasm_files:?}" + )); + } + } + Ok(()) +} + +fn normalized_package_path(path: &str) -> String { + path.trim_start_matches("./").to_owned() +} + +fn forbidden_npm_payload_path(path: &str) -> Option<&'static str> { + if path.starts_with("src/") { + return Some("source path"); + } + if path.starts_with("contracts/") { + return Some("contract path"); + } + if path.contains("sdk-manifest") { + return Some("manifest provenance path"); + } + if path.ends_with(".tsbuildinfo") { + return Some("TypeScript build info path"); + } + if path + .split('/') + .any(|segment| matches!(segment, ".gitignore" | ".npmignore")) + { + return Some("ignore file"); + } + None +} + fn check_public_wasm_declaration_inventory( surface_paths: &BTreeSet<String>, spec: WasmPackageSpec, @@ -672,6 +842,7 @@ mod tests { check_binding_crate_sources, check_generated_package_artifact_inventory, check_no_typescript_files, check_package_distribution_metadata, check_package_index, check_package_json, check_package_surface_artifacts, check_wasm_package_surface, + validate_npm_pack_payload, }; #[test] @@ -881,6 +1052,90 @@ mod tests { } #[test] + fn npm_pack_payload_requires_metadata_and_export_files() { + let payload_files = BTreeSet::from([ + "package.json".to_owned(), + "README.md".to_owned(), + "LICENSE-MIT".to_owned(), + "dist/index.js".to_owned(), + ]); + let required_files = BTreeSet::from([ + "package.json".to_owned(), + "README.md".to_owned(), + "LICENSE-MIT".to_owned(), + "LICENSE-APACHE".to_owned(), + "dist/index.js".to_owned(), + "dist/index.d.ts".to_owned(), + ]); + + let error = + validate_npm_pack_payload("@radroots/example", &payload_files, &required_files, None) + .expect_err("missing files should fail"); + assert!(error.contains("missing required files")); + assert!(error.contains("LICENSE-APACHE")); + assert!(error.contains("dist/index.d.ts")); + } + + #[test] + fn npm_pack_payload_rejects_source_and_provenance_internals() { + let required_files = BTreeSet::from(["package.json".to_owned()]); + for forbidden in [ + "src/generated/types.ts", + "contracts/provenance/typescript/core.json", + "dist/sdk-manifest.json", + "dist/index.tsbuildinfo", + "dist/.gitignore", + ".npmignore", + ] { + let payload_files = BTreeSet::from(["package.json".to_owned(), forbidden.to_owned()]); + + let error = validate_npm_pack_payload( + "@radroots/example", + &payload_files, + &required_files, + None, + ) + .expect_err("forbidden payload path should fail"); + assert!(error.contains("includes forbidden")); + assert!(error.contains(forbidden)); + } + } + + #[test] + fn npm_pack_payload_requires_exact_wasm_file() { + let required_files = BTreeSet::from([ + "package.json".to_owned(), + "dist/example.js".to_owned(), + "dist/example_bg.wasm".to_owned(), + ]); + let missing_wasm = + BTreeSet::from(["package.json".to_owned(), "dist/example.js".to_owned()]); + let error = validate_npm_pack_payload( + "@radroots/example-wasm", + &missing_wasm, + &required_files, + Some("dist/example_bg.wasm"), + ) + .expect_err("missing wasm should fail"); + assert!(error.contains("missing required files")); + + let extra_wasm = BTreeSet::from([ + "package.json".to_owned(), + "dist/example.js".to_owned(), + "dist/example_bg.wasm".to_owned(), + "dist/extra_bg.wasm".to_owned(), + ]); + let error = validate_npm_pack_payload( + "@radroots/example-wasm", + &extra_wasm, + &required_files, + Some("dist/example_bg.wasm"), + ) + .expect_err("extra wasm should fail"); + assert!(error.contains("must include exactly dist/example_bg.wasm")); + } + + #[test] fn wasm_package_surface_requires_exported_dist_files() { let root = test_root("wasm_surface"); let package_dir = root.join("packages").join("example-wasm");