handsontable 18.1.1 → 18.1.2-rc2

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/base.js +2 -2
  3. package/base.mjs +2 -2
  4. package/dataMap/metaManager/metaSchema.js +4 -2
  5. package/dataMap/metaManager/metaSchema.mjs +4 -2
  6. package/dist/handsontable.full.js +223 -44
  7. package/dist/handsontable.full.min.js +129 -129
  8. package/dist/handsontable.js +223 -44
  9. package/dist/handsontable.min.js +129 -129
  10. package/dist/themes/classic.js +2 -2
  11. package/dist/themes/classic.min.js +2 -2
  12. package/dist/themes/horizon.js +2 -2
  13. package/dist/themes/horizon.min.js +2 -2
  14. package/dist/themes/main.js +2 -2
  15. package/dist/themes/main.min.js +2 -2
  16. package/dist/themes/static/variables/colors/ant.js +2 -2
  17. package/dist/themes/static/variables/colors/ant.min.js +2 -2
  18. package/dist/themes/static/variables/colors/classic.js +2 -2
  19. package/dist/themes/static/variables/colors/classic.min.js +2 -2
  20. package/dist/themes/static/variables/colors/horizon.js +2 -2
  21. package/dist/themes/static/variables/colors/horizon.min.js +2 -2
  22. package/dist/themes/static/variables/colors/main.js +2 -2
  23. package/dist/themes/static/variables/colors/main.min.js +2 -2
  24. package/dist/themes/static/variables/colors/material.js +2 -2
  25. package/dist/themes/static/variables/colors/material.min.js +2 -2
  26. package/dist/themes/static/variables/colors/shadcn.js +2 -2
  27. package/dist/themes/static/variables/colors/shadcn.min.js +2 -2
  28. package/dist/themes/static/variables/density.js +2 -2
  29. package/dist/themes/static/variables/density.min.js +2 -2
  30. package/dist/themes/static/variables/helpers/iconsMap.js +2 -2
  31. package/dist/themes/static/variables/helpers/iconsMap.min.js +2 -2
  32. package/dist/themes/static/variables/icons/horizon.js +2 -2
  33. package/dist/themes/static/variables/icons/horizon.min.js +2 -2
  34. package/dist/themes/static/variables/icons/main.js +2 -2
  35. package/dist/themes/static/variables/icons/main.min.js +2 -2
  36. package/dist/themes/static/variables/sizing.js +2 -2
  37. package/dist/themes/static/variables/sizing.min.js +2 -2
  38. package/dist/themes/static/variables/tokens/classic.js +2 -2
  39. package/dist/themes/static/variables/tokens/classic.min.js +2 -2
  40. package/dist/themes/static/variables/tokens/horizon.js +2 -2
  41. package/dist/themes/static/variables/tokens/horizon.min.js +2 -2
  42. package/dist/themes/static/variables/tokens/main.js +2 -2
  43. package/dist/themes/static/variables/tokens/main.min.js +2 -2
  44. package/helpers/mixed.js +5 -5
  45. package/helpers/mixed.mjs +5 -5
  46. package/package.json +1 -1
  47. package/styles/handsontable.css +2 -2
  48. package/styles/handsontable.min.css +2 -2
  49. package/styles/handsontableStyles.js +1 -1
  50. package/styles/handsontableStyles.mjs +1 -1
  51. package/styles/ht-icons-horizon.min.css +2 -2
  52. package/styles/ht-icons-main.min.css +2 -2
  53. package/styles/ht-theme-classic-no-icons.min.css +2 -2
  54. package/styles/ht-theme-classic.min.css +2 -2
  55. package/styles/ht-theme-horizon-no-icons.min.css +2 -2
  56. package/styles/ht-theme-horizon.min.css +2 -2
  57. package/styles/ht-theme-main-no-icons.min.css +2 -2
  58. package/styles/ht-theme-main.min.css +2 -2
  59. package/utils/entitlementLicenseKey/constants.d.ts +7 -1
  60. package/utils/entitlementLicenseKey/constants.js +5 -1
  61. package/utils/entitlementLicenseKey/constants.mjs +6 -1
  62. package/utils/entitlementLicenseKey/encoding.d.ts +1 -1
  63. package/utils/entitlementLicenseKey/encoding.js +4 -1
  64. package/utils/entitlementLicenseKey/encoding.mjs +4 -1
  65. package/utils/entitlementLicenseKey/extractKeyData.d.ts +47 -8
  66. package/utils/entitlementLicenseKey/extractKeyData.js +155 -14
  67. package/utils/entitlementLicenseKey/extractKeyData.mjs +188 -23
  68. package/utils/entitlementLicenseKey/types.d.ts +5 -0
  69. package/utils/licenseBranding/index.js +2 -2
  70. package/utils/licenseBranding/index.mjs +2 -2
  71. package/utils/licenseNotification.js +2 -2
  72. package/utils/licenseNotification.mjs +2 -2
@@ -1,14 +1,90 @@
1
- import { CHECKSUM_LENGTH, DATE_FIELDS } from './constants.mjs';
1
+ import { CHECKSUM_LENGTH, DATE_FIELDS, PROSE_DIGEST_LENGTH } from './constants.mjs';
2
2
  import { sha512 } from './sha512.mjs';
3
3
  import { base64ToString, stringToUtf8Bytes, parseIsoDateToTimestamp } from './encoding.mjs';
4
4
  /**
5
- * The alphabet of the encoded payload - URL-safe base64 without padding. The
6
- * checksum (lowercase hex) is a subset of it, which is what lets the two be
7
- * split by a fixed length from the right.
5
+ * The alphabets of the two parts of the machine-readable block. The second
6
+ * part has a fixed length, so the two are split from the right.
8
7
  *
9
8
  * @type {RegExp}
10
9
  */ const ENCODED_PAYLOAD = /^[A-Za-z0-9\-_]+$/;
11
10
  const CHECKSUM = /^[0-9a-f]+$/;
11
+ /**
12
+ * The whitespace the reader ignores, in the text and inside the block: TAB,
13
+ * LF, VT, FF, CR, SPACE, NO-BREAK SPACE,
14
+ * OGHAM SPACE MARK, the U+2000-U+200A spaces, LINE SEPARATOR, PARAGRAPH
15
+ * SEPARATOR, NARROW NO-BREAK SPACE, MEDIUM MATHEMATICAL SPACE, IDEOGRAPHIC
16
+ * SPACE, and the BOM.
17
+ *
18
+ * Listed explicitly instead of `\s`, whose set has changed between JavaScript
19
+ * engines (U+180E) and differs in other languages (U+0085), so it matches the
20
+ * key generator exactly.
21
+ *
22
+ * @type {RegExp}
23
+ */ const PROSE_WHITESPACE = /[\t\n\v\f\r \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/g;
24
+ /**
25
+ * A line break or tab that was saved as text - a backslash followed by "n",
26
+ * "r", or "t" - also ignored.
27
+ *
28
+ * Several places a key is stored keep a line break that way rather than as a
29
+ * real one: a single-quoted or unquoted `.env` value, Docker's `--env-file`,
30
+ * and many CI secret fields. The block survives that intact, so without this
31
+ * a genuine key would fail only because of how it was stored. The generator
32
+ * strips backslashes from every free-text field, so a key it issues never
33
+ * carries one of its own.
34
+ *
35
+ * @type {RegExp}
36
+ */ const ESCAPED_WHITESPACE = /\\[nrt]/g;
37
+ /**
38
+ * Removes every escaped line break or tab (`\n`, `\r`, `\t` saved as text) and
39
+ * then every whitespace character, in that order.
40
+ *
41
+ * The block is one long word, so a mail client or an editor that wraps the key
42
+ * can break it across lines, and a `.env` file can save that line break as the
43
+ * text `\n`. The block's alphabet has neither, so removing them cannot change
44
+ * a genuine block.
45
+ *
46
+ * @param {string} text The text to remove the whitespace from.
47
+ * @returns {string}
48
+ */ function removeWhitespace(text) {
49
+ return text.replace(ESCAPED_WHITESPACE, '').replace(PROSE_WHITESPACE, '');
50
+ }
51
+ /**
52
+ * Brings the human-readable text of a key to the form the key protects.
53
+ *
54
+ * Only the whitespace, its escaped forms, and the Unicode composition are
55
+ * ignored. A mail client that rewraps the text (also between two CJK
56
+ * characters or inside a word) or collapses a blank line, a `.env` file that
57
+ * saves a line break as `\n`, or a system that stores "u" + U+0308 instead of
58
+ * U+00FC leaves the key valid. A changed, added, or removed letter, digit, or
59
+ * symbol does not.
60
+ * Exported for the test key builder; the library calls `extractEntitlementKeyData`.
61
+ *
62
+ * @param {string} prose The text in front of the machine-readable block.
63
+ * @returns {string}
64
+ */ export function canonicalizeProse(prose) {
65
+ // NFC runs last: a line break between a letter and its combining mark (an
66
+ // NFD copy rewrapped there) has to be gone before the two can compose.
67
+ return removeWhitespace(prose).normalize('NFC');
68
+ }
69
+ /**
70
+ * Computes the checksum that closes the block. It must match the canonical
71
+ * reader in every key format, so Handsontable 18.1 keeps reading newer keys.
72
+ * Exported for the test key builder.
73
+ *
74
+ * @param {string} encodedPayload The encoded payload.
75
+ * @returns {string}
76
+ */ export function computePayloadChecksum(encodedPayload) {
77
+ return sha512(stringToUtf8Bytes(encodedPayload));
78
+ }
79
+ /**
80
+ * Computes the value a current key stores for its text. Exported for the test
81
+ * key builder.
82
+ *
83
+ * @param {string} canonicalProse The text, already passed through `canonicalizeProse`.
84
+ * @returns {string}
85
+ */ export function computeProseDigest(canonicalProse) {
86
+ return sha512(stringToUtf8Bytes(canonicalProse)).slice(0, PROSE_DIGEST_LENGTH);
87
+ }
12
88
  /**
13
89
  * Reports own-property presence without trusting a payload's inherited or
14
90
  * overridden `hasOwnProperty`.
@@ -45,6 +121,39 @@ const CHECKSUM = /^[0-9a-f]+$/;
45
121
  */ function isStringArray(value) {
46
122
  return Array.isArray(value) && value.every((item)=>typeof item === 'string');
47
123
  }
124
+ /**
125
+ * Freezes the value and everything nested in it. The read result is shared
126
+ * between callers (see the memo below), so a caller that sorted or pushed into
127
+ * it would silently rewrite what the next caller reads.
128
+ *
129
+ * It walks with an explicit stack, not by recursion. An unknown extra field is
130
+ * kept as it is, and a key can nest one thousands of levels deep - recursion
131
+ * would then overflow the call stack and throw out of the reader, where a key
132
+ * is only ever allowed to read as data or as `null`.
133
+ *
134
+ * @param {*} value The value to freeze.
135
+ * @returns {*}
136
+ */ function deepFreeze(value) {
137
+ const pending = [
138
+ value
139
+ ];
140
+ while(pending.length > 0){
141
+ const current = pending.pop();
142
+ if (isFreezable(current)) {
143
+ Object.freeze(current);
144
+ Object.keys(current).forEach((key)=>pending.push(current[key]));
145
+ }
146
+ }
147
+ return value;
148
+ }
149
+ /**
150
+ * Narrows an unknown value to an object (or array) that is not frozen yet.
151
+ *
152
+ * @param {*} value The value to check.
153
+ * @returns {boolean}
154
+ */ function isFreezable(value) {
155
+ return value !== null && typeof value === 'object' && !Object.isFrozen(value);
156
+ }
48
157
  /**
49
158
  * Adds an own, ordinary property.
50
159
  *
@@ -97,7 +206,7 @@ const CHECKSUM = /^[0-9a-f]+$/;
97
206
  if (presentDateFields.length !== 1) {
98
207
  return null;
99
208
  }
100
- if (parseIsoDateToTimestamp(`${entry[presentDateFields[0]]}`) === null) {
209
+ if (parseIsoDateToTimestamp(entry[presentDateFields[0]]) === null) {
101
210
  return null;
102
211
  }
103
212
  if (!isNonNegativeInteger(entry.notice) || !isNonNegativeInteger(entry.grace)) {
@@ -122,6 +231,41 @@ const CHECKSUM = /^[0-9a-f]+$/;
122
231
  defineOwn(normalized, presentDateFields[0], entry[presentDateFields[0]]);
123
232
  return normalized;
124
233
  }
234
+ /**
235
+ * Reads the format version of a payload. Returns `null` for a value no
236
+ * generator writes. A version newer than this reader knows is accepted, so a
237
+ * build already in the field keeps reading newer keys.
238
+ *
239
+ * @param {object} payload The decoded payload.
240
+ * @returns {number|null}
241
+ */ function readFormatVersion(payload) {
242
+ // Own properties only, so a value another script put on `Object.prototype`
243
+ // cannot change how a key is read.
244
+ if (!hasOwn(payload, 'v')) {
245
+ return 1;
246
+ }
247
+ if (!isNonNegativeInteger(payload.v) || payload.v < 2) {
248
+ return null;
249
+ }
250
+ return payload.v;
251
+ }
252
+ /**
253
+ * Checks that the text of a current key is intact and that nothing but
254
+ * whitespace follows its block. A key always states its terms, so the bare
255
+ * block is rejected. A key in the earlier format is read the way Handsontable
256
+ * 18.1 reads it.
257
+ *
258
+ * @param {object} payload The decoded payload.
259
+ * @param {string} prose The text in front of the block.
260
+ * @param {string} textAfterBlock The text after the block.
261
+ * @returns {boolean}
262
+ */ function coversItsText(payload, prose, textAfterBlock) {
263
+ if (canonicalizeProse(textAfterBlock) !== '') {
264
+ return false;
265
+ }
266
+ const canonicalProse = canonicalizeProse(prose);
267
+ return canonicalProse !== '' && hasOwn(payload, 'prose') && typeof payload.prose === 'string' && computeProseDigest(canonicalProse) === payload.prose;
268
+ }
125
269
  /**
126
270
  * Reads and verifies one key. Split out from the memoized public entry point so
127
271
  * the memo can wrap every exit path uniformly.
@@ -139,7 +283,7 @@ const CHECKSUM = /^[0-9a-f]+$/;
139
283
  if (blockEnd === -1) {
140
284
  return null;
141
285
  }
142
- const content = licenseKey.slice(blockStart + 1, blockEnd);
286
+ const content = removeWhitespace(licenseKey.slice(blockStart + 1, blockEnd));
143
287
  if (content.length <= CHECKSUM_LENGTH) {
144
288
  return null;
145
289
  }
@@ -148,7 +292,7 @@ const CHECKSUM = /^[0-9a-f]+$/;
148
292
  if (!ENCODED_PAYLOAD.test(encodedPayload) || !CHECKSUM.test(checksum)) {
149
293
  return null;
150
294
  }
151
- if (sha512(stringToUtf8Bytes(encodedPayload)) !== checksum) {
295
+ if (computePayloadChecksum(encodedPayload) !== checksum) {
152
296
  return null;
153
297
  }
154
298
  const payloadJson = base64ToString(encodedPayload);
@@ -164,6 +308,13 @@ const CHECKSUM = /^[0-9a-f]+$/;
164
308
  if (!isPlainObject(payload) || !isPlainObject(payload.products)) {
165
309
  return null;
166
310
  }
311
+ const version = readFormatVersion(payload);
312
+ if (version === null) {
313
+ return null;
314
+ }
315
+ if (version >= 2 && !coversItsText(payload, licenseKey.slice(0, blockStart), licenseKey.slice(blockEnd + 1))) {
316
+ return null;
317
+ }
167
318
  const products = {};
168
319
  const entries = payload.products;
169
320
  let malformed = false;
@@ -178,40 +329,54 @@ const CHECKSUM = /^[0-9a-f]+$/;
178
329
  if (malformed) {
179
330
  return null;
180
331
  }
181
- return {
332
+ return deepFreeze({
333
+ version,
182
334
  products
183
- };
335
+ });
184
336
  }
185
337
  // The license key is read twice per grid init - the bottom bar
186
338
  // (`initLicenseNotification`) and the branding UI (`initLicenseBranding`) each resolve the license
187
- // state - and reading runs the full SHA-512 + base64 + JSON parse. A one-entry memo on the key makes
188
- // the second read free. The returned data is treated as read-only by every caller, so sharing one
339
+ // state - and reading runs the full verification and decoding. A one-entry memo on the key makes
340
+ // the second read free. The returned data is frozen, as in the canonical reader, so sharing one
189
341
  // object is safe.
190
342
  let memoizedKey = null;
191
343
  let memoizedData = null;
192
344
  /**
193
345
  * Extracts the machine-readable data from an entitlement license key.
194
346
  *
195
- * The checksum is verified first, so the returned data is guaranteed to belong
196
- * to an intact block. A malformed or tampered key reads as `null` - reporting
197
- * an invalid key is the caller's job, not this function's.
347
+ * The key is verified first, so the returned data is guaranteed to belong to
348
+ * an intact key. A malformed or tampered key reads as `null` - reporting an
349
+ * invalid key is the caller's job, not this function's.
350
+ *
351
+ * A current key protects its text as well: edited or removed text (the bare
352
+ * `[...]` block), or anything but whitespace after the block, reads as `null`.
353
+ * A key in the earlier format reads the way Handsontable 18.1 reads it. The
354
+ * verification rules are those of the canonical reader in the private
355
+ * `license-key` repository.
198
356
  *
199
- * Only the bracketed block matters. The prose in front of it is neither parsed
200
- * nor covered by the checksum, so the caller may pass the whole key or just the
201
- * `[...]` block, and rewrapped or re-pasted prose still validates. The block
202
- * itself has to be intact: its alphabet has no whitespace, so a newline inside
203
- * it makes the key unreadable, exactly as it does for the key generator.
357
+ * The caller passes the whole key. The text is never parsed, and its
358
+ * whitespace and Unicode composition are ignored, so rewrapped or re-pasted
359
+ * text still validates. Whitespace inside the block is ignored too, so a
360
+ * block wrapped by a mail client still validates.
204
361
  *
205
362
  * Unknown products, capability tokens and flags are all tolerated, so nothing
206
363
  * about reading a key depends on the commercial vocabulary.
207
364
  *
365
+ * The result is frozen. Copy an array before sorting or changing it.
366
+ *
208
367
  * @param {string} licenseKey The license key to extract the data from.
209
368
  * @returns {EntitlementKeyData|null}
210
369
  */ export function extractEntitlementKeyData(licenseKey) {
211
- const key = `${licenseKey}`;
212
- if (key !== memoizedKey) {
213
- memoizedKey = key;
214
- memoizedData = readEntitlementKeyData(key);
370
+ if (typeof licenseKey !== 'string') {
371
+ return null;
372
+ }
373
+ if (licenseKey !== memoizedKey) {
374
+ // Read first, remember second. Were the key remembered before the read,
375
+ // a read that throws would leave the new key paired with the previous
376
+ // key's data, and the next read of the new key would return it.
377
+ const data = readEntitlementKeyData(licenseKey);
378
+ memoizedKey = licenseKey;
379
+ memoizedData = data;
215
380
  }
216
381
  return memoizedData;
217
382
  }
@@ -29,6 +29,11 @@ export interface ProductEntitlement {
29
29
  * known ones.
30
30
  */
31
31
  export interface EntitlementKeyData {
32
+ /**
33
+ * The format version of the key; 1 for the keys issued before versions
34
+ * existed.
35
+ */
36
+ version: number;
32
37
  products: {
33
38
  [productName: string]: ProductEntitlement;
34
39
  };
@@ -62,9 +62,9 @@ const _lockScreen = require("./lockScreen");
62
62
  }
63
63
  function initLicenseBranding(hotInstance) {
64
64
  // DO NOT add a `typeof process` guard here - see the matching comment in `initLicenseNotification`.
65
- // The bundler inlines `"15/09/2026"` to a string literal (the bare read cannot
65
+ // The bundler inlines `"07/10/2026"` to a string literal (the bare read cannot
66
66
  // crash); a guard is left un-inlined and compiles to `false` in browser bundles, blanking the date
67
67
  // and silently killing expired-key detection.
68
- const releaseDate = "15/09/2026" || '';
68
+ const releaseDate = "07/10/2026" || '';
69
69
  mountBrandingSurface(hotInstance, (0, _mixed._getLicenseState)(hotInstance.getSettings().licenseKey, releaseDate));
70
70
  }
@@ -63,9 +63,9 @@ import { mountLicenseLock } from './lockScreen.mjs';
63
63
  * @returns {void}
64
64
  */ export function initLicenseBranding(hotInstance) {
65
65
  // DO NOT add a `typeof process` guard here - see the matching comment in `initLicenseNotification`.
66
- // The bundler inlines `"15/09/2026"` to a string literal (the bare read cannot
66
+ // The bundler inlines `"07/10/2026"` to a string literal (the bare read cannot
67
67
  // crash); a guard is left un-inlined and compiles to `false` in browser bundles, blanking the date
68
68
  // and silently killing expired-key detection.
69
- const releaseDate = "15/09/2026" || '';
69
+ const releaseDate = "07/10/2026" || '';
70
70
  mountBrandingSurface(hotInstance, _getLicenseState(hotInstance.getSettings().licenseKey, releaseDate));
71
71
  }
@@ -84,12 +84,12 @@ function initLicenseNotification(hotInstance) {
84
84
  }
85
85
  const licenseKey = hotInstance.getSettings().licenseKey;
86
86
  // DO NOT wrap this in a `typeof process` guard (or any `process` existence check). The bundler
87
- // replaces the whole `"15/09/2026"` expression with a string literal at build time,
87
+ // replaces the whole `"07/10/2026"` expression with a string literal at build time,
88
88
  // so the bare read never touches a runtime `process` object and cannot crash. A guard is NOT
89
89
  // inlined - it survives as `typeof process !== 'undefined' && "..."`, which is `false` in every
90
90
  // browser bundle, blanks the release date, and silently kills expired-key detection. That was the
91
91
  // 18.0.0 regression this read restores; re-adding the guard reintroduces it.
92
- const releaseDate = "15/09/2026" || '';
92
+ const releaseDate = "07/10/2026" || '';
93
93
  const notificationElement = (0, _mixed._injectProductInfo)({
94
94
  className: LICENSE_INFO_CLASS,
95
95
  key: licenseKey,
@@ -88,12 +88,12 @@ export const LICENSE_INFO_CLASS = 'hot-display-license-info';
88
88
  }
89
89
  const licenseKey = hotInstance.getSettings().licenseKey;
90
90
  // DO NOT wrap this in a `typeof process` guard (or any `process` existence check). The bundler
91
- // replaces the whole `"15/09/2026"` expression with a string literal at build time,
91
+ // replaces the whole `"07/10/2026"` expression with a string literal at build time,
92
92
  // so the bare read never touches a runtime `process` object and cannot crash. A guard is NOT
93
93
  // inlined - it survives as `typeof process !== 'undefined' && "..."`, which is `false` in every
94
94
  // browser bundle, blanks the release date, and silently kills expired-key detection. That was the
95
95
  // 18.0.0 regression this read restores; re-adding the guard reintroduces it.
96
- const releaseDate = "15/09/2026" || '';
96
+ const releaseDate = "07/10/2026" || '';
97
97
  const notificationElement = _injectProductInfo({
98
98
  className: LICENSE_INFO_CLASS,
99
99
  key: licenseKey,