@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 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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
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("<", "&lt;").replaceAll(">", "&gt;");
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 source = new EventSource(config.url);
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: config.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 response = await fetch(requestUrl, {
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: JSON.stringify(requestEnvelope)
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
- const result = json.result;
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 message = {};
4031
+ let parsedMessage = {};
3838
4032
  try {
3839
- message = event.data ? JSON.parse(event.data) : {};
4033
+ parsedMessage = event.data ? JSON.parse(event.data) : {};
3840
4034
  } catch {
3841
- message = { value: event.data ?? "" };
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 isPlainObject(value) {
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 (isPlainObject(currentValue) || Array.isArray(currentValue)) {
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 (isPlainObject(entry)) {
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 (isPlainObject(entry)) {
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) => isPlainObject(item) && typeof item.widget === "string") : [];
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) => isPlainObject(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) => isPlainObject(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 (isPlainObject(template)) {
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 isPlainObject(value) && value.kind === "list-element" && typeof value.listId === "string" && typeof value.index === "number" && isPlainObject(value.descriptor) && typeof value.descriptor.widget === "string";
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
- let output = await this.executeAction(action, inputValue, context);
4352
- if (action.andThen?.length) {
4353
- if (Array.isArray(output)) {
4354
- const collected = [];
4355
- for (const entry of output) {
4356
- if (entry === null || entry === void 0) {
4357
- continue;
4358
- }
4359
- const nestedOutput = await this.runActions(action.andThen, entry, { ...context, currentValue: entry });
4360
- if (nestedOutput !== null && nestedOutput !== void 0) {
4361
- collected.push(nestedOutput);
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 isPlainObject2(value) {
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 || !isPlainObject2(descriptor) || typeof descriptor.widget !== "string") {
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 && typeof node.url === "string" ? node.url : "#";
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 isPlainObject3(value) {
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
- return `${this.renderNode(this.description, "root", null)}${this.renderGlobalOverlays()}`;
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 nextUrl = new URL(url, window.location.href);
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 || !isPlainObject3(descriptor) || typeof descriptor.widget !== "string") {
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;
@@ -1 +1,2 @@
1
1
  export declare function logRuntimeError(scope: string, error: unknown, details?: Record<string, unknown>): void;
2
+ export declare function logRuntimeDebug(enabled: boolean, scope: string, payload?: unknown): void;
@@ -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;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vortexm/vjt",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",