diff --git a/README.md b/README.md index 601e1f6..fd53719 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,27 @@ Scriptify is a library to evaluate scripts written in JavaScript with ability to This library is designed to execute JavaScript scripts and has the ability to register global functions and constants. It also allows you to configure security for executing scripts. +## Quick Start + +```java +import org.densy.scriptify.api.exception.ScriptException; +import org.densy.scriptify.js.graalvm.script.JsScript; + +public class Main { + public static void main(String[] args) throws ScriptException { + JsScript script = new JsScript(); + Object result = script.evalOneShot("1 + 2 + 3"); + System.out.println(result); + } +} +``` + +For Rhino use `org.densy.scriptify.js.rhino.script.JsScript` instead. + +## Documentation + +Full documentation is available in [docs/index.md](docs/index.md). + ## Other scripts support - [TypeScript](https://github.com/DensyDev/Scriptify-TypeScript) - TS support using swc4j - [TypeScript Declaration Generator](https://github.com/DensyDev/Scriptify-DTS-Generator) - Declaration generator for JS or TS @@ -61,4 +82,4 @@ For adding a library with JS for Rhino or GraalVM: ```groovy implementation "org.densy.scriptify:script-js-rhino:1.6.0-SNAPSHOT" implementation "org.densy.scriptify:script-js-graalvm:1.6.0-SNAPSHOT" -``` \ No newline at end of file +``` diff --git a/docs/constants.md b/docs/constants.md new file mode 100644 index 0000000..b27045f --- /dev/null +++ b/docs/constants.md @@ -0,0 +1,83 @@ +# Constants + +Constants expose named Java values to scripts. + +## Constant Interface + +```java +org.densy.scriptify.api.script.constant.ScriptConstant +``` + +Create a constant: + +```java +ScriptConstant constant = ScriptConstant.of("appName", "Scriptify"); +``` + +Or implement it: + +```java +public final class AppNameConstant implements ScriptConstant { + @Override + public String getName() { + return "appName"; + } + + @Override + public Object getValue() { + return "Scriptify"; + } +} +``` + +## Register Globally + +```java +script.getConstantManager().register(ScriptConstant.of("appName", "Scriptify")); +script.evalOneShot("appName"); +``` + +Global constants are exported to the global module during compilation. + +## Export From a Module + +```java +SimpleScriptInternalModule app = new SimpleScriptInternalModule("app"); +app.export(new ScriptConstantExport(ScriptConstant.of("name", "Scriptify"))); +script.getModuleManager().addModule(app); +``` + +```js +import { name } from "app"; +name; +``` + +## Constant Manager + +`ScriptConstantManager` provides: + +```java +Map getConstants(); +ScriptConstant getConstant(String name); +void register(ScriptConstant constant); +void remove(String name); +``` + +Default implementation: + +```java +org.densy.scriptify.core.script.constant.StandardConstantManager +``` + +## Built-In Core Constants + +| Constant | Value | +| --- | --- | +| `baseDir` | Current JVM working directory as an absolute string. | +| `osName` | `System.getProperty("os.name")`. | + +These are exported by `StandardScriptModule`. + +## Deprecated Common Manager + +`CommonConstantManager` is deprecated. Prefer module exports. diff --git a/docs/custom-runtime.md b/docs/custom-runtime.md new file mode 100644 index 0000000..51e2845 --- /dev/null +++ b/docs/custom-runtime.md @@ -0,0 +1,98 @@ +# Custom Runtime + +Scriptify can support additional scripting runtimes by implementing `Script`. + +## Required Contract + +```java +public final class MyScript implements Script { + private final ScriptSecurityManager securityManager = new StandardSecurityManager(); + private final ScriptFunctionManager functionManager = new StandardFunctionManager(); + private final ScriptConstantManager constantManager = new StandardConstantManager(); + private final ScriptModuleManager moduleManager = createModuleManager(); + + @Override + public ScriptSecurityManager getSecurityManager() { + return securityManager; + } + + @Override + public ScriptModuleManager getModuleManager() { + return moduleManager; + } + + @Override + public ScriptFunctionManager getFunctionManager() { + return functionManager; + } + + @Override + public ScriptConstantManager getConstantManager() { + return constantManager; + } + + @Override + public CompiledScript compile(String source) throws ScriptException { + throw new UnsupportedOperationException(); + } +} +``` + +## Runtime Responsibilities + +A runtime implementation should: + +- create the engine context; +- configure security before script execution; +- expose global functions; +- expose global constants; +- expose internal modules; +- load external modules; +- apply `ScriptAccess.ALL` and `ScriptAccess.EXPLICIT`; +- convert script values into Java values before invoking `ScriptFunction`; +- convert Java return values back into runtime values; +- wrap runtime failures in `ScriptException`; +- close engine resources in `CompiledScript.close`. + +## Function Bridge + +Your runtime needs an engine-specific callable wrapper for `ScriptFunctionDefinition`. + +That wrapper should: + +- receive script arguments; +- convert them to Java values; +- find a matching `ScriptFunctionExecutor`; +- call `executor.execute(script, args...)`; +- convert the result back to the script engine. + +## Module Export Resolver + +Implement: + +```java +ScriptModuleExportResolverFactory +ScriptModuleExportResolver +``` + +The resolver maps Scriptify exports to runtime values: + +- `ScriptFunctionExport`; +- `ScriptFunctionDefinitionExport`; +- `ScriptConstantExport`; +- `ScriptValueExport`; +- custom `ScriptExport` implementations if you add them. + +Throw `ScriptModuleWrongContextException` if the factory receives a context object from another runtime. + +## Security Integration + +If the engine supports host class lookup restrictions, wire it to `ScriptSecurityManager.getExcludes()`. + +If the engine supports file system hooks, route file paths through: + +```java +script.getSecurityManager().getPathAccessor() +``` + +If the engine exposes Java objects/classes, implement `ScriptAccess.EXPLICIT`. diff --git a/docs/examples/custom_script_runtime.md b/docs/examples/custom_script_runtime.md deleted file mode 100644 index fc5e75e..0000000 --- a/docs/examples/custom_script_runtime.md +++ /dev/null @@ -1,117 +0,0 @@ -# Custom script runtime - -You can register your script runtime as you do with Rhino or GraalVM - -___ - -### Registration of custom runtime on the example of GraalVM - -Let's create our script: -```java -import script.org.densy.scriptify.api.Script; -import constant.script.org.densy.scriptify.api.ScriptConstant; -import constant.script.org.densy.scriptify.api.ScriptConstantManager; -import function.script.org.densy.scriptify.api.ScriptFunction; -import function.script.org.densy.scriptify.api.ScriptFunctionManager; -import org.graalvm.polyglot.*; - -public class JsScript implements Script { - - /** - * Creating the runtime engine itself - */ - private final Context context = Context.create(); - - private ScriptFunctionManager functionManager; - private ScriptConstantManager constantManager; - - // Getters and setters for function and constant managers - - @Override - public ScriptFunctionManager getFunctionManager() { - return functionManager; - } - - @Override - public void setFunctionManager(ScriptFunctionManager functionManager) { - this.functionManager = functionManager; - } - - @Override - public ScriptConstantManager getConstantManager() { - return constantManager; - } - - @Override - public void setConstantManager(ScriptConstantManager constantManager) { - this.constantManager = constantManager; - } - - /** - * Function that is called when the script is evaluated. - * - * @param script Script code itself - */ - @Override - public void eval(String script) { - Value bindings = context.getBindings("js"); - - // Add all custom functions and constants to our script. - // It is important to check that the function and constant managers are set, - // because the user may not specify them and then we will get an error when trying to evaluate the script. - - if (functionManager != null) { - for (ScriptFunction function : functionManager.getFunctions().values()) { - bindings.putMember(function.getName(), new JsFunction(this, function)); - } - } - - if (constantManager != null) { - for (ScriptConstant constant : constantManager.getConstants().values()) { - bindings.putMember(constant.getName(), constant.getValue()); - } - } - - // Finally evaluating our script. - context.eval("js", script); - } -} -``` - -Now let's create a function: -```java -import exception.org.densy.scriptify.api.ScriptFunctionException; -import script.org.densy.scriptify.api.Script; -import function.script.org.densy.scriptify.api.ScriptFunction; -import org.graalvm.polyglot.proxy.ProxyExecutable; -import org.graalvm.polyglot.Value; - -public class JsFunction implements ProxyExecutable { - - private final Script script; - private final ScriptFunction function; - - public JsFunction(Script script, ScriptFunction function) { - this.script = script; - this.function = function; - } - - @Override - public Object execute(Value... arguments) { - // Convert Value... into Object[] so that we could pass arguments to our function: - ScriptFunctionArgument[] args = new ScriptFunctionArgument[arguments.length]; - for (int i = 0; i < arguments.length; i++) { - args[i] = ScriptFunctionArgument.of(arguments[i].as(Object.class)); - } - - // Now let's call the function with arguments and handle possible exceptions: - try { - return function.invoke(script, args); - } catch (ScriptFunctionException e) { - throw new RuntimeException(e); - } - } -} -``` - -That's it! We have created JS scripts implementation using GraalVM. You can also write an implementation for any other language you want. \ No newline at end of file diff --git a/docs/examples/functions_and_constants.md b/docs/examples/functions_and_constants.md deleted file mode 100644 index 3831969..0000000 --- a/docs/examples/functions_and_constants.md +++ /dev/null @@ -1,51 +0,0 @@ -# Custom functions and consultants - -You can register a custom function or constant and then use it in your script - -___ - -Registration of custom functions: -```java -script.getFunctionManager().register(new ScriptFunction() { - @Override - public String getName() { - return "yourFunction"; - } - - @Override - public Object invoke(Script script, ScriptFunctionArgument[] args) { - System.out.println("Invoked custom function with arguments: " + Arrays.toString(args)); - return null; - } -}); -``` - -Registration of custom constants: -```java -script.getConstantManager().register(ScriptConstant.of("yourConstant", "Hello world!")); -``` -or: -```java -script.getConstantManager().register(new ScriptConstant() { - @Override - public String getName() { - return "yourConstant"; - } - - @Override - public Object getValue() { - return "Hello world!"; - } -}); -``` - -___ - -Using custom functions or constants: -```java -yourFunction(yourConstant); -``` -Result: -``` -Invoked custom function with arguments: [Hello world!] -``` \ No newline at end of file diff --git a/docs/examples/scripts/file_system.md b/docs/examples/scripts/file_system.md deleted file mode 100644 index 6999fb8..0000000 --- a/docs/examples/scripts/file_system.md +++ /dev/null @@ -1,30 +0,0 @@ -# File System -Example of working with the file system using scripts - -```js -// Downloading a file from url -const url = "https://repo.densy.org/snapshots/org/densy/scriptify/script-js/1.0.3-SNAPSHOT/maven-metadata.xml"; -const fileName = "maven-metadata.xml"; - -if (!existsFile(fileName)) { - print(`File ${fileName} not found. Downloading...`) - downloadFromUrl(url, fileName) -} - -const foundFiles = []; - -// Search all files in the current folder -listFiles("").forEach(file => { - print(`Found file in folder ${file}.`) - // Get the file name and delete the file maven-metadata.xml - const foundFileName = normalizePath(file).replace(`${normalizePath(baseDir)}/`, ""); - if (foundFileName === fileName) { - print(`File ${file} found. Delete it.`); - deleteFile(file); - } - foundFiles.push(file); -}); - -// Write all saved current actions to a file -writeFile("summary.json", JSON.stringify(foundFiles)); -``` \ No newline at end of file diff --git a/docs/examples/using_scriptify.md b/docs/examples/using_scriptify.md deleted file mode 100644 index 0527db9..0000000 --- a/docs/examples/using_scriptify.md +++ /dev/null @@ -1,35 +0,0 @@ -# Using Scriptify - -___ - -### Using JS with GraalVM -```kotlin -implementation "org.densy.scriptify:script-js-graalvm:1.3.0-SNAPSHOT" -``` -### Using JS with Rhino -```kotlin -implementation "org.densy.scriptify:script-js-rhino:1.3.0-SNAPSHOT" -``` -___ - -Running the script (GraalVM): -```java -import org.densy.scriptify.js.graalvm.script.JsScript; -import org.densy.scriptify.core.script.StandardConstantManager; -import org.densy.scriptify.core.script.StandardFunctionManager; -import org.densy.scriptify.api.ScriptException; - -JsScript script = new JsScript(); -script.setFunctionManager(new StandardFunctionManager()); -script.setConstantManager(new StandardConstantManager()); -try { - script.eval("print('Hello world from JS!')"); -} catch(ScriptException e) { - throw new RuntimeException(e); -} -``` - -Running a script from a file: -```java -script.eval(Files.readString(Path.of("./script.js"))); -``` \ No newline at end of file diff --git a/docs/exceptions.md b/docs/exceptions.md new file mode 100644 index 0000000..9757c27 --- /dev/null +++ b/docs/exceptions.md @@ -0,0 +1,49 @@ +# Exceptions + +Scriptify wraps script and integration failures in typed exceptions. + +## Base Exception + +```java +ScriptException +``` + +Base checked exception for script failures. + +## Function Exceptions + +| Exception | Meaning | +| --- | --- | +| `ScriptFunctionException` | Function invocation failed. | +| `ScriptFunctionArgumentCountMismatchException` | Script called a function with too few or too many arguments. | +| `ScriptFunctionArgumentTypeMismatchException` | A script argument could not be assigned to the Java executor parameter type. | + +Function executor failures are wrapped in `ScriptFunctionException`. + +## Module Exceptions + +| Exception | Meaning | +| --- | --- | +| `ScriptModuleLoadException` | External module source could not be loaded. | +| `ScriptModuleCopyException` | Copying module exports encountered a conflicting export. | +| `ScriptModuleWrongContextException` | A module export resolver received an incompatible runtime context. | + +## Runtime Wrapping + +Runtime implementations catch engine-specific errors and usually wrap them in `ScriptException`. + +Example: + +```java +try { + script.evalOneShot(source); +} catch (ScriptException e) { + throw new RuntimeException("Script failed", e); +} +``` + +## Security Exceptions + +Path access may throw `SecurityException` when `securityMode` is enabled and a path is not allowed. + +Security exceptions can be wrapped by runtime code if they occur during script evaluation. diff --git a/docs/external-modules.md b/docs/external-modules.md new file mode 100644 index 0000000..a7eaa75 --- /dev/null +++ b/docs/external-modules.md @@ -0,0 +1,90 @@ +# External Modules + +External modules load JavaScript source from files or byte streams. + +## File Module + +```java +script.getModuleManager().addModule( + new SimpleScriptFileExternalModule("external", Path.of("external.mjs")) +); +``` + +If the path is a directory, `SimpleScriptFileExternalModule` loads `index.mjs` by default. + +Custom entry point: + +```java +new SimpleScriptFileExternalModule("external", Path.of("scripts"), "main.mjs"); +``` + +## Stream Module + +```java +script.getModuleManager().addModule( + new SimpleScriptStreamExternalModule( + "dynamic", + "dynamic.mjs", + "export const value = 10;".getBytes(StandardCharsets.UTF_8) + ) +); +``` + +Supplier-based modules reload bytes on every load: + +```java +new SimpleScriptStreamExternalModule("dynamic", "dynamic.mjs", () -> loadBytes()); +``` + +## GraalVM Behavior + +GraalVM uses `VirtualModuleFileSystem`. + +Behavior: + +- internal modules are generated as virtual JavaScript module source; +- file external modules are resolved to real files after path security checks; +- stream external modules are loaded into virtual channels; +- scripts are evaluated as JavaScript modules. + +Supported JavaScript: + +```js +import { value } from "external"; +``` + +## Rhino Behavior + +Rhino loads source bytes and transforms supported imports/exports. + +Supported import forms: + +```js +import * as name from "module"; +import { value, other as alias } from "module"; +import defaultValue from "module"; +import "module"; +``` + +Supported export forms: + +```js +export { value, internalName as publicName }; +export const value = 1; +export let value = 1; +export var value = 1; +export function name() { } +export class Name { } +export default expression; +``` + +Rhino transformation is not a full JavaScript parser. Keep import/export declarations top-level and normally formatted. + +## Security + +File modules should be used with `securityMode` enabled when scripts or module paths are not fully trusted. + +```java +script.getSecurityManager().setSecurityMode(true); +script.getSecurityManager().addExclude(SecurityExclude.ofPath("scripts")); +``` diff --git a/docs/functions.md b/docs/functions.md new file mode 100644 index 0000000..81b6c3c --- /dev/null +++ b/docs/functions.md @@ -0,0 +1,136 @@ +# Functions + +Functions expose Java methods to scripts. + +## Function Interface + +A function implements: + +```java +org.densy.scriptify.api.script.function.ScriptFunction +``` + +Minimal function: + +```java +public final class AddFunction implements ScriptFunction { + @Override + public String getName() { + return "add"; + } + + @ExecuteAt + public Number execute( + @Argument(name = "left") Number left, + @Argument(name = "right") Number right + ) { + return left.doubleValue() + right.doubleValue(); + } +} +``` + +## Register Globally + +```java +script.getFunctionManager().register(new AddFunction()); +script.evalOneShot("add(2, 3)"); +``` + +Global functions are added to the runtime global module during compilation. + +## Export From a Module + +```java +SimpleScriptInternalModule math = new SimpleScriptInternalModule("math"); +math.export(new ScriptFunctionExport(new AddFunction())); +script.getModuleManager().addModule(math); +``` + +```js +import * as math from "math"; +math.add(2, 3); +``` + +## Execution Methods + +Mark callable methods with `@ExecuteAt`. + +```java +@ExecuteAt +public String execute(@Argument(name = "value") String value) { + return value.toUpperCase(); +} +``` + +A single function class may have multiple `@ExecuteAt` methods. Runtime dispatch selects a compatible executor by argument count and vararg compatibility; the core executor then validates Java types. + +Avoid ambiguous overloads with the same script argument count unless conversion is predictable. + +## Arguments + +Use `@Argument` for script-provided values. + +```java +@Argument(name = "filePath") String filePath +``` + +Optional arguments: + +```java +@Argument(name = "recursive", required = false) Boolean recursive +``` + +Varargs: + +```java +@ExecuteAt +public void execute(@Argument(name = "args") Object... args) { +} +``` + +## Executor Injection + +Use `@Executor` to receive the current `Script`. + +```java +@ExecuteAt +public String execute( + @Executor Script script, + @Argument(name = "filePath") String filePath +) { + Path path = script.getSecurityManager().getFileSystem().getPath(filePath); + return Files.readString(path); +} +``` + +This is how standard file functions access security-aware paths. + +## Function Manager + +`ScriptFunctionManager` provides: + +```java +ScriptFunctionDefinitionFactory getFunctionDefinitionFactory(); +void setFunctionDefinitionFactory(ScriptFunctionDefinitionFactory factory); +Map getFunctions(); +ScriptFunctionDefinition getFunction(String name); +void register(ScriptFunction function); +void remove(String name); +``` + +Default implementation: + +```java +org.densy.scriptify.core.script.function.StandardFunctionManager +``` + +Behavior: + +- duplicate function names are rejected; +- removing a missing function throws; +- returned maps are unmodifiable views; +- `ScriptFunctionDefinitionFactory` can be replaced for custom function metadata. + +## Deprecated Common Manager + +`CommonFunctionManager` exists for compatibility but is deprecated. Use `StandardScriptModule` or your own internal modules instead. diff --git a/docs/http-module.md b/docs/http-module.md new file mode 100644 index 0000000..5468950 --- /dev/null +++ b/docs/http-module.md @@ -0,0 +1,101 @@ +# HTTP Module + +The HTTP module is: + +```java +org.densy.scriptify.http.script.module.HttpScriptModule +``` + +Module name: + +```text +http +``` + +## Setup + +```java +script.getModuleManager().addModule(new HttpScriptModule()); +``` + +With restricted Java member access: + +```java +script.getModuleManager().setScriptAccess(ScriptAccess.EXPLICIT); +``` + +`HttpRequest`, `HttpMethod`, and `OutputType` are annotated for explicit access. + +## Exports + +| Export | Type | Description | +| --- | --- | --- | +| `HttpRequest` | Java class | Request object. | +| `HttpMethod` | Java enum | HTTP method enum. | +| `OutputType` | Java enum | Response output type enum. | +| `createHttpRequest` | Scriptify function | Factory function for `HttpRequest`. | + +## HttpMethod + +Supported values: + +- `GET` +- `PUT` +- `POST` +- `DELETE` +- `PATCH` +- `HEAD` +- `OPTIONS` +- `TRACE` + +## OutputType + +Supported values: + +- `STRING` +- `BYTES` + +## HttpRequest + +Constructor: + +```java +new HttpRequest(String url, HttpMethod method) +``` + +Exported methods: + +| Method | Description | +| --- | --- | +| `setBody(body, mediaType)` | Sets request body and media type. | +| `addHeader(key, value)` | Adds a header. | +| `send(outputType)` | Sends the request. Accepts `OutputType` or string. | + +## JavaScript Usage + +```js +import * as http from "http"; + +const request = new http.HttpRequest("https://example.com", http.HttpMethod.GET); +request.addHeader("Accept", "text/plain"); + +const response = request.send(http.OutputType.STRING); +``` + +Factory function: + +```js +import { createHttpRequest } from "http"; + +const request = createHttpRequest("https://example.com", "GET"); +``` + +## Behavior + +The module uses OkHttp. Each `send` creates a new `OkHttpClient`, builds an OkHttp request, sends it synchronously, and returns either response text or response bytes. + +For `POST` and `PUT`, an empty request body is created when no body was configured. + +## Security Notes + +The HTTP module performs network requests. Treat it as a privileged capability. `ScriptAccess.EXPLICIT` controls Java members on exported classes, but it does not block the exported `createHttpRequest` function or network access itself. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..9907016 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,52 @@ +# Scriptify Documentation + +Scriptify is a JVM library for embedding JavaScript execution into Java applications. It provides script runtimes, Java function exports, constants, modules, host-access control, and security-aware file access. + +This documentation is organized by feature. Start with installation and runtime selection, then move to the API areas you need. + +## Contents + +| Page | Topic | +| --- | --- | +| [Installation](installation.md) | Gradle/Maven coordinates and module artifacts. | +| [Runtime Selection](runtimes.md) | GraalVM vs Rhino behavior and tradeoffs. | +| [Script Lifecycle](script-lifecycle.md) | `evalOneShot`, `compile`, `CompiledScript`, `addExtraScript`. | +| [Functions](functions.md) | Custom Java functions, annotations, argument binding, managers. | +| [Constants](constants.md) | Global constants and module constant exports. | +| [Modules](modules.md) | Internal modules, exports, global module, import support. | +| [External Modules](external-modules.md) | File and stream modules, GraalVM/Rhino differences. | +| [ScriptAccess](script-access.md) | `ALL`, `EXPLICIT`, and `@ScriptAccess.Export`. | +| [Security](security.md) | Class/path restrictions, security mode, safe exposure patterns. | +| [Standard Module](standard-module.md) | Utility, file, crypto, random, OS, and zip functions. | +| [HTTP Module](http-module.md) | `HttpScriptModule`, `HttpRequest`, methods, output types. | +| [Custom Runtime](custom-runtime.md) | Implementing another engine behind Scriptify APIs. | +| [Exceptions](exceptions.md) | Error model and exception types. | +| [Limitations](limitations.md) | Runtime differences, deprecated APIs, known constraints. | + +## Minimal Setup + +```java +import org.densy.scriptify.js.graalvm.script.JsScript; + +JsScript script = new JsScript(); +Object result = script.evalOneShot("1 + 2 + 3"); +``` + +For Rhino, use `org.densy.scriptify.js.rhino.script.JsScript`. + +## Recommended API Style + +Use modules for new integrations: + +```java +SimpleScriptInternalModule module = new SimpleScriptInternalModule("app"); +module.export(new ScriptValueExport("version", "1.0.0")); +script.getModuleManager().addModule(module); +``` + +```js +import { version } from "app"; +version; +``` + +Global function and constant managers still exist, but module exports are easier to reason about and are the preferred surface for application APIs. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..d8cb873 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,85 @@ +# Installation + +Scriptify is published as several artifacts. Choose the JavaScript runtime you want and add optional modules as needed. + +## Repository + +```kotlin +repositories { + maven { + name = "densyRepositorySnapshots" + url = uri("https://repo.densy.org/snapshots") + } +} +``` + +Maven: + +```xml + + + densy-repository-snapshots + https://repo.densy.org/snapshots + + +``` + +## Runtime Artifacts + +Use one runtime artifact in most applications: + +```kotlin +dependencies { + implementation("org.densy.scriptify:script-js-graalvm:1.6.1-SNAPSHOT") +} +``` + +or: + +```kotlin +dependencies { + implementation("org.densy.scriptify:script-js-rhino:1.6.1-SNAPSHOT") +} +``` + +## Optional Artifacts + +| Artifact | Use | +| --- | --- | +| `api` | Interfaces and exceptions for integrations. | +| `core` | Default managers, modules, exports, and security implementations. | +| `common` | Standard utility module. | +| `http` | HTTP module built on OkHttp. | +| `script-js-graalvm` | GraalVM JavaScript runtime. | +| `script-js-rhino` | Rhino JavaScript runtime. | + +Add optional modules directly when your application references their classes: + +```kotlin +dependencies { + implementation("org.densy.scriptify:common:1.6.1-SNAPSHOT") + implementation("org.densy.scriptify:http:1.6.1-SNAPSHOT") +} +``` + +## Java Version + +The project is built with a Java 17 toolchain. + +## Choosing Dependencies + +For a simple app that runs JavaScript and exposes your own modules, a runtime artifact is enough. + +For an app that imports the standard utility module: + +```kotlin +implementation("org.densy.scriptify:script-js-graalvm:1.6.1-SNAPSHOT") +implementation("org.densy.scriptify:common:1.6.1-SNAPSHOT") +``` + +For HTTP: + +```kotlin +implementation("org.densy.scriptify:script-js-graalvm:1.6.1-SNAPSHOT") +implementation("org.densy.scriptify:http:1.6.1-SNAPSHOT") +``` diff --git a/docs/limitations.md b/docs/limitations.md new file mode 100644 index 0000000..baf35a9 --- /dev/null +++ b/docs/limitations.md @@ -0,0 +1,65 @@ +# Limitations and Compatibility Notes + +This page lists behavior that is important when designing a Scriptify integration. + +## Deprecated APIs + +Use: + +```java +evalOneShot(source) +compile(source) +``` + +Avoid: + +```java +eval(source) +``` + +`eval` is deprecated and delegates to `evalOneShot`. + +`CommonFunctionManager` and `CommonConstantManager` are deprecated. Use modules instead. + +## Runtime Differences + +GraalVM and Rhino do not return identical value types. + +- GraalVM returns `org.graalvm.polyglot.Value`. +- Rhino returns Java/Rhino objects. + +If your application needs runtime-independent result handling, normalize results at your integration boundary. + +## Rhino Module Parsing + +Rhino module support is implemented by Scriptify source transformation. + +Supported import/export forms are documented in [External Modules](external-modules.md). Avoid unusual formatting, nested import/export text, and import/export statements inside strings or comments. + +## Security Scope + +`ScriptAccess.EXPLICIT` controls members of exported Java classes and objects. It does not remove Scriptify functions that you explicitly export. + +`securityMode` controls host class lookup and Scriptify path access. It does not automatically make arbitrary exported Java APIs safe. + +## File Access + +Path security applies when code uses `SecurityFileSystem` or runtime file hooks wired to `SecurityPathAccessor`. + +If you export a Java object that performs file I/O internally, Scriptify cannot automatically enforce path restrictions inside that object. + +## Standard Module Capabilities + +`StandardScriptModule` is broad. It includes file access, command execution, environment access, zip operations, and network download. + +For untrusted scripts, build a smaller internal module with only the functions you intend to expose. + +## Function Overloads + +Function executor matching is intentionally simple. Prefer clear overloads and avoid multiple `@ExecuteAt` methods with the same script argument count unless their Java types are easy to distinguish after runtime conversion. + +## Mutable Managers + +Managers are mutable. Configure script instances before compiling. + +Compiled scripts should be treated as snapshots of the runtime configuration at compile time. diff --git a/docs/modules.md b/docs/modules.md new file mode 100644 index 0000000..226a50e --- /dev/null +++ b/docs/modules.md @@ -0,0 +1,95 @@ +# Modules + +Modules are the preferred way to expose application APIs to scripts. + +## Module Manager + +```java +ScriptModuleExportResolverFactory getModuleExportResolver(); +void setModuleExportResolver(ScriptModuleExportResolverFactory factory); + +ScriptAccess getScriptAccess(); +void setScriptAccess(ScriptAccess scriptAccess); + +ScriptInternalModule getGlobalModule(); +Map getModules(); +ScriptModule getModule(String name); +void addModule(ScriptModule module); +void removeModule(String name); +``` + +Every runtime has its own module manager implementation. + +## Module Types + +| Type | Interface | Description | +| --- | --- | --- | +| Internal | `ScriptInternalModule` | Java-defined exports. | +| External | `ScriptExternalModule` | JavaScript source loaded from file or bytes. | + +## Internal Module + +```java +SimpleScriptInternalModule app = new SimpleScriptInternalModule("app"); +app.export(new ScriptValueExport("version", "1.0.0")); +script.getModuleManager().addModule(app); +``` + +```js +import { version } from "app"; +version; +``` + +## Global Module + +Exports in `getGlobalModule()` are available without import. + +Runtime implementations also copy globally registered functions and constants into the global module before compilation. + +Use global exports sparingly. Modules are clearer for larger APIs. + +## Export Types + +| Export | Description | +| --- | --- | +| `ScriptFunctionExport` | Exports a `ScriptFunction`; runtime creates a callable wrapper. | +| `ScriptFunctionDefinitionExport` | Exports an existing function definition. | +| `ScriptConstantExport` | Exports a `ScriptConstant`. | +| `ScriptValueExport` | Exports a Java value, object instance, enum, or `Class`. | + +## Java Value Exports + +```java +module.export(new ScriptValueExport("config", Map.of("mode", "dev"))); +module.export(new ScriptValueExport("Service", UserService.class)); +module.export(new ScriptValueExport("service", new UserService())); +``` + +Access to Java members depends on `ScriptAccess`. + +## Copying Modules + +`ScriptInternalModule.copy` copies exports from another internal module. + +```java +SimpleScriptInternalModule std = new SimpleScriptInternalModule("std"); +std.copy(new StandardScriptModule()); +script.getModuleManager().addModule(std); +``` + +Copy rejects conflicting exports with the same name and different hash. + +## Import Syntax + +GraalVM supports native module imports through its Scriptify virtual file system. + +Rhino supports these top-level forms: + +```js +import * as name from "module"; +import { value, other as alias } from "module"; +import defaultValue from "module"; +import "module"; +``` + +For runtime-specific details, see [Runtime Selection](runtimes.md) and [External Modules](external-modules.md). diff --git a/docs/runtimes.md b/docs/runtimes.md new file mode 100644 index 0000000..52d78d2 --- /dev/null +++ b/docs/runtimes.md @@ -0,0 +1,87 @@ +# Runtime Selection + +Scriptify defines runtime-independent contracts in `api`. JavaScript execution is provided by runtime modules. + +## GraalVM Runtime + +Class: + +```java +org.densy.scriptify.js.graalvm.script.JsScript +``` + +Return type: + +```java +Script +``` + +GraalVM uses `org.graalvm.polyglot.Context` and evaluates JavaScript as modules. + +Supported behavior: + +- ES module source evaluation; +- imports through a virtual module file system; +- internal and external Scriptify modules; +- Java functions through `ProxyExecutable`; +- Java values/classes through GraalVM host interop; +- `ScriptAccess.ALL`; +- `ScriptAccess.EXPLICIT` through GraalVM `HostAccess.EXPLICIT`; +- class lookup restrictions through `allowHostClassLookup`; +- path restrictions for external file modules through Scriptify path access. + +Use GraalVM when you need modern JavaScript module behavior and stronger engine-level host access controls. + +## Rhino Runtime + +Class: + +```java +org.densy.scriptify.js.rhino.script.JsScript +``` + +Return type: + +```java +Script +``` + +Rhino uses `org.mozilla.javascript.Context` in ES6 mode. + +Supported behavior: + +- one-shot and compiled JavaScript evaluation; +- internal Scriptify modules; +- external Scriptify modules through source transformation; +- supported top-level import/export forms; +- Java functions through Rhino `Function`; +- `ScriptAccess.ALL`; +- `ScriptAccess.EXPLICIT` through restricted Java object/class wrappers; +- class lookup restrictions through Rhino `ClassShutter`. + +Rhino is useful when you want a lighter runtime dependency. Its module support is Scriptify-provided, not a full ECMAScript module implementation. + +## Runtime Differences + +| Feature | GraalVM | Rhino | +| --- | --- | --- | +| Evaluation result | `org.graalvm.polyglot.Value` | Java/Rhino object | +| Module execution | Native ESM-style source | Source transformation | +| Java function bridge | `ProxyExecutable` | Rhino `Function` | +| `ScriptAccess.EXPLICIT` | GraalVM host access | Restricted wrappers | +| External file modules | Virtual file system | Loaded and transformed by Scriptify | +| Best fit | Modern JS/module use | Lightweight embedding | + +## Import Recommendation + +Use the same Scriptify module API with both runtimes: + +```java +script.getModuleManager().addModule(module); +``` + +Then import from JavaScript: + +```js +import * as app from "app"; +``` diff --git a/docs/script-access.md b/docs/script-access.md new file mode 100644 index 0000000..449c81c --- /dev/null +++ b/docs/script-access.md @@ -0,0 +1,96 @@ +# ScriptAccess + +`ScriptAccess` controls which Java members scripts can access on exported Java classes and objects. + +```java +script.getModuleManager().setScriptAccess(ScriptAccess.ALL); +script.getModuleManager().setScriptAccess(ScriptAccess.EXPLICIT); +``` + +## ALL + +`ALL` is the default mode. + +In this mode, scripts can access public Java members that the runtime exposes and the security manager allows. + +Use `ALL` only when exported values/classes are intentionally part of the script API. + +## EXPLICIT + +`EXPLICIT` exposes only members annotated with: + +```java +@ScriptAccess.Export +``` + +Supported elements: + +- constructors; +- public instance methods; +- public static methods; +- public instance fields; +- public static fields; +- enum constants. + +## Example + +```java +public final class UserService { + private final String user; + + @ScriptAccess.Export + public UserService(String user) { + this.user = user; + } + + @ScriptAccess.Export + public String getUser() { + return user; + } + + public String internalToken() { + return "hidden"; + } +} +``` + +Export: + +```java +SimpleScriptInternalModule services = new SimpleScriptInternalModule("services"); +services.export(new ScriptValueExport("UserService", UserService.class)); + +script.getModuleManager().setScriptAccess(ScriptAccess.EXPLICIT); +script.getModuleManager().addModule(services); +``` + +JavaScript: + +```js +import { UserService } from "services"; + +const service = new UserService("alice"); +service.getUser(); // allowed +service.internalToken; // unavailable +``` + +## GraalVM Implementation + +GraalVM maps: + +- `ScriptAccess.ALL` to `HostAccess.ALL`; +- `ScriptAccess.EXPLICIT` to `HostAccess.EXPLICIT`. + +It also allows members annotated by `ScriptAccess.Export`. + +## Rhino Implementation + +Rhino uses restricted wrappers for Java classes and Java objects when `EXPLICIT` is enabled. + +The wrapper exposes only annotated fields, methods, and constructors. Non-exported members are hidden from scripts. + +## Scope of ScriptAccess + +`ScriptAccess` affects Java members on values/classes exported through modules. It does not remove functions you explicitly export as `ScriptFunctionExport`. + +For example, if you export `execCommand` as a Scriptify function, `ScriptAccess.EXPLICIT` does not block that function. Use smaller modules and security configuration for capability control. diff --git a/docs/script-lifecycle.md b/docs/script-lifecycle.md new file mode 100644 index 0000000..85d956a --- /dev/null +++ b/docs/script-lifecycle.md @@ -0,0 +1,86 @@ +# Script Lifecycle + +The central interface is: + +```java +org.densy.scriptify.api.script.Script +``` + +Runtime implementations provide concrete `JsScript` classes. + +## Main Methods + +```java +ScriptSecurityManager getSecurityManager(); +ScriptModuleManager getModuleManager(); +ScriptFunctionManager getFunctionManager(); +ScriptConstantManager getConstantManager(); + +void addExtraScript(String script); +CompiledScript compile(String script) throws ScriptException; +T evalOneShot(String script) throws ScriptException; +``` + +`eval(String)` still exists as a deprecated default method. Use `evalOneShot` or `compile`. + +## One-Shot Evaluation + +Use `evalOneShot` when the script is executed once. + +```java +JsScript script = new JsScript(); +Object result = script.evalOneShot("1 + 2"); +``` + +The runtime compiles/evaluates the script and closes runtime resources. + +## Compiled Scripts + +Use `compile` when you need to keep a compiled script object. + +```java +try (CompiledScript compiled = script.compile(""" + (left, right) => left + right + """)) { + Object result = compiled.eval(2, 3); +} +``` + +Always close compiled scripts. They own runtime resources such as engine contexts. + +## CompiledScript Contract + +```java +T get(); +T eval(Object... args); +void close(); +``` + +Runtime behavior: + +- GraalVM: `get()` returns the evaluated `Value`; `eval(args...)` executes it when it is callable. +- Rhino: `get()` executes the compiled script; `eval(args...)` executes it and calls the result if the result is a function. + +## Extra Script + +`addExtraScript` prepends code to every future compilation. + +```java +script.addExtraScript("const prefix = 'app';"); +script.evalOneShot("prefix + '-script'"); +``` + +Extra scripts are applied in insertion order and become part of the final source. + +## Configuration Timing + +Configure these before compiling: + +- security mode and excludes; +- module registrations; +- `ScriptAccess`; +- global functions; +- global constants; +- extra scripts. + +Compiled scripts do not automatically inherit later manager changes unless the runtime recompiles a new script. diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..5bb3ecc --- /dev/null +++ b/docs/security.md @@ -0,0 +1,120 @@ +# Security + +Scriptify security is opt-in. By default, `securityMode` is disabled. + +```java +script.getSecurityManager().setSecurityMode(true); +``` + +When security mode is enabled, class and path access must be explicitly allowed through excludes. + +## Security Manager API + +```java +boolean getSecurityMode(); +void setSecurityMode(boolean securityMode); + +SecurityFileSystem getFileSystem(); +SecurityPathAccessor getPathAccessor(); + +Set getExcludes(); +void addExclude(SecurityExclude exclude); +void removeExclude(SecurityExclude exclude); +``` + +Default implementation: + +```java +org.densy.scriptify.core.script.security.StandardSecurityManager +``` + +## Class Access + +Allow a class: + +```java +script.getSecurityManager().addExclude(SecurityExclude.ofClass(java.time.Instant.class)); +``` + +Allow a package prefix: + +```java +script.getSecurityManager().addExclude(SecurityExclude.ofPackage("java.time")); +``` + +Runtime behavior: + +- GraalVM uses excludes for host class lookup. +- Rhino uses excludes through `ClassShutter`. + +## Path Access + +Set a base path: + +```java +script.getSecurityManager().getPathAccessor().setBasePath(Path.of("workspace")); +``` + +Allow a path prefix: + +```java +script.getSecurityManager().addExclude(SecurityExclude.ofPath("workspace/input")); +``` + +Security path access resolves paths against the configured base path and normalizes them. + +When access is denied, `SecurityFileSystem.getPath` throws `SecurityException`. + +## File-System Surfaces + +Path security is used by Scriptify-provided file operations: + +- `existsFile`; +- `readFile`; +- `writeFile`; +- `deleteFile`; +- `moveFile`; +- `listFiles`; +- `downloadFromUrl`; +- zip/unzip functions; +- GraalVM external file module resolution. + +## Powerful Capabilities + +The standard and HTTP modules expose privileged operations: + +| Capability | Risk | +| --- | --- | +| `execCommand` | Runs OS commands. | +| `env` | Reads environment variables. | +| file functions | Read/write/delete/move host files. | +| zip functions | Write extracted archive entries. | +| `downloadFromUrl` | Reads remote content and writes files. | +| HTTP module | Sends network requests. | + +Do not expose these to untrusted scripts unless you have reviewed the capabilities and configured security. + +## Recommended Safe Pattern + +Create a narrow module instead of exporting `StandardScriptModule`. + +```java +SimpleScriptInternalModule safe = new SimpleScriptInternalModule("safe"); +safe.export(new ScriptFunctionExport(new ScriptFunctionPrint())); + +script.getSecurityManager().setSecurityMode(true); +script.getModuleManager().setScriptAccess(ScriptAccess.EXPLICIT); +script.getModuleManager().addModule(safe); +``` + +Security mode restricts class/path access. Module design controls which Scriptify functions exist at all. + +## Exclude Matching + +`SecurityExclude.isExcluded` uses prefix matching: + +```java +return value.startsWith(this.getValue()); +``` + +Use precise prefixes and normalize paths consistently. diff --git a/docs/standard-module.md b/docs/standard-module.md new file mode 100644 index 0000000..b7bdf26 --- /dev/null +++ b/docs/standard-module.md @@ -0,0 +1,112 @@ +# Standard Module + +The standard module is: + +```java +org.densy.scriptify.common.script.module.StandardScriptModule +``` + +Module name: + +```text +standard +``` + +You can add it directly: + +```java +script.getModuleManager().addModule(new StandardScriptModule()); +``` + +```js +import * as standard from "standard"; +``` + +Or copy it to another name: + +```java +SimpleScriptInternalModule std = new SimpleScriptInternalModule("std"); +std.copy(new StandardScriptModule()); +script.getModuleManager().addModule(std); +``` + +```js +import * as std from "std"; +``` + +## Constants + +| Name | Description | +| --- | --- | +| `baseDir` | Current JVM working directory. | +| `osName` | Operating system name. | + +## Utility Functions + +| Function | Signature | Description | +| --- | --- | --- | +| `print` | `print(...args)` | Prints arguments joined by spaces to `System.out`. | +| `arrayOf` | `arrayOf(...args)` | Returns a Java array. | +| `listOf` | `listOf(...args)` | Returns a Java `List`. | +| `setOf` | `setOf(...args)` | Returns a Java `Set`. | +| `shuffleArray` | `shuffleArray(array)` | Returns a shuffled copy of a list. | +| `regex_pattern` | `regex_pattern(pattern)` | Compiles a Java regex `Pattern`. | +| `regex_match` | `regex_match(regex, value)` or `regex_match(pattern, value)` | Matches a full string. | + +## Crypto Functions + +| Function | Signature | Description | +| --- | --- | --- | +| `base64encode` | `base64encode(string)` | Encodes a string to Base64. | +| `base64decode` | `base64decode(string)` | Decodes Base64 as UTF-8. | +| `md5` | `md5(input)` | Returns MD5 hex digest. | +| `sha256` | `sha256(input)` | Returns SHA-256 hex digest. | + +## Random Functions + +| Function | Signature | Description | +| --- | --- | --- | +| `randomUUID` | `randomUUID()` | Random UUID string. | +| `randomBoolean` | `randomBoolean()` | Random boolean. | +| `randomInt` | `randomInt(max)` or `randomInt(min, max)` | Random int. | +| `randomLong` | `randomLong(max)` or `randomLong(min, max)` | Random long. | +| `randomFloat` | `randomFloat(max)` or `randomFloat(min, max)` | Random float. | +| `randomDouble` | `randomDouble(max)` or `randomDouble(min, max)` | Random double. | + +Each call creates a new `java.util.Random`. + +## File Functions + +| Function | Signature | Description | +| --- | --- | --- | +| `existsFile` | `existsFile(filePath)` | Checks path existence. | +| `readFile` | `readFile(filePath)` | Reads a file as string. | +| `writeFile` | `writeFile(filePath, fileContent)` | Writes a string; returns written path. | +| `deleteFile` | `deleteFile(filePath, recursive?)` | Deletes file or directory. | +| `moveFile` | `moveFile(original, target)` | Moves/renames a file. | +| `listFiles` | `listFiles(filePath)` | Lists absolute paths in a directory. | +| `downloadFromUrl` | `downloadFromUrl(url, filePath)` | Downloads URL content into a file. | +| `joinPath` | `joinPath(...args)` | Joins path parts with `/`. | +| `normalizePath` | `normalizePath(path)` | Replaces `\` with `/`. | + +File functions use Scriptify `SecurityFileSystem`. + +## OS Functions + +| Function | Signature | Description | +| --- | --- | --- | +| `env` | `env(name)` | Returns an environment variable. | +| `execCommand` | `execCommand(input)` | Runs an OS command and returns stdout/stderr. | + +`execCommand` is a privileged capability. Do not expose it to untrusted scripts. + +## Zip Functions + +| Function | Signature | Description | +| --- | --- | --- | +| `zipFile` | `zipFile(filePath, compressedFilePath)` | Zips a file or directory. | +| `unzipFile` | `unzipFile(compressedFilePath, decompressedPath)` | Extracts an archive. | +| `smartZipFile` | `smartZipFile(filesPath, compressedFilePath, patterns)` | Zips entries whose names match regex patterns. | +| `smartUnzipFile` | `smartUnzipFile(compressedFilePath, decompressedPath, patterns)` | Extracts archive entries whose names match regex patterns. | + +Unzip functions validate extracted paths to prevent archive entries from escaping the destination directory.