feat: add user module registration for reusable ES modules#45
feat: add user module registration for reusable ES modules#45simongdavies wants to merge 1 commit intohyperlight-dev:mainfrom
Conversation
e4f0b5d to
bcd0328
Compare
Add add_module/remove_module/clear_modules API to JSSandbox, allowing handlers (and other modules) to import reusable ES modules using the 'namespace:name' convention (default namespace: 'user'). Guest runtime: - Add UserModuleLoader (Resolver + Loader) for lazy compilation - Add register_module guest function - Modules compiled on first import, avoiding ordering issues Host library (hyperlight-js): - Add modules HashMap to JSSandbox with full CRUD API - add_module / add_module_ns / remove_module / remove_module_ns / clear_modules - Modules registered before handlers in get_loaded_sandbox() - Export DEFAULT_MODULE_NAMESPACE constant - Namespace validation: 'host' reserved, no colons allowed NAPI wrapper (js-host-api): - Add addModule / removeModule / clearModules on JSSandboxWrapper - Error enrichment wrapping for new sync methods Import capabilities: - User modules can import other user modules - User modules can import built-in modules (crypto, console, etc.) - User modules can import host function modules (host:X) - Missing modules fail at getLoadedSandbox() with clear errors Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
bcd0328 to
164b3f9
Compare
ludfjig
left a comment
There was a problem hiding this comment.
Reviewed the rust code, looks good!
| fn validate_module_identifier(value: &str, label: &str) -> napi::Result<()> { | ||
| if value.is_empty() || value.trim().is_empty() { | ||
| return Err(invalid_arg_error(&format!("{label} must not be empty"))); | ||
| } | ||
| if value.contains(':') { | ||
| return Err(invalid_arg_error(&format!("{label} must not contain ':'"))); | ||
| } | ||
| if value.chars().any(|c| c.is_control()) { | ||
| return Err(invalid_arg_error(&format!( | ||
| "{label} must not contain control characters" | ||
| ))); | ||
| } | ||
| if value.len() > MAX_MODULE_IDENTIFIER_LEN { | ||
| return Err(invalid_arg_error(&format!( | ||
| "{label} must not exceed {MAX_MODULE_IDENTIFIER_LEN} characters" | ||
| ))); | ||
| } | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
Similar logic is also used by add_module_ns in js_sandbox.rs (but not exactly). Could we factor it out maybe to have consistent implementations? I'm not sure I agree with the comment
/// Validation is intentionally duplicated here and in the inner Rust layer (`JSSandbox`)
/// for defense-in-depth. The NAPI layer validates first so that consumers get correct
/// `ERR_INVALID_ARG` error codes rather than the generic `ERR_INTERNAL` that would result
/// from the inner layer's `new_error!()`.
| self.user_modules | ||
| .modules | ||
| .borrow_mut() | ||
| .insert(module_name, module_source.into()); |
There was a problem hiding this comment.
what happens if this is already registered? Can we document this?
| /// # Validation | ||
| /// | ||
| /// The name must not be empty. Primary validation (colons, reserved | ||
| /// namespaces, duplicates) is enforced by the host-side `JSSandbox` layer; | ||
| /// this check provides defense-in-depth at the guest boundary. |
There was a problem hiding this comment.
I think it's unclear what happens if it's empty, maybe we could mention that it just returns an error in this case
| } | ||
| if value.len() > MAX_MODULE_IDENTIFIER_LEN { | ||
| return Err(invalid_arg_error(&format!( | ||
| "{label} must not exceed {MAX_MODULE_IDENTIFIER_LEN} characters" |
There was a problem hiding this comment.
nit: error message should state bytes instead of characters
| if module_name.is_empty() { | ||
| return Err(new_error!("Module name must not be empty")); | ||
| } | ||
| if namespace.is_empty() { | ||
| return Err(new_error!("Module namespace must not be empty")); | ||
| } | ||
| if module_name.contains(':') { | ||
| return Err(new_error!("Module name must not contain ':'")); | ||
| } | ||
| if namespace.contains(':') { | ||
| return Err(new_error!("Module namespace must not contain ':'")); | ||
| } |
There was a problem hiding this comment.
this is duplicated with add_module_rs, but why do we need to validate this regardless? Wouldn't it be caught by below self.modules.remove()?
|
|
||
| /// Validates that a namespace is not reserved (e.g. `"host"`). | ||
| fn validate_namespace_not_reserved(namespace: &str) -> napi::Result<()> { | ||
| // Keep in sync with RESERVED_NAMESPACES in js_sandbox.rs |
There was a problem hiding this comment.
can we share a variable between crates instead?
| } | ||
|
|
||
| #[test] | ||
| fn test_remove_module() { |
There was a problem hiding this comment.
could we add a test that also runs some script after removing a module, to make sure things are removed?
Add add_module/remove_module/clear_modules API to JSSandbox, allowing handlers (and other modules) to import reusable ES modules using the 'namespace:name' convention (default namespace: 'user').
Guest runtime:
Host library (hyperlight-js):
NAPI wrapper (js-host-api):
Import capabilities: