@vortexm/vjt 0.1.2 → 0.1.4
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/dist/index.js +344 -46
- package/dist/lib/action-runtime.d.ts +2 -0
- package/dist/lib/logging.d.ts +1 -0
- package/dist/lib/network.d.ts +2 -0
- package/dist/lib/security.d.ts +13 -0
- package/dist/lib/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3682,9 +3682,173 @@ function logRuntimeError(scope, error, details) {
|
|
|
3682
3682
|
}
|
|
3683
3683
|
console.error("[VJT error]", payload);
|
|
3684
3684
|
}
|
|
3685
|
+
function logRuntimeDebug(enabled, scope, payload) {
|
|
3686
|
+
if (!enabled) {
|
|
3687
|
+
return;
|
|
3688
|
+
}
|
|
3689
|
+
if (payload === void 0) {
|
|
3690
|
+
console.log("[VJT debug]", scope);
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
console.log("[VJT debug]", scope, payload);
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
// src/lib/security.ts
|
|
3697
|
+
var BLOCKED_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
3698
|
+
var TRUSTED_CONFIG_ONLY_KEYS = /* @__PURE__ */ new Set(["widget", "style", "css", "actions", "events", "onClick", "onRefresh"]);
|
|
3699
|
+
function isPlainObject(value) {
|
|
3700
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3701
|
+
}
|
|
3702
|
+
function escapeHtmlAttribute(value) {
|
|
3703
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
3704
|
+
}
|
|
3705
|
+
function coerceFiniteNumber(value, kind, path) {
|
|
3706
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
3707
|
+
return kind === "int" ? Math.trunc(value) : value;
|
|
3708
|
+
}
|
|
3709
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
3710
|
+
const parsed = kind === "int" ? Number.parseInt(value, 10) : Number.parseFloat(value);
|
|
3711
|
+
if (Number.isFinite(parsed)) {
|
|
3712
|
+
return kind === "int" ? Math.trunc(parsed) : parsed;
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
throw new Error(`Invalid ${kind} at ${path}`);
|
|
3716
|
+
}
|
|
3717
|
+
function isBlockedObjectKey(key) {
|
|
3718
|
+
return BLOCKED_OBJECT_KEYS.has(key);
|
|
3719
|
+
}
|
|
3720
|
+
function hasBlockedReferenceSegment(reference) {
|
|
3721
|
+
return reference.split(".").some((segment) => isBlockedObjectKey(segment));
|
|
3722
|
+
}
|
|
3723
|
+
function isTrustedConfigOnlyKey(key) {
|
|
3724
|
+
return TRUSTED_CONFIG_ONLY_KEYS.has(key);
|
|
3725
|
+
}
|
|
3726
|
+
function templateValueUsesReference(value) {
|
|
3727
|
+
if (typeof value === "string") {
|
|
3728
|
+
return value.includes("$ref:");
|
|
3729
|
+
}
|
|
3730
|
+
if (Array.isArray(value)) {
|
|
3731
|
+
return value.some((entry) => templateValueUsesReference(entry));
|
|
3732
|
+
}
|
|
3733
|
+
if (isPlainObject(value)) {
|
|
3734
|
+
return Object.values(value).some((entry) => templateValueUsesReference(entry));
|
|
3735
|
+
}
|
|
3736
|
+
return false;
|
|
3737
|
+
}
|
|
3738
|
+
function sanitizeConfigStyleMap(styleMap) {
|
|
3739
|
+
const safe = {};
|
|
3740
|
+
for (const [key, value] of Object.entries(styleMap)) {
|
|
3741
|
+
if (isBlockedObjectKey(key) || typeof value !== "string") {
|
|
3742
|
+
continue;
|
|
3743
|
+
}
|
|
3744
|
+
safe[key] = value;
|
|
3745
|
+
}
|
|
3746
|
+
return safe;
|
|
3747
|
+
}
|
|
3748
|
+
function sanitizeUrl(rawValue, options = {}) {
|
|
3749
|
+
if (typeof rawValue !== "string") {
|
|
3750
|
+
return null;
|
|
3751
|
+
}
|
|
3752
|
+
const value = rawValue.trim();
|
|
3753
|
+
if (!value) {
|
|
3754
|
+
return null;
|
|
3755
|
+
}
|
|
3756
|
+
if (/^(javascript|data|vbscript):/i.test(value)) {
|
|
3757
|
+
return null;
|
|
3758
|
+
}
|
|
3759
|
+
if (value.startsWith("//")) {
|
|
3760
|
+
return null;
|
|
3761
|
+
}
|
|
3762
|
+
const schemeMatch = value.match(/^([A-Za-z][A-Za-z\d+\-.]*):/);
|
|
3763
|
+
if (!schemeMatch) {
|
|
3764
|
+
return options.allowRelative !== false ? value : null;
|
|
3765
|
+
}
|
|
3766
|
+
const scheme = schemeMatch[1].toLowerCase();
|
|
3767
|
+
if (scheme === "http" || scheme === "https") {
|
|
3768
|
+
return value;
|
|
3769
|
+
}
|
|
3770
|
+
if (scheme === "mailto" && options.allowMailto) {
|
|
3771
|
+
return value;
|
|
3772
|
+
}
|
|
3773
|
+
return null;
|
|
3774
|
+
}
|
|
3775
|
+
function sanitizeMarkdownSource(markdown) {
|
|
3776
|
+
return markdown.replaceAll("<", "<").replaceAll(">", ">");
|
|
3777
|
+
}
|
|
3778
|
+
function sanitizeGeneratedHtml(html) {
|
|
3779
|
+
const withoutDangerousTags = html.replace(/<\s*(script|style|iframe|object|embed|link|meta|base|form)\b[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi, "").replace(/<\s*(script|style|iframe|object|embed|link|meta|base|form)\b[^>]*\/?>/gi, "").replace(/\son[a-z]+\s*=\s*(".*?"|'.*?'|[^\s>]+)/gi, "");
|
|
3780
|
+
return withoutDangerousTags.replace(/\s(href|src)\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/gi, (_match, attributeName, _full, dq, sq, bare) => {
|
|
3781
|
+
const rawUrl = dq ?? sq ?? bare ?? "";
|
|
3782
|
+
const safeUrl = sanitizeUrl(rawUrl, {
|
|
3783
|
+
allowRelative: true,
|
|
3784
|
+
allowMailto: attributeName.toLowerCase() === "href"
|
|
3785
|
+
});
|
|
3786
|
+
const fallback = attributeName.toLowerCase() === "href" ? "#" : "";
|
|
3787
|
+
return ` ${attributeName}="${escapeHtmlAttribute(safeUrl ?? fallback)}"`;
|
|
3788
|
+
});
|
|
3789
|
+
}
|
|
3790
|
+
function sanitizeSchemaValue(schema, value, path = "value") {
|
|
3791
|
+
if (value === void 0 || value === null) {
|
|
3792
|
+
return void 0;
|
|
3793
|
+
}
|
|
3794
|
+
if (schema.type === "object") {
|
|
3795
|
+
if (!isPlainObject(value)) {
|
|
3796
|
+
throw new Error(`Expected object at ${path}`);
|
|
3797
|
+
}
|
|
3798
|
+
const result = {};
|
|
3799
|
+
for (const property of schema.properties ?? []) {
|
|
3800
|
+
if (!property.name || isBlockedObjectKey(property.name)) {
|
|
3801
|
+
continue;
|
|
3802
|
+
}
|
|
3803
|
+
result[property.name] = sanitizeSchemaValue(property, value[property.name], `${path}.${property.name}`);
|
|
3804
|
+
}
|
|
3805
|
+
return result;
|
|
3806
|
+
}
|
|
3807
|
+
if (schema.type === "array") {
|
|
3808
|
+
if (!Array.isArray(value)) {
|
|
3809
|
+
throw new Error(`Expected array at ${path}`);
|
|
3810
|
+
}
|
|
3811
|
+
return value.map((entry, index) => sanitizeSchemaValue(schema.elements, entry, `${path}[${index}]`));
|
|
3812
|
+
}
|
|
3813
|
+
switch (schema.type) {
|
|
3814
|
+
case "int":
|
|
3815
|
+
return coerceFiniteNumber(value, "int", path);
|
|
3816
|
+
case "float":
|
|
3817
|
+
return coerceFiniteNumber(value, "float", path);
|
|
3818
|
+
case "boolean":
|
|
3819
|
+
if (typeof value === "boolean") {
|
|
3820
|
+
return value;
|
|
3821
|
+
}
|
|
3822
|
+
if (value === "true" || value === "1" || value === 1) {
|
|
3823
|
+
return true;
|
|
3824
|
+
}
|
|
3825
|
+
if (value === "false" || value === "0" || value === 0) {
|
|
3826
|
+
return false;
|
|
3827
|
+
}
|
|
3828
|
+
throw new Error(`Invalid boolean at ${path}`);
|
|
3829
|
+
case "string": {
|
|
3830
|
+
const stringValue = typeof value === "string" ? value : typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" ? String(value) : (() => {
|
|
3831
|
+
throw new Error(`Invalid string at ${path}`);
|
|
3832
|
+
})();
|
|
3833
|
+
if (typeof schema.maxLength === "number" && stringValue.length > schema.maxLength) {
|
|
3834
|
+
throw new Error(`String too long at ${path}`);
|
|
3835
|
+
}
|
|
3836
|
+
if (typeof schema.regexp === "string" && schema.regexp) {
|
|
3837
|
+
const regexp = new RegExp(schema.regexp);
|
|
3838
|
+
if (!regexp.test(stringValue)) {
|
|
3839
|
+
throw new Error(`String does not match regexp at ${path}`);
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
return stringValue;
|
|
3843
|
+
}
|
|
3844
|
+
default:
|
|
3845
|
+
return value;
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3685
3848
|
|
|
3686
3849
|
// src/lib/network.ts
|
|
3687
3850
|
var NetworkRuntime = class {
|
|
3851
|
+
debugLogging;
|
|
3688
3852
|
requestsMap;
|
|
3689
3853
|
sseConfigs;
|
|
3690
3854
|
backendUrl;
|
|
@@ -3692,6 +3856,7 @@ var NetworkRuntime = class {
|
|
|
3692
3856
|
runActions;
|
|
3693
3857
|
rerenderRoot;
|
|
3694
3858
|
constructor(options) {
|
|
3859
|
+
this.debugLogging = options.debugLogging;
|
|
3695
3860
|
this.requestsMap = options.requestsMap;
|
|
3696
3861
|
this.sseConfigs = options.sseConfigs;
|
|
3697
3862
|
this.backendUrl = options.backendUrl;
|
|
@@ -3707,7 +3872,12 @@ var NetworkRuntime = class {
|
|
|
3707
3872
|
if (!config?.url) {
|
|
3708
3873
|
continue;
|
|
3709
3874
|
}
|
|
3710
|
-
const
|
|
3875
|
+
const safeUrl = sanitizeUrl(config.url, { allowRelative: true });
|
|
3876
|
+
if (!safeUrl) {
|
|
3877
|
+
logRuntimeError("sseConfig", new Error("Blocked unsafe SSE url"), { url: config.url });
|
|
3878
|
+
continue;
|
|
3879
|
+
}
|
|
3880
|
+
const source = new EventSource(safeUrl);
|
|
3711
3881
|
this.eventSources.push(source);
|
|
3712
3882
|
for (const eventDefinition of config.events ?? []) {
|
|
3713
3883
|
if (!eventDefinition?.name) {
|
|
@@ -3717,7 +3887,7 @@ var NetworkRuntime = class {
|
|
|
3717
3887
|
void this.handleSseEvent(eventDefinition, event).catch((error) => {
|
|
3718
3888
|
logRuntimeError("handleSseEvent", error, {
|
|
3719
3889
|
eventName: eventDefinition.name,
|
|
3720
|
-
url:
|
|
3890
|
+
url: safeUrl
|
|
3721
3891
|
});
|
|
3722
3892
|
});
|
|
3723
3893
|
});
|
|
@@ -3745,12 +3915,23 @@ var NetworkRuntime = class {
|
|
|
3745
3915
|
if (!requestUrl) {
|
|
3746
3916
|
throw new Error(`Request ${requestName} has no url in config`);
|
|
3747
3917
|
}
|
|
3748
|
-
const
|
|
3918
|
+
const safeRequestUrl = sanitizeUrl(requestUrl, { allowRelative: true });
|
|
3919
|
+
if (!safeRequestUrl) {
|
|
3920
|
+
throw new Error(`Request ${requestName} has unsafe url`);
|
|
3921
|
+
}
|
|
3922
|
+
const requestBody = JSON.stringify(requestEnvelope);
|
|
3923
|
+
logRuntimeDebug(this.debugLogging, "request", {
|
|
3924
|
+
requestName,
|
|
3925
|
+
url: safeRequestUrl,
|
|
3926
|
+
envelope: requestEnvelope,
|
|
3927
|
+
body: requestBody
|
|
3928
|
+
});
|
|
3929
|
+
const response = await fetch(safeRequestUrl, {
|
|
3749
3930
|
method: "POST",
|
|
3750
3931
|
headers: {
|
|
3751
3932
|
"Content-Type": "application/json"
|
|
3752
3933
|
},
|
|
3753
|
-
body:
|
|
3934
|
+
body: requestBody
|
|
3754
3935
|
});
|
|
3755
3936
|
const responseText = await response.text();
|
|
3756
3937
|
if (!response.ok) {
|
|
@@ -3765,7 +3946,20 @@ var NetworkRuntime = class {
|
|
|
3765
3946
|
} catch {
|
|
3766
3947
|
throw new Error(`Request ${requestName} returned non-JSON response: ${responseText.slice(0, 120)}`);
|
|
3767
3948
|
}
|
|
3768
|
-
|
|
3949
|
+
if (json.error) {
|
|
3950
|
+
throw new Error(`Request ${requestName} returned JSON-RPC error: ${this.stringifyPrimitive(json.error.message ?? "unknown error")}`);
|
|
3951
|
+
}
|
|
3952
|
+
if (!definition.response) {
|
|
3953
|
+
throw new Error(`Request ${requestName} must define response schema`);
|
|
3954
|
+
}
|
|
3955
|
+
const result = sanitizeSchemaValue(definition.response, json.result, `response.${requestName}`);
|
|
3956
|
+
logRuntimeDebug(this.debugLogging, "response", {
|
|
3957
|
+
requestName,
|
|
3958
|
+
url: safeRequestUrl,
|
|
3959
|
+
status: response.status,
|
|
3960
|
+
json,
|
|
3961
|
+
result
|
|
3962
|
+
});
|
|
3769
3963
|
if (definition.onResponse?.length) {
|
|
3770
3964
|
await this.runActions(definition.onResponse, null, {
|
|
3771
3965
|
currentValue: null,
|
|
@@ -3778,7 +3972,7 @@ var NetworkRuntime = class {
|
|
|
3778
3972
|
if (schema.type === "object") {
|
|
3779
3973
|
const result = {};
|
|
3780
3974
|
for (const property of schema.properties ?? []) {
|
|
3781
|
-
if (!property.name) {
|
|
3975
|
+
if (!property.name || isBlockedObjectKey(property.name)) {
|
|
3782
3976
|
continue;
|
|
3783
3977
|
}
|
|
3784
3978
|
result[property.name] = await this.buildSchemaValue(property, currentValue, responseValue);
|
|
@@ -3834,12 +4028,21 @@ var NetworkRuntime = class {
|
|
|
3834
4028
|
}
|
|
3835
4029
|
}
|
|
3836
4030
|
async handleSseEvent(eventDefinition, event) {
|
|
3837
|
-
let
|
|
4031
|
+
let parsedMessage = {};
|
|
3838
4032
|
try {
|
|
3839
|
-
|
|
4033
|
+
parsedMessage = event.data ? JSON.parse(event.data) : {};
|
|
3840
4034
|
} catch {
|
|
3841
|
-
|
|
4035
|
+
parsedMessage = { value: event.data ?? "" };
|
|
3842
4036
|
}
|
|
4037
|
+
const message = eventDefinition.message ? sanitizeSchemaValue(eventDefinition.message, parsedMessage, `sse.${eventDefinition.name}`) : (() => {
|
|
4038
|
+
throw new Error(`SSE event ${eventDefinition.name} must define message schema`);
|
|
4039
|
+
})();
|
|
4040
|
+
logRuntimeDebug(this.debugLogging, "sse", {
|
|
4041
|
+
eventName: eventDefinition.name,
|
|
4042
|
+
raw: event.data,
|
|
4043
|
+
parsedMessage,
|
|
4044
|
+
message
|
|
4045
|
+
});
|
|
3843
4046
|
if (!eventDefinition.onEvent?.length) {
|
|
3844
4047
|
return;
|
|
3845
4048
|
}
|
|
@@ -3852,12 +4055,15 @@ var NetworkRuntime = class {
|
|
|
3852
4055
|
};
|
|
3853
4056
|
|
|
3854
4057
|
// src/lib/references.ts
|
|
3855
|
-
function
|
|
4058
|
+
function isPlainObject2(value) {
|
|
3856
4059
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3857
4060
|
}
|
|
3858
4061
|
function readPath(target, path) {
|
|
3859
4062
|
let current = target;
|
|
3860
4063
|
for (const segment of path) {
|
|
4064
|
+
if (isBlockedObjectKey(segment)) {
|
|
4065
|
+
return void 0;
|
|
4066
|
+
}
|
|
3861
4067
|
if (current == null) {
|
|
3862
4068
|
return void 0;
|
|
3863
4069
|
}
|
|
@@ -3869,6 +4075,9 @@ function readPath(target, path) {
|
|
|
3869
4075
|
if (typeof current !== "object") {
|
|
3870
4076
|
return void 0;
|
|
3871
4077
|
}
|
|
4078
|
+
if (!Object.prototype.hasOwnProperty.call(current, segment)) {
|
|
4079
|
+
return void 0;
|
|
4080
|
+
}
|
|
3872
4081
|
current = current[segment];
|
|
3873
4082
|
}
|
|
3874
4083
|
return current;
|
|
@@ -4008,7 +4217,7 @@ var ReferenceRuntime = class {
|
|
|
4008
4217
|
}
|
|
4009
4218
|
return readPath(currentValue.descriptor, reference.split("."));
|
|
4010
4219
|
}
|
|
4011
|
-
if (
|
|
4220
|
+
if (isPlainObject2(currentValue) || Array.isArray(currentValue)) {
|
|
4012
4221
|
return readPath(currentValue, reference.split("."));
|
|
4013
4222
|
}
|
|
4014
4223
|
return void 0;
|
|
@@ -4033,7 +4242,7 @@ var ReferenceRuntime = class {
|
|
|
4033
4242
|
if (state.widget === "combobox") {
|
|
4034
4243
|
const index = state.selected ?? 0;
|
|
4035
4244
|
const entry = (state.comboboxElements ?? [])[index];
|
|
4036
|
-
if (
|
|
4245
|
+
if (isPlainObject2(entry)) {
|
|
4037
4246
|
return entry.value ?? entry.text ?? index;
|
|
4038
4247
|
}
|
|
4039
4248
|
return index;
|
|
@@ -4041,7 +4250,7 @@ var ReferenceRuntime = class {
|
|
|
4041
4250
|
if (state.widget === "listbox") {
|
|
4042
4251
|
const index = state.selected ?? 0;
|
|
4043
4252
|
const entry = (state.listboxElements ?? [])[index];
|
|
4044
|
-
if (
|
|
4253
|
+
if (isPlainObject2(entry)) {
|
|
4045
4254
|
return entry.value ?? entry.text ?? index;
|
|
4046
4255
|
}
|
|
4047
4256
|
return index;
|
|
@@ -4091,7 +4300,7 @@ var ReferenceRuntime = class {
|
|
|
4091
4300
|
}
|
|
4092
4301
|
}
|
|
4093
4302
|
assignReference(reference, inputValue, _currentValue, options) {
|
|
4094
|
-
if (reference.startsWith("current.")) {
|
|
4303
|
+
if (reference.startsWith("current.") || hasBlockedReferenceSegment(reference)) {
|
|
4095
4304
|
return;
|
|
4096
4305
|
}
|
|
4097
4306
|
if (reference.startsWith("cookies.")) {
|
|
@@ -4136,7 +4345,7 @@ var ReferenceRuntime = class {
|
|
|
4136
4345
|
case "elements":
|
|
4137
4346
|
if (state.widget === "list" || state.widget === "grid-view") {
|
|
4138
4347
|
this.clearListElementState(state.key);
|
|
4139
|
-
state.elements = Array.isArray(inputValue) ? inputValue.filter((item) =>
|
|
4348
|
+
state.elements = Array.isArray(inputValue) ? inputValue.filter((item) => isPlainObject2(item) && typeof item.widget === "string") : [];
|
|
4140
4349
|
state.elements.forEach((element, index) => {
|
|
4141
4350
|
const listNode = this.host.nodeById.get(widgetId);
|
|
4142
4351
|
if (listNode?.widget === "list" || listNode?.widget === "grid-view") {
|
|
@@ -4145,10 +4354,10 @@ var ReferenceRuntime = class {
|
|
|
4145
4354
|
});
|
|
4146
4355
|
}
|
|
4147
4356
|
if (state.widget === "listbox") {
|
|
4148
|
-
state.listboxElements = Array.isArray(inputValue) ? inputValue.filter((item) =>
|
|
4357
|
+
state.listboxElements = Array.isArray(inputValue) ? inputValue.filter((item) => isPlainObject2(item)) : [];
|
|
4149
4358
|
}
|
|
4150
4359
|
if (state.widget === "combobox") {
|
|
4151
|
-
state.comboboxElements = Array.isArray(inputValue) ? inputValue.filter((item) =>
|
|
4360
|
+
state.comboboxElements = Array.isArray(inputValue) ? inputValue.filter((item) => isPlainObject2(item)) : [];
|
|
4152
4361
|
}
|
|
4153
4362
|
break;
|
|
4154
4363
|
default:
|
|
@@ -4187,9 +4396,15 @@ var ReferenceRuntime = class {
|
|
|
4187
4396
|
if (Array.isArray(template)) {
|
|
4188
4397
|
return template.map((entry) => this.resolveMappedValue(entry, currentValue, responseValue));
|
|
4189
4398
|
}
|
|
4190
|
-
if (
|
|
4399
|
+
if (isPlainObject2(template)) {
|
|
4191
4400
|
const result = {};
|
|
4192
4401
|
for (const [key, value] of Object.entries(template)) {
|
|
4402
|
+
if (isBlockedObjectKey(key)) {
|
|
4403
|
+
continue;
|
|
4404
|
+
}
|
|
4405
|
+
if (isTrustedConfigOnlyKey(key) && templateValueUsesReference(value)) {
|
|
4406
|
+
continue;
|
|
4407
|
+
}
|
|
4193
4408
|
result[key] = this.resolveMappedValue(value, currentValue, responseValue);
|
|
4194
4409
|
}
|
|
4195
4410
|
return result;
|
|
@@ -4197,7 +4412,7 @@ var ReferenceRuntime = class {
|
|
|
4197
4412
|
return template;
|
|
4198
4413
|
}
|
|
4199
4414
|
isListElementContext(value) {
|
|
4200
|
-
return
|
|
4415
|
+
return isPlainObject2(value) && value.kind === "list-element" && typeof value.listId === "string" && typeof value.index === "number" && isPlainObject2(value.descriptor) && typeof value.descriptor.widget === "string";
|
|
4201
4416
|
}
|
|
4202
4417
|
findNamedWidget(node, name) {
|
|
4203
4418
|
if (!node) {
|
|
@@ -4269,6 +4484,7 @@ var ReferenceRuntime = class {
|
|
|
4269
4484
|
|
|
4270
4485
|
// src/lib/action-runtime.ts
|
|
4271
4486
|
var ActionRuntime = class {
|
|
4487
|
+
debugLogging;
|
|
4272
4488
|
actionsMap;
|
|
4273
4489
|
actionFunctions;
|
|
4274
4490
|
nodeById;
|
|
@@ -4293,6 +4509,7 @@ var ActionRuntime = class {
|
|
|
4293
4509
|
getInlineActions;
|
|
4294
4510
|
isWidgetEnabled;
|
|
4295
4511
|
constructor(options) {
|
|
4512
|
+
this.debugLogging = options.debugLogging;
|
|
4296
4513
|
this.actionsMap = options.actionsMap;
|
|
4297
4514
|
this.actionFunctions = options.actionFunctions;
|
|
4298
4515
|
this.nodeById = options.nodeById;
|
|
@@ -4348,30 +4565,55 @@ var ActionRuntime = class {
|
|
|
4348
4565
|
return this.getInlineActions(node);
|
|
4349
4566
|
}
|
|
4350
4567
|
async runSingleAction(action, inputValue, context) {
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4568
|
+
const actionName = action.action.trim();
|
|
4569
|
+
try {
|
|
4570
|
+
let output = await this.executeAction(action, inputValue, context);
|
|
4571
|
+
if (action.andThen?.length) {
|
|
4572
|
+
if (Array.isArray(output)) {
|
|
4573
|
+
const collected = [];
|
|
4574
|
+
for (const entry of output) {
|
|
4575
|
+
if (entry === null || entry === void 0) {
|
|
4576
|
+
continue;
|
|
4577
|
+
}
|
|
4578
|
+
const nestedOutput = await this.runActions(action.andThen, entry, { ...context, currentValue: entry });
|
|
4579
|
+
if (nestedOutput !== null && nestedOutput !== void 0) {
|
|
4580
|
+
collected.push(nestedOutput);
|
|
4581
|
+
}
|
|
4362
4582
|
}
|
|
4583
|
+
output = collected;
|
|
4584
|
+
} else if (output !== null && output !== void 0) {
|
|
4585
|
+
output = await this.runActions(action.andThen, output, { ...context, currentValue: output });
|
|
4586
|
+
} else {
|
|
4587
|
+
output = null;
|
|
4363
4588
|
}
|
|
4364
|
-
output = collected;
|
|
4365
|
-
} else if (output !== null && output !== void 0) {
|
|
4366
|
-
output = await this.runActions(action.andThen, output, { ...context, currentValue: output });
|
|
4367
|
-
} else {
|
|
4368
|
-
output = null;
|
|
4369
4589
|
}
|
|
4590
|
+
logRuntimeDebug(this.debugLogging, "action-result", {
|
|
4591
|
+
action: actionName,
|
|
4592
|
+
output
|
|
4593
|
+
});
|
|
4594
|
+
return output;
|
|
4595
|
+
} catch (error) {
|
|
4596
|
+
logRuntimeError("action", error, {
|
|
4597
|
+
action: actionName,
|
|
4598
|
+
args: action.args,
|
|
4599
|
+
inputValue,
|
|
4600
|
+
currentValue: context.currentValue,
|
|
4601
|
+
responseValue: context.responseValue,
|
|
4602
|
+
pointer: context.pointer ?? null
|
|
4603
|
+
});
|
|
4604
|
+
throw error;
|
|
4370
4605
|
}
|
|
4371
|
-
return output;
|
|
4372
4606
|
}
|
|
4373
4607
|
async executeAction(action, inputValue, context) {
|
|
4374
4608
|
const name = action.action.trim();
|
|
4609
|
+
logRuntimeDebug(this.debugLogging, "action", {
|
|
4610
|
+
action: name,
|
|
4611
|
+
args: action.args,
|
|
4612
|
+
inputValue,
|
|
4613
|
+
currentValue: context.currentValue,
|
|
4614
|
+
responseValue: context.responseValue,
|
|
4615
|
+
pointer: context.pointer ?? null
|
|
4616
|
+
});
|
|
4375
4617
|
if (this.actionsMap[name]) {
|
|
4376
4618
|
return this.runActions(this.actionsMap[name], inputValue, context);
|
|
4377
4619
|
}
|
|
@@ -4486,7 +4728,7 @@ function isIfElseArgs(value) {
|
|
|
4486
4728
|
}
|
|
4487
4729
|
|
|
4488
4730
|
// src/lib/dom-state.ts
|
|
4489
|
-
function
|
|
4731
|
+
function isPlainObject3(value) {
|
|
4490
4732
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4491
4733
|
}
|
|
4492
4734
|
function supportsTextSelection(element) {
|
|
@@ -4656,7 +4898,7 @@ function getListElementContextFromElement(element, stateByKey) {
|
|
|
4656
4898
|
}
|
|
4657
4899
|
const listState = stateByKey.get(listId);
|
|
4658
4900
|
const descriptor = listState?.elements?.[index];
|
|
4659
|
-
if (!descriptor || !
|
|
4901
|
+
if (!descriptor || !isPlainObject3(descriptor) || typeof descriptor.widget !== "string") {
|
|
4660
4902
|
return null;
|
|
4661
4903
|
}
|
|
4662
4904
|
return {
|
|
@@ -5173,7 +5415,7 @@ var renderMdText = (env, node, state, key) => {
|
|
|
5173
5415
|
const verticalAlign = VERTICAL_ALIGN_MAP2[node["vert-align"] ?? "center"];
|
|
5174
5416
|
const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
|
|
5175
5417
|
const markdown = env.resolveI18nValue(state.text ?? node.text);
|
|
5176
|
-
const html = markdownConverter.makeHtml(markdown);
|
|
5418
|
+
const html = sanitizeGeneratedHtml(markdownConverter.makeHtml(sanitizeMarkdownSource(markdown)));
|
|
5177
5419
|
const inlineStyle = [
|
|
5178
5420
|
"display:flex",
|
|
5179
5421
|
`align-items:${verticalAlign}`,
|
|
@@ -5279,7 +5521,7 @@ var renderCheckbox = (env, node, state, key) => {
|
|
|
5279
5521
|
|
|
5280
5522
|
// src/lib/widgets/image.ts
|
|
5281
5523
|
var renderImage = (env, node, state, key) => {
|
|
5282
|
-
const url = env.escapeHtml(state.url ?? node.url ?? "");
|
|
5524
|
+
const url = env.escapeHtml(sanitizeUrl(state.url ?? node.url ?? "", { allowRelative: true }) ?? "");
|
|
5283
5525
|
const styleParts = [
|
|
5284
5526
|
"box-sizing:border-box",
|
|
5285
5527
|
"width:100%",
|
|
@@ -5306,7 +5548,7 @@ var renderLink = (env, node, state, key) => {
|
|
|
5306
5548
|
const classes = `vjt-link vjt-link--${type}${enabled ? "" : " is-disabled"}`;
|
|
5307
5549
|
const styleString = env.buildStyleString(node);
|
|
5308
5550
|
const styleAttribute = styleString ? ` style="${env.escapeHtml(styleString)}"` : "";
|
|
5309
|
-
const href = enabled
|
|
5551
|
+
const href = enabled ? sanitizeUrl(state.url ?? node.url, { allowRelative: true, allowMailto: true }) ?? "#" : "#";
|
|
5310
5552
|
const tabIndex = enabled ? "" : ' tabindex="-1" aria-disabled="true"';
|
|
5311
5553
|
return `<a${state.id ? ` id="${env.escapeHtml(state.id)}"` : ""}${env.buildWidgetDataAttributes(key, node)} class="${classes}" data-widget="link" href="${env.escapeHtml(href)}"${tabIndex}${styleAttribute}>${text}</a>`;
|
|
5312
5554
|
};
|
|
@@ -5868,7 +6110,7 @@ function toFiniteNumber3(value) {
|
|
|
5868
6110
|
}
|
|
5869
6111
|
return null;
|
|
5870
6112
|
}
|
|
5871
|
-
function
|
|
6113
|
+
function isPlainObject4(value) {
|
|
5872
6114
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5873
6115
|
}
|
|
5874
6116
|
function deepClone(value) {
|
|
@@ -5878,6 +6120,9 @@ function sanitizeIdFragment2(value) {
|
|
|
5878
6120
|
const cleaned = value.replaceAll(/[^A-Za-z0-9_-]+/g, "");
|
|
5879
6121
|
return cleaned || "item";
|
|
5880
6122
|
}
|
|
6123
|
+
function buildDefinitionSignature(value) {
|
|
6124
|
+
return JSON.stringify(value) ?? "null";
|
|
6125
|
+
}
|
|
5881
6126
|
function isGeneratedElementId(value) {
|
|
5882
6127
|
return !value.includes(".") && !value.includes(":") && /Element\d+/.test(value);
|
|
5883
6128
|
}
|
|
@@ -5926,6 +6171,7 @@ var RuntimeRenderer = class {
|
|
|
5926
6171
|
systemEvents;
|
|
5927
6172
|
resourceManager;
|
|
5928
6173
|
actionFunctions;
|
|
6174
|
+
debugLogging;
|
|
5929
6175
|
backendUrl;
|
|
5930
6176
|
initialRuntimeSnapshot;
|
|
5931
6177
|
onRuntimeSnapshot;
|
|
@@ -5952,7 +6198,7 @@ var RuntimeRenderer = class {
|
|
|
5952
6198
|
constructor(description, options = {}) {
|
|
5953
6199
|
this.description = deepClone(description);
|
|
5954
6200
|
this.resourceManager = options.resourceManager ?? null;
|
|
5955
|
-
const customStyleMap = options.styleMap ?? this.resourceManager?.getStyleMap() ?? {};
|
|
6201
|
+
const customStyleMap = sanitizeConfigStyleMap(options.styleMap ?? this.resourceManager?.getStyleMap() ?? {});
|
|
5956
6202
|
this.styleMap = {
|
|
5957
6203
|
...DEFAULT_STYLE_MAP,
|
|
5958
6204
|
...customStyleMap
|
|
@@ -5966,6 +6212,7 @@ var RuntimeRenderer = class {
|
|
|
5966
6212
|
this.sseConfigs = Array.isArray(resolvedSseConfigs) ? resolvedSseConfigs : resolvedSseConfigs ? [resolvedSseConfigs] : [];
|
|
5967
6213
|
this.systemEvents = options.systemEvents ?? this.resourceManager?.getSystemEvents() ?? {};
|
|
5968
6214
|
this.actionFunctions = options.actionFunctions ?? {};
|
|
6215
|
+
this.debugLogging = options.debugLogging === true;
|
|
5969
6216
|
this.backendUrl = options.backendUrl;
|
|
5970
6217
|
this.initialRuntimeSnapshot = options.runtimeSnapshot ? deepClone(options.runtimeSnapshot) : null;
|
|
5971
6218
|
this.onRuntimeSnapshot = options.onRuntimeSnapshot;
|
|
@@ -6013,6 +6260,7 @@ var RuntimeRenderer = class {
|
|
|
6013
6260
|
indexListElementNodes: (listNode, element, index) => this.indexListElementNodes(listNode, element, index)
|
|
6014
6261
|
});
|
|
6015
6262
|
this.networkRuntime = new NetworkRuntime({
|
|
6263
|
+
debugLogging: this.debugLogging,
|
|
6016
6264
|
requestsMap: this.requestsMap,
|
|
6017
6265
|
sseConfigs: this.sseConfigs,
|
|
6018
6266
|
backendUrl: this.backendUrl,
|
|
@@ -6021,6 +6269,7 @@ var RuntimeRenderer = class {
|
|
|
6021
6269
|
rerenderRoot: () => this.rerenderRoot()
|
|
6022
6270
|
});
|
|
6023
6271
|
this.actionRuntime = new ActionRuntime({
|
|
6272
|
+
debugLogging: this.debugLogging,
|
|
6024
6273
|
actionsMap: this.actionsMap,
|
|
6025
6274
|
actionFunctions: this.actionFunctions,
|
|
6026
6275
|
nodeById: this.nodeById,
|
|
@@ -6066,7 +6315,9 @@ var RuntimeRenderer = class {
|
|
|
6066
6315
|
this.applyRuntimeSnapshot(this.initialRuntimeSnapshot);
|
|
6067
6316
|
}
|
|
6068
6317
|
renderMarkup() {
|
|
6069
|
-
|
|
6318
|
+
const markup = `${this.renderNode(this.description, "root", null)}${this.renderGlobalOverlays()}`;
|
|
6319
|
+
logRuntimeDebug(this.debugLogging, "html", markup);
|
|
6320
|
+
return markup;
|
|
6070
6321
|
}
|
|
6071
6322
|
mount(root, options = {}) {
|
|
6072
6323
|
if (!(root instanceof HTMLElement)) {
|
|
@@ -6160,7 +6411,11 @@ var RuntimeRenderer = class {
|
|
|
6160
6411
|
if (typeof window === "undefined" || !url.trim()) {
|
|
6161
6412
|
return;
|
|
6162
6413
|
}
|
|
6163
|
-
const
|
|
6414
|
+
const safeUrl = sanitizeUrl(url, { allowRelative: true });
|
|
6415
|
+
if (!safeUrl) {
|
|
6416
|
+
throw new Error("Blocked unsafe navigation url");
|
|
6417
|
+
}
|
|
6418
|
+
const nextUrl = new URL(safeUrl, window.location.href);
|
|
6164
6419
|
if (nextUrl.origin !== window.location.origin) {
|
|
6165
6420
|
window.location.assign(nextUrl.toString());
|
|
6166
6421
|
return;
|
|
@@ -6398,6 +6653,7 @@ var RuntimeRenderer = class {
|
|
|
6398
6653
|
existing.id = stateId2;
|
|
6399
6654
|
this.stateById.set(stateId2, existing);
|
|
6400
6655
|
}
|
|
6656
|
+
this.syncStateDefinition(existing, node);
|
|
6401
6657
|
return existing;
|
|
6402
6658
|
}
|
|
6403
6659
|
const stateId = node.id ?? (isGeneratedElementId(key) ? key : void 0);
|
|
@@ -6467,6 +6723,7 @@ var RuntimeRenderer = class {
|
|
|
6467
6723
|
id: stateId,
|
|
6468
6724
|
name: node.name,
|
|
6469
6725
|
enabled: normalizeBoolean(node.enabled, true),
|
|
6726
|
+
definitionSignature: buildDefinitionSignature(node.elements ?? []),
|
|
6470
6727
|
elements: deepClone(node.elements ?? [])
|
|
6471
6728
|
};
|
|
6472
6729
|
break;
|
|
@@ -6477,6 +6734,7 @@ var RuntimeRenderer = class {
|
|
|
6477
6734
|
id: stateId,
|
|
6478
6735
|
name: node.name,
|
|
6479
6736
|
enabled: normalizeBoolean(node.enabled, true),
|
|
6737
|
+
definitionSignature: buildDefinitionSignature(node.elements ?? []),
|
|
6480
6738
|
listboxElements: deepClone(node.elements ?? []),
|
|
6481
6739
|
selected: toFiniteNumber3(node.selected) ?? 0
|
|
6482
6740
|
};
|
|
@@ -6488,6 +6746,7 @@ var RuntimeRenderer = class {
|
|
|
6488
6746
|
id: stateId,
|
|
6489
6747
|
name: node.name,
|
|
6490
6748
|
enabled: normalizeBoolean(node.enabled, true),
|
|
6749
|
+
definitionSignature: buildDefinitionSignature(node.elements ?? []),
|
|
6491
6750
|
comboboxElements: deepClone(node.elements ?? []),
|
|
6492
6751
|
selected: toFiniteNumber3(node.selected) ?? 0
|
|
6493
6752
|
};
|
|
@@ -6551,6 +6810,45 @@ var RuntimeRenderer = class {
|
|
|
6551
6810
|
}
|
|
6552
6811
|
return state;
|
|
6553
6812
|
}
|
|
6813
|
+
syncStateDefinition(state, node) {
|
|
6814
|
+
if (state.widget !== node.widget) {
|
|
6815
|
+
return;
|
|
6816
|
+
}
|
|
6817
|
+
switch (node.widget) {
|
|
6818
|
+
case "list":
|
|
6819
|
+
case "grid-view": {
|
|
6820
|
+
const nextSignature = buildDefinitionSignature(node.elements ?? []);
|
|
6821
|
+
if (state.definitionSignature !== nextSignature) {
|
|
6822
|
+
if (state.widget === "list" || state.widget === "grid-view") {
|
|
6823
|
+
this.clearListElementState(state.key);
|
|
6824
|
+
state.elements = deepClone(node.elements ?? []);
|
|
6825
|
+
state.definitionSignature = nextSignature;
|
|
6826
|
+
}
|
|
6827
|
+
}
|
|
6828
|
+
break;
|
|
6829
|
+
}
|
|
6830
|
+
case "listbox": {
|
|
6831
|
+
const nextSignature = buildDefinitionSignature(node.elements ?? []);
|
|
6832
|
+
if (state.definitionSignature !== nextSignature) {
|
|
6833
|
+
state.listboxElements = deepClone(node.elements ?? []);
|
|
6834
|
+
state.selected = toFiniteNumber3(node.selected) ?? 0;
|
|
6835
|
+
state.definitionSignature = nextSignature;
|
|
6836
|
+
}
|
|
6837
|
+
break;
|
|
6838
|
+
}
|
|
6839
|
+
case "combobox": {
|
|
6840
|
+
const nextSignature = buildDefinitionSignature(node.elements ?? []);
|
|
6841
|
+
if (state.definitionSignature !== nextSignature) {
|
|
6842
|
+
state.comboboxElements = deepClone(node.elements ?? []);
|
|
6843
|
+
state.selected = toFiniteNumber3(node.selected) ?? 0;
|
|
6844
|
+
state.definitionSignature = nextSignature;
|
|
6845
|
+
}
|
|
6846
|
+
break;
|
|
6847
|
+
}
|
|
6848
|
+
default:
|
|
6849
|
+
break;
|
|
6850
|
+
}
|
|
6851
|
+
}
|
|
6554
6852
|
getStateForNode(node, fallbackPath) {
|
|
6555
6853
|
const key = fallbackPath ? this.resolveNodeKey(node, fallbackPath) : node.id ?? this.resolveNodeKey(node, `auto:${node.widget}`);
|
|
6556
6854
|
return this.ensureWidgetState(node, key);
|
|
@@ -6941,7 +7239,7 @@ var RuntimeRenderer = class {
|
|
|
6941
7239
|
return;
|
|
6942
7240
|
}
|
|
6943
7241
|
const descriptor = listState.elements[indexValue];
|
|
6944
|
-
if (!descriptor || !
|
|
7242
|
+
if (!descriptor || !isPlainObject4(descriptor) || typeof descriptor.widget !== "string") {
|
|
6945
7243
|
return;
|
|
6946
7244
|
}
|
|
6947
7245
|
mutate(descriptor);
|
|
@@ -8,6 +8,7 @@ export type ActionExecutionContext = {
|
|
|
8
8
|
} | null;
|
|
9
9
|
};
|
|
10
10
|
type ActionRuntimeOptions = {
|
|
11
|
+
debugLogging: boolean;
|
|
11
12
|
actionsMap: ActionMap;
|
|
12
13
|
actionFunctions: Record<string, () => unknown>;
|
|
13
14
|
nodeById: Map<string, DescriptionNode>;
|
|
@@ -48,6 +49,7 @@ type ActionRuntimeOptions = {
|
|
|
48
49
|
isWidgetEnabled: (node: DescriptionNode, key: string) => boolean;
|
|
49
50
|
};
|
|
50
51
|
export declare class ActionRuntime {
|
|
52
|
+
private readonly debugLogging;
|
|
51
53
|
private readonly actionsMap;
|
|
52
54
|
private readonly actionFunctions;
|
|
53
55
|
private readonly nodeById;
|
package/dist/lib/logging.d.ts
CHANGED
package/dist/lib/network.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ type ActionRunner = (actions: ActionDefinition[], inputValue: unknown, context:
|
|
|
4
4
|
responseValue?: unknown;
|
|
5
5
|
}) => Promise<unknown>;
|
|
6
6
|
type NetworkRuntimeOptions = {
|
|
7
|
+
debugLogging: boolean;
|
|
7
8
|
requestsMap: RequestMap;
|
|
8
9
|
sseConfigs: SseConfig[];
|
|
9
10
|
backendUrl?: string;
|
|
@@ -12,6 +13,7 @@ type NetworkRuntimeOptions = {
|
|
|
12
13
|
rerenderRoot: () => Promise<void>;
|
|
13
14
|
};
|
|
14
15
|
export declare class NetworkRuntime {
|
|
16
|
+
private readonly debugLogging;
|
|
15
17
|
private readonly requestsMap;
|
|
16
18
|
private readonly sseConfigs;
|
|
17
19
|
private readonly backendUrl?;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RequestSchema, StyleMap } from './types.js';
|
|
2
|
+
export declare function isBlockedObjectKey(key: string): boolean;
|
|
3
|
+
export declare function hasBlockedReferenceSegment(reference: string): boolean;
|
|
4
|
+
export declare function isTrustedConfigOnlyKey(key: string): boolean;
|
|
5
|
+
export declare function templateValueUsesReference(value: unknown): boolean;
|
|
6
|
+
export declare function sanitizeConfigStyleMap(styleMap: StyleMap): StyleMap;
|
|
7
|
+
export declare function sanitizeUrl(rawValue: unknown, options?: {
|
|
8
|
+
allowRelative?: boolean;
|
|
9
|
+
allowMailto?: boolean;
|
|
10
|
+
}): string | null;
|
|
11
|
+
export declare function sanitizeMarkdownSource(markdown: string): string;
|
|
12
|
+
export declare function sanitizeGeneratedHtml(html: string): string;
|
|
13
|
+
export declare function sanitizeSchemaValue(schema: RequestSchema, value: unknown, path?: string): unknown;
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -91,6 +91,7 @@ export type WidgetState = {
|
|
|
91
91
|
widget: DescriptionNode['widget'];
|
|
92
92
|
id?: string;
|
|
93
93
|
name?: string;
|
|
94
|
+
definitionSignature?: string;
|
|
94
95
|
currentRef?: string;
|
|
95
96
|
text?: string;
|
|
96
97
|
url?: string;
|
|
@@ -131,6 +132,7 @@ export type RenderJsonOptions = {
|
|
|
131
132
|
i18n?: I18nMap;
|
|
132
133
|
language?: string;
|
|
133
134
|
theme?: Theme;
|
|
135
|
+
debugLogging?: boolean;
|
|
134
136
|
actionsMap?: ActionMap;
|
|
135
137
|
requestsMap?: RequestMap;
|
|
136
138
|
sseConfigs?: SseConfigInput;
|