@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 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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
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("<", "&lt;").replaceAll(">", "&gt;");
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 source = new EventSource(config.url);
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: config.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 response = await fetch(requestUrl, {
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
- const result = json.result;
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 message = {};
4005
+ let parsedMessage = {};
3838
4006
  try {
3839
- message = event.data ? JSON.parse(event.data) : {};
4007
+ parsedMessage = event.data ? JSON.parse(event.data) : {};
3840
4008
  } catch {
3841
- message = { value: event.data ?? "" };
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 isPlainObject(value) {
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 (isPlainObject(currentValue) || Array.isArray(currentValue)) {
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 (isPlainObject(entry)) {
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 (isPlainObject(entry)) {
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) => isPlainObject(item) && typeof item.widget === "string") : [];
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) => isPlainObject(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) => isPlainObject(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 (isPlainObject(template)) {
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 isPlainObject(value) && value.kind === "list-element" && typeof value.listId === "string" && typeof value.index === "number" && isPlainObject(value.descriptor) && typeof value.descriptor.widget === "string";
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 isPlainObject2(value) {
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 || !isPlainObject2(descriptor) || typeof descriptor.widget !== "string") {
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 && typeof node.url === "string" ? node.url : "#";
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 isPlainObject3(value) {
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 nextUrl = new URL(url, window.location.href);
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 || !isPlainObject3(descriptor) || typeof descriptor.widget !== "string") {
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;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vortexm/vjt",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",