@xh/hoist 77.0.0-SNAPSHOT.1761155172844 → 77.0.0-SNAPSHOT.1761229417098

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 77.0.0-SNAPSHOT - unreleased
4
+
5
+ ### 💥 Breaking Changes
6
+
7
+ * The `disableXssProtection` flag supported by `AppSpec` and `FieldSpec` has been removed and
8
+ replaced with its opposite, `enableXssProtection`, now an opt-in feature.
9
+ * While store-based XSS protection via DomPurify is still available to apps that can display
10
+ untrusted or potentially malicious data, this is an uncommon use case for Hoist apps and was
11
+ deemed to not provide enough benefit relative to potential performance pitfalls for most
12
+ applications. In addition, the core change to React-based AG Grid rendering has reduced the
13
+ attack surface for such exploits relative to when this system was first implemented.
14
+ * Apps that were previously opting-out via `disableXssProtection` should simply remove that
15
+ flag. Apps for which this protection remains important should enable at either the app level
16
+ or for selected Fields and/or Stores.
17
+
3
18
  ## 76.2.0 - 2025-10-22
4
19
 
5
20
  ### ⚙️ Technical
@@ -8,7 +23,6 @@
8
23
  * New property `cubeRowType` on `ViewRowData` supports identifying bucketed rows.
9
24
  * `waitFor` can accept a null value for a timeout.
10
25
 
11
-
12
26
  ## 76.1.0 - 2025-10-17
13
27
 
14
28
  ### 🎁 New Features
@@ -51,7 +51,7 @@ export class LogLevelDialogModel extends HoistModel {
51
51
  showRefreshButton: true,
52
52
  store: {
53
53
  url: 'rest/logLevelAdmin',
54
- fieldDefaults: {disableXssProtection: true},
54
+ fieldDefaults: {enableXssProtection: false},
55
55
  fields: [
56
56
  {
57
57
  name: 'name',
@@ -57,7 +57,7 @@ export class ConfigPanelModel extends HoistModel {
57
57
  store: new RestStore({
58
58
  url: 'rest/configAdmin',
59
59
  reloadLookupsOnLoad: true,
60
- fieldDefaults: {disableXssProtection: true},
60
+ fieldDefaults: {enableXssProtection: false},
61
61
  fields: [
62
62
  {...(Col.name.field as FieldSpec), required},
63
63
  {
@@ -51,7 +51,7 @@ const modelSpec: RestGridConfig = {
51
51
  showRefreshButton: true,
52
52
  store: {
53
53
  url: 'rest/monitorAdmin',
54
- fieldDefaults: {disableXssProtection: true},
54
+ fieldDefaults: {enableXssProtection: false},
55
55
  fields: [
56
56
  {...(MCol.code.field as FieldSpec), required},
57
57
  MCol.metricUnit.field,
@@ -51,7 +51,7 @@ export class JsonBlobModel extends HoistModel {
51
51
  store: {
52
52
  url: 'rest/jsonBlobAdmin',
53
53
  reloadLookupsOnLoad: true,
54
- fieldDefaults: {disableXssProtection: true},
54
+ fieldDefaults: {enableXssProtection: false},
55
55
  fields: [
56
56
  {...(JBCol.token.field as FieldSpec), editable: false},
57
57
  JBCol.owner.field,
@@ -34,7 +34,7 @@ export class UserPreferenceModel extends HoistModel {
34
34
  store: {
35
35
  url: 'rest/userPreferenceAdmin',
36
36
  reloadLookupsOnLoad: true,
37
- fieldDefaults: {disableXssProtection: true},
37
+ fieldDefaults: {enableXssProtection: false},
38
38
  fields: [
39
39
  {
40
40
  ...(Col.name.field as FieldSpec),
@@ -47,7 +47,7 @@ export class PrefEditorModel extends HoistModel {
47
47
  store: {
48
48
  url: 'rest/preferenceAdmin',
49
49
  reloadLookupsOnLoad: true,
50
- fieldDefaults: {disableXssProtection: true},
50
+ fieldDefaults: {enableXssProtection: false},
51
51
  fields: [
52
52
  {...(Col.name.field as FieldSpec), required},
53
53
  {
@@ -58,12 +58,19 @@ export declare class AppSpec<T extends HoistAppModel = HoistAppModel> {
58
58
  */
59
59
  disableWebSockets?: boolean;
60
60
  /**
61
- * True to disable Field-level XSS protection by default across all Stores/Fields in the app.
62
- * For use with secure, internal apps that do not display arbitrary/external user input and
63
- * have tight performance tolerances and/or load very large record sets.
64
- * @see FieldSpec.disableXssProtection
61
+ * True to enable Field-level XSS protection by default across all Stores/Fields in the app.
62
+ * Available as an extra precaution for use with apps that might display arbitrary input from
63
+ * untrusted or external users. This feature does exact a minor performance penalty during data
64
+ * parsing, which can be significant in aggregate for very large stores containing records with
65
+ * many `string` fields.
66
+ *
67
+ * Note: this flag and its default behavior was changed as of Hoist v77 to be `false`, i.e.
68
+ * Store-level XSS protection *disabled* by default, in keeping with Hoist's primary use-case:
69
+ * building secured internal apps with large datasets and tight performance tolerances.
70
+ *
71
+ * @see FieldSpec.enableXssProtection
65
72
  */
66
- disableXssProtection?: boolean;
73
+ enableXssProtection?: boolean;
67
74
  /**
68
75
  * True to show a login form on initialization when not authenticated. Default is `false` as
69
76
  * most Hoist applications are expected to use OAuth or SSO for authn.
@@ -111,7 +118,7 @@ export declare class AppSpec<T extends HoistAppModel = HoistAppModel> {
111
118
  trackAppLoad?: boolean;
112
119
  /** @deprecated - use {@link AppSpec.disableWebSockets} instead. */
113
120
  webSocketsEnabled?: boolean;
114
- constructor({ authModelClass, checkAccess, clientAppCode, clientAppName, componentClass, containerClass, disableWebSockets, disableXssProtection, enableLoginForm, enableLogout, idlePanel, isMobileApp, lockoutMessage, lockoutPanel, loginMessage, modelClass, showBrowserContextMenu, trackAppLoad, webSocketsEnabled }: {
121
+ constructor({ authModelClass, checkAccess, clientAppCode, clientAppName, componentClass, containerClass, disableWebSockets, enableXssProtection, enableLoginForm, enableLogout, idlePanel, isMobileApp, lockoutMessage, lockoutPanel, loginMessage, modelClass, showBrowserContextMenu, trackAppLoad, webSocketsEnabled }: {
115
122
  authModelClass?: typeof HoistAuthModel;
116
123
  checkAccess: any;
117
124
  clientAppCode?: string;
@@ -119,7 +126,7 @@ export declare class AppSpec<T extends HoistAppModel = HoistAppModel> {
119
126
  componentClass: any;
120
127
  containerClass: any;
121
128
  disableWebSockets?: boolean;
122
- disableXssProtection?: boolean;
129
+ enableXssProtection?: boolean;
123
130
  enableLoginForm?: boolean;
124
131
  enableLogout?: boolean;
125
132
  idlePanel?: any;
@@ -17,15 +17,24 @@ export interface FieldSpec {
17
17
  /** Rules to apply to this field. */
18
18
  rules?: RuleLike[];
19
19
  /**
20
- * True to disable built-in XSS (cross-site scripting) protection, applied by default to all
21
- * incoming String values using {@link https://github.com/cure53/DOMPurify | DOMPurify}.
20
+ * True to enable built-in XSS (cross-site scripting) protection to all incoming String values
21
+ * using {@link https://github.com/cure53/DOMPurify | DOMPurify}.
22
22
  *
23
23
  * DOMPurify provides fast escaping of dangerous HTML, scripting, and other content that can be
24
24
  * used to execute XSS attacks, while allowing common and expected HTML and style tags.
25
25
  *
26
- * Please contact XH if you find yourself needing to disable this protection!
26
+ * This feature does exact a minor performance penalty during data parsing, which can be
27
+ * significant in aggregate for very large stores containing records with many `string` fields.
28
+ *
29
+ * For extra safety, apps which are open to potentially-untrusted users or display other
30
+ * potentially dangerous string content can opt into this setting app-wide via
31
+ * {@link AppSpec.enableXssProtection}. Field-level setting will override any app-level default.
32
+ *
33
+ * Note: this flag and its default behavior was changed as of Hoist v77 to be `false`, i.e.
34
+ * Store-level XSS protection *disabled* by default, in keeping with Hoist's primary use-case:
35
+ * building secured internal apps with large datasets and tight performance tolerances.
27
36
  */
28
- disableXssProtection?: boolean;
37
+ enableXssProtection?: boolean;
29
38
  }
30
39
  /** Metadata for an individual data field within a {@link StoreRecord}. */
31
40
  export declare class Field {
@@ -35,8 +44,8 @@ export declare class Field {
35
44
  readonly displayName: string;
36
45
  readonly defaultValue: any;
37
46
  readonly rules: Rule[];
38
- readonly disableXssProtection: boolean;
39
- constructor({ name, type, displayName, defaultValue, rules, disableXssProtection }: FieldSpec);
47
+ readonly enableXssProtection: boolean;
48
+ constructor({ name, type, displayName, defaultValue, rules, enableXssProtection }: FieldSpec);
40
49
  parseVal(val: any): any;
41
50
  isEqual(val1: any, val2: any): boolean;
42
51
  private processRuleSpecs;
@@ -46,11 +55,11 @@ export declare class Field {
46
55
  * @param val - raw value to parse.
47
56
  * @param type - data type of the field to use for possible conversion.
48
57
  * @param defaultValue - typed value to return if `val` undefined or null.
49
- * @param disableXssProtection - true to disable XSS (cross-site scripting) protection.
50
- * @see {@link FieldConfig} docs for additional details.
58
+ * @param enableXssProtection - true to enable XSS (cross-site scripting) protection.
59
+ * See {@link FieldSpec.enableXssProtection} for additional details.
51
60
  * @returns resulting value, potentially parsed or cast as per type.
52
61
  */
53
- export declare function parseFieldValue(val: any, type: FieldType, defaultValue?: any, disableXssProtection?: boolean): any;
62
+ export declare function parseFieldValue(val: any, type: FieldType, defaultValue?: any, enableXssProtection?: boolean): any;
54
63
  /** Data types for Fields used within Hoist Store Records and Cubes. */
55
64
  export declare const FieldType: Readonly<{
56
65
  TAGS: "tags";
@@ -12,7 +12,7 @@ export interface StoreConfig {
12
12
  * Default configs applied to `Field` instances constructed internally by this Store.
13
13
  * @see FieldSpec
14
14
  */
15
- fieldDefaults?: any;
15
+ fieldDefaults?: Omit<FieldSpec, 'name'>;
16
16
  /**
17
17
  * Specification for producing an immutable unique id for each record. May be provided as
18
18
  * either a string property name (default is 'id') or a function that receives the raw data
@@ -81,7 +81,7 @@ export interface QueryConfig {
81
81
  *
82
82
  * This can be used to break selected aggregations into sub-groups dynamically, without having
83
83
  * to define another dimension in the Cube and have it apply to all aggregations. See the
84
- * {@link BucketSpec} interface for additional information.
84
+ * {@link BucketSpecFn} type and {@link BucketSpec} interface for additional information.
85
85
  *
86
86
  * Defaults to {@link Cube.bucketSpecFn}.
87
87
  */
package/core/AppSpec.ts CHANGED
@@ -71,12 +71,19 @@ export class AppSpec<T extends HoistAppModel = HoistAppModel> {
71
71
  disableWebSockets?: boolean;
72
72
 
73
73
  /**
74
- * True to disable Field-level XSS protection by default across all Stores/Fields in the app.
75
- * For use with secure, internal apps that do not display arbitrary/external user input and
76
- * have tight performance tolerances and/or load very large record sets.
77
- * @see FieldSpec.disableXssProtection
74
+ * True to enable Field-level XSS protection by default across all Stores/Fields in the app.
75
+ * Available as an extra precaution for use with apps that might display arbitrary input from
76
+ * untrusted or external users. This feature does exact a minor performance penalty during data
77
+ * parsing, which can be significant in aggregate for very large stores containing records with
78
+ * many `string` fields.
79
+ *
80
+ * Note: this flag and its default behavior was changed as of Hoist v77 to be `false`, i.e.
81
+ * Store-level XSS protection *disabled* by default, in keeping with Hoist's primary use-case:
82
+ * building secured internal apps with large datasets and tight performance tolerances.
83
+ *
84
+ * @see FieldSpec.enableXssProtection
78
85
  */
79
- disableXssProtection?: boolean;
86
+ enableXssProtection?: boolean;
80
87
 
81
88
  /**
82
89
  * True to show a login form on initialization when not authenticated. Default is `false` as
@@ -144,7 +151,7 @@ export class AppSpec<T extends HoistAppModel = HoistAppModel> {
144
151
  componentClass,
145
152
  containerClass,
146
153
  disableWebSockets = false,
147
- disableXssProtection = false,
154
+ enableXssProtection = false,
148
155
  enableLoginForm = false,
149
156
  enableLogout = false,
150
157
  idlePanel = null,
@@ -191,7 +198,7 @@ export class AppSpec<T extends HoistAppModel = HoistAppModel> {
191
198
  this.componentClass = componentClass;
192
199
  this.containerClass = containerClass;
193
200
  this.disableWebSockets = disableWebSockets;
194
- this.disableXssProtection = disableXssProtection;
201
+ this.enableXssProtection = enableXssProtection;
195
202
  this.enableLoginForm = enableLoginForm;
196
203
  this.enableLogout = enableLogout;
197
204
  this.idlePanel = idlePanel;
package/data/Field.ts CHANGED
@@ -36,15 +36,24 @@ export interface FieldSpec {
36
36
  rules?: RuleLike[];
37
37
 
38
38
  /**
39
- * True to disable built-in XSS (cross-site scripting) protection, applied by default to all
40
- * incoming String values using {@link https://github.com/cure53/DOMPurify | DOMPurify}.
39
+ * True to enable built-in XSS (cross-site scripting) protection to all incoming String values
40
+ * using {@link https://github.com/cure53/DOMPurify | DOMPurify}.
41
41
  *
42
42
  * DOMPurify provides fast escaping of dangerous HTML, scripting, and other content that can be
43
43
  * used to execute XSS attacks, while allowing common and expected HTML and style tags.
44
44
  *
45
- * Please contact XH if you find yourself needing to disable this protection!
45
+ * This feature does exact a minor performance penalty during data parsing, which can be
46
+ * significant in aggregate for very large stores containing records with many `string` fields.
47
+ *
48
+ * For extra safety, apps which are open to potentially-untrusted users or display other
49
+ * potentially dangerous string content can opt into this setting app-wide via
50
+ * {@link AppSpec.enableXssProtection}. Field-level setting will override any app-level default.
51
+ *
52
+ * Note: this flag and its default behavior was changed as of Hoist v77 to be `false`, i.e.
53
+ * Store-level XSS protection *disabled* by default, in keeping with Hoist's primary use-case:
54
+ * building secured internal apps with large datasets and tight performance tolerances.
46
55
  */
47
- disableXssProtection?: boolean;
56
+ enableXssProtection?: boolean;
48
57
  }
49
58
 
50
59
  /** Metadata for an individual data field within a {@link StoreRecord}. */
@@ -58,7 +67,7 @@ export class Field {
58
67
  readonly displayName: string;
59
68
  readonly defaultValue: any;
60
69
  readonly rules: Rule[];
61
- readonly disableXssProtection: boolean;
70
+ readonly enableXssProtection: boolean;
62
71
 
63
72
  constructor({
64
73
  name,
@@ -66,19 +75,19 @@ export class Field {
66
75
  displayName,
67
76
  defaultValue = null,
68
77
  rules = [],
69
- disableXssProtection = XH.appSpec.disableXssProtection
78
+ enableXssProtection = XH.appSpec.enableXssProtection
70
79
  }: FieldSpec) {
71
80
  this.name = name;
72
81
  this.type = type;
73
82
  this.displayName = withDefault(displayName, genDisplayName(name));
74
83
  this.defaultValue = defaultValue;
75
84
  this.rules = this.processRuleSpecs(rules);
76
- this.disableXssProtection = disableXssProtection;
85
+ this.enableXssProtection = enableXssProtection;
77
86
  }
78
87
 
79
88
  parseVal(val: any): any {
80
- const {type, defaultValue, disableXssProtection} = this;
81
- return parseFieldValue(val, type, defaultValue, disableXssProtection);
89
+ const {type, defaultValue, enableXssProtection} = this;
90
+ return parseFieldValue(val, type, defaultValue, enableXssProtection);
82
91
  }
83
92
 
84
93
  isEqual(val1: any, val2: any): boolean {
@@ -102,35 +111,30 @@ export class Field {
102
111
  * @param val - raw value to parse.
103
112
  * @param type - data type of the field to use for possible conversion.
104
113
  * @param defaultValue - typed value to return if `val` undefined or null.
105
- * @param disableXssProtection - true to disable XSS (cross-site scripting) protection.
106
- * @see {@link FieldConfig} docs for additional details.
114
+ * @param enableXssProtection - true to enable XSS (cross-site scripting) protection.
115
+ * See {@link FieldSpec.enableXssProtection} for additional details.
107
116
  * @returns resulting value, potentially parsed or cast as per type.
108
117
  */
109
118
  export function parseFieldValue(
110
119
  val: any,
111
120
  type: FieldType,
112
121
  defaultValue: any = null,
113
- disableXssProtection = XH.appSpec.disableXssProtection
122
+ enableXssProtection: boolean = XH.appSpec.enableXssProtection
114
123
  ): any {
115
124
  if (val === undefined || val === null) val = defaultValue;
116
125
  if (val === null) return val;
117
126
 
118
- const sanitizeValue = v => {
119
- if (disableXssProtection || !isString(v)) return v;
120
- return DOMPurify.sanitize(v);
121
- };
122
-
123
127
  switch (type) {
124
128
  case 'tags':
125
129
  val = castArray(val);
126
130
  val = val.map(v => {
127
- v = sanitizeValue(v);
131
+ v = !enableXssProtection || !isString(v) ? v : DOMPurify.sanitize(v);
128
132
  return v.toString();
129
133
  });
130
134
  return val;
131
135
  case 'auto':
132
136
  case 'json':
133
- return sanitizeValue(val);
137
+ return !enableXssProtection || !isString(val) ? val : DOMPurify.sanitize(val);
134
138
  case 'int':
135
139
  val = toNumber(val);
136
140
  return isFinite(val) ? Math.trunc(val) : null;
@@ -140,7 +144,7 @@ export function parseFieldValue(
140
144
  return !!val;
141
145
  case 'pwd':
142
146
  case 'string':
143
- val = sanitizeValue(val);
147
+ val = !enableXssProtection || !isString(val) ? val : DOMPurify.sanitize(val);
144
148
  return val.toString();
145
149
  case 'date':
146
150
  return isDate(val) ? val : new Date(val);
package/data/Store.ts CHANGED
@@ -44,7 +44,7 @@ export interface StoreConfig {
44
44
  * Default configs applied to `Field` instances constructed internally by this Store.
45
45
  * @see FieldSpec
46
46
  */
47
- fieldDefaults?: any;
47
+ fieldDefaults?: Omit<FieldSpec, 'name'>;
48
48
 
49
49
  /**
50
50
  * Specification for producing an immutable unique id for each record. May be provided as
@@ -978,17 +978,20 @@ export class Store extends HoistBase {
978
978
  this.summaryRecords = null;
979
979
  }
980
980
 
981
- private parseFields(fields: any[], defaults: any): Field[] {
981
+ private parseFields(
982
+ fields: Array<string | FieldSpec | Field>,
983
+ defaults: Omit<FieldSpec, 'name'>
984
+ ): Field[] {
982
985
  const ret = fields.map(f => {
983
986
  if (f instanceof Field) return f;
984
987
 
985
- if (isString(f)) f = {name: f};
988
+ let fieldSpec: FieldSpec = isString(f) ? {name: f} : f;
986
989
 
987
990
  if (!isEmpty(defaults)) {
988
- f = defaultsDeep({}, f, defaults);
991
+ fieldSpec = defaultsDeep({}, fieldSpec, defaults);
989
992
  }
990
993
 
991
- return new this.defaultFieldClass(f);
994
+ return new this.defaultFieldClass(fieldSpec);
992
995
  });
993
996
 
994
997
  throwIf(
@@ -109,7 +109,7 @@ export interface QueryConfig {
109
109
  *
110
110
  * This can be used to break selected aggregations into sub-groups dynamically, without having
111
111
  * to define another dimension in the Cube and have it apply to all aggregations. See the
112
- * {@link BucketSpec} interface for additional information.
112
+ * {@link BucketSpecFn} type and {@link BucketSpec} interface for additional information.
113
113
  *
114
114
  * Defaults to {@link Cube.bucketSpecFn}.
115
115
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "77.0.0-SNAPSHOT.1761155172844",
3
+ "version": "77.0.0-SNAPSHOT.1761229417098",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": "github:xh/hoist-react",
6
6
  "homepage": "https://xh.io",