1use boa_engine::{Context, Source};
2
3use crate::model::{
4 RawWorkflowDefinition, WorkflowDefinition, WorkflowRuntimeError, WorkflowRuntimeErrorKind,
5 WorkflowRuntimeOptions, WorkflowRuntimeResult,
6};
7
8const DENIED_AMBIENT_PATTERNS: &[(&str, &str)] = &[
9 (
10 "import(",
11 "dynamic module loading is not available in workflow scripts",
12 ),
13 (
14 "import ",
15 "module loading is not available in workflow scripts",
16 ),
17 (
18 "require(",
19 "CommonJS loading is not available in workflow scripts",
20 ),
21 ("eval(", "eval is not available in workflow scripts"),
22 (
23 "Function(",
24 "dynamic function construction is not available in workflow scripts",
25 ),
26 (
27 "process.",
28 "ambient process access is not available in workflow scripts",
29 ),
30 (
31 "process[",
32 "ambient process access is not available in workflow scripts",
33 ),
34 (
35 "Deno.",
36 "ambient runtime access is not available in workflow scripts",
37 ),
38 (
39 "Bun.",
40 "ambient runtime access is not available in workflow scripts",
41 ),
42 (
43 "fetch(",
44 "network access is not available in workflow scripts",
45 ),
46 (
47 "XMLHttpRequest",
48 "network access is not available in workflow scripts",
49 ),
50 (
51 "WebSocket",
52 "network access is not available in workflow scripts",
53 ),
54 (
55 "setTimeout(",
56 "ambient timers are not available in workflow scripts",
57 ),
58 (
59 "setInterval(",
60 "ambient timers are not available in workflow scripts",
61 ),
62];
63
64pub fn parse_workflow_definition(
65 source: &str,
66 options: &WorkflowRuntimeOptions,
67) -> WorkflowRuntimeResult<WorkflowDefinition> {
68 preflight_script(source)?;
69 let mut context = new_context(options);
70 install_definition_prelude(&mut context)?;
71 eval_js(
72 &mut context,
73 source,
74 WorkflowRuntimeErrorKind::ScriptExecution,
75 )?;
76 let metadata = read_global_string(&mut context, "globalThis.__roderWorkflowMetadataJson")?
77 .filter(|value| !value.is_empty())
78 .ok_or_else(|| {
79 WorkflowRuntimeError::new(
80 WorkflowRuntimeErrorKind::MissingDefinition,
81 "script must call workflow.define(metadata, handler)",
82 )
83 })?;
84 parse_definition_json(&metadata, options)
85}
86
87pub(crate) fn preflight_script(source: &str) -> WorkflowRuntimeResult<()> {
88 for (pattern, message) in DENIED_AMBIENT_PATTERNS {
89 if source.contains(pattern) {
90 return Err(WorkflowRuntimeError::new(
91 WorkflowRuntimeErrorKind::DeniedAmbientApi,
92 *message,
93 ));
94 }
95 }
96 Ok(())
97}
98
99pub(crate) fn new_context(options: &WorkflowRuntimeOptions) -> Context {
100 let mut context = Context::default();
101 context
102 .runtime_limits_mut()
103 .set_loop_iteration_limit(options.max_loop_iterations);
104 context.runtime_limits_mut().set_recursion_limit(128);
105 context
106}
107
108pub(crate) fn install_definition_prelude(context: &mut Context) -> WorkflowRuntimeResult<()> {
109 eval_js(
110 context,
111 r#"
112Object.defineProperty(globalThis, "workflow", {
113 value: Object.freeze({
114 define(metadata, handler) {
115 if (typeof handler !== "function") {
116 throw new Error("workflow.define requires a function handler");
117 }
118 globalThis.__roderWorkflowMetadataJson = JSON.stringify(metadata || {});
119 globalThis.__roderWorkflowHandler = handler;
120 }
121 }),
122 writable: false,
123 configurable: false
124});
125"#,
126 WorkflowRuntimeErrorKind::ScriptExecution,
127 )
128}
129
130pub(crate) fn eval_js(
131 context: &mut Context,
132 source: &str,
133 kind: WorkflowRuntimeErrorKind,
134) -> WorkflowRuntimeResult<()> {
135 context
136 .eval(Source::from_bytes(source))
137 .map(|_| ())
138 .map_err(|err| classify_js_error(err.to_string(), kind))
139}
140
141pub(crate) fn read_global_string(
142 context: &mut Context,
143 expression: &str,
144) -> WorkflowRuntimeResult<Option<String>> {
145 let value = context
146 .eval(Source::from_bytes(expression))
147 .map_err(|err| {
148 classify_js_error(err.to_string(), WorkflowRuntimeErrorKind::ScriptExecution)
149 })?;
150 if value.is_null_or_undefined() {
151 return Ok(None);
152 }
153 let text = value
154 .to_string(context)
155 .map_err(|err| {
156 classify_js_error(err.to_string(), WorkflowRuntimeErrorKind::ScriptExecution)
157 })?
158 .to_std_string_escaped();
159 Ok(Some(text))
160}
161
162pub(crate) fn parse_definition_json(
163 metadata: &str,
164 options: &WorkflowRuntimeOptions,
165) -> WorkflowRuntimeResult<WorkflowDefinition> {
166 let raw: RawWorkflowDefinition = serde_json::from_str(metadata).map_err(|err| {
167 WorkflowRuntimeError::new(
168 WorkflowRuntimeErrorKind::InvalidMetadata,
169 format!("invalid workflow metadata: {err}"),
170 )
171 })?;
172 raw.into_definition(&options.limits)
173}
174
175pub(crate) fn classify_js_error(
176 message: String,
177 default_kind: WorkflowRuntimeErrorKind,
178) -> WorkflowRuntimeError {
179 if message.contains("loop iteration limit")
180 || message.contains("limit:")
181 || message.contains("Maximum call stack")
182 {
183 return WorkflowRuntimeError::new(WorkflowRuntimeErrorKind::LimitExceeded, message);
184 }
185 if message.contains("abort:") {
186 return WorkflowRuntimeError::new(WorkflowRuntimeErrorKind::Aborted, message);
187 }
188 WorkflowRuntimeError::new(default_kind, message)
189}