@uicast/core 0.0.1-beta.1 → 0.0.1
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.
- package/CHANGELOG.md +3 -6
- package/README.md +120 -1
- package/dist/build-elements-by-key.d.ts +13 -0
- package/dist/build-elements-by-key.js +39 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +4 -0
- package/dist/create-component-definition.d.ts +26 -0
- package/dist/create-component-definition.js +74 -0
- package/dist/entry-error.d.ts +57 -0
- package/dist/entry-error.js +85 -0
- package/dist/entry-shape.d.ts +7 -0
- package/dist/entry-shape.js +88 -0
- package/dist/expr/evaluate.d.ts +5 -0
- package/dist/expr/evaluate.js +54 -0
- package/dist/expr/extract-deps.d.ts +4 -0
- package/dist/expr/extract-deps.js +30 -0
- package/dist/expr/plan-step-waves.d.ts +8 -0
- package/dist/expr/plan-step-waves.js +45 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +6 -0
- package/dist/internal.d.ts +12 -0
- package/dist/internal.js +13 -0
- package/dist/json-schema.d.ts +58 -0
- package/dist/json-schema.js +73 -0
- package/dist/prompt/format.d.ts +5 -0
- package/dist/prompt/format.js +22 -0
- package/dist/prompt/get-common-instructions-partial-prompt.d.ts +23 -0
- package/dist/prompt/get-common-instructions-partial-prompt.js +16 -0
- package/dist/prompt/get-components-partial-prompt.d.ts +27 -0
- package/dist/prompt/get-components-partial-prompt.js +133 -0
- package/dist/prompt/get-edit-request-prompt.d.ts +23 -0
- package/dist/prompt/get-edit-request-prompt.js +15 -0
- package/dist/prompt/get-error-recovery-prompt.d.ts +35 -0
- package/dist/prompt/get-error-recovery-prompt.js +26 -0
- package/dist/prompt/get-expressions-partial-prompt.d.ts +23 -0
- package/dist/prompt/get-expressions-partial-prompt.js +17 -0
- package/dist/prompt/get-functions-partial-prompt.d.ts +22 -0
- package/dist/prompt/get-functions-partial-prompt.js +58 -0
- package/dist/prompt/get-scope-partial-prompt.d.ts +24 -0
- package/dist/prompt/get-scope-partial-prompt.js +30 -0
- package/dist/prompt/index.d.ts +7 -0
- package/dist/prompt/index.js +7 -0
- package/dist/prompt/json-schema-to-ts.d.ts +6 -0
- package/dist/prompt/json-schema-to-ts.js +146 -0
- package/dist/prompt/md/EXPRESSIONS.json +1 -0
- package/dist/prompt/md/INSTRUCTIONS.json +1 -0
- package/dist/prompt/shared-types.d.ts +5 -0
- package/dist/prompt/shared-types.js +82 -0
- package/dist/scope/create-proxy-scope.d.ts +55 -0
- package/dist/scope/create-proxy-scope.js +295 -0
- package/dist/scope/parse-scope.d.ts +2 -0
- package/dist/scope/parse-scope.js +11 -0
- package/dist/scope/parse-set-address.d.ts +16 -0
- package/dist/scope/parse-set-address.js +52 -0
- package/dist/stream-json-lines.d.ts +14 -0
- package/dist/stream-json-lines.js +59 -0
- package/dist/types.d.ts +124 -0
- package/dist/types.js +19 -0
- package/dist/url-policy.d.ts +45 -0
- package/dist/url-policy.js +252 -0
- package/package.json +27 -18
- package/src/def/create-component-definition.ts +0 -21
- package/src/entry-error.ts +0 -104
- package/src/expr/analyze.ts +0 -190
- package/src/expr/ast-utils.ts +0 -28
- package/src/expr/evaluate.ts +0 -74
- package/src/expr/extract-deps.ts +0 -32
- package/src/expr/globals.ts +0 -118
- package/src/expr/safer-eval.ts +0 -233
- package/src/expr/validate.ts +0 -149
- package/src/index.ts +0 -28
- package/src/prompt/get-common-instructions-partial-prompt.ts +0 -20
- package/src/prompt/get-components-partial-prompt.ts +0 -153
- package/src/prompt/get-edit-request-prompt.ts +0 -37
- package/src/prompt/get-error-recovery-prompt.ts +0 -37
- package/src/prompt/get-expressions-partial-prompt.ts +0 -33
- package/src/prompt/get-functions-partial-prompt.ts +0 -62
- package/src/prompt/get-scope-partial-prompt.ts +0 -57
- package/src/prompt/index.ts +0 -32
- package/src/prompt/md/EXPRESSIONS.json +0 -1
- package/src/prompt/md/EXPRESSIONS.md +0 -29
- package/src/prompt/md/INSTRUCTIONS.json +0 -1
- package/src/prompt/md/INSTRUCTIONS.md +0 -172
- package/src/prompt-utils/json-schema-to-ts.ts +0 -225
- package/src/scope/create-proxy-scope.ts +0 -131
- package/src/scope/parse-scope.ts +0 -36
- package/src/stream/stream-json-lines.ts +0 -56
- package/src/types.ts +0 -124
- package/src/utils/utils.ts +0 -64
package/src/expr/evaluate.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import type { StandardToolV0 } from "standard-tool";
|
|
2
|
-
import type { ValueSource } from "../types";
|
|
3
|
-
import { EntryError } from "../entry-error";
|
|
4
|
-
import { ALLOWED_GLOBALS } from "./globals";
|
|
5
|
-
import { SaferEval, SaferEvalError } from "./safer-eval";
|
|
6
|
-
|
|
7
|
-
const saferEval = new SaferEval({
|
|
8
|
-
allowGlobals: ALLOWED_GLOBALS,
|
|
9
|
-
enforceAllowlist: true,
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
// Every scopes.X.Y path an expression reads, via the shared SaferEval singleton
|
|
13
|
-
// (so extractDeps doesn't reach into the evaluator). Same parse cache as eval().
|
|
14
|
-
export const getScopeReads = (expr: string): string[] =>
|
|
15
|
-
saferEval.scopeReads(expr);
|
|
16
|
-
|
|
17
|
-
// Classify an evaluation failure by provenance: SaferEval's own rejections
|
|
18
|
-
// carry their reason (syntax / policy / unknown identifier); an EntryError
|
|
19
|
-
// passes through (a host function already tagged it); anything else threw
|
|
20
|
-
// while the compiled expression ran.
|
|
21
|
-
const wrapEvalError = (err: unknown): EntryError => {
|
|
22
|
-
if (EntryError.is(err)) return err;
|
|
23
|
-
if (err instanceof SaferEvalError) return EntryError.wrap(err, err.reason);
|
|
24
|
-
return EntryError.wrap(err, "expression-runtime");
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// Evaluate a ValueSource: a `literal` is returned as-is, an `expr` runs through
|
|
28
|
-
// SaferEval against context + host functions. Returns unknown — an async expr
|
|
29
|
-
// resolves to a Promise (callers check `value instanceof Promise`). `functions`
|
|
30
|
-
// (a StandardToolV0[]) are spread after context, so a host fn wins a name clash.
|
|
31
|
-
// Every failure surfaces as a classified EntryError.
|
|
32
|
-
export const evaluate = (
|
|
33
|
-
expr: ValueSource,
|
|
34
|
-
context: Record<string, unknown>,
|
|
35
|
-
options?: { functions?: StandardToolV0[]; allowedGlobals?: string[] },
|
|
36
|
-
): unknown => {
|
|
37
|
-
if ("literal" in expr) return expr.literal;
|
|
38
|
-
if (!expr.expr) return null;
|
|
39
|
-
const functions = Object.fromEntries(
|
|
40
|
-
(options?.functions ?? []).map((tool) => [
|
|
41
|
-
tool.name,
|
|
42
|
-
// This closure is the engine/host boundary: anything escaping
|
|
43
|
-
// `tool.execute` is host code failing — unless the host already threw a
|
|
44
|
-
// classified EntryError itself (e.g. "invalid-arguments"), which wrap()
|
|
45
|
-
// passes through.
|
|
46
|
-
(input: unknown) => {
|
|
47
|
-
try {
|
|
48
|
-
const result = tool.execute(input);
|
|
49
|
-
return result instanceof Promise
|
|
50
|
-
? result.catch((err) => {
|
|
51
|
-
throw EntryError.wrap(err, "host-function");
|
|
52
|
-
})
|
|
53
|
-
: result;
|
|
54
|
-
} catch (err) {
|
|
55
|
-
throw EntryError.wrap(err, "host-function");
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
]),
|
|
59
|
-
);
|
|
60
|
-
try {
|
|
61
|
-
const result = saferEval.eval(
|
|
62
|
-
expr.expr,
|
|
63
|
-
{ ...context, ...functions },
|
|
64
|
-
options?.allowedGlobals,
|
|
65
|
-
);
|
|
66
|
-
return result instanceof Promise
|
|
67
|
-
? result.catch((err) => {
|
|
68
|
-
throw wrapEvalError(err);
|
|
69
|
-
})
|
|
70
|
-
: result;
|
|
71
|
-
} catch (err) {
|
|
72
|
-
throw wrapEvalError(err);
|
|
73
|
-
}
|
|
74
|
-
};
|
package/src/expr/extract-deps.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { type ComponentEntry, isComponentListEntry } from "../types";
|
|
2
|
-
import { getScopeReads } from "./evaluate";
|
|
3
|
-
|
|
4
|
-
// Entries are immutable, so cached reads never go stale; WeakMap so a dropped
|
|
5
|
-
// entry can be collected.
|
|
6
|
-
const cache = new WeakMap<ComponentEntry, string[]>();
|
|
7
|
-
|
|
8
|
-
// The reactive scopes.X.Y paths an entry reads, across its props, hidden, and each
|
|
9
|
-
// expressions — the renderer subscribes to these. (seed run once and
|
|
10
|
-
// callbacks read at fire time, so neither is scanned.)
|
|
11
|
-
export function extractDeps(entry: ComponentEntry): string[] {
|
|
12
|
-
const cached = cache.get(entry);
|
|
13
|
-
if (cached) return cached;
|
|
14
|
-
|
|
15
|
-
const out = new Set<string>();
|
|
16
|
-
|
|
17
|
-
if (entry.props && "expr" in entry.props && entry.props.expr) {
|
|
18
|
-
for (const r of getScopeReads(entry.props.expr)) out.add(r);
|
|
19
|
-
}
|
|
20
|
-
if (entry.hidden) {
|
|
21
|
-
for (const r of getScopeReads(entry.hidden)) out.add(r);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// `each` reads both the list and anything its filter touches (e.g. a search term).
|
|
25
|
-
if (isComponentListEntry(entry)) {
|
|
26
|
-
for (const r of getScopeReads(entry.each)) out.add(r);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const result = [...out];
|
|
30
|
-
cache.set(entry, result);
|
|
31
|
-
return result;
|
|
32
|
-
}
|
package/src/expr/globals.ts
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
// Which JavaScript globals an expression can see. The engine enforces an
|
|
2
|
-
// allowlist (safer-eval.ts): a free identifier that is neither an allowed global
|
|
3
|
-
// nor an injected context name is rejected before the expression runs.
|
|
4
|
-
// ALLOWED_GLOBALS is that list — also handed to SaferEval as allowGlobals and
|
|
5
|
-
// shown to the LLM in the prompt. GLOBALS_TO_SHADOW are bound to undefined as a
|
|
6
|
-
// second line of defence, so an allowed-by-mistake reference still stays inert.
|
|
7
|
-
|
|
8
|
-
/** Globals an expression may reference. Every entry is also usable with `new`. */
|
|
9
|
-
export const ALLOWED_GLOBALS: string[] = [
|
|
10
|
-
// Namespaces / utilities
|
|
11
|
-
"Math",
|
|
12
|
-
"JSON",
|
|
13
|
-
"Intl",
|
|
14
|
-
"Object",
|
|
15
|
-
"Array",
|
|
16
|
-
// Coercion (use as calls — `new Number(x)` etc. boxes the value)
|
|
17
|
-
"Number",
|
|
18
|
-
"String",
|
|
19
|
-
"Boolean",
|
|
20
|
-
"parseInt",
|
|
21
|
-
"parseFloat",
|
|
22
|
-
// Predicates / constants
|
|
23
|
-
"isNaN",
|
|
24
|
-
"isFinite",
|
|
25
|
-
"undefined",
|
|
26
|
-
"NaN",
|
|
27
|
-
"Infinity",
|
|
28
|
-
// URI helpers
|
|
29
|
-
"encodeURIComponent",
|
|
30
|
-
"decodeURIComponent",
|
|
31
|
-
"encodeURI",
|
|
32
|
-
"decodeURI",
|
|
33
|
-
// Constructors & async
|
|
34
|
-
"Date",
|
|
35
|
-
"Map",
|
|
36
|
-
"Set",
|
|
37
|
-
"RegExp",
|
|
38
|
-
"URL",
|
|
39
|
-
"URLSearchParams",
|
|
40
|
-
"Promise",
|
|
41
|
-
"BigInt",
|
|
42
|
-
// Error constructors
|
|
43
|
-
"Error",
|
|
44
|
-
"TypeError",
|
|
45
|
-
"RangeError",
|
|
46
|
-
"SyntaxError",
|
|
47
|
-
"ReferenceError",
|
|
48
|
-
"EvalError",
|
|
49
|
-
"URIError",
|
|
50
|
-
"AggregateError",
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
// Capability globals shadowed to undefined. (`eval` / `arguments` can't be param
|
|
54
|
-
// names in strict mode — blocked in validate.ts instead.)
|
|
55
|
-
export const GLOBALS_TO_SHADOW: string[] = [
|
|
56
|
-
// Global objects
|
|
57
|
-
"globalThis",
|
|
58
|
-
"self",
|
|
59
|
-
"window",
|
|
60
|
-
"global",
|
|
61
|
-
"document",
|
|
62
|
-
"navigator",
|
|
63
|
-
"location",
|
|
64
|
-
"history",
|
|
65
|
-
"localStorage",
|
|
66
|
-
"sessionStorage",
|
|
67
|
-
"indexedDB",
|
|
68
|
-
|
|
69
|
-
// Network / IO
|
|
70
|
-
"fetch",
|
|
71
|
-
"XMLHttpRequest",
|
|
72
|
-
"WebSocket",
|
|
73
|
-
"EventSource",
|
|
74
|
-
"Worker",
|
|
75
|
-
"SharedWorker",
|
|
76
|
-
"ServiceWorker",
|
|
77
|
-
"importScripts",
|
|
78
|
-
// Fire a network request via `.src` — an exfiltration channel even with fetch
|
|
79
|
-
// shadowed.
|
|
80
|
-
"Image",
|
|
81
|
-
"Audio",
|
|
82
|
-
|
|
83
|
-
// Code execution
|
|
84
|
-
"Function",
|
|
85
|
-
"WebAssembly", // runs arbitrary code
|
|
86
|
-
"setTimeout",
|
|
87
|
-
"setInterval",
|
|
88
|
-
"setImmediate",
|
|
89
|
-
"requestAnimationFrame",
|
|
90
|
-
"requestIdleCallback",
|
|
91
|
-
"queueMicrotask",
|
|
92
|
-
|
|
93
|
-
// Process / non-browser runtimes
|
|
94
|
-
"process",
|
|
95
|
-
"require",
|
|
96
|
-
"module",
|
|
97
|
-
"exports",
|
|
98
|
-
"__dirname",
|
|
99
|
-
"__filename",
|
|
100
|
-
"Buffer",
|
|
101
|
-
"Deno", // runtime god-object (fs/net/env) outside a browser
|
|
102
|
-
"Bun",
|
|
103
|
-
|
|
104
|
-
// DOM
|
|
105
|
-
"alert",
|
|
106
|
-
"confirm",
|
|
107
|
-
"prompt",
|
|
108
|
-
"close",
|
|
109
|
-
"open",
|
|
110
|
-
"print",
|
|
111
|
-
"postMessage",
|
|
112
|
-
|
|
113
|
-
// Constructors that can escape
|
|
114
|
-
"Proxy",
|
|
115
|
-
"Reflect",
|
|
116
|
-
"SharedArrayBuffer",
|
|
117
|
-
"Atomics",
|
|
118
|
-
];
|
package/src/expr/safer-eval.ts
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
import * as acorn from "acorn";
|
|
2
|
-
import {
|
|
3
|
-
containsAwait,
|
|
4
|
-
extractFreeIdentifiers,
|
|
5
|
-
extractScopeReads,
|
|
6
|
-
} from "./analyze";
|
|
7
|
-
import { GLOBALS_TO_SHADOW } from "./globals";
|
|
8
|
-
import { SaferEvalError, validateNode } from "./validate";
|
|
9
|
-
|
|
10
|
-
// Compiles and runs the expressions validate.ts approves: parse + analyse once
|
|
11
|
-
// (cached), compile to a Function (AsyncFunction when the expr uses `await`),
|
|
12
|
-
// shadow ambient globals as undefined params, run in-realm so Proxy state works.
|
|
13
|
-
// Guardrail, not a sandbox — see the Expressions docs page, § Safety.
|
|
14
|
-
|
|
15
|
-
// Re-exported from validate.ts for callers of this module.
|
|
16
|
-
export { SaferEvalError };
|
|
17
|
-
|
|
18
|
-
// The AsyncFunction constructor, for expressions that use `await`.
|
|
19
|
-
const AsyncFunction = Object.getPrototypeOf(async () => {})
|
|
20
|
-
.constructor as typeof Function;
|
|
21
|
-
|
|
22
|
-
type CacheEntry = {
|
|
23
|
-
isAsync: boolean;
|
|
24
|
-
scopeReads: string[];
|
|
25
|
-
freeIds: string[];
|
|
26
|
-
// Built lazily on first compile(); validate()/scopeReads() leave it unset.
|
|
27
|
-
fn?: (context: Record<string, unknown>) => unknown;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export class SaferEval {
|
|
31
|
-
#cache = new Map<string, CacheEntry>();
|
|
32
|
-
#maxCacheSize: number;
|
|
33
|
-
#shadowParams: string[];
|
|
34
|
-
#allowGlobals: Set<string>;
|
|
35
|
-
#enforceAllowlist: boolean;
|
|
36
|
-
|
|
37
|
-
constructor(
|
|
38
|
-
options: {
|
|
39
|
-
maxCacheSize?: number;
|
|
40
|
-
extraGlobalsToShadow?: string[];
|
|
41
|
-
allowGlobals?: string[];
|
|
42
|
-
enforceAllowlist?: boolean;
|
|
43
|
-
} = {},
|
|
44
|
-
) {
|
|
45
|
-
const {
|
|
46
|
-
maxCacheSize = 500,
|
|
47
|
-
extraGlobalsToShadow = [],
|
|
48
|
-
allowGlobals = [],
|
|
49
|
-
enforceAllowlist = false,
|
|
50
|
-
} = options;
|
|
51
|
-
|
|
52
|
-
this.#maxCacheSize = maxCacheSize;
|
|
53
|
-
this.#enforceAllowlist = enforceAllowlist;
|
|
54
|
-
|
|
55
|
-
const allowSet = new Set(allowGlobals);
|
|
56
|
-
this.#allowGlobals = allowSet;
|
|
57
|
-
this.#shadowParams = [...GLOBALS_TO_SHADOW, ...extraGlobalsToShadow].filter(
|
|
58
|
-
(g) => !allowSet.has(g),
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Parse + validate without running. Throws SaferEvalError if invalid; returns
|
|
63
|
-
// isAsync + scopeReads and caches the analysis (compile() builds the fn later).
|
|
64
|
-
validate(expression: string): {
|
|
65
|
-
isAsync: boolean;
|
|
66
|
-
scopeReads: string[];
|
|
67
|
-
freeIds: string[];
|
|
68
|
-
} {
|
|
69
|
-
if (typeof expression !== "string") {
|
|
70
|
-
throw new SaferEvalError("Expression must be a string", "expression-syntax");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const trimmed = expression.trim();
|
|
74
|
-
if (!trimmed) {
|
|
75
|
-
throw new SaferEvalError("Expression cannot be empty", "expression-syntax");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const cached = this.#cache.get(expression);
|
|
79
|
-
if (cached) {
|
|
80
|
-
return {
|
|
81
|
-
isAsync: cached.isAsync,
|
|
82
|
-
scopeReads: cached.scopeReads,
|
|
83
|
-
freeIds: cached.freeIds,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Parse as `"use strict"; void (expr)`: void(...) forces expression context
|
|
88
|
-
// (so `{...}` is an object literal, not a block), and strict mode rejects
|
|
89
|
-
// sloppy-only constructs (octal, `with`, dup params) here, not at compile.
|
|
90
|
-
const wrapper = `"use strict"; void (${trimmed})`;
|
|
91
|
-
let ast: acorn.Program;
|
|
92
|
-
try {
|
|
93
|
-
ast = acorn.parse(wrapper, {
|
|
94
|
-
ecmaVersion: 2022,
|
|
95
|
-
sourceType: "script",
|
|
96
|
-
allowAwaitOutsideFunction: true,
|
|
97
|
-
});
|
|
98
|
-
} catch (e: unknown) {
|
|
99
|
-
throw new SaferEvalError(
|
|
100
|
-
`Syntax error: ${e instanceof Error ? e.message : String(e)}. Expression: ${expression}`,
|
|
101
|
-
"expression-syntax",
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
validateNode(ast);
|
|
106
|
-
|
|
107
|
-
const analysis = {
|
|
108
|
-
isAsync: containsAwait(ast),
|
|
109
|
-
scopeReads: extractScopeReads(ast),
|
|
110
|
-
freeIds: extractFreeIdentifiers(ast),
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
// Cache the analysis (no fn yet); evict oldest when full.
|
|
114
|
-
if (this.#cache.size >= this.#maxCacheSize) {
|
|
115
|
-
const firstKey = this.#cache.keys().next().value;
|
|
116
|
-
if (firstKey !== undefined) this.#cache.delete(firstKey);
|
|
117
|
-
}
|
|
118
|
-
this.#cache.set(expression, { ...analysis });
|
|
119
|
-
|
|
120
|
-
return analysis;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Compile to a reusable fn (returns a Promise if the expr uses `await`).
|
|
124
|
-
compile(expression: string): (context: Record<string, unknown>) => unknown {
|
|
125
|
-
const { isAsync } = this.validate(expression);
|
|
126
|
-
const entry = this.#cache.get(expression);
|
|
127
|
-
if (!entry) {
|
|
128
|
-
// Unreachable — validate() just inserted this entry.
|
|
129
|
-
throw new SaferEvalError(
|
|
130
|
-
`Cache entry missing for expression: ${expression}`,
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
if (entry.fn) return entry.fn;
|
|
134
|
-
|
|
135
|
-
const expr = expression.trim();
|
|
136
|
-
const Ctor = isAsync ? AsyncFunction : Function;
|
|
137
|
-
|
|
138
|
-
// Single-slot cache: new Ctor bakes in the param names (context keys ∪ shadow
|
|
139
|
-
// params), so it recompiles only if the context shape changes — which it
|
|
140
|
-
// doesn't in practice (a given expr always sees the same keys).
|
|
141
|
-
let cachedSig: string | undefined;
|
|
142
|
-
let cachedFn: ((...args: unknown[]) => unknown) | undefined;
|
|
143
|
-
|
|
144
|
-
const evaluator = (context: Record<string, unknown>) => {
|
|
145
|
-
const contextKeys = Object.keys(context);
|
|
146
|
-
const sig = contextKeys.join("\u0000");
|
|
147
|
-
if (sig !== cachedSig) {
|
|
148
|
-
// Context keys become params; shadow params follow, bound to undefined.
|
|
149
|
-
const allParams = [...contextKeys, ...this.#shadowParams];
|
|
150
|
-
try {
|
|
151
|
-
cachedFn = new Ctor(
|
|
152
|
-
...allParams,
|
|
153
|
-
`"use strict"; return (${expr})`,
|
|
154
|
-
) as (...args: unknown[]) => unknown;
|
|
155
|
-
} catch (e: unknown) {
|
|
156
|
-
// Parsed but won't compile — surface as SaferEvalError, not a raw one.
|
|
157
|
-
throw new SaferEvalError(
|
|
158
|
-
`Failed to compile expression: ${
|
|
159
|
-
e instanceof Error ? e.message : String(e)
|
|
160
|
-
}. Expression: ${expression}`,
|
|
161
|
-
"expression-syntax",
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
cachedSig = sig;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Args match allParams: context values (Object.keys order) then shadow fills.
|
|
168
|
-
const allArgs = [
|
|
169
|
-
...Object.values(context),
|
|
170
|
-
...new Array(this.#shadowParams.length).fill(undefined),
|
|
171
|
-
];
|
|
172
|
-
return (cachedFn as (...args: unknown[]) => unknown)(...allArgs);
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
entry.fn = evaluator;
|
|
176
|
-
return evaluator;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
isAsync(expression: string): boolean {
|
|
180
|
-
const cached = this.#cache.get(expression);
|
|
181
|
-
if (cached) return cached.isAsync;
|
|
182
|
-
return this.validate(expression).isAsync;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Every `scopes.X.Y` path the expression reads — the renderer auto-subscribes
|
|
186
|
-
// to these instead of the LLM writing a deps array.
|
|
187
|
-
scopeReads(expression: string): string[] {
|
|
188
|
-
const cached = this.#cache.get(expression);
|
|
189
|
-
if (cached) return cached.scopeReads;
|
|
190
|
-
return this.validate(expression).scopeReads;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Allowlist gate: every free identifier must be an injected context name, a
|
|
194
|
-
// base allowed global, or one the host opted into via allowedGlobals.
|
|
195
|
-
#checkAllowlist(
|
|
196
|
-
freeIds: string[],
|
|
197
|
-
contextKeys: string[],
|
|
198
|
-
allowedGlobals: string[],
|
|
199
|
-
): void {
|
|
200
|
-
const allowed = new Set([
|
|
201
|
-
...contextKeys,
|
|
202
|
-
...this.#allowGlobals,
|
|
203
|
-
...allowedGlobals,
|
|
204
|
-
]);
|
|
205
|
-
for (const id of freeIds) {
|
|
206
|
-
if (!allowed.has(id)) {
|
|
207
|
-
// The expression named something that doesn't exist here — an
|
|
208
|
-
// unregistered function or a global outside the allowlist.
|
|
209
|
-
throw new SaferEvalError(
|
|
210
|
-
`"${id}" is not available in expressions`,
|
|
211
|
-
"unknown-reference",
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
eval(
|
|
218
|
-
expression: string,
|
|
219
|
-
context: Record<string, unknown> = {},
|
|
220
|
-
allowedGlobals: string[] = [],
|
|
221
|
-
): unknown {
|
|
222
|
-
if (this.#enforceAllowlist) {
|
|
223
|
-
const { freeIds } = this.validate(expression);
|
|
224
|
-
this.#checkAllowlist(freeIds, Object.keys(context), allowedGlobals);
|
|
225
|
-
}
|
|
226
|
-
const evaluator = this.compile(expression);
|
|
227
|
-
return evaluator(context);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
clearCache(): void {
|
|
231
|
-
this.#cache.clear();
|
|
232
|
-
}
|
|
233
|
-
}
|
package/src/expr/validate.ts
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import type * as acorn from "acorn";
|
|
2
|
-
import { isNode } from "./ast-utils";
|
|
3
|
-
|
|
4
|
-
// Expression validation — the guardrail. Walks a parsed expression and throws on
|
|
5
|
-
// anything that shouldn't run; safer-eval.ts compiles only what this approves.
|
|
6
|
-
// Guardrail, NOT a sandbox: a computed key (`obj["con"+"structor"]`) escapes it.
|
|
7
|
-
// Full threat model: the Expressions docs page, § Safety.
|
|
8
|
-
|
|
9
|
-
// Which EntryError reason an evaluation failure maps to. Defaults to
|
|
10
|
-
// "sandbox-violation" (every policy rejection in this file); the parse and
|
|
11
|
-
// allowlist sites in safer-eval.ts override it.
|
|
12
|
-
export type SaferEvalErrorReason =
|
|
13
|
-
| "expression-syntax"
|
|
14
|
-
| "sandbox-violation"
|
|
15
|
-
| "unknown-reference";
|
|
16
|
-
|
|
17
|
-
export class SaferEvalError extends Error {
|
|
18
|
-
readonly reason: SaferEvalErrorReason;
|
|
19
|
-
|
|
20
|
-
constructor(
|
|
21
|
-
message: string,
|
|
22
|
-
reason: SaferEvalErrorReason = "sandbox-violation",
|
|
23
|
-
) {
|
|
24
|
-
super(message);
|
|
25
|
-
this.name = "SaferEvalError";
|
|
26
|
-
this.reason = reason;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// `eval` / `arguments` can't be shadowed as strict-mode params, so reject them as
|
|
31
|
-
// references here instead (the rest are shadowed — see GLOBALS_TO_SHADOW).
|
|
32
|
-
const FORBIDDEN_IDENTIFIERS = new Set(["eval", "arguments"]);
|
|
33
|
-
|
|
34
|
-
// Most dangerous syntax never reaches this walk: every expression is parsed as
|
|
35
|
-
// `"use strict"; void (EXPR)`, so a top-level statement / declaration / `with` /
|
|
36
|
-
// octal / import is a syntax error. What survives, to reject by hand, is below.
|
|
37
|
-
|
|
38
|
-
// Forbidden everywhere, even in a function body: dynamic `import()` loads code,
|
|
39
|
-
// and `import.meta` / `new.target` (MetaProperty) reach host / constructor
|
|
40
|
-
// internals. Keywords, so shadowing can't touch them — block structurally.
|
|
41
|
-
const FORBIDDEN_EXPRESSIONS = new Set(["ImportExpression", "MetaProperty"]);
|
|
42
|
-
|
|
43
|
-
// Forbidden as the whole expression (which must be one pure value); fine inside a
|
|
44
|
-
// callback body where imperative code is allowed.
|
|
45
|
-
const BODY_ONLY_EXPRESSIONS = new Set([
|
|
46
|
-
"AssignmentExpression", // x = 1, x += 1
|
|
47
|
-
"UpdateExpression", // x++, --x
|
|
48
|
-
"SequenceExpression", // a, b — comma operator
|
|
49
|
-
]);
|
|
50
|
-
|
|
51
|
-
// Escape-hatch property names: `.constructor` reaches the Function constructor,
|
|
52
|
-
// `.__proto__` the prototype chain. Only static keys are caught; a computed key
|
|
53
|
-
// escapes (the guardrail gap — banning it would ban all `arr[i]` access).
|
|
54
|
-
const FORBIDDEN_PROPERTIES = new Set([
|
|
55
|
-
"constructor",
|
|
56
|
-
"__proto__",
|
|
57
|
-
"prototype",
|
|
58
|
-
"__defineGetter__",
|
|
59
|
-
"__defineSetter__",
|
|
60
|
-
"__lookupGetter__",
|
|
61
|
-
"__lookupSetter__",
|
|
62
|
-
]);
|
|
63
|
-
|
|
64
|
-
// Recursively walk the parsed expression, throwing on anything disallowed.
|
|
65
|
-
// insideFunctionBody flips once inside an arrow / function body, where the
|
|
66
|
-
// BODY_ONLY_EXPRESSIONS become legal. Entry point is acorn's Program node.
|
|
67
|
-
export function validateNode(
|
|
68
|
-
node: acorn.AnyNode | null | undefined,
|
|
69
|
-
insideFunctionBody = false,
|
|
70
|
-
): void {
|
|
71
|
-
if (!node || typeof node !== "object") return;
|
|
72
|
-
|
|
73
|
-
// The wrapped program must hold exactly one expression (the filter drops the
|
|
74
|
-
// "use strict" directive). More than one means the expr smuggled a `)` to break
|
|
75
|
-
// out of `void (...)` — e.g. `1); evil(); (2`.
|
|
76
|
-
if (node.type === "Program") {
|
|
77
|
-
const statements = node.body.filter(
|
|
78
|
-
(s) => !(s.type === "ExpressionStatement" && s.directive),
|
|
79
|
-
);
|
|
80
|
-
if (statements.length !== 1) {
|
|
81
|
-
throw new SaferEvalError(
|
|
82
|
-
`Expression must be a single expression, got ${statements.length} statements`,
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
validateNode(statements[0]);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Dynamic import / meta-properties: never allowed, not even in a body.
|
|
90
|
-
if (FORBIDDEN_EXPRESSIONS.has(node.type)) {
|
|
91
|
-
throw new SaferEvalError(`"${node.type}" is not allowed in expressions`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Assignment / update / comma: allowed in a callback body, not as the whole expr.
|
|
95
|
-
if (BODY_ONLY_EXPRESSIONS.has(node.type) && !insideFunctionBody) {
|
|
96
|
-
throw new SaferEvalError(
|
|
97
|
-
`"${node.type}" is only allowed inside a function body, not as the whole expression`,
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Tagged templates invoke an arbitrary tag function.
|
|
102
|
-
if (node.type === "TaggedTemplateExpression") {
|
|
103
|
-
throw new SaferEvalError("Tagged template literals are not allowed");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Static access to escape-hatch properties; dynamic keys slip past.
|
|
107
|
-
if (node.type === "MemberExpression") {
|
|
108
|
-
const { property, computed } = node;
|
|
109
|
-
const staticKey =
|
|
110
|
-
!computed && property.type === "Identifier"
|
|
111
|
-
? property.name
|
|
112
|
-
: computed &&
|
|
113
|
-
property.type === "Literal" &&
|
|
114
|
-
typeof property.value === "string"
|
|
115
|
-
? property.value
|
|
116
|
-
: null;
|
|
117
|
-
if (staticKey !== null && FORBIDDEN_PROPERTIES.has(staticKey)) {
|
|
118
|
-
throw new SaferEvalError(`Access to "${staticKey}" is not allowed`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// `eval` / `arguments` in reference position (the recursion below skips a
|
|
123
|
-
// non-computed member's `.property`, so a harmless `row.arguments` isn't hit).
|
|
124
|
-
if (node.type === "Identifier" && FORBIDDEN_IDENTIFIERS.has(node.name)) {
|
|
125
|
-
throw new SaferEvalError(`Access to "${node.name}" is not allowed`);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Recurse. Two carries: descending into a function `body` sets
|
|
129
|
-
// insideFunctionBody for the subtree; a non-computed member's `.property` is a
|
|
130
|
-
// name already checked, so skip it (a computed `obj[eval]` is not skipped).
|
|
131
|
-
const fields = node as unknown as Record<string, unknown>;
|
|
132
|
-
const isFunction =
|
|
133
|
-
node.type === "ArrowFunctionExpression" ||
|
|
134
|
-
node.type === "FunctionExpression";
|
|
135
|
-
for (const key of Object.keys(node)) {
|
|
136
|
-
if (key === "type" || key === "start" || key === "end") continue;
|
|
137
|
-
if (node.type === "MemberExpression" && !node.computed && key === "property")
|
|
138
|
-
continue;
|
|
139
|
-
const child = fields[key];
|
|
140
|
-
const childInside = insideFunctionBody || (isFunction && key === "body");
|
|
141
|
-
if (Array.isArray(child)) {
|
|
142
|
-
for (const c of child) {
|
|
143
|
-
if (isNode(c)) validateNode(c, childInside);
|
|
144
|
-
}
|
|
145
|
-
} else if (isNode(child)) {
|
|
146
|
-
validateNode(child, childInside);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// uicast public API. Prompt-partial builders live in uicast/prompt.
|
|
2
|
-
|
|
3
|
-
export { createComponentDefinition } from "./def/create-component-definition";
|
|
4
|
-
|
|
5
|
-
export { evaluate } from "./expr/evaluate";
|
|
6
|
-
export { extractDeps } from "./expr/extract-deps";
|
|
7
|
-
|
|
8
|
-
export {
|
|
9
|
-
EntryError,
|
|
10
|
-
type EntryErrorReason,
|
|
11
|
-
type EntryFault,
|
|
12
|
-
} from "./entry-error";
|
|
13
|
-
|
|
14
|
-
export { createProxyScope, type ReactiveProxy } from "./scope/create-proxy-scope";
|
|
15
|
-
export { parseScope } from "./scope/parse-scope";
|
|
16
|
-
|
|
17
|
-
export { buildElementsById } from "./utils/utils";
|
|
18
|
-
|
|
19
|
-
export { streamJsonLines } from "./stream/stream-json-lines";
|
|
20
|
-
|
|
21
|
-
export {
|
|
22
|
-
type ComponentEntry,
|
|
23
|
-
type ComponentListEntry,
|
|
24
|
-
type ComponentDefinition,
|
|
25
|
-
type CombinedSpec,
|
|
26
|
-
isComponentEntry,
|
|
27
|
-
isComponentListEntry,
|
|
28
|
-
} from "./types";
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import INSTRUCTIONS from "./md/INSTRUCTIONS.json" with { type: "json" };
|
|
2
|
-
|
|
3
|
-
/** No options yet — the type is reserved so the signature can grow without breaking callers. */
|
|
4
|
-
export type CommonInstructionsPromptOptions = Record<string, never>;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The LLM-facing instruction block — the contract authored in
|
|
8
|
-
* `md/INSTRUCTIONS.md` and imported here as a JSON string via the md-to-json
|
|
9
|
-
* pipeline (`npm run md-to-json` regenerates the `.json` sibling after edits).
|
|
10
|
-
*
|
|
11
|
-
* A partial-prompt primitive: the consuming app's prompt assembler composes
|
|
12
|
-
* this — alongside the other `get*PartialPrompt` builders — into the full
|
|
13
|
-
* system prompt it sends to the generation LLM. core ships the pieces; the app
|
|
14
|
-
* owns the assembly.
|
|
15
|
-
*/
|
|
16
|
-
export function getCommonInstructionsPartialPrompt(
|
|
17
|
-
_options: CommonInstructionsPromptOptions = {},
|
|
18
|
-
): string {
|
|
19
|
-
return INSTRUCTIONS;
|
|
20
|
-
}
|