feat(wfe-yaml): add HTTP ops, module loader, and npm support via esm.sh

Phase 4 — Permission-gated HTTP fetch op:
- op_fetch with net permission check on every request
- globalThis.fetch() wrapper with .json()/.text() methods
- Supports GET/POST/PUT/DELETE with headers and body

Phase 5 — Module loader:
- WfeModuleLoader resolving npm: → esm.sh, https://, file://, relative paths
- All resolution paths permission-checked
- Bare path resolution (/) for esm.sh sub-module redirects
- Dynamic import rejection unless permissions.dynamic_import: true
- esm.sh auto-added to net allowlist when modules declared

Mandatory npm integration test (is-number via esm.sh).
25 new tests. 133 total deno tests, 326 total workspace tests.
This commit is contained in:
2026-03-25 23:02:51 +00:00
parent 6fec7dbab5
commit 1a84da40bf
11 changed files with 1026 additions and 6 deletions

View File

@@ -1,10 +1,13 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use deno_core::JsRuntime;
use deno_core::RuntimeOptions;
use wfe_core::WfeError;
use super::config::DenoConfig;
use super::module_loader::WfeModuleLoader;
use super::ops::workflow::{wfe_ops, StepMeta, StepOutputs, WorkflowInputs};
use super::permissions::PermissionChecker;
@@ -16,8 +19,18 @@ pub fn create_runtime(
) -> Result<JsRuntime, WfeError> {
let ext = wfe_ops::init();
// Build permissions, auto-adding esm.sh if modules are declared.
let mut permissions = config.permissions.clone();
if !config.modules.is_empty() && !permissions.net.iter().any(|h| h == "esm.sh") {
permissions.net.push("esm.sh".to_string());
}
let checker = Rc::new(RefCell::new(PermissionChecker::from_config(&permissions)));
let module_loader = WfeModuleLoader::new(checker.clone());
let runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![ext],
module_loader: Some(Rc::new(module_loader)),
..Default::default()
});
@@ -34,12 +47,18 @@ pub fn create_runtime(
state.put(StepMeta {
name: step_name.to_string(),
});
state.put(PermissionChecker::from_config(&config.permissions));
state.put(PermissionChecker::from_config(&permissions));
}
Ok(runtime)
}
/// Returns whether esm.sh would be auto-added for the given config.
/// Exposed for testing.
pub fn would_auto_add_esm_sh(config: &DenoConfig) -> bool {
!config.modules.is_empty() && !config.permissions.net.iter().any(|h| h == "esm.sh")
}
#[cfg(test)]
mod tests {
use super::*;
@@ -78,4 +97,46 @@ mod tests {
let meta = state.borrow::<StepMeta>();
assert_eq!(meta.name, "my-step");
}
#[test]
fn auto_add_esm_sh_when_modules_declared() {
let config = DenoConfig {
script: Some("1".to_string()),
file: None,
permissions: DenoPermissions::default(),
modules: vec!["npm:lodash@4".to_string()],
env: HashMap::new(),
timeout_ms: None,
};
assert!(would_auto_add_esm_sh(&config));
}
#[test]
fn no_auto_add_esm_sh_when_no_modules() {
let config = DenoConfig {
script: Some("1".to_string()),
file: None,
permissions: DenoPermissions::default(),
modules: vec![],
env: HashMap::new(),
timeout_ms: None,
};
assert!(!would_auto_add_esm_sh(&config));
}
#[test]
fn no_auto_add_esm_sh_when_already_present() {
let config = DenoConfig {
script: Some("1".to_string()),
file: None,
permissions: DenoPermissions {
net: vec!["esm.sh".to_string()],
..Default::default()
},
modules: vec!["npm:lodash@4".to_string()],
env: HashMap::new(),
timeout_ms: None,
};
assert!(!would_auto_add_esm_sh(&config));
}
}