@uicast/core 0.0.1-beta.2 → 0.0.2
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 -13
- package/README.md +126 -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 +41 -31
- package/dist/entry-error.js +49 -20
- package/dist/entry-shape.d.ts +7 -0
- package/dist/entry-shape.js +88 -0
- package/dist/expr/evaluate.d.ts +4 -6
- package/dist/expr/evaluate.js +36 -46
- package/dist/expr/extract-deps.d.ts +3 -1
- package/dist/expr/extract-deps.js +20 -20
- package/dist/expr/plan-step-waves.d.ts +8 -0
- package/dist/expr/plan-step-waves.js +45 -0
- package/dist/index.d.ts +7 -8
- package/dist/index.js +5 -9
- 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 +20 -10
- package/dist/prompt/get-common-instructions-partial-prompt.js +11 -9
- package/dist/prompt/get-components-partial-prompt.d.ts +20 -28
- package/dist/prompt/get-components-partial-prompt.js +122 -123
- package/dist/prompt/get-edit-request-prompt.d.ts +16 -16
- package/dist/prompt/get-edit-request-prompt.js +11 -18
- package/dist/prompt/get-error-recovery-prompt.d.ts +25 -14
- package/dist/prompt/get-error-recovery-prompt.js +17 -14
- package/dist/prompt/get-expressions-partial-prompt.d.ts +19 -20
- package/dist/prompt/get-expressions-partial-prompt.js +11 -21
- package/dist/prompt/get-functions-partial-prompt.d.ts +16 -16
- package/dist/prompt/get-functions-partial-prompt.js +52 -40
- package/dist/prompt/get-scope-partial-prompt.d.ts +17 -17
- package/dist/prompt/get-scope-partial-prompt.js +21 -29
- package/dist/prompt/index.js +0 -2
- 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 -1
- package/dist/prompt/md/INSTRUCTIONS.json +1 -1
- 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 +52 -17
- package/dist/scope/create-proxy-scope.js +280 -86
- package/dist/scope/parse-scope.d.ts +2 -20
- package/dist/scope/parse-scope.js +10 -35
- 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 +81 -46
- package/dist/types.js +6 -9
- package/dist/url-policy.d.ts +45 -0
- package/dist/url-policy.js +252 -0
- package/package.json +18 -16
- package/dist/def/create-component-definition.d.ts +0 -9
- package/dist/def/create-component-definition.js +0 -10
- package/dist/expr/analyze.d.ts +0 -5
- package/dist/expr/analyze.js +0 -189
- package/dist/expr/ast-utils.d.ts +0 -4
- package/dist/expr/ast-utils.js +0 -23
- package/dist/expr/globals.d.ts +0 -3
- package/dist/expr/globals.js +0 -111
- package/dist/expr/safer-eval.d.ts +0 -21
- package/dist/expr/safer-eval.js +0 -170
- package/dist/expr/validate.d.ts +0 -7
- package/dist/expr/validate.js +0 -109
- package/dist/prompt-utils/json-schema-to-ts.d.ts +0 -73
- package/dist/prompt-utils/json-schema-to-ts.js +0 -199
- package/dist/stream/stream-json-lines.d.ts +0 -1
- package/dist/stream/stream-json-lines.js +0 -94
- package/dist/utils/utils.d.ts +0 -13
- package/dist/utils/utils.js +0 -58
package/dist/types.d.ts
CHANGED
|
@@ -1,89 +1,124 @@
|
|
|
1
|
-
import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@uicast/expr";
|
|
2
|
+
type Expression = string;
|
|
3
|
+
type ScopePath = string;
|
|
4
|
+
/**
|
|
5
|
+
* One value: `literal` is used as is, `expr` is an expression evaluated against the scopes.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const props: ValueSource = { expr: "({ text: scopes.root.title })" };
|
|
9
|
+
* const fixed: ValueSource = { literal: { variant: "outline" } };
|
|
10
|
+
*/
|
|
4
11
|
export type ValueSource = {
|
|
5
12
|
expr: Expression;
|
|
6
13
|
} | {
|
|
7
14
|
literal: unknown;
|
|
8
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* A `seed` step: a value source and the field it writes.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const step: ValueSourceAssignment = { set: "scopes.root.users", expr: "getUsers()" };
|
|
21
|
+
*/
|
|
9
22
|
export type ValueSourceAssignment = {
|
|
23
|
+
/** The field to write: `scopes.<scope>.<field>`, never deeper. */
|
|
10
24
|
set: ScopePath;
|
|
11
25
|
} & ValueSource;
|
|
12
|
-
|
|
26
|
+
/**
|
|
27
|
+
* A callback step: a value source, optionally written to a field, confirmed first or debounced.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const toggle: CallbackValueSourceAssignment = { set: "scopes.root.open", expr: "!currentValue" };
|
|
31
|
+
* const remove: CallbackValueSourceAssignment = { confirm: "Delete it?", expr: "deleteRow({ id: scopes.row.id })" };
|
|
32
|
+
*/
|
|
33
|
+
export type CallbackValueSourceAssignment = {
|
|
34
|
+
/** The field to write: `scopes.<scope>.<field>`. Absent: the step runs for its effect only. */
|
|
35
|
+
set?: ScopePath;
|
|
36
|
+
/** A question asked before this step, e.g. `"Delete this row?"`. Declined, this step and the rest are skipped. */
|
|
13
37
|
confirm?: string;
|
|
14
|
-
|
|
38
|
+
/** This step and the rest wait for 300 ms of quiet; only the latest call runs. */
|
|
39
|
+
debounce?: boolean;
|
|
40
|
+
} & ValueSource;
|
|
15
41
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* `each` (the array to iterate) plus `as` and an optional `keyBy`.
|
|
42
|
+
* One line of a document: the JSON for one element. List fields are optional, so a `ComponentEntry[]` holds lists
|
|
43
|
+
* too; `each` marks a list.
|
|
19
44
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* `ComponentEntry` → `ComponentListEntry` with `isComponentListEntry` (presence
|
|
26
|
-
* of `each`).
|
|
45
|
+
* @example
|
|
46
|
+
* const entry: ComponentEntry = {
|
|
47
|
+
* key: "open-count", component: "Typography",
|
|
48
|
+
* props: { expr: "({ text: 'Open: ' + scopes.root.open })" },
|
|
49
|
+
* };
|
|
27
50
|
*/
|
|
28
51
|
export interface ComponentEntry {
|
|
52
|
+
/** Unique id. Parents name children by it; re-emitting it replaces the element and its subtree. */
|
|
29
53
|
key: string;
|
|
54
|
+
/** The component name, matched exactly against the catalog. */
|
|
30
55
|
component: string;
|
|
56
|
+
/** The props object, e.g. `{ expr: "({ text: scopes.root.title })" }`. Reactive. */
|
|
31
57
|
props?: ValueSource;
|
|
58
|
+
/** Steps that set up state once, at mount. Each writes its field only while it is still undefined. */
|
|
32
59
|
seed?: ValueSourceAssignment[];
|
|
60
|
+
/** An expression; truthy hides the element. Reactive. */
|
|
33
61
|
hidden?: Expression;
|
|
34
|
-
|
|
62
|
+
/** An expression; truthy renders the element as busy. Reactive, like `hidden`. */
|
|
63
|
+
busy?: Expression;
|
|
64
|
+
/** Steps per event, e.g. `{ onClick: [{ set: "scopes.root.open", expr: "!currentValue" }] }`. */
|
|
65
|
+
callbacks?: Record<string, CallbackValueSourceAssignment[]>;
|
|
66
|
+
/** Child keys, in render order. */
|
|
35
67
|
children?: string[];
|
|
68
|
+
/** An expression returning the array to repeat over; it makes the entry a list. */
|
|
36
69
|
each?: Expression;
|
|
70
|
+
/** Names each item's two scopes: `scopes.<as>` is the item, `scopes.$<as>` its row. */
|
|
37
71
|
as?: string;
|
|
38
|
-
|
|
72
|
+
/** The item field that gives the row its `id`. Absent, or missing on an item: the index. */
|
|
73
|
+
keyBy?: string;
|
|
39
74
|
}
|
|
40
75
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
76
|
+
* An entry that repeats once per item of `each`.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* const rows: ComponentListEntry = { key: "task-row", component: "TableRow", each: "scopes.root.tasks", as: "task" };
|
|
44
80
|
*/
|
|
45
81
|
export interface ComponentListEntry extends ComponentEntry {
|
|
82
|
+
/** An expression returning the array to repeat over. */
|
|
46
83
|
each: Expression;
|
|
84
|
+
/** Names each item's two scopes: `scopes.<as>` is the item, `scopes.$<as>` its row. */
|
|
47
85
|
as: string;
|
|
48
86
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Narrow a `ComponentEntry` to a `ComponentListEntry`. List-ness is the
|
|
51
|
-
* presence of `each` — there is no `kind` discriminator, exactly like root-ness
|
|
52
|
-
* (derived from an entry being referenced by no other entry's `children`).
|
|
53
|
-
*/
|
|
54
87
|
export declare function isComponentListEntry(entry: ComponentEntry): entry is ComponentListEntry;
|
|
55
88
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
89
|
+
* Whether parsed JSON is an entry: string `key` and `component`, and `children`, if present, an array. The renderer
|
|
90
|
+
* checks the other fields at mount.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* const entries = parsedLines.filter(isComponentEntry);
|
|
58
94
|
*/
|
|
59
95
|
export declare function isComponentEntry(value: unknown): value is ComponentEntry;
|
|
60
96
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
97
|
+
* A schema that validates at run time (Standard Schema) and converts to JSON Schema for the prompt (Standard JSON
|
|
98
|
+
* Schema). Zod 4.2+, Valibot and ArkType schemas fit.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* const props: CombinedSpec = z.strictObject({ text: z.string() });
|
|
102
|
+
*/
|
|
63
103
|
export type CombinedSpec<Input = unknown, Output = Input> = StandardSchemaV1<Input, Output> & StandardJSONSchemaV1<Input, Output>;
|
|
64
104
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* `ComponentDefinition` is the heterogeneous form used in collections
|
|
72
|
-
* (`ComponentDefinition[]`, the `allDefinitions` registry, the prompt
|
|
73
|
-
* serializer). The `createComponentDefinition` factory (in
|
|
74
|
-
* `def/create-component-definition.ts`) is the value-side constructor for this.
|
|
105
|
+
* What the model reads about a component: its name, description, props and events. Make one with
|
|
106
|
+
* `createComponentDefinition`.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* const definitions: ComponentDefinition[] = [...defs, BadgeDef];
|
|
110
|
+
* getComponentsPartialPrompt({ definitions });
|
|
75
111
|
*/
|
|
76
112
|
export type ComponentDefinition<TProps extends CombinedSpec = CombinedSpec, TCallbacks extends Record<string, CombinedSpec> = Record<string, CombinedSpec>> = {
|
|
113
|
+
/** What an entry's `component` names, e.g. `"Badge"`. Must be unique: the prompt builder and `<RendererProvider>` throw on a repeat. */
|
|
77
114
|
name: string;
|
|
115
|
+
/** When to use the component, printed word for word in the prompt. */
|
|
78
116
|
description: string;
|
|
117
|
+
/** The props schema: the prompt prints it, and the renderer parses props through it before `render`. */
|
|
79
118
|
props: TProps;
|
|
119
|
+
/** Event name → payload schema, e.g. `{ onChange: z.object({ value: z.string() }), onClear: z.null() }`. */
|
|
80
120
|
callbacks?: TCallbacks;
|
|
81
|
-
/**
|
|
82
|
-
* Host-only component. Registered in the renderer registry so entries
|
|
83
|
-
* referencing it mount correctly, but filtered out of the LLM-facing
|
|
84
|
-
* prompt list in `getComponentsPartialPrompt`. Use for internal infrastructure
|
|
85
|
-
* (e.g. RootFragment — the synthetic wrapper used by `Renderer`'s `init`
|
|
86
|
-
* prop machinery) that the LLM should never emit.
|
|
87
|
-
*/
|
|
121
|
+
/** Kept out of the prompt; an entry that names it still renders. */
|
|
88
122
|
hidden?: boolean;
|
|
89
123
|
};
|
|
124
|
+
export {};
|
package/dist/types.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Narrow a `ComponentEntry` to a `ComponentListEntry`. List-ness is the
|
|
3
|
-
* presence of `each` — there is no `kind` discriminator, exactly like root-ness
|
|
4
|
-
* (derived from an entry being referenced by no other entry's `children`).
|
|
5
|
-
*/
|
|
6
1
|
export function isComponentListEntry(entry) {
|
|
7
2
|
return entry.each !== undefined;
|
|
8
3
|
}
|
|
9
4
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* Whether parsed JSON is an entry: string `key` and `component`, and `children`, if present, an array. The renderer
|
|
6
|
+
* checks the other fields at mount.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const entries = parsedLines.filter(isComponentEntry);
|
|
12
10
|
*/
|
|
13
11
|
export function isComponentEntry(value) {
|
|
14
12
|
if (!value || typeof value !== "object")
|
|
@@ -16,7 +14,6 @@ export function isComponentEntry(value) {
|
|
|
16
14
|
const entry = value;
|
|
17
15
|
return (typeof entry.key === "string" &&
|
|
18
16
|
typeof entry.component === "string" &&
|
|
19
|
-
//
|
|
20
|
-
// slip) would be iterated char-by-char downstream into phantom child slots.
|
|
17
|
+
// A string `children` (a plausible LLM slip) would be iterated char by char downstream.
|
|
21
18
|
(entry.children === undefined || Array.isArray(entry.children)));
|
|
22
19
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type JSONSchema } from "./json-schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Which URLs a prop declared as a URL (`z.url()`) may hold. An object changes the default rules field by field; a
|
|
4
|
+
* predicate replaces them.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const urlPolicy: UrlPolicy = { hosts: ["cdn.example.com", "*.imgix.net"] };
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const urlPolicy: UrlPolicy = (url) => url.startsWith("https://cdn.example.com/");
|
|
11
|
+
*/
|
|
12
|
+
export type UrlPolicy = {
|
|
13
|
+
/** Relative URLs: `/a`, `a/b`, `?q=1`, `#x`. Default `true`. */
|
|
14
|
+
allowRelative?: boolean;
|
|
15
|
+
/** Absolute URLs on the page origin. Default `true`. */
|
|
16
|
+
allowSameOrigin?: boolean;
|
|
17
|
+
/** Extra http/https hosts: an exact match, or `*.example.com` for subdomains (not the apex, so list both). */
|
|
18
|
+
hosts?: readonly string[];
|
|
19
|
+
/** `data:` raster images. Default `true`. SVG is never allowed: it can carry script. */
|
|
20
|
+
allowDataImages?: boolean;
|
|
21
|
+
/** The page origin, for where there is no `location` (SSR); without either, only `hosts` matches an absolute URL. */
|
|
22
|
+
origin?: string;
|
|
23
|
+
} | ((url: string) => boolean);
|
|
24
|
+
type UrlCheck = {
|
|
25
|
+
ok: true;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
reason: string;
|
|
29
|
+
};
|
|
30
|
+
export declare const resolveUrlPolicy: (policy?: Exclude<UrlPolicy, (url: string) => boolean>) => {
|
|
31
|
+
allowRelative: boolean;
|
|
32
|
+
allowSameOrigin: boolean;
|
|
33
|
+
hosts: readonly string[];
|
|
34
|
+
allowDataImages: boolean;
|
|
35
|
+
origin: string | undefined;
|
|
36
|
+
};
|
|
37
|
+
export declare const checkUrl: (value: string, policy?: UrlPolicy) => UrlCheck;
|
|
38
|
+
type UrlViolation = {
|
|
39
|
+
path: string;
|
|
40
|
+
url: string;
|
|
41
|
+
reason: string;
|
|
42
|
+
};
|
|
43
|
+
export declare const findUrlViolations: (schema: JSONSchema | undefined, value: unknown, policy?: UrlPolicy) => UrlViolation[];
|
|
44
|
+
export declare const schemaHasUrlFormat: (schema: JSONSchema | undefined) => boolean;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { isSchemaObject, resolvePointer } from "./json-schema.js";
|
|
2
|
+
// The defaults, in one place: the check applies them and the prompt describes them.
|
|
3
|
+
export const resolveUrlPolicy = (policy = {}) => {
|
|
4
|
+
const { allowRelative = true, allowSameOrigin = true, hosts = [], allowDataImages = true, origin } = policy;
|
|
5
|
+
return { allowRelative, allowSameOrigin, hosts, allowDataImages, origin };
|
|
6
|
+
};
|
|
7
|
+
const OK = { ok: true };
|
|
8
|
+
// Never fetch on their own.
|
|
9
|
+
const INERT_SCHEMES = new Set(["mailto", "tel", "sms", "blob"]);
|
|
10
|
+
// No `svg+xml`: SVG can carry script.
|
|
11
|
+
const DATA_IMAGE_RE = /^data:image\/(png|jpe?g|gif|webp|avif|bmp|x-icon|vnd\.microsoft\.icon)[;,]/i;
|
|
12
|
+
const SCHEME_RE = /^([a-z][a-z0-9+.-]*):/i;
|
|
13
|
+
// For http(s) the URL parser reads `\` as `/`, so a value with no scheme names a host exactly when it starts with two.
|
|
14
|
+
const NAMES_HOST_RE = /^[\\/]{2}/;
|
|
15
|
+
// Supplies only the scheme: a value that names a host keeps its own.
|
|
16
|
+
const NETWORK_PATH_BASE = "https://base.invalid/";
|
|
17
|
+
// Strip what the URL parser strips, or `"java\nscript:"` reads as relative here and `javascript:` in the DOM.
|
|
18
|
+
const normalize = (raw) => {
|
|
19
|
+
const url = raw.replace(/[\t\n\r]/g, "");
|
|
20
|
+
// A loop, not a regex: a trailing `[\0- ]+$` takes quadratic time on a long run of spaces.
|
|
21
|
+
let start = 0;
|
|
22
|
+
let end = url.length;
|
|
23
|
+
while (start < end && url.charCodeAt(start) <= 0x20)
|
|
24
|
+
start++;
|
|
25
|
+
while (end > start && url.charCodeAt(end - 1) <= 0x20)
|
|
26
|
+
end--;
|
|
27
|
+
return url.slice(start, end);
|
|
28
|
+
};
|
|
29
|
+
const parseUrl = (url, base) => {
|
|
30
|
+
try {
|
|
31
|
+
return new URL(url, base);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const hostAllowed = (host, hosts) => {
|
|
38
|
+
const lower = host.toLowerCase();
|
|
39
|
+
return hosts.some((entry) => {
|
|
40
|
+
const pattern = entry.toLowerCase();
|
|
41
|
+
if (pattern.startsWith("*.")) {
|
|
42
|
+
return lower.endsWith(pattern.slice(1)) && lower !== pattern.slice(2);
|
|
43
|
+
}
|
|
44
|
+
return lower === pattern;
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
// Parsed once per string: an explicit origin with a path or a trailing slash still names its origin.
|
|
48
|
+
const explicitOrigins = new Map();
|
|
49
|
+
const currentOrigin = (explicit) => {
|
|
50
|
+
if (!explicit)
|
|
51
|
+
return typeof location !== "undefined" ? location.origin : null;
|
|
52
|
+
let origin = explicitOrigins.get(explicit);
|
|
53
|
+
if (origin === undefined) {
|
|
54
|
+
origin = parseUrl(explicit)?.origin ?? null;
|
|
55
|
+
explicitOrigins.set(explicit, origin);
|
|
56
|
+
}
|
|
57
|
+
return origin;
|
|
58
|
+
};
|
|
59
|
+
const checkHost = (url, allowSameOrigin, explicitOrigin, hosts) => {
|
|
60
|
+
if (allowSameOrigin && url.origin === currentOrigin(explicitOrigin))
|
|
61
|
+
return OK;
|
|
62
|
+
if (hosts.length && hostAllowed(url.hostname, hosts))
|
|
63
|
+
return OK;
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
reason: `"${url.hostname}" is not an allowed host. Add it to the renderer's ` +
|
|
67
|
+
`urlPolicy ({ hosts: ["${url.hostname}"] }) if this destination is trusted`,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
// An empty string passes: nothing to fetch.
|
|
71
|
+
export const checkUrl = (value, policy) => {
|
|
72
|
+
const url = normalize(value);
|
|
73
|
+
if (!url)
|
|
74
|
+
return OK;
|
|
75
|
+
if (typeof policy === "function") {
|
|
76
|
+
return policy(url) ? OK : { ok: false, reason: "rejected by the host's urlPolicy predicate" };
|
|
77
|
+
}
|
|
78
|
+
const { allowRelative, allowSameOrigin, hosts, allowDataImages, origin: explicitOrigin } = resolveUrlPolicy(policy);
|
|
79
|
+
const scheme = SCHEME_RE.exec(url)?.[1]?.toLowerCase() ?? null;
|
|
80
|
+
if (scheme === null) {
|
|
81
|
+
if (NAMES_HOST_RE.test(url)) {
|
|
82
|
+
const resolved = parseUrl(url, NETWORK_PATH_BASE);
|
|
83
|
+
if (!resolved)
|
|
84
|
+
return { ok: false, reason: "not a parseable URL" };
|
|
85
|
+
return checkHost(resolved, allowSameOrigin, explicitOrigin, hosts);
|
|
86
|
+
}
|
|
87
|
+
return allowRelative ? OK : { ok: false, reason: "relative URLs are not allowed by this urlPolicy" };
|
|
88
|
+
}
|
|
89
|
+
if (scheme === "data") {
|
|
90
|
+
if (allowDataImages && DATA_IMAGE_RE.test(url))
|
|
91
|
+
return OK;
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
reason: DATA_IMAGE_RE.test(url)
|
|
95
|
+
? "data: URLs are not allowed by this urlPolicy"
|
|
96
|
+
: 'only "data:image/<raster>" URLs are allowed (svg+xml can carry script)',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (INERT_SCHEMES.has(scheme))
|
|
100
|
+
return OK;
|
|
101
|
+
if (scheme !== "http" && scheme !== "https") {
|
|
102
|
+
return { ok: false, reason: `the "${scheme}:" scheme is not allowed` };
|
|
103
|
+
}
|
|
104
|
+
const parsed = parseUrl(url);
|
|
105
|
+
if (!parsed)
|
|
106
|
+
return { ok: false, reason: "not a parseable absolute URL" };
|
|
107
|
+
return checkHost(parsed, allowSameOrigin, explicitOrigin, hosts);
|
|
108
|
+
};
|
|
109
|
+
const URL_FORMATS = new Set(["uri", "url", "uri-reference", "iri", "iri-reference"]);
|
|
110
|
+
const hasUrlFormat = (node) => {
|
|
111
|
+
const format = Object.hasOwn(node, "format") ? Reflect.get(node, "format") : undefined;
|
|
112
|
+
return typeof format === "string" && URL_FORMATS.has(format);
|
|
113
|
+
};
|
|
114
|
+
const indexes = new WeakMap();
|
|
115
|
+
const indexOf = (root) => {
|
|
116
|
+
let index = indexes.get(root);
|
|
117
|
+
if (!index) {
|
|
118
|
+
index = { root, reach: new Map(), applicable: new Map() };
|
|
119
|
+
indexes.set(root, index);
|
|
120
|
+
}
|
|
121
|
+
return index;
|
|
122
|
+
};
|
|
123
|
+
// Whether a URL format sits at or below `start`, through any keyword and any `$ref` chain. A stack, so depth sets no limit.
|
|
124
|
+
const reachesUrlFormat = (index, start) => {
|
|
125
|
+
if (typeof start !== "object" || start === null)
|
|
126
|
+
return false;
|
|
127
|
+
const cached = index.reach.get(start);
|
|
128
|
+
if (cached !== undefined)
|
|
129
|
+
return cached;
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
const stack = [start];
|
|
132
|
+
let found = false;
|
|
133
|
+
while (!found && stack.length > 0) {
|
|
134
|
+
const node = stack.pop();
|
|
135
|
+
if (typeof node !== "object" || node === null || seen.has(node))
|
|
136
|
+
continue;
|
|
137
|
+
seen.add(node);
|
|
138
|
+
found = hasUrlFormat(node);
|
|
139
|
+
for (const [key, child] of Object.entries(node)) {
|
|
140
|
+
stack.push(key === "$ref" && typeof child === "string" ? resolvePointer(child, index.root) : child);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
index.reach.set(start, found);
|
|
144
|
+
return found;
|
|
145
|
+
};
|
|
146
|
+
// The nodes that hold for one value: `start` and every applicator under it that keeps the value.
|
|
147
|
+
// All but `not` (a URL format under it forbids a URL); one that holds for only some values counts for all.
|
|
148
|
+
const applicableNodes = (index, start) => {
|
|
149
|
+
const cached = index.applicable.get(start);
|
|
150
|
+
if (cached)
|
|
151
|
+
return cached;
|
|
152
|
+
const out = new Set();
|
|
153
|
+
const stack = [start];
|
|
154
|
+
while (stack.length > 0) {
|
|
155
|
+
const node = stack.pop();
|
|
156
|
+
if (!isSchemaObject(node) || out.has(node) || !reachesUrlFormat(index, node))
|
|
157
|
+
continue;
|
|
158
|
+
out.add(node);
|
|
159
|
+
if (typeof node.$ref === "string")
|
|
160
|
+
stack.push(resolvePointer(node.$ref, index.root));
|
|
161
|
+
stack.push(...(node.allOf ?? []), ...(node.anyOf ?? []), ...(node.oneOf ?? []));
|
|
162
|
+
stack.push(node.if, node.then, node.else, ...Object.values(node.dependentSchemas ?? {}));
|
|
163
|
+
}
|
|
164
|
+
const nodes = [...out];
|
|
165
|
+
index.applicable.set(start, nodes);
|
|
166
|
+
return nodes;
|
|
167
|
+
};
|
|
168
|
+
const pathOf = (visit) => {
|
|
169
|
+
let path = "";
|
|
170
|
+
for (let at = visit; at?.parent; at = at.parent) {
|
|
171
|
+
const step = typeof at.key === "number" ? `[${at.key}]` : `.${at.key}`;
|
|
172
|
+
path = step + path;
|
|
173
|
+
}
|
|
174
|
+
return path.startsWith(".") ? path.slice(1) : path;
|
|
175
|
+
};
|
|
176
|
+
// A subschema with no URL format below it is never walked, so a URL-free table costs nothing.
|
|
177
|
+
const pushVisit = (stack, index, node, value, parent, key) => {
|
|
178
|
+
if (isSchemaObject(node) && reachesUrlFormat(index, node))
|
|
179
|
+
stack.push({ node, value, parent, key });
|
|
180
|
+
};
|
|
181
|
+
// Only declared URL props are checked: a URL-looking `text` prop is content.
|
|
182
|
+
export const findUrlViolations = (schema, value, policy) => {
|
|
183
|
+
if (!schema)
|
|
184
|
+
return [];
|
|
185
|
+
const index = indexOf(schema);
|
|
186
|
+
if (!reachesUrlFormat(index, schema))
|
|
187
|
+
return [];
|
|
188
|
+
const out = [];
|
|
189
|
+
const flagged = new Set();
|
|
190
|
+
// Per object, the nodes already applied: a shared or cyclic value is walked once per node.
|
|
191
|
+
const walked = new WeakMap();
|
|
192
|
+
// Children are pushed last to first, so values are checked, and violations reported, in document order.
|
|
193
|
+
const stack = [{ node: schema, value, parent: null, key: "" }];
|
|
194
|
+
for (let visit = stack.pop(); visit; visit = stack.pop()) {
|
|
195
|
+
const val = visit.value;
|
|
196
|
+
const nodes = applicableNodes(index, visit.node);
|
|
197
|
+
if (typeof val === "string") {
|
|
198
|
+
if (!nodes.some(hasUrlFormat))
|
|
199
|
+
continue;
|
|
200
|
+
const result = checkUrl(val, policy);
|
|
201
|
+
if (result.ok)
|
|
202
|
+
continue;
|
|
203
|
+
const path = pathOf(visit);
|
|
204
|
+
if (!flagged.has(path)) {
|
|
205
|
+
flagged.add(path);
|
|
206
|
+
out.push({ path, url: val, reason: result.reason });
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (typeof val !== "object" || val === null)
|
|
211
|
+
continue;
|
|
212
|
+
let done = walked.get(val);
|
|
213
|
+
if (!done) {
|
|
214
|
+
done = new Set();
|
|
215
|
+
walked.set(val, done);
|
|
216
|
+
}
|
|
217
|
+
for (let n = nodes.length - 1; n >= 0; n--) {
|
|
218
|
+
const node = nodes[n];
|
|
219
|
+
if (done.has(node))
|
|
220
|
+
continue;
|
|
221
|
+
done.add(node);
|
|
222
|
+
if (Array.isArray(val)) {
|
|
223
|
+
const tuple = node.prefixItems ?? [];
|
|
224
|
+
for (let i = val.length - 1; i >= 0; i--) {
|
|
225
|
+
pushVisit(stack, index, node.unevaluatedItems, val[i], visit, i);
|
|
226
|
+
pushVisit(stack, index, node.contains, val[i], visit, i);
|
|
227
|
+
pushVisit(stack, index, i < tuple.length ? tuple[i] : node.items, val[i], visit, i);
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const declared = node.properties ?? {};
|
|
232
|
+
// Patterns are not matched: each one applies to every key.
|
|
233
|
+
const patterns = Object.values(node.patternProperties ?? {});
|
|
234
|
+
const entries = Object.entries(val);
|
|
235
|
+
for (let e = entries.length - 1; e >= 0; e--) {
|
|
236
|
+
const [key, child] = entries[e];
|
|
237
|
+
if (Object.hasOwn(declared, key)) {
|
|
238
|
+
pushVisit(stack, index, declared[key], child, visit, key);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
pushVisit(stack, index, node.unevaluatedProperties, child, visit, key);
|
|
242
|
+
pushVisit(stack, index, node.additionalProperties, child, visit, key);
|
|
243
|
+
}
|
|
244
|
+
for (const sub of patterns)
|
|
245
|
+
pushVisit(stack, index, sub, child, visit, key);
|
|
246
|
+
pushVisit(stack, index, node.propertyNames, key, visit, key);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
};
|
|
252
|
+
export const schemaHasUrlFormat = (schema) => schema !== undefined && reachesUrlFormat(indexOf(schema), schema);
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uicast/core",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "The framework-agnostic engine of uicast, the expression-driven generative UI framework: entry format, guarded expressions (allow-list, not a sandbox), reactive scopes, and prompt builders.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": "Andrey Gubanov (https://github.com/finom)",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
9
|
"url": "git+https://github.com/finom/uicast.git",
|
|
9
10
|
"directory": "packages/core"
|
|
10
11
|
},
|
|
11
|
-
"homepage": "https://
|
|
12
|
+
"homepage": "https://uicast.dev",
|
|
12
13
|
"keywords": [
|
|
13
14
|
"generative-ui",
|
|
14
15
|
"llm",
|
|
15
|
-
"jsonlines",
|
|
16
16
|
"streaming",
|
|
17
17
|
"ui",
|
|
18
18
|
"agent",
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"types": "./dist/index.d.ts",
|
|
25
25
|
"default": "./dist/index.js"
|
|
26
26
|
},
|
|
27
|
+
"./internal": {
|
|
28
|
+
"types": "./dist/internal.d.ts",
|
|
29
|
+
"default": "./dist/internal.js"
|
|
30
|
+
},
|
|
27
31
|
"./prompt": {
|
|
28
32
|
"types": "./dist/prompt/index.d.ts",
|
|
29
33
|
"default": "./dist/prompt/index.js"
|
|
@@ -35,25 +39,23 @@
|
|
|
35
39
|
"CHANGELOG.md"
|
|
36
40
|
],
|
|
37
41
|
"scripts": {
|
|
38
|
-
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
|
39
|
-
"prepublishOnly": "npm run build",
|
|
42
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
|
43
|
+
"prepublishOnly": "node -e \"if (!process.env.CI) throw new Error('Publish from CI: PACKAGE=<dir> npm run patch or minor tags a release, and the tag publishes it')\" && npm run build",
|
|
40
44
|
"test": "vitest run",
|
|
41
45
|
"test:watch": "vitest",
|
|
42
46
|
"test:coverage": "vitest run --coverage",
|
|
43
|
-
"typecheck": "tsc --noEmit"
|
|
44
|
-
"npm-publish": "if [ -z \"$NPM_TAG\" ]; then echo 'Error: NPM_TAG is not set'; exit 1; fi; npm publish --tag=$NPM_TAG"
|
|
45
|
-
},
|
|
46
|
-
"dependencies": {
|
|
47
|
-
"@standard-schema/spec": "^1.1.0",
|
|
48
|
-
"acorn": "^8.16.0",
|
|
49
|
-
"standard-tool": "^0.1.0"
|
|
47
|
+
"typecheck": "tsc --noEmit"
|
|
50
48
|
},
|
|
51
49
|
"devDependencies": {
|
|
52
|
-
"@vitest/coverage-v8": "^
|
|
53
|
-
"
|
|
54
|
-
"
|
|
50
|
+
"@vitest/coverage-v8": "^5.0.2",
|
|
51
|
+
"standard-tool": "^0.1.0",
|
|
52
|
+
"vitest": "^5.0.1",
|
|
53
|
+
"zod": "~4.6.5"
|
|
55
54
|
},
|
|
56
55
|
"publishConfig": {
|
|
57
56
|
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@uicast/expr": "^0.0.2"
|
|
58
60
|
}
|
|
59
61
|
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { CombinedSpec, ComponentDefinition } from "../types.js";
|
|
2
|
-
/**
|
|
3
|
-
* Value-side constructor for a {@link ComponentDefinition} (the type lives in
|
|
4
|
-
* `types.ts`). Infers the concrete `props` / `callbacks` specs at the call site
|
|
5
|
-
* and returns the def unchanged — its purpose is the inference + a single typed
|
|
6
|
-
* authoring shape; `createComponentImplementation` later reads those inferred
|
|
7
|
-
* types off the returned def.
|
|
8
|
-
*/
|
|
9
|
-
export declare const createComponentDefinition: <TProps extends CombinedSpec, TCallbacks extends Record<string, CombinedSpec> = Record<string, never>>({ name, description, props, callbacks, hidden, }: ComponentDefinition<TProps, TCallbacks>) => ComponentDefinition<TProps, TCallbacks>;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Value-side constructor for a {@link ComponentDefinition} (the type lives in
|
|
3
|
-
* `types.ts`). Infers the concrete `props` / `callbacks` specs at the call site
|
|
4
|
-
* and returns the def unchanged — its purpose is the inference + a single typed
|
|
5
|
-
* authoring shape; `createComponentImplementation` later reads those inferred
|
|
6
|
-
* types off the returned def.
|
|
7
|
-
*/
|
|
8
|
-
export const createComponentDefinition = ({ name, description, props, callbacks, hidden, }) => {
|
|
9
|
-
return { name, description, props, callbacks, hidden };
|
|
10
|
-
};
|
package/dist/expr/analyze.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type * as acorn from "acorn";
|
|
2
|
-
/** Does the expression `await` anything? */
|
|
3
|
-
export declare function containsAwait(node: acorn.AnyNode | null | undefined): boolean;
|
|
4
|
-
export declare function extractScopeReads(ast: acorn.AnyNode): string[];
|
|
5
|
-
export declare function extractFreeIdentifiers(ast: acorn.AnyNode): string[];
|