@unoverse-platform/studio 0.1.89 → 0.1.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/local/plugin.ts +20 -12
  2. package/package.json +13 -3
  3. package/screens/TemplatesLibraryView.tsx +31 -6
  4. package/screens/TemplatesView.tsx +1 -1
  5. package/vendor/base/src/agent-mcp/discovery.ts +203 -0
  6. package/vendor/base/src/agent-mcp/handoff.ts +56 -0
  7. package/vendor/base/src/agent-mcp/index.ts +44 -0
  8. package/vendor/base/src/agent-mcp/invoke.ts +140 -0
  9. package/vendor/base/src/agent-mcp/lean.ts +111 -0
  10. package/vendor/base/src/agent-mcp/memoryContext.ts +104 -0
  11. package/vendor/base/src/agent-mcp/memoryIngest.ts +88 -0
  12. package/vendor/base/src/agent-mcp/rowComponents.ts +172 -0
  13. package/vendor/base/src/agent-mcp/tools.ts +134 -0
  14. package/vendor/base/src/agent-mcp/types.ts +130 -0
  15. package/vendor/base/src/auth/auth.ts +278 -0
  16. package/vendor/base/src/auth/authConfig.ts +73 -0
  17. package/vendor/base/src/boot.ts +245 -0
  18. package/vendor/base/src/definitions/definitions.ts +18 -0
  19. package/vendor/base/src/definitions/dsPackage.ts +45 -0
  20. package/vendor/base/src/definitions/fsCache.ts +128 -0
  21. package/vendor/base/src/definitions/index.ts +19 -0
  22. package/vendor/base/src/definitions/inputs.ts +30 -0
  23. package/vendor/base/src/definitions/load/compile/component.ts +80 -0
  24. package/vendor/base/src/definitions/load/compile/index.ts +12 -0
  25. package/vendor/base/src/definitions/load/compile/sections.ts +132 -0
  26. package/vendor/base/src/definitions/load/compile/template.ts +153 -0
  27. package/vendor/base/src/definitions/load/expand/includes.ts +27 -0
  28. package/vendor/base/src/definitions/load/expand/index.ts +17 -0
  29. package/vendor/base/src/definitions/load/expand/inherit.ts +169 -0
  30. package/vendor/base/src/definitions/load/expand/referenced.ts +164 -0
  31. package/vendor/base/src/definitions/load/expand/refs.ts +106 -0
  32. package/vendor/base/src/definitions/load/expand/walk.ts +91 -0
  33. package/vendor/base/src/definitions/load/load.ts +487 -0
  34. package/vendor/base/src/definitions/model.ts +238 -0
  35. package/vendor/base/src/definitions/registry/catalog.ts +643 -0
  36. package/vendor/base/src/definitions/registry/homes.ts +252 -0
  37. package/vendor/base/src/definitions/theme.ts +383 -0
  38. package/vendor/base/src/executor.ts +284 -0
  39. package/vendor/base/src/http/uwsFetchAdapter.ts +260 -0
  40. package/vendor/base/src/items/baseVersion.ts +96 -0
  41. package/vendor/base/src/items/collect.ts +199 -0
  42. package/vendor/base/src/items/fingerprint.ts +34 -0
  43. package/vendor/base/src/items/publish.ts +198 -0
  44. package/vendor/base/src/lint/context.mjs +56 -0
  45. package/vendor/base/src/lint/design/defs.mjs +46 -0
  46. package/vendor/base/src/lint/design/file.mjs +737 -0
  47. package/vendor/base/src/lint/design/index.d.mts +30 -0
  48. package/vendor/base/src/lint/design/index.mjs +996 -0
  49. package/vendor/base/src/lint/design/tokens.mjs +128 -0
  50. package/vendor/base/src/lint/design/vocabulary.mjs +181 -0
  51. package/vendor/base/src/lint/design/walk.mjs +444 -0
  52. package/vendor/base/src/lint/index.mjs +28 -0
  53. package/vendor/base/src/lint/nodes/blocks.mjs +63 -0
  54. package/vendor/base/src/lint/nodes/index.mjs +64 -0
  55. package/vendor/base/src/lint/nodes/load.mjs +126 -0
  56. package/vendor/base/src/lint/nodes/node.mjs +764 -0
  57. package/vendor/base/src/lint/nodes/package.mjs +112 -0
  58. package/vendor/base/src/lint/nodes/schema.mjs +75 -0
  59. package/vendor/base/src/logger.ts +33 -0
  60. package/vendor/base/src/manifests/compose.ts +539 -0
  61. package/vendor/base/src/manifests/executor/callback.ts +107 -0
  62. package/vendor/base/src/manifests/executor/context.ts +107 -0
  63. package/vendor/base/src/manifests/executor/index.ts +47 -0
  64. package/vendor/base/src/manifests/executor/lookup.ts +21 -0
  65. package/vendor/base/src/manifests/executor/memory.ts +62 -0
  66. package/vendor/base/src/manifests/executor/promise.ts +71 -0
  67. package/vendor/base/src/manifests/executor/refire.ts +34 -0
  68. package/vendor/base/src/manifests/executor/send.ts +65 -0
  69. package/vendor/base/src/manifests/executor/session.ts +46 -0
  70. package/vendor/base/src/manifests/executor/toolBridge.ts +266 -0
  71. package/vendor/base/src/manifests/index.ts +280 -0
  72. package/vendor/base/src/manifests/integrity.ts +93 -0
  73. package/vendor/base/src/manifests/rows.ts +33 -0
  74. package/vendor/base/src/manifests/runtime/auth/aws.ts +240 -0
  75. package/vendor/base/src/manifests/runtime/auth/oauth.ts +76 -0
  76. package/vendor/base/src/manifests/runtime/authorize.ts +159 -0
  77. package/vendor/base/src/manifests/runtime/channels/final.ts +171 -0
  78. package/vendor/base/src/manifests/runtime/channels/service.ts +125 -0
  79. package/vendor/base/src/manifests/runtime/channels/workflow.ts +310 -0
  80. package/vendor/base/src/manifests/runtime/context.ts +150 -0
  81. package/vendor/base/src/manifests/runtime/docstore/handlers.ts +547 -0
  82. package/vendor/base/src/manifests/runtime/docstore/index.ts +135 -0
  83. package/vendor/base/src/manifests/runtime/docstore/sectionizer.ts +184 -0
  84. package/vendor/base/src/manifests/runtime/docstore/store.ts +172 -0
  85. package/vendor/base/src/manifests/runtime/docstore/types.ts +60 -0
  86. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +165 -0
  87. package/vendor/base/src/manifests/runtime/duplex/session.ts +511 -0
  88. package/vendor/base/src/manifests/runtime/events.ts +223 -0
  89. package/vendor/base/src/manifests/runtime/helpers.ts +53 -0
  90. package/vendor/base/src/manifests/runtime/http/allowedHosts.ts +118 -0
  91. package/vendor/base/src/manifests/runtime/http/request.ts +637 -0
  92. package/vendor/base/src/manifests/runtime/http/response.ts +162 -0
  93. package/vendor/base/src/manifests/runtime/index.ts +57 -0
  94. package/vendor/base/src/manifests/runtime/loops/chunk.ts +98 -0
  95. package/vendor/base/src/manifests/runtime/loops/loop.ts +191 -0
  96. package/vendor/base/src/manifests/runtime/loops/paginate.ts +143 -0
  97. package/vendor/base/src/manifests/runtime/loops/poll.ts +169 -0
  98. package/vendor/base/src/manifests/runtime/narrate.ts +53 -0
  99. package/vendor/base/src/manifests/runtime/state.ts +171 -0
  100. package/vendor/base/src/manifests/runtime/templating.ts +110 -0
  101. package/vendor/base/src/manifests/runtime/tools/regionSkills.ts +125 -0
  102. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +542 -0
  103. package/vendor/base/src/manifests/runtime/usage.ts +87 -0
  104. package/vendor/base/src/manifests/source.ts +187 -0
  105. package/vendor/base/src/manifests/watch.ts +67 -0
  106. package/vendor/base/src/markdown/document.ts +141 -0
  107. package/vendor/base/src/paths.ts +180 -0
  108. package/vendor/base/src/platform/audioManager.ts +100 -0
  109. package/vendor/base/src/platform/clientTransport.ts +65 -0
  110. package/vendor/base/src/platform/executionContext.ts +154 -0
  111. package/vendor/base/src/platform/index.ts +10 -0
  112. package/vendor/base/src/platform/pluginAPI.ts +146 -0
  113. package/vendor/base/src/platform/redis.ts +54 -0
  114. package/vendor/base/src/platform/serviceCalls.ts +222 -0
  115. package/vendor/base/src/pluginBase.ts +248 -0
  116. package/vendor/base/src/plugins/README.md +57 -0
  117. package/vendor/base/src/plugins/discovery.ts +87 -0
  118. package/vendor/base/src/plugins/index.ts +16 -0
  119. package/vendor/base/src/plugins/loader.ts +179 -0
  120. package/vendor/base/src/plugins/redis.ts +36 -0
  121. package/vendor/base/src/plugins/state.ts +53 -0
  122. package/vendor/base/src/registry.ts +95 -0
  123. package/vendor/base/src/security/permissions.ts +93 -0
  124. package/vendor/base/src/security/rules.ts +73 -0
  125. package/vendor/base/src/template/SafeExpression.ts +556 -0
  126. package/vendor/base/src/template/StringTemplateResolver.ts +75 -0
  127. package/vendor/base/src/template/index.ts +22 -0
  128. package/vendor/base/src/types.ts +50 -0
  129. package/vendor/base/src/workflow/GetMemoryService.ts +158 -0
  130. package/vendor/base/src/workflow/PlatformMemoryCore.ts +438 -0
  131. package/vendor/base/src/workflow/routing.ts +122 -0
  132. package/vendor/sdk/lib/core/store.ts +26 -6
@@ -0,0 +1,556 @@
1
+ /**
2
+ * SafeExpression — a restricted evaluator for `return …` template config.
3
+ *
4
+ * WHY (SECURITY.md §"Untrusted template expressions"): template expressions ride inside
5
+ * workflow definitions, which travel by paste / marketplace component / shared export —
6
+ * an untrusted surface. The old resolver ran them via `new Function`, which is NOT a
7
+ * sandbox: the body reaches every Node global (`process.env` → secret theft, `fetch` →
8
+ * exfiltration, prototype-chain → RCE). A trojaned component could hide code in a prop.
9
+ *
10
+ * This evaluator parses the expression once (cached, like the old memoize) and walks an
11
+ * ALLOWLIST of AST node types, evaluating in-process against the data context only. Security
12
+ * is by ABSENCE: there is no `process`, `require`, `constructor` for an expression to reach —
13
+ * the interpreter simply never implements them, so there is nothing to escape to. No V8
14
+ * isolate, no marshalling → negligible overhead vs a native function call, and pure-JS (no
15
+ * native module). It supports the real use — data-shaping: member access, indexing, object /
16
+ * array literals, operators, ternaries, template strings, and safe array/string/JSON/Math
17
+ * methods (incl. `.map(x => …)` arrow callbacks). Anything else throws (and is logged).
18
+ */
19
+ import { parseExpressionAt, type Node as AcornNode } from "acorn";
20
+ import { createHash } from "node:crypto";
21
+ import memoizeImport from "fast-memoize";
22
+ /**
23
+ * fast-memoize ships CommonJS (`module.exports = memoize`) with ESM-shaped typings
24
+ * (`export default`). Under NodeNext those disagree: the runtime value IS the function,
25
+ * but TypeScript types the default import as the module namespace and calls it uncallable.
26
+ * Verified at runtime before casting, rather than assuming.
27
+ */
28
+ type Memoizer = <F extends (...args: any[]) => any>(fn: F) => F;
29
+ const memoize = memoizeImport as unknown as Memoizer;
30
+
31
+ /** Global namespaces an expression may reference — pure, no I/O, no ambient authority. */
32
+ const SAFE_GLOBALS: Record<string, unknown> = {
33
+ JSON,
34
+ Math,
35
+ Number,
36
+ String,
37
+ Boolean,
38
+ parseInt,
39
+ parseFloat,
40
+ isNaN,
41
+ isFinite,
42
+ // URL encoding. Pure string transforms with no I/O and nothing to reach, so they meet the
43
+ // same bar as parseInt above. Needed because a manifest builds request URLs from data: a
44
+ // table NAME or a record id goes into a path segment, and without these an id containing a
45
+ // space or a slash silently produces a malformed URL rather than an encoded one.
46
+ encodeURIComponent,
47
+ encodeURI,
48
+ // Content addressing. A pure function of its argument with no I/O and nothing to reach,
49
+ // so it meets the same bar as parseInt above: hashing a string cannot observe or change
50
+ // anything outside the expression.
51
+ //
52
+ // Needed because STABLE IDS ARE DATA, not decoration. A node that pulls a page derives a
53
+ // universal id from the url and a content id from the text, and downstream dedup and ref
54
+ // hydration join on exactly those. Without this the ids have to be minted in TypeScript,
55
+ // which means the node cannot be a manifest at all, for the sake of one pure function.
56
+ sha256: (value: unknown) => createHash("sha256").update(String(value ?? "")).digest("hex"),
57
+ // Text to bytes. A pure function of its argument with no I/O and nothing to reach, the
58
+ // same bar as sha256 above.
59
+ //
60
+ // Needed because BYTES TRAVEL AS BASE64 between calls (`encoding: binary` decodes them at
61
+ // send time), and the sandbox can express no encoder of its own: `charCodeAt` is not a
62
+ // safe method and there is no Buffer. So a node holding plain TEXT it wants to store —
63
+ // parsed markdown written back to S3 is the case that forced this — could not reach any
64
+ // binary body path at all, for want of one pure transform. UTF-8, because the text is
65
+ // real prose, not ASCII.
66
+ toBase64: (value: unknown) => Buffer.from(String(value ?? ""), "utf8").toString("base64"),
67
+ Object: { keys: Object.keys, values: Object.values, entries: Object.entries, fromEntries: Object.fromEntries, assign: (...a: object[]) => Object.assign({}, ...a) },
68
+ Array: { isArray: Array.isArray, from: Array.from, of: Array.of },
69
+ /**
70
+ * TIME, readable and formattable, but never constructible.
71
+ *
72
+ * `now` alone was not enough to be useful. Half the APIs a node talks to take a date RANGE —
73
+ * "transactions since", "runs between" — and every one of them wants `YYYY-MM-DD` or a full ISO
74
+ * string. An expression could do the arithmetic (`Date.now() - 30 * 86400000`) and then had no way
75
+ * to render the answer: `new Date(...)` is a NewExpression and refused, and `toISOString` is not a
76
+ * safe method, so the number stayed a number. A node needing a date range therefore could not be a
77
+ * manifest at all, for want of one pure formatter.
78
+ *
79
+ * ONE function, not a family. `iso` returns the full ISO string and `.split("T")[0]` — already
80
+ * allowed — gives the date part, so "YYYY-MM-DD" needs no second primitive. Fewer things to learn
81
+ * and fewer to keep consistent.
82
+ *
83
+ * It meets the same bar as `sha256` above: a pure function of its argument, no I/O, nothing
84
+ * reachable through it. `Date.now` was already exposed, so nothing new is observable — this only
85
+ * formats what an expression could already read.
86
+ */
87
+ Date: {
88
+ now: Date.now,
89
+ iso: (ms?: unknown) => {
90
+ const t = ms === undefined || ms === null ? Date.now() : Number(ms);
91
+ // A clear message beats `new Date(NaN)`'s RangeError, which names neither the value nor the
92
+ // field it came from. Getting a string where a timestamp was meant is the likely mistake.
93
+ if (!Number.isFinite(t))
94
+ throw new UnsafeExpressionError(`Date.iso needs a timestamp in milliseconds, got ${JSON.stringify(ms)}`);
95
+ return new Date(t).toISOString();
96
+ },
97
+ },
98
+ };
99
+
100
+ /** Method names callable on user data (array/string/JSON/Math) — all side-effect-free reads
101
+ * or pure transforms. NOT included: anything that could reach a constructor or mutate host. */
102
+ const SAFE_METHODS = new Set([
103
+ // array (non-mutating)
104
+ "map", "filter", "slice", "concat", "join", "includes", "indexOf", "lastIndexOf",
105
+ "find", "findIndex", "some", "every", "reduce", "flat", "flatMap", "at", "keys", "values", "entries",
106
+ // `toSorted`, and deliberately NOT `sort`. Both order an array; only one of them is a pure
107
+ // transform. `sort` reorders its receiver IN PLACE, and the receiver here is live run data — an
108
+ // expression doing `signal.items.sort()` would permanently reorder the upstream node's own output
109
+ // for every other reader of it, which is a side effect this evaluator is supposed to make
110
+ // impossible. `toSorted` returns a new array and leaves the original alone.
111
+ //
112
+ // Needed because CANONICAL ORDER IS PART OF A CONTENT HASH. A content id is
113
+ // `sha256(JSON.stringify(value, Object.keys(value).toSorted()))`, and without the sort the same
114
+ // object hashes differently depending on the order its keys happened to be built in — so dedup
115
+ // and ref hydration, which join on exactly those ids, would miss.
116
+ "toSorted",
117
+ // string
118
+ "split", "toUpperCase", "toLowerCase", "trim", "trimStart", "trimEnd", "replace", "replaceAll",
119
+ "substring", "substr", "startsWith", "endsWith", "padStart", "padEnd", "repeat", "charAt", "match",
120
+ // number
121
+ "toFixed", "toPrecision",
122
+ // shared
123
+ "toString",
124
+ ]);
125
+
126
+ /** Property names that are the classic prototype-chain escape — never readable. */
127
+ const BLOCKED_PROPS = new Set(["constructor", "__proto__", "prototype", "__defineGetter__", "__defineSetter__", "__lookupGetter__", "__lookupSetter__"]);
128
+
129
+ class UnsafeExpressionError extends Error {}
130
+
131
+ const parseCached = memoize((expr: string): AcornNode => {
132
+ // Strip a leading `return` and trailing `;` — the field's convention is a return-expression.
133
+ const cleaned = expr.trim().replace(/^return\b/, "").replace(/;\s*$/, "").trim();
134
+ if (!cleaned) throw new UnsafeExpressionError("empty expression");
135
+ return parseExpressionAt(cleaned, 0, { ecmaVersion: 2022 }) as AcornNode;
136
+ });
137
+
138
+ type Scope = Record<string, unknown>;
139
+
140
+ /**
141
+ * STATEMENTS, inside a block-body arrow. The Code node's contract is "any JS return statement", and
142
+ * for two days it could not honour it.
143
+ *
144
+ * Replacing `new Function` with this interpreter (2026-07-25) closed a real hole, but it also
145
+ * rejected block-body arrows on the reasoning that "data-shaping never needs them". The Code node
146
+ * disproves that: real extraction code declares locals, matches, branches, and returns — the exact
147
+ * shape of every `return (() => { ... })()` anyone had written. Worse, the rejection was INVISIBLE.
148
+ * `TemplateResolver` catches a throw and never reassigns, so the raw SOURCE TEXT stayed as the
149
+ * field's value and the node emitted its own code as data.
150
+ *
151
+ * WHAT THIS DOES NOT CHANGE, which is the whole point: no new globals, no `new`, no `constructor`,
152
+ * no `require`, no `process`, no property outside BLOCKED_PROPS. Security here is by ABSENCE, and
153
+ * absence is untouched — statements are evaluated by this interpreter, not handed to the host.
154
+ *
155
+ * NO LOOPS, deliberately. `while`/`for` would let a pasted expression hang the engine, and iteration
156
+ * is already covered by `.map`/`.filter`/`.reduce`, which are bounded by their input. A denial of
157
+ * service is a smaller hole than RCE but it is still a hole, and nothing needs it.
158
+ */
159
+
160
+ /** Names DECLARED in a given scope, so an assignment can find the right one to write. */
161
+ const DECLARED = Symbol("declared");
162
+
163
+ /** A fresh block scope. Declarations land here; assignment walks up to where the name was declared. */
164
+ function childScope(parent: Scope): Scope {
165
+ const s: Scope = Object.create(parent);
166
+ Object.defineProperty(s, DECLARED, { value: new Set<string>(), enumerable: false });
167
+ return s;
168
+ }
169
+
170
+ function declare(scope: Scope, name: string, value: unknown): void {
171
+ const set = (scope as any)[DECLARED] as Set<string> | undefined;
172
+ set?.add(name);
173
+ scope[name] = value;
174
+ }
175
+
176
+ /**
177
+ * Assign to the nearest scope that DECLARED the name.
178
+ *
179
+ * Undeclared assignment throws rather than creating a binding. That is what keeps the injected data
180
+ * context read-only: without it, `signal = whatever` would either mutate the caller's object or
181
+ * silently shadow it, and a template must never be able to rewrite the run's own data.
182
+ */
183
+ function assign(scope: Scope, name: string, value: unknown): void {
184
+ for (let s: any = scope; s; s = Object.getPrototypeOf(s)) {
185
+ const own = Object.getOwnPropertyDescriptor(s, DECLARED)?.value as Set<string> | undefined;
186
+ if (own?.has(name)) {
187
+ s[name] = value;
188
+ return;
189
+ }
190
+ }
191
+ throw new UnsafeExpressionError(`assignment to undeclared '${name}'`);
192
+ }
193
+
194
+ /**
195
+ * Functions THIS interpreter created, i.e. arrows written in the expression itself.
196
+ *
197
+ * Calling one has to be allowed for an IIFE — `(() => { ... })()` is the only way an expression
198
+ * language gets a statement body — but the allowlist for a DIRECT call was "is it a safe global?",
199
+ * so the interpreter refused to call its own arrow with "call of non-allowlisted function".
200
+ *
201
+ * A WeakSet and not a flag on the function: nothing a template can reach may add to it, because the
202
+ * only place that registers is the arrow case below. A host function that arrived through the data
203
+ * context is still not callable, which is the property that matters.
204
+ */
205
+ const OURS = new WeakSet<Function>();
206
+
207
+ /**
208
+ * NAMED HELPER BAGS — objects whose members may be CALLED as methods.
209
+ *
210
+ * The member-call rule is "the receiver is a safe-global namespace, or the method name is a
211
+ * known-safe builtin", which is what keeps `whatever.call(...)` and `x.constructor(...)` out.
212
+ * A helper bag is neither, so without this a manifest could pass `helpers.row` to `.map()`
213
+ * and yet not call `helpers.row(r)` directly — the same function, callable one way and not
214
+ * the other, which is the kind of rule nobody can hold in their head.
215
+ *
216
+ * A WeakSet and not a marker property, for the same reason as OURS: only `makeHelpers` below
217
+ * registers, and nothing an expression can reach may add to it. An object that arrived on the
218
+ * data context is still not a namespace, however it is shaped.
219
+ */
220
+ const NAMESPACES = new WeakSet<object>();
221
+
222
+ /** One named helper: a sandboxed function a manifest can call by name. */
223
+ export interface HelperDef {
224
+ args?: string[];
225
+ body: string;
226
+ }
227
+
228
+ /**
229
+ * Build a callable bag from `helpers:` declarations in a package's shared/ folder.
230
+ *
231
+ * WHY THIS EXISTS: an expression is one string, so a projection of any size had to be written
232
+ * as one string — a 45-line JavaScript blob inside a YAML scalar, unreadable and undiffable,
233
+ * with no way to give the parts names. Splitting it needed a way for one expression to call
234
+ * another, and there wasn't one.
235
+ *
236
+ * The bodies are ORDINARY expressions with no extra authority: same sandbox, same allowlist,
237
+ * same absence of process/require/fetch. A helper sees ONLY its declared arguments and its
238
+ * sibling helpers — not `config`, not `credentials`, not the caller's scope. That is
239
+ * deliberate: a named function whose result depends on ambient state it never named is the
240
+ * thing this format is trying to get away from.
241
+ *
242
+ * Parsed EAGERLY, so a typo in a helper fails when the package loads, naming the helper,
243
+ * rather than on the first request that happens to reach it.
244
+ */
245
+ export function makeHelpers(defs: Record<string, HelperDef>): Record<string, (...a: unknown[]) => unknown> {
246
+ const bag: Record<string, (...a: unknown[]) => unknown> = {};
247
+ for (const [name, def] of Object.entries(defs ?? {})) {
248
+ const params = Array.isArray(def?.args) ? def.args.map(String) : [];
249
+ const code = String(def?.body ?? "").replace(/^return\s+/, "");
250
+ if (!code.trim()) throw new UnsafeExpressionError(`helper '${name}' has an empty body`);
251
+ // Both checks up front: parsable, and made only of forms the interpreter implements.
252
+ // The second is what catches an illegal construct on a branch no test happens to enter.
253
+ parseCached(code);
254
+ assertSupportedExpression(code);
255
+ const fn = (...callArgs: unknown[]) => {
256
+ // Rooted on the bag alone: siblings are reachable, the caller's scope is not.
257
+ const scope = childScope({ helpers: bag } as Scope);
258
+ params.forEach((p, i) => declare(scope, p, callArgs[i]));
259
+ return evalNode(parseCached(code), scope);
260
+ };
261
+ OURS.add(fn);
262
+ bag[name] = fn;
263
+ }
264
+ NAMESPACES.add(bag);
265
+ return bag;
266
+ }
267
+
268
+ /** Marks a `return`, so it unwinds through nested blocks without using a host exception. */
269
+ const RETURNED = Symbol("returned");
270
+ type Completion = { [RETURNED]: true; value: unknown } | undefined;
271
+
272
+ function execStatement(node: any, scope: Scope): Completion {
273
+ switch (node.type) {
274
+ case "VariableDeclaration":
275
+ for (const d of node.declarations) {
276
+ if (d.id.type !== "Identifier") throw new UnsafeExpressionError("destructuring declaration");
277
+ declare(scope, d.id.name, d.init ? evalNode(d.init, scope) : undefined);
278
+ }
279
+ return undefined;
280
+
281
+ case "ExpressionStatement":
282
+ evalNode(node.expression, scope);
283
+ return undefined;
284
+
285
+ case "ReturnStatement":
286
+ return { [RETURNED]: true, value: node.argument ? evalNode(node.argument, scope) : undefined };
287
+
288
+ case "IfStatement":
289
+ if (evalNode(node.test, scope)) return execStatement(node.consequent, scope);
290
+ return node.alternate ? execStatement(node.alternate, scope) : undefined;
291
+
292
+ case "BlockStatement": {
293
+ // Its own scope, so a `const` inside an if-block cannot leak out — and an assignment to an
294
+ // OUTER `let` still reaches it, via `assign` walking the chain. Getting that backwards would
295
+ // silently drop every `if (m) title = ...`, which is the commonest shape there is.
296
+ const inner = childScope(scope);
297
+ for (const s of node.body) {
298
+ const done = execStatement(s, inner);
299
+ if (done) return done;
300
+ }
301
+ return undefined;
302
+ }
303
+
304
+ case "EmptyStatement":
305
+ return undefined;
306
+
307
+ default:
308
+ throw new UnsafeExpressionError(`disallowed statement: ${node.type}`);
309
+ }
310
+ }
311
+
312
+ function prop(obj: unknown, key: PropertyKey): unknown {
313
+ if (BLOCKED_PROPS.has(String(key))) throw new UnsafeExpressionError(`blocked property '${String(key)}'`);
314
+ if (obj == null) return undefined;
315
+ return (obj as Record<PropertyKey, unknown>)[key];
316
+ }
317
+
318
+ /* eslint-disable @typescript-eslint/no-explicit-any */
319
+ function evalNode(node: any, scope: Scope): unknown {
320
+ switch (node.type) {
321
+ case "Literal":
322
+ return node.value;
323
+ case "Identifier":
324
+ if (node.name in scope) return scope[node.name];
325
+ if (Object.prototype.hasOwnProperty.call(SAFE_GLOBALS, node.name)) return SAFE_GLOBALS[node.name];
326
+ if (node.name === "undefined") return undefined;
327
+ throw new UnsafeExpressionError(`unknown identifier '${node.name}'`);
328
+ case "TemplateLiteral": {
329
+ let out = "";
330
+ node.quasis.forEach((q: any, i: number) => {
331
+ out += q.value.cooked;
332
+ if (i < node.expressions.length) out += String(evalNode(node.expressions[i], scope));
333
+ });
334
+ return out;
335
+ }
336
+ case "MemberExpression": {
337
+ const obj = evalNode(node.object, scope);
338
+ const key = node.computed ? (evalNode(node.property, scope) as PropertyKey) : node.property.name;
339
+ return prop(obj, key);
340
+ }
341
+ case "ObjectExpression": {
342
+ const o: Record<string, unknown> = {};
343
+ for (const p of node.properties) {
344
+ if (p.type === "SpreadElement") {
345
+ Object.assign(o, evalNode(p.argument, scope) as object);
346
+ } else {
347
+ const k = p.computed ? String(evalNode(p.key, scope)) : (p.key.name ?? p.key.value);
348
+ if (BLOCKED_PROPS.has(String(k))) throw new UnsafeExpressionError(`blocked key '${k}'`);
349
+ o[k] = evalNode(p.value, scope);
350
+ }
351
+ }
352
+ return o;
353
+ }
354
+ case "ArrayExpression": {
355
+ const arr: unknown[] = [];
356
+ for (const el of node.elements) {
357
+ if (el == null) arr.push(undefined);
358
+ else if (el.type === "SpreadElement") arr.push(...(evalNode(el.argument, scope) as unknown[]));
359
+ else arr.push(evalNode(el, scope));
360
+ }
361
+ return arr;
362
+ }
363
+ case "BinaryExpression": {
364
+ const l = evalNode(node.left, scope) as any;
365
+ const r = evalNode(node.right, scope) as any;
366
+ switch (node.operator) {
367
+ case "+": return l + r;
368
+ case "-": return l - r;
369
+ case "*": return l * r;
370
+ case "/": return l / r;
371
+ case "%": return l % r;
372
+ case "**": return l ** r;
373
+ case "==": return l == r;
374
+ case "!=": return l != r;
375
+ case "===": return l === r;
376
+ case "!==": return l !== r;
377
+ case "<": return l < r;
378
+ case ">": return l > r;
379
+ case "<=": return l <= r;
380
+ case ">=": return l >= r;
381
+ default: throw new UnsafeExpressionError(`operator '${node.operator}'`);
382
+ }
383
+ }
384
+ case "LogicalExpression": {
385
+ const l = evalNode(node.left, scope) as any;
386
+ if (node.operator === "&&") return l && evalNode(node.right, scope);
387
+ if (node.operator === "||") return l || evalNode(node.right, scope);
388
+ if (node.operator === "??") return l ?? evalNode(node.right, scope);
389
+ throw new UnsafeExpressionError(`logical '${node.operator}'`);
390
+ }
391
+ case "UnaryExpression": {
392
+ const v = evalNode(node.argument, scope) as any;
393
+ switch (node.operator) {
394
+ case "!": return !v;
395
+ case "-": return -v;
396
+ case "+": return +v;
397
+ case "typeof": return typeof v;
398
+ default: throw new UnsafeExpressionError(`unary '${node.operator}'`);
399
+ }
400
+ }
401
+ case "ConditionalExpression":
402
+ return evalNode(node.test, scope) ? evalNode(node.consequent, scope) : evalNode(node.alternate, scope);
403
+ case "CallExpression": {
404
+ const args = node.arguments.map((a: any) => evalNode(a, scope));
405
+ if (node.callee.type === "MemberExpression") {
406
+ const obj = evalNode(node.callee.object, scope);
407
+ const method = node.callee.computed ? String(evalNode(node.callee.property, scope)) : node.callee.property.name;
408
+ // Allow a method call if the receiver is a safe-global namespace (JSON/Math/Object/…),
409
+ // OR the method is a known-safe array/string builtin. Never a user-supplied name that
410
+ // could be `constructor`, `call`, `apply`, `bind`, etc.
411
+ const isSafeGlobalNs =
412
+ Object.values(SAFE_GLOBALS).includes(obj as never) || (obj !== null && typeof obj === "object" && NAMESPACES.has(obj));
413
+ if (!isSafeGlobalNs && !SAFE_METHODS.has(method)) throw new UnsafeExpressionError(`method '${method}'`);
414
+ if (BLOCKED_PROPS.has(method)) throw new UnsafeExpressionError(`method '${method}'`);
415
+ const fn = prop(obj, method);
416
+ if (typeof fn !== "function") {
417
+ // A HELPER BAG IS A CLOSED NAMESPACE. `helpers.nopeReport(...)` is a misspelling, and
418
+ // the general member-call rule below — undefined, quietly — turned it into a node that
419
+ // emits undefined on every connector with nothing anywhere reporting why. Throwing
420
+ // UnsafeExpressionError (not TypeError) is what lets the author-time expression scan
421
+ // report it: the scan runs every expression against a dummy scope and deliberately
422
+ // ignores every error EXCEPT the sandbox's own verdict, because missing DATA is
423
+ // expected there and a missing NAME never is.
424
+ if (obj !== null && typeof obj === "object" && NAMESPACES.has(obj))
425
+ throw new UnsafeExpressionError(`unknown helper '${method}'`);
426
+ return undefined;
427
+ }
428
+ return (fn as (...a: unknown[]) => unknown).apply(obj, args);
429
+ }
430
+ // Direct call: only a safe-global function (parseInt, String, …).
431
+ const fn = evalNode(node.callee, scope);
432
+ if (typeof fn !== "function") throw new UnsafeExpressionError("call of non-function");
433
+ // A safe global (parseInt, String, …) OR an arrow this expression itself defined — the latter
434
+ // is what makes `(() => { ... })()` work. Anything else, including a function that arrived on
435
+ // the data context, is still refused.
436
+ if (!Object.values(SAFE_GLOBALS).includes(fn as never) && !OURS.has(fn as Function))
437
+ throw new UnsafeExpressionError("call of non-allowlisted function");
438
+ return (fn as (...a: unknown[]) => unknown)(...args);
439
+ }
440
+ case "ArrowFunctionExpression": {
441
+ // BOTH BODY FORMS. `x => x.name` is the common .map/.filter callback; the block form is what
442
+ // every `return (() => { ... })()` in a Code node is made of, and rejecting it made that node
443
+ // emit its own source text instead of running (see the statement section above).
444
+ const params: string[] = node.params.map((p: any) => {
445
+ if (p.type !== "Identifier") throw new UnsafeExpressionError("non-identifier arrow param");
446
+ return p.name;
447
+ });
448
+ const isBlock = node.body.type === "BlockStatement";
449
+ const fn = (...callArgs: unknown[]) => {
450
+ // A DECLARING scope, not a bare Object.create: parameters and any `const` inside the body
451
+ // must be assignable, and `assign` only writes where a name was declared.
452
+ const local = childScope(scope);
453
+ params.forEach((name, i) => declare(local, name, callArgs[i]));
454
+ if (!isBlock) return evalNode(node.body, local);
455
+ // Falling off the end with no `return` is undefined, exactly as in JS.
456
+ const done = execStatement(node.body, local);
457
+ return done ? done.value : undefined;
458
+ };
459
+ OURS.add(fn);
460
+ return fn;
461
+ }
462
+
463
+ /**
464
+ * Assignment, and ONLY to a name this expression declared.
465
+ *
466
+ * `let title = ''; if (m) title = m[1]` is the shape of nearly every extraction, so without this
467
+ * the block form would parse and then quietly do nothing. `assign` throws for an undeclared
468
+ * name, which is what keeps the run's own data (`signal`, `config`, `calls`) read-only: a
469
+ * template may compute from them and may never rewrite them.
470
+ *
471
+ * Member assignment (`obj.x = 1`) is NOT allowed. It is the one form that could reach through to
472
+ * a caller's object, and building a new object literal expresses the same thing without it.
473
+ */
474
+ case "AssignmentExpression": {
475
+ if (node.left.type !== "Identifier")
476
+ throw new UnsafeExpressionError(`assignment to ${node.left.type === "MemberExpression" ? "a property" : node.left.type}`);
477
+ const name = node.left.name;
478
+ const right = evalNode(node.right, scope) as any;
479
+ if (node.operator === "=") {
480
+ assign(scope, name, right);
481
+ return right;
482
+ }
483
+ const current = evalNode(node.left, scope) as any;
484
+ const next =
485
+ node.operator === "+=" ? current + right
486
+ : node.operator === "-=" ? current - right
487
+ : node.operator === "*=" ? current * right
488
+ : node.operator === "/=" ? current / right
489
+ : node.operator === "%=" ? current % right
490
+ : node.operator === "??=" ? (current ?? right)
491
+ : node.operator === "||=" ? (current || right)
492
+ : node.operator === "&&=" ? (current && right)
493
+ : (() => { throw new UnsafeExpressionError(`assignment operator '${node.operator}'`); })();
494
+ assign(scope, name, next);
495
+ return next;
496
+ }
497
+ default:
498
+ throw new UnsafeExpressionError(`disallowed expression: ${node.type}`);
499
+ }
500
+ }
501
+ /* eslint-enable @typescript-eslint/no-explicit-any */
502
+
503
+ /**
504
+ * Evaluate a `return …` template expression against a data context, safely.
505
+ * Throws UnsafeExpressionError for anything outside the allowlist (logged by the caller).
506
+ */
507
+ export function evaluateSafeExpression(code: string, context: Scope): unknown {
508
+ const ast = parseCached(code);
509
+ return evalNode(ast, context);
510
+ }
511
+
512
+ /** Syntax-check a `return …` expression without evaluating it (throws on a parse error).
513
+ * Used by config validation — replaces a bare `new Function(code)` compile check. */
514
+ export function assertParsableExpression(code: string): void {
515
+ parseCached(code);
516
+ }
517
+
518
+ /** Every AST node type `evalNode` implements. Anything else is refused at run time. */
519
+ const SUPPORTED = new Set([
520
+ "ArrayExpression", "ArrowFunctionExpression", "AssignmentExpression", "BinaryExpression",
521
+ "BlockStatement", "CallExpression", "ConditionalExpression", "EmptyStatement",
522
+ "ExpressionStatement", "Identifier", "IfStatement", "Literal", "LogicalExpression",
523
+ "MemberExpression", "ObjectExpression", "Program", "Property", "ReturnStatement",
524
+ "TemplateLiteral", "TemplateElement", "UnaryExpression", "VariableDeclaration",
525
+ "VariableDeclarator",
526
+ ]);
527
+
528
+ /**
529
+ * STATICALLY reject anything the interpreter cannot run, on EVERY branch.
530
+ *
531
+ * `assertParsableExpression` above only parses, and the sandbox check that complements it
532
+ * only sees the path a dummy scope happens to reach. Both missed the same real bug: a
533
+ * `...spread` inside a `.map()` callback that a dummy scope never entered, because
534
+ * `response.results` was `{}` and the expression died on "map is not a function" long before
535
+ * it got there. That error is ignored on purpose, so the spread shipped, reached the vendor,
536
+ * embedded the query, and failed on the way back with `disallowed expression: SpreadElement`.
537
+ *
538
+ * Walking the tree needs no scope and no execution, so a dead branch is checked exactly like
539
+ * a live one. That is the whole point: an expression is legal or it is not, and which
540
+ * arguments it happens to receive has nothing to do with it.
541
+ */
542
+ export function assertSupportedExpression(code: string): void {
543
+ const walk = (node: any): void => {
544
+ if (!node || typeof node.type !== "string") return;
545
+ if (!SUPPORTED.has(node.type)) throw new UnsafeExpressionError(`disallowed expression: ${node.type}`);
546
+ for (const key of Object.keys(node)) {
547
+ if (key === "type" || key === "start" || key === "end" || key === "loc") continue;
548
+ const child = (node as any)[key];
549
+ if (Array.isArray(child)) child.forEach(walk);
550
+ else if (child && typeof child === "object") walk(child);
551
+ }
552
+ };
553
+ walk(parseCached(code));
554
+ }
555
+
556
+ export { UnsafeExpressionError };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * String Template Resolver using Handlebars
3
+ * For handling template string mode with ${variable} syntax
4
+ */
5
+
6
+ import Handlebars from "handlebars";
7
+ import memoizeImport from "fast-memoize";
8
+ /**
9
+ * fast-memoize ships CommonJS (`module.exports = memoize`) with ESM-shaped typings
10
+ * (`export default`). Under NodeNext those disagree: the runtime value IS the function,
11
+ * but TypeScript types the default import as the module namespace and calls it uncallable.
12
+ * Verified at runtime before casting, rather than assuming.
13
+ */
14
+ type Memoizer = <F extends (...args: any[]) => any>(fn: F) => F;
15
+ const memoize = memoizeImport as unknown as Memoizer;
16
+
17
+ /**
18
+ * The data a template renders against. DECLARED HERE rather than imported from the engine's
19
+ * TemplateResolver, which is where it used to live: this module is the lower layer of the
20
+ * two, and pointing it back up at its own consumer is what tied the whole template folder
21
+ * to the engine. Structurally identical, so nothing that already passes a context changes.
22
+ */
23
+ export interface TemplateContext {
24
+ input?: any;
25
+ workflow?: any;
26
+ [key: string]: any;
27
+ }
28
+
29
+ // Register custom helpers
30
+ Handlebars.registerHelper("toJSON", function (context) {
31
+ return JSON.stringify(context, null, 2);
32
+ });
33
+
34
+ // Register a "filter" helper
35
+ Handlebars.registerHelper("filter", function (array, key, value) {
36
+ if (!Array.isArray(array)) return [];
37
+ return array.filter((item) => item && item[key] === value);
38
+ });
39
+
40
+ // Register equality helper for conditionals
41
+ Handlebars.registerHelper("eq", function (a, b) {
42
+ return a === b;
43
+ });
44
+
45
+ // Register contains helper for string matching
46
+ Handlebars.registerHelper("contains", function (str, substring) {
47
+ if (typeof str !== "string" || typeof substring !== "string") return false;
48
+ return str.toLowerCase().includes(substring.toLowerCase());
49
+ });
50
+
51
+ // Create a memoized template compiler for performance
52
+ const compileTemplate = memoize((template: string) => {
53
+ // Disable HTML escaping to prevent issues like "I'll" becoming "I&#x27;ll"
54
+ return Handlebars.compile(template, { noEscape: true });
55
+ });
56
+
57
+ export function resolveStringTemplate(template: string, context: TemplateContext, logger: any): any {
58
+ try {
59
+ // Template processing
60
+
61
+ // Compile and execute the template
62
+ const compiledTemplate = compileTemplate(template);
63
+ const result = compiledTemplate(context);
64
+
65
+ // Template processed
66
+ return result;
67
+ } catch (error) {
68
+ logger.debug("Failed to resolve string template", {
69
+ error: error instanceof Error ? error.message : String(error),
70
+ template: template.substring(0, 200) + (template.length > 200 ? "..." : ""),
71
+ });
72
+
73
+ throw error;
74
+ }
75
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Expressions and templates — the two ways a manifest or a workflow turns data into a value.
3
+ *
4
+ * LIVES HERE, not in the engine, because neither file knows anything about workflows,
5
+ * XState or the database: one is a sandboxed expression evaluator, the other a Handlebars
6
+ * wrapper. They sat under `engine/` only because that is where they were first needed,
7
+ * before manifests existed, and the manifest runtime then had to reach five directories
8
+ * upward by SOURCE URL to get at them. That path resolved inside this monorepo and nowhere
9
+ * else, which was the single thing keeping this package unpublishable.
10
+ *
11
+ * Three consumers now, and all three import it the same way: the manifest runtime, the
12
+ * engine's own template resolution, and node config validation.
13
+ */
14
+ export {
15
+ evaluateSafeExpression,
16
+ assertParsableExpression,
17
+ assertSupportedExpression,
18
+ makeHelpers,
19
+ UnsafeExpressionError,
20
+ type HelperDef,
21
+ } from "./SafeExpression.js";
22
+ export { resolveStringTemplate, type TemplateContext } from "./StringTemplateResolver.js";