@vortexm/vjt 0.1.2 → 0.1.3
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 +260 -27
- package/dist/lib/security.d.ts +13 -0
- package/dist/lib/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3683,6 +3683,159 @@ function logRuntimeError(scope, error, details) {
|
|
|
3683
3683
|
console.error("[VJT error]", payload);
|
|
3684
3684
|
}
|
|
3685
3685
|
|
|
3686
|
+
// src/lib/security.ts
|
|
3687
|
+
var BLOCKED_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
3688
|
+
var TRUSTED_CONFIG_ONLY_KEYS = /* @__PURE__ */ new Set(["widget", "style", "css", "actions", "events", "onClick", "onRefresh"]);
|
|
3689
|
+
function isPlainObject(value) {
|
|
3690
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3691
|
+
}
|
|
3692
|
+
function escapeHtmlAttribute(value) {
|
|
3693
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
3694
|
+
}
|
|
3695
|
+
function coerceFiniteNumber(value, kind, path) {
|
|
3696
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
3697
|
+
return kind === "int" ? Math.trunc(value) : value;
|
|
3698
|
+
}
|
|
3699
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
3700
|
+
const parsed = kind === "int" ? Number.parseInt(value, 10) : Number.parseFloat(value);
|
|
3701
|
+
if (Number.isFinite(parsed)) {
|
|
3702
|
+
return kind === "int" ? Math.trunc(parsed) : parsed;
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
throw new Error(`Invalid ${kind} at ${path}`);
|
|
3706
|
+
}
|
|
3707
|
+
function isBlockedObjectKey(key) {
|
|
3708
|
+
return BLOCKED_OBJECT_KEYS.has(key);
|
|
3709
|
+
}
|
|
3710
|
+
function hasBlockedReferenceSegment(reference) {
|
|
3711
|
+
return reference.split(".").some((segment) => isBlockedObjectKey(segment));
|
|
3712
|
+
}
|
|
3713
|
+
function isTrustedConfigOnlyKey(key) {
|
|
3714
|
+
return TRUSTED_CONFIG_ONLY_KEYS.has(key);
|
|
3715
|
+
}
|
|
3716
|
+
function templateValueUsesReference(value) {
|
|
3717
|
+
if (typeof value === "string") {
|
|
3718
|
+
return value.includes("$ref:");
|
|
3719
|
+
}
|
|
3720
|
+
if (Array.isArray(value)) {
|
|
3721
|
+
return value.some((entry) => templateValueUsesReference(entry));
|
|
3722
|
+
}
|
|
3723
|
+
if (isPlainObject(value)) {
|
|
3724
|
+
return Object.values(value).some((entry) => templateValueUsesReference(entry));
|
|
3725
|
+
}
|
|
3726
|
+
return false;
|
|
3727
|
+
}
|
|
3728
|
+
function sanitizeConfigStyleMap(styleMap) {
|
|
3729
|
+
const safe = {};
|
|
3730
|
+
for (const [key, value] of Object.entries(styleMap)) {
|
|
3731
|
+
if (isBlockedObjectKey(key) || typeof value !== "string") {
|
|
3732
|
+
continue;
|
|
3733
|
+
}
|
|
3734
|
+
safe[key] = value;
|
|
3735
|
+
}
|
|
3736
|
+
return safe;
|
|
3737
|
+
}
|
|
3738
|
+
function sanitizeUrl(rawValue, options = {}) {
|
|
3739
|
+
if (typeof rawValue !== "string") {
|
|
3740
|
+
return null;
|
|
3741
|
+
}
|
|
3742
|
+
const value = rawValue.trim();
|
|
3743
|
+
if (!value) {
|
|
3744
|
+
return null;
|
|
3745
|
+
}
|
|
3746
|
+
if (/^(javascript|data|vbscript):/i.test(value)) {
|
|
3747
|
+
return null;
|
|
3748
|
+
}
|
|
3749
|
+
if (value.startsWith("//")) {
|
|
3750
|
+
return null;
|
|
3751
|
+
}
|
|
3752
|
+
const schemeMatch = value.match(/^([A-Za-z][A-Za-z\d+\-.]*):/);
|
|
3753
|
+
if (!schemeMatch) {
|
|
3754
|
+
return options.allowRelative !== false ? value : null;
|
|
3755
|
+
}
|
|
3756
|
+
const scheme = schemeMatch[1].toLowerCase();
|
|
3757
|
+
if (scheme === "http" || scheme === "https") {
|
|
3758
|
+
return value;
|
|
3759
|
+
}
|
|
3760
|
+
if (scheme === "mailto" && options.allowMailto) {
|
|
3761
|
+
return value;
|
|
3762
|
+
}
|
|
3763
|
+
return null;
|
|
3764
|
+
}
|
|
3765
|
+
function sanitizeMarkdownSource(markdown) {
|
|
3766
|
+
return markdown.replaceAll("<", "<").replaceAll(">", ">");
|
|
3767
|
+
}
|
|
3768
|
+
function sanitizeGeneratedHtml(html) {
|
|
3769
|
+
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, "");
|
|
3770
|
+
return withoutDangerousTags.replace(/\s(href|src)\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/gi, (_match, attributeName, _full, dq, sq, bare) => {
|
|
3771
|
+
const rawUrl = dq ?? sq ?? bare ?? "";
|
|
3772
|
+
const safeUrl = sanitizeUrl(rawUrl, {
|
|
3773
|
+
allowRelative: true,
|
|
3774
|
+
allowMailto: attributeName.toLowerCase() === "href"
|
|
3775
|
+
});
|
|
3776
|
+
const fallback = attributeName.toLowerCase() === "href" ? "#" : "";
|
|
3777
|
+
return ` ${attributeName}="${escapeHtmlAttribute(safeUrl ?? fallback)}"`;
|
|
3778
|
+
});
|
|
3779
|
+
}
|
|
3780
|
+
function sanitizeSchemaValue(schema, value, path = "value") {
|
|
3781
|
+
if (value === void 0 || value === null) {
|
|
3782
|
+
return void 0;
|
|
3783
|
+
}
|
|
3784
|
+
if (schema.type === "object") {
|
|
3785
|
+
if (!isPlainObject(value)) {
|
|
3786
|
+
throw new Error(`Expected object at ${path}`);
|
|
3787
|
+
}
|
|
3788
|
+
const result = {};
|
|
3789
|
+
for (const property of schema.properties ?? []) {
|
|
3790
|
+
if (!property.name || isBlockedObjectKey(property.name)) {
|
|
3791
|
+
continue;
|
|
3792
|
+
}
|
|
3793
|
+
result[property.name] = sanitizeSchemaValue(property, value[property.name], `${path}.${property.name}`);
|
|
3794
|
+
}
|
|
3795
|
+
return result;
|
|
3796
|
+
}
|
|
3797
|
+
if (schema.type === "array") {
|
|
3798
|
+
if (!Array.isArray(value)) {
|
|
3799
|
+
throw new Error(`Expected array at ${path}`);
|
|
3800
|
+
}
|
|
3801
|
+
return value.map((entry, index) => sanitizeSchemaValue(schema.elements, entry, `${path}[${index}]`));
|
|
3802
|
+
}
|
|
3803
|
+
switch (schema.type) {
|
|
3804
|
+
case "int":
|
|
3805
|
+
return coerceFiniteNumber(value, "int", path);
|
|
3806
|
+
case "float":
|
|
3807
|
+
return coerceFiniteNumber(value, "float", path);
|
|
3808
|
+
case "boolean":
|
|
3809
|
+
if (typeof value === "boolean") {
|
|
3810
|
+
return value;
|
|
3811
|
+
}
|
|
3812
|
+
if (value === "true" || value === "1" || value === 1) {
|
|
3813
|
+
return true;
|
|
3814
|
+
}
|
|
3815
|
+
if (value === "false" || value === "0" || value === 0) {
|
|
3816
|
+
return false;
|
|
3817
|
+
}
|
|
3818
|
+
throw new Error(`Invalid boolean at ${path}`);
|
|
3819
|
+
case "string": {
|
|
3820
|
+
const stringValue = typeof value === "string" ? value : typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" ? String(value) : (() => {
|
|
3821
|
+
throw new Error(`Invalid string at ${path}`);
|
|
3822
|
+
})();
|
|
3823
|
+
if (typeof schema.maxLength === "number" && stringValue.length > schema.maxLength) {
|
|
3824
|
+
throw new Error(`String too long at ${path}`);
|
|
3825
|
+
}
|
|
3826
|
+
if (typeof schema.regexp === "string" && schema.regexp) {
|
|
3827
|
+
const regexp = new RegExp(schema.regexp);
|
|
3828
|
+
if (!regexp.test(stringValue)) {
|
|
3829
|
+
throw new Error(`String does not match regexp at ${path}`);
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
return stringValue;
|
|
3833
|
+
}
|
|
3834
|
+
default:
|
|
3835
|
+
return value;
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3686
3839
|
// src/lib/network.ts
|
|
3687
3840
|
var NetworkRuntime = class {
|
|
3688
3841
|
requestsMap;
|
|
@@ -3707,7 +3860,12 @@ var NetworkRuntime = class {
|
|
|
3707
3860
|
if (!config?.url) {
|
|
3708
3861
|
continue;
|
|
3709
3862
|
}
|
|
3710
|
-
const
|
|
3863
|
+
const safeUrl = sanitizeUrl(config.url, { allowRelative: true });
|
|
3864
|
+
if (!safeUrl) {
|
|
3865
|
+
logRuntimeError("sseConfig", new Error("Blocked unsafe SSE url"), { url: config.url });
|
|
3866
|
+
continue;
|
|
3867
|
+
}
|
|
3868
|
+
const source = new EventSource(safeUrl);
|
|
3711
3869
|
this.eventSources.push(source);
|
|
3712
3870
|
for (const eventDefinition of config.events ?? []) {
|
|
3713
3871
|
if (!eventDefinition?.name) {
|
|
@@ -3717,7 +3875,7 @@ var NetworkRuntime = class {
|
|
|
3717
3875
|
void this.handleSseEvent(eventDefinition, event).catch((error) => {
|
|
3718
3876
|
logRuntimeError("handleSseEvent", error, {
|
|
3719
3877
|
eventName: eventDefinition.name,
|
|
3720
|
-
url:
|
|
3878
|
+
url: safeUrl
|
|
3721
3879
|
});
|
|
3722
3880
|
});
|
|
3723
3881
|
});
|
|
@@ -3745,7 +3903,11 @@ var NetworkRuntime = class {
|
|
|
3745
3903
|
if (!requestUrl) {
|
|
3746
3904
|
throw new Error(`Request ${requestName} has no url in config`);
|
|
3747
3905
|
}
|
|
3748
|
-
const
|
|
3906
|
+
const safeRequestUrl = sanitizeUrl(requestUrl, { allowRelative: true });
|
|
3907
|
+
if (!safeRequestUrl) {
|
|
3908
|
+
throw new Error(`Request ${requestName} has unsafe url`);
|
|
3909
|
+
}
|
|
3910
|
+
const response = await fetch(safeRequestUrl, {
|
|
3749
3911
|
method: "POST",
|
|
3750
3912
|
headers: {
|
|
3751
3913
|
"Content-Type": "application/json"
|
|
@@ -3765,7 +3927,13 @@ var NetworkRuntime = class {
|
|
|
3765
3927
|
} catch {
|
|
3766
3928
|
throw new Error(`Request ${requestName} returned non-JSON response: ${responseText.slice(0, 120)}`);
|
|
3767
3929
|
}
|
|
3768
|
-
|
|
3930
|
+
if (json.error) {
|
|
3931
|
+
throw new Error(`Request ${requestName} returned JSON-RPC error: ${String(json.error.message ?? "unknown error")}`);
|
|
3932
|
+
}
|
|
3933
|
+
if (!definition.response) {
|
|
3934
|
+
throw new Error(`Request ${requestName} must define response schema`);
|
|
3935
|
+
}
|
|
3936
|
+
const result = sanitizeSchemaValue(definition.response, json.result, `response.${requestName}`);
|
|
3769
3937
|
if (definition.onResponse?.length) {
|
|
3770
3938
|
await this.runActions(definition.onResponse, null, {
|
|
3771
3939
|
currentValue: null,
|
|
@@ -3778,7 +3946,7 @@ var NetworkRuntime = class {
|
|
|
3778
3946
|
if (schema.type === "object") {
|
|
3779
3947
|
const result = {};
|
|
3780
3948
|
for (const property of schema.properties ?? []) {
|
|
3781
|
-
if (!property.name) {
|
|
3949
|
+
if (!property.name || isBlockedObjectKey(property.name)) {
|
|
3782
3950
|
continue;
|
|
3783
3951
|
}
|
|
3784
3952
|
result[property.name] = await this.buildSchemaValue(property, currentValue, responseValue);
|
|
@@ -3834,12 +4002,15 @@ var NetworkRuntime = class {
|
|
|
3834
4002
|
}
|
|
3835
4003
|
}
|
|
3836
4004
|
async handleSseEvent(eventDefinition, event) {
|
|
3837
|
-
let
|
|
4005
|
+
let parsedMessage = {};
|
|
3838
4006
|
try {
|
|
3839
|
-
|
|
4007
|
+
parsedMessage = event.data ? JSON.parse(event.data) : {};
|
|
3840
4008
|
} catch {
|
|
3841
|
-
|
|
4009
|
+
parsedMessage = { value: event.data ?? "" };
|
|
3842
4010
|
}
|
|
4011
|
+
const message = eventDefinition.message ? sanitizeSchemaValue(eventDefinition.message, parsedMessage, `sse.${eventDefinition.name}`) : (() => {
|
|
4012
|
+
throw new Error(`SSE event ${eventDefinition.name} must define message schema`);
|
|
4013
|
+
})();
|
|
3843
4014
|
if (!eventDefinition.onEvent?.length) {
|
|
3844
4015
|
return;
|
|
3845
4016
|
}
|
|
@@ -3852,12 +4023,15 @@ var NetworkRuntime = class {
|
|
|
3852
4023
|
};
|
|
3853
4024
|
|
|
3854
4025
|
// src/lib/references.ts
|
|
3855
|
-
function
|
|
4026
|
+
function isPlainObject2(value) {
|
|
3856
4027
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3857
4028
|
}
|
|
3858
4029
|
function readPath(target, path) {
|
|
3859
4030
|
let current = target;
|
|
3860
4031
|
for (const segment of path) {
|
|
4032
|
+
if (isBlockedObjectKey(segment)) {
|
|
4033
|
+
return void 0;
|
|
4034
|
+
}
|
|
3861
4035
|
if (current == null) {
|
|
3862
4036
|
return void 0;
|
|
3863
4037
|
}
|
|
@@ -3869,6 +4043,9 @@ function readPath(target, path) {
|
|
|
3869
4043
|
if (typeof current !== "object") {
|
|
3870
4044
|
return void 0;
|
|
3871
4045
|
}
|
|
4046
|
+
if (!Object.prototype.hasOwnProperty.call(current, segment)) {
|
|
4047
|
+
return void 0;
|
|
4048
|
+
}
|
|
3872
4049
|
current = current[segment];
|
|
3873
4050
|
}
|
|
3874
4051
|
return current;
|
|
@@ -4008,7 +4185,7 @@ var ReferenceRuntime = class {
|
|
|
4008
4185
|
}
|
|
4009
4186
|
return readPath(currentValue.descriptor, reference.split("."));
|
|
4010
4187
|
}
|
|
4011
|
-
if (
|
|
4188
|
+
if (isPlainObject2(currentValue) || Array.isArray(currentValue)) {
|
|
4012
4189
|
return readPath(currentValue, reference.split("."));
|
|
4013
4190
|
}
|
|
4014
4191
|
return void 0;
|
|
@@ -4033,7 +4210,7 @@ var ReferenceRuntime = class {
|
|
|
4033
4210
|
if (state.widget === "combobox") {
|
|
4034
4211
|
const index = state.selected ?? 0;
|
|
4035
4212
|
const entry = (state.comboboxElements ?? [])[index];
|
|
4036
|
-
if (
|
|
4213
|
+
if (isPlainObject2(entry)) {
|
|
4037
4214
|
return entry.value ?? entry.text ?? index;
|
|
4038
4215
|
}
|
|
4039
4216
|
return index;
|
|
@@ -4041,7 +4218,7 @@ var ReferenceRuntime = class {
|
|
|
4041
4218
|
if (state.widget === "listbox") {
|
|
4042
4219
|
const index = state.selected ?? 0;
|
|
4043
4220
|
const entry = (state.listboxElements ?? [])[index];
|
|
4044
|
-
if (
|
|
4221
|
+
if (isPlainObject2(entry)) {
|
|
4045
4222
|
return entry.value ?? entry.text ?? index;
|
|
4046
4223
|
}
|
|
4047
4224
|
return index;
|
|
@@ -4091,7 +4268,7 @@ var ReferenceRuntime = class {
|
|
|
4091
4268
|
}
|
|
4092
4269
|
}
|
|
4093
4270
|
assignReference(reference, inputValue, _currentValue, options) {
|
|
4094
|
-
if (reference.startsWith("current.")) {
|
|
4271
|
+
if (reference.startsWith("current.") || hasBlockedReferenceSegment(reference)) {
|
|
4095
4272
|
return;
|
|
4096
4273
|
}
|
|
4097
4274
|
if (reference.startsWith("cookies.")) {
|
|
@@ -4136,7 +4313,7 @@ var ReferenceRuntime = class {
|
|
|
4136
4313
|
case "elements":
|
|
4137
4314
|
if (state.widget === "list" || state.widget === "grid-view") {
|
|
4138
4315
|
this.clearListElementState(state.key);
|
|
4139
|
-
state.elements = Array.isArray(inputValue) ? inputValue.filter((item) =>
|
|
4316
|
+
state.elements = Array.isArray(inputValue) ? inputValue.filter((item) => isPlainObject2(item) && typeof item.widget === "string") : [];
|
|
4140
4317
|
state.elements.forEach((element, index) => {
|
|
4141
4318
|
const listNode = this.host.nodeById.get(widgetId);
|
|
4142
4319
|
if (listNode?.widget === "list" || listNode?.widget === "grid-view") {
|
|
@@ -4145,10 +4322,10 @@ var ReferenceRuntime = class {
|
|
|
4145
4322
|
});
|
|
4146
4323
|
}
|
|
4147
4324
|
if (state.widget === "listbox") {
|
|
4148
|
-
state.listboxElements = Array.isArray(inputValue) ? inputValue.filter((item) =>
|
|
4325
|
+
state.listboxElements = Array.isArray(inputValue) ? inputValue.filter((item) => isPlainObject2(item)) : [];
|
|
4149
4326
|
}
|
|
4150
4327
|
if (state.widget === "combobox") {
|
|
4151
|
-
state.comboboxElements = Array.isArray(inputValue) ? inputValue.filter((item) =>
|
|
4328
|
+
state.comboboxElements = Array.isArray(inputValue) ? inputValue.filter((item) => isPlainObject2(item)) : [];
|
|
4152
4329
|
}
|
|
4153
4330
|
break;
|
|
4154
4331
|
default:
|
|
@@ -4187,9 +4364,15 @@ var ReferenceRuntime = class {
|
|
|
4187
4364
|
if (Array.isArray(template)) {
|
|
4188
4365
|
return template.map((entry) => this.resolveMappedValue(entry, currentValue, responseValue));
|
|
4189
4366
|
}
|
|
4190
|
-
if (
|
|
4367
|
+
if (isPlainObject2(template)) {
|
|
4191
4368
|
const result = {};
|
|
4192
4369
|
for (const [key, value] of Object.entries(template)) {
|
|
4370
|
+
if (isBlockedObjectKey(key)) {
|
|
4371
|
+
continue;
|
|
4372
|
+
}
|
|
4373
|
+
if (isTrustedConfigOnlyKey(key) && templateValueUsesReference(value)) {
|
|
4374
|
+
continue;
|
|
4375
|
+
}
|
|
4193
4376
|
result[key] = this.resolveMappedValue(value, currentValue, responseValue);
|
|
4194
4377
|
}
|
|
4195
4378
|
return result;
|
|
@@ -4197,7 +4380,7 @@ var ReferenceRuntime = class {
|
|
|
4197
4380
|
return template;
|
|
4198
4381
|
}
|
|
4199
4382
|
isListElementContext(value) {
|
|
4200
|
-
return
|
|
4383
|
+
return isPlainObject2(value) && value.kind === "list-element" && typeof value.listId === "string" && typeof value.index === "number" && isPlainObject2(value.descriptor) && typeof value.descriptor.widget === "string";
|
|
4201
4384
|
}
|
|
4202
4385
|
findNamedWidget(node, name) {
|
|
4203
4386
|
if (!node) {
|
|
@@ -4486,7 +4669,7 @@ function isIfElseArgs(value) {
|
|
|
4486
4669
|
}
|
|
4487
4670
|
|
|
4488
4671
|
// src/lib/dom-state.ts
|
|
4489
|
-
function
|
|
4672
|
+
function isPlainObject3(value) {
|
|
4490
4673
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4491
4674
|
}
|
|
4492
4675
|
function supportsTextSelection(element) {
|
|
@@ -4656,7 +4839,7 @@ function getListElementContextFromElement(element, stateByKey) {
|
|
|
4656
4839
|
}
|
|
4657
4840
|
const listState = stateByKey.get(listId);
|
|
4658
4841
|
const descriptor = listState?.elements?.[index];
|
|
4659
|
-
if (!descriptor || !
|
|
4842
|
+
if (!descriptor || !isPlainObject3(descriptor) || typeof descriptor.widget !== "string") {
|
|
4660
4843
|
return null;
|
|
4661
4844
|
}
|
|
4662
4845
|
return {
|
|
@@ -5173,7 +5356,7 @@ var renderMdText = (env, node, state, key) => {
|
|
|
5173
5356
|
const verticalAlign = VERTICAL_ALIGN_MAP2[node["vert-align"] ?? "center"];
|
|
5174
5357
|
const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
|
|
5175
5358
|
const markdown = env.resolveI18nValue(state.text ?? node.text);
|
|
5176
|
-
const html = markdownConverter.makeHtml(markdown);
|
|
5359
|
+
const html = sanitizeGeneratedHtml(markdownConverter.makeHtml(sanitizeMarkdownSource(markdown)));
|
|
5177
5360
|
const inlineStyle = [
|
|
5178
5361
|
"display:flex",
|
|
5179
5362
|
`align-items:${verticalAlign}`,
|
|
@@ -5279,7 +5462,7 @@ var renderCheckbox = (env, node, state, key) => {
|
|
|
5279
5462
|
|
|
5280
5463
|
// src/lib/widgets/image.ts
|
|
5281
5464
|
var renderImage = (env, node, state, key) => {
|
|
5282
|
-
const url = env.escapeHtml(state.url ?? node.url ?? "");
|
|
5465
|
+
const url = env.escapeHtml(sanitizeUrl(state.url ?? node.url ?? "", { allowRelative: true }) ?? "");
|
|
5283
5466
|
const styleParts = [
|
|
5284
5467
|
"box-sizing:border-box",
|
|
5285
5468
|
"width:100%",
|
|
@@ -5306,7 +5489,7 @@ var renderLink = (env, node, state, key) => {
|
|
|
5306
5489
|
const classes = `vjt-link vjt-link--${type}${enabled ? "" : " is-disabled"}`;
|
|
5307
5490
|
const styleString = env.buildStyleString(node);
|
|
5308
5491
|
const styleAttribute = styleString ? ` style="${env.escapeHtml(styleString)}"` : "";
|
|
5309
|
-
const href = enabled
|
|
5492
|
+
const href = enabled ? sanitizeUrl(state.url ?? node.url, { allowRelative: true, allowMailto: true }) ?? "#" : "#";
|
|
5310
5493
|
const tabIndex = enabled ? "" : ' tabindex="-1" aria-disabled="true"';
|
|
5311
5494
|
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
5495
|
};
|
|
@@ -5868,7 +6051,7 @@ function toFiniteNumber3(value) {
|
|
|
5868
6051
|
}
|
|
5869
6052
|
return null;
|
|
5870
6053
|
}
|
|
5871
|
-
function
|
|
6054
|
+
function isPlainObject4(value) {
|
|
5872
6055
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5873
6056
|
}
|
|
5874
6057
|
function deepClone(value) {
|
|
@@ -5878,6 +6061,9 @@ function sanitizeIdFragment2(value) {
|
|
|
5878
6061
|
const cleaned = value.replaceAll(/[^A-Za-z0-9_-]+/g, "");
|
|
5879
6062
|
return cleaned || "item";
|
|
5880
6063
|
}
|
|
6064
|
+
function buildDefinitionSignature(value) {
|
|
6065
|
+
return JSON.stringify(value) ?? "null";
|
|
6066
|
+
}
|
|
5881
6067
|
function isGeneratedElementId(value) {
|
|
5882
6068
|
return !value.includes(".") && !value.includes(":") && /Element\d+/.test(value);
|
|
5883
6069
|
}
|
|
@@ -5952,7 +6138,7 @@ var RuntimeRenderer = class {
|
|
|
5952
6138
|
constructor(description, options = {}) {
|
|
5953
6139
|
this.description = deepClone(description);
|
|
5954
6140
|
this.resourceManager = options.resourceManager ?? null;
|
|
5955
|
-
const customStyleMap = options.styleMap ?? this.resourceManager?.getStyleMap() ?? {};
|
|
6141
|
+
const customStyleMap = sanitizeConfigStyleMap(options.styleMap ?? this.resourceManager?.getStyleMap() ?? {});
|
|
5956
6142
|
this.styleMap = {
|
|
5957
6143
|
...DEFAULT_STYLE_MAP,
|
|
5958
6144
|
...customStyleMap
|
|
@@ -6160,7 +6346,11 @@ var RuntimeRenderer = class {
|
|
|
6160
6346
|
if (typeof window === "undefined" || !url.trim()) {
|
|
6161
6347
|
return;
|
|
6162
6348
|
}
|
|
6163
|
-
const
|
|
6349
|
+
const safeUrl = sanitizeUrl(url, { allowRelative: true });
|
|
6350
|
+
if (!safeUrl) {
|
|
6351
|
+
throw new Error("Blocked unsafe navigation url");
|
|
6352
|
+
}
|
|
6353
|
+
const nextUrl = new URL(safeUrl, window.location.href);
|
|
6164
6354
|
if (nextUrl.origin !== window.location.origin) {
|
|
6165
6355
|
window.location.assign(nextUrl.toString());
|
|
6166
6356
|
return;
|
|
@@ -6398,6 +6588,7 @@ var RuntimeRenderer = class {
|
|
|
6398
6588
|
existing.id = stateId2;
|
|
6399
6589
|
this.stateById.set(stateId2, existing);
|
|
6400
6590
|
}
|
|
6591
|
+
this.syncStateDefinition(existing, node);
|
|
6401
6592
|
return existing;
|
|
6402
6593
|
}
|
|
6403
6594
|
const stateId = node.id ?? (isGeneratedElementId(key) ? key : void 0);
|
|
@@ -6467,6 +6658,7 @@ var RuntimeRenderer = class {
|
|
|
6467
6658
|
id: stateId,
|
|
6468
6659
|
name: node.name,
|
|
6469
6660
|
enabled: normalizeBoolean(node.enabled, true),
|
|
6661
|
+
definitionSignature: buildDefinitionSignature(node.elements ?? []),
|
|
6470
6662
|
elements: deepClone(node.elements ?? [])
|
|
6471
6663
|
};
|
|
6472
6664
|
break;
|
|
@@ -6477,6 +6669,7 @@ var RuntimeRenderer = class {
|
|
|
6477
6669
|
id: stateId,
|
|
6478
6670
|
name: node.name,
|
|
6479
6671
|
enabled: normalizeBoolean(node.enabled, true),
|
|
6672
|
+
definitionSignature: buildDefinitionSignature(node.elements ?? []),
|
|
6480
6673
|
listboxElements: deepClone(node.elements ?? []),
|
|
6481
6674
|
selected: toFiniteNumber3(node.selected) ?? 0
|
|
6482
6675
|
};
|
|
@@ -6488,6 +6681,7 @@ var RuntimeRenderer = class {
|
|
|
6488
6681
|
id: stateId,
|
|
6489
6682
|
name: node.name,
|
|
6490
6683
|
enabled: normalizeBoolean(node.enabled, true),
|
|
6684
|
+
definitionSignature: buildDefinitionSignature(node.elements ?? []),
|
|
6491
6685
|
comboboxElements: deepClone(node.elements ?? []),
|
|
6492
6686
|
selected: toFiniteNumber3(node.selected) ?? 0
|
|
6493
6687
|
};
|
|
@@ -6551,6 +6745,45 @@ var RuntimeRenderer = class {
|
|
|
6551
6745
|
}
|
|
6552
6746
|
return state;
|
|
6553
6747
|
}
|
|
6748
|
+
syncStateDefinition(state, node) {
|
|
6749
|
+
if (state.widget !== node.widget) {
|
|
6750
|
+
return;
|
|
6751
|
+
}
|
|
6752
|
+
switch (node.widget) {
|
|
6753
|
+
case "list":
|
|
6754
|
+
case "grid-view": {
|
|
6755
|
+
const nextSignature = buildDefinitionSignature(node.elements ?? []);
|
|
6756
|
+
if (state.definitionSignature !== nextSignature) {
|
|
6757
|
+
if (state.widget === "list" || state.widget === "grid-view") {
|
|
6758
|
+
this.clearListElementState(state.key);
|
|
6759
|
+
state.elements = deepClone(node.elements ?? []);
|
|
6760
|
+
state.definitionSignature = nextSignature;
|
|
6761
|
+
}
|
|
6762
|
+
}
|
|
6763
|
+
break;
|
|
6764
|
+
}
|
|
6765
|
+
case "listbox": {
|
|
6766
|
+
const nextSignature = buildDefinitionSignature(node.elements ?? []);
|
|
6767
|
+
if (state.definitionSignature !== nextSignature) {
|
|
6768
|
+
state.listboxElements = deepClone(node.elements ?? []);
|
|
6769
|
+
state.selected = toFiniteNumber3(node.selected) ?? 0;
|
|
6770
|
+
state.definitionSignature = nextSignature;
|
|
6771
|
+
}
|
|
6772
|
+
break;
|
|
6773
|
+
}
|
|
6774
|
+
case "combobox": {
|
|
6775
|
+
const nextSignature = buildDefinitionSignature(node.elements ?? []);
|
|
6776
|
+
if (state.definitionSignature !== nextSignature) {
|
|
6777
|
+
state.comboboxElements = deepClone(node.elements ?? []);
|
|
6778
|
+
state.selected = toFiniteNumber3(node.selected) ?? 0;
|
|
6779
|
+
state.definitionSignature = nextSignature;
|
|
6780
|
+
}
|
|
6781
|
+
break;
|
|
6782
|
+
}
|
|
6783
|
+
default:
|
|
6784
|
+
break;
|
|
6785
|
+
}
|
|
6786
|
+
}
|
|
6554
6787
|
getStateForNode(node, fallbackPath) {
|
|
6555
6788
|
const key = fallbackPath ? this.resolveNodeKey(node, fallbackPath) : node.id ?? this.resolveNodeKey(node, `auto:${node.widget}`);
|
|
6556
6789
|
return this.ensureWidgetState(node, key);
|
|
@@ -6941,7 +7174,7 @@ var RuntimeRenderer = class {
|
|
|
6941
7174
|
return;
|
|
6942
7175
|
}
|
|
6943
7176
|
const descriptor = listState.elements[indexValue];
|
|
6944
|
-
if (!descriptor || !
|
|
7177
|
+
if (!descriptor || !isPlainObject4(descriptor) || typeof descriptor.widget !== "string") {
|
|
6945
7178
|
return;
|
|
6946
7179
|
}
|
|
6947
7180
|
mutate(descriptor);
|
|
@@ -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