Skip to content

Commit 523316e

Browse files
HF-329: re-port the license key reader to the entitlement key format
Upstream handsontable/license-key 4.0.0 (DEV-2512) deleted src/typed-key/ and replaced the tagged key format with the entitlement key format: <prose>, blank line, [<base64url-payload><sha512-checksum>]. The tagged format was never issued to anyone (its 3.5.0 carrier was never released), so the old reader is removed rather than kept alongside. Re-vendored from src/entitlement-key/ at tag 4.0.0: detect-format and extract-key-data are new ports; sha512 and utils are byte-identical upstream and carry over. The reader is schema-free by upstream design, so default-schema is no longer vendored and TIER_TO_CAPABILITY_TOKEN (the tagged format's tier adapter) is gone with the format that fed it. Resolution reads HyperFormula's own product entry only: capabilities verbatim, exactly one of usage_until/release_until (the reader enforces the shape), notice/grace, flags (trial + the three silent spellings). Legacy 25-character keys and the literals are untouched; the invariant stands - only a VALID entitlement key may restrict the entitlement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019pxNP45obT2LZfjitaCv9o
1 parent 4ab3cc6 commit 523316e

14 files changed

Lines changed: 476 additions & 601 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1616
### Changed
1717

1818
- Changed `getAvailableFunctions()` and `getFunctionDetails()` to describe only the functions the instance's license key includes, so they no longer advertise a function that would evaluate to a `#LIC!` error. A missing, invalid, or expired key does not shorten the list. [#1731](https://github.com/handsontable/hyperformula/pull/1731)
19+
- Changed the parser for the new proprietary license keys to the entitlement key format (a human-readable text ending with a machine-readable block in square brackets), following its upstream specification. This replaces the tagged key format, which was never issued to anyone. Classic 25-character license keys and `gpl-v3` are unaffected. [#1740](https://github.com/handsontable/hyperformula/pull/1740)
1920

2021
## [3.4.0] - 2026-08-10
2122

docs/guide/license-key.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,31 @@ const options = {
2929
}
3030
```
3131

32+
### Proprietary license key formats
33+
34+
Your proprietary license key is in one of two formats, and both work the same way:
35+
36+
* A classic key: 25 characters in five dash-separated groups, for example
37+
`1a2b3-4c5d6-7e8f9-0a1b2-3c4d5`.
38+
* An entitlement key: a short, human-readable license text that ends with a machine-readable
39+
block in square brackets. Assign the whole text to the `licenseKey` option, or just the
40+
bracketed block — the block is the only part HyperFormula reads, so both work. The text around
41+
the block may be re-wrapped on its way to you (for example, by an email client) without
42+
affecting the key; the block itself has to arrive character for character.
43+
3244
### Proprietary license key validation
3345

3446
::: tip
3547
HyperFormula doesn't use an internet connection to validate your proprietary license key.
3648
:::
3749

3850
To determine whether a user is still entitled to use a particular
39-
version of the software, HyperFormula compares the time between
40-
two dates:
41-
* The HyperFormula build date
42-
* The date in your proprietary license key
51+
version of the software, HyperFormula compares the date in your
52+
proprietary license key against one of two references, depending on
53+
the license you purchased:
54+
* The HyperFormula build date, when the key ends maintenance on a set
55+
date (versions released before that date keep working indefinitely)
56+
* The current date (in UTC), when the key ends usage on a set date
4357

4458
This process doesn't require any connection to the server.
4559

@@ -96,6 +110,10 @@ Arithmetic keeps working: operators such as `=A1+B1` are not function calls and
96110
are `VERSION()` and `OFFSET()`, which sit outside the licence system entirely. So a sheet with a
97111
key problem does not go blank — it keeps producing values wherever no function is called.
98112

113+
A **valid** key can print one notification too: if it expires on a set date and that date is
114+
within the notice period your license carries, the console names the last day the key covers. It
115+
is a heads-up only — nothing is restricted while a key is valid, and the message appears once.
116+
99117
## License key support
100118

101119
If you have any issues with your license key, [contact our team](contact.md).

src/Config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ export class Config implements ConfigParams, ParserConfig {
341341

342342
/**
343343
* Whether gate B (the entitlement check in the interpreter) needs to run at all for this
344-
* config. `false` — the common case, for `gpl-v3`, legacy keys, and an unrestricted typed
344+
* config. `false` — the common case, for `gpl-v3`, legacy keys, and an unrestricted entitlement
345345
* key — is a single boolean read, cheaper than the string-enum comparison it replaces.
346346
*
347347
* @internal

src/helpers/licenseKeyValidator.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Copyright (c) 2025 Handsoncode. All rights reserved.
44
*/
55

6+
import {ENTITLEMENT_KEY_CHECKSUM_LENGTH} from '../license/vendor/constants'
67
import {checkKeySchema, extractTime} from './licenseKeyHelper'
78

89
/**
@@ -75,7 +76,7 @@ export function resetLicenseKeyNotificationForTests(): void {
7576
/**
7677
* Prints the console message for a non-valid license key state, at most once per page load.
7778
*
78-
* Extracted so the typed-key path in `src/license/licenseResolution.ts` reports the same states
79+
* Extracted so the entitlement-key path in `src/license/licenseResolution.ts` reports the same states
7980
* with the same wording and the same once-only behaviour, without duplicating the message table
8081
* or getting a second `_notified` flag of its own — two flags would let a page print two
8182
* warnings for one key.
@@ -96,8 +97,8 @@ export function notifyLicenseKeyState(state: LicenseKeyValidityState, keyValidit
9697
}
9798

9899
/**
99-
* Prints a one-time notice that a VALID typed key's usage-until expiry is approaching, at most
100-
* once per distinct license key.
100+
* Prints a one-time notice that a VALID entitlement key's usage-until expiry is approaching, at
101+
* most once per distinct license key.
101102
*
102103
* Called from `src/license/licenseResolution.ts`'s `resolveLicense`, alongside
103104
* {@link notifyLicenseKeyState} — see that function's doc for why the two share this module
@@ -125,17 +126,21 @@ export function notifyLicenseKeyNotice(licenseKey: string, expiryDate: Date): vo
125126
}
126127

127128
/**
128-
* The warn-once identity of a key: its trailing 128 characters — for an intact typed key, the
129-
* sha512 checksum, unique per distinct key content — after trimming.
129+
* The warn-once identity of a key: its trailing 129 characters, after trimming — for an intact
130+
* entitlement key, the sha512 checksum plus the closing bracket that ends the machine-readable
131+
* block, unique per distinct key content.
130132
*
131-
* Trimmed because `extractTypedKeyData` trims before validating, so `'KEY'` and `'KEY\n'` are one
132-
* license to the validator and must be one identity here too. Truncated because the set retains
133-
* its entries for the life of the process: a multi-tenant server building one engine per
134-
* customer-supplied key would otherwise accumulate every full key string it has ever warned
135-
* about; 128 characters per entry bounds that to the checksum alone.
133+
* Trimmed because the reader ignores trailing whitespace (it looks for the block, not for the end
134+
* of the string), so `'KEY'` and `'KEY\n'` are one license and must be one identity here too.
135+
* Reading from the END rather than the start also makes the whole artifact and its bare `[...]`
136+
* block — which the format says are equally valid spellings of the same license — one identity.
137+
*
138+
* Truncated because the set retains its entries for the life of the process: a multi-tenant server
139+
* building one engine per customer-supplied key would otherwise accumulate every full key string
140+
* it has ever warned about; 129 characters per entry bounds that to the checksum alone.
136141
*/
137142
function noticeIdentityOf(licenseKey: string): string {
138-
return licenseKey.trim().slice(-128)
143+
return licenseKey.trim().slice(-(ENTITLEMENT_KEY_CHECKSUM_LENGTH + 1))
139144
}
140145

141146
/**
@@ -178,7 +183,7 @@ export function checkLicenseKeyValidity(licenseKey: string): LicenseKeyValidityS
178183
* Formats a Date instance to hard-coded format MMMM DD, YYYY.
179184
*
180185
* Read in UTC, not local time. Every date reaching this function is built at UTC midnight — the
181-
* legacy path from a whole number of days since the epoch, the typed-key path from a calendar
186+
* legacy path from a whole number of days since the epoch, the entitlement-key path from a calendar
182187
* date in the payload — so local getters shifted the day backwards for anyone west of UTC and
183188
* printed an expiry one day earlier than the one the key actually carries.
184189
*

src/license/LicenseEntitlement.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export interface LicenseEntitlement {
8484
/**
8585
* The unrestricted entitlement: legacy keys and `gpl-v3` resolve to this today.
8686
*
87-
* HF-307 decision D3 (fail-closed, silent) means a typed key whose tokens this library version
87+
* HF-307 decision D3 (fail-closed, silent) means an entitlement key whose tokens this library version
8888
* does not recognize at all no longer maps here — it resolves to an entitlement with an empty,
8989
* silent capability set instead of falling back to unrestricted access. Do not reuse this
9090
* function for that case.

src/license/capabilities.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ export const NAMED_EXPRESSIONS_FEATURE_TOKEN = 'feat:named_expressions'
2727
export const BATCHING_FEATURE_TOKEN = 'feat:batching'
2828

2929
/**
30-
* Every feature token, in one list, for the shipped-shape adapter: the shipped key vocabulary
31-
* predates feature tokens entirely, so a commercial tier is translated into its functions token
32-
* PLUS all of these — see `licenseTermsOf` for the reasoning.
30+
* Every feature token, in one list, for the opt-in rule in `licenseTermsOf`: a key naming no
31+
* `feat:*` token at all is granted all of these, because no key vocabulary in circulation can
32+
* express "no features" — see that function for the reasoning.
3333
*/
3434
export const ALL_FEATURE_TOKENS = [
3535
CRUD_FEATURE_TOKEN, UNDO_REDO_FEATURE_TOKEN, CLIPBOARD_FEATURE_TOKEN,
@@ -79,7 +79,7 @@ const OPERATOR_FUNCTIONS = [
7979
]
8080

8181
// An earlier revision granted all five features from CORE_TOKEN, which made feature gating inert
82-
// by construction: no typed key could ever lose an API area. Kuba's call (task comment, 12.08):
82+
// by construction: no restricted key could ever lose an API area. Kuba's call (task comment, 12.08):
8383
// "Feature gating should work, but the legacy keys should grant all feat:* capabilities" — legacy
8484
// keys already resolve to the unrestricted entitlement, so the carve-out costs nothing, and the
8585
// five features moved onto their own `feat:*` tokens below.
@@ -201,9 +201,9 @@ const functions4Grant: CapabilityGrant = {
201201
* in `unit/license/capability-registry.spec.ts` fails on.
202202
*
203203
* The five `feat:*` tokens carry the gated API areas, one feature each, spelled after the draft
204-
* vocabulary in the task. A rev-5 key states them explicitly; the shipped-shape adapter grants
205-
* all five alongside the tier (that vocabulary predates feature tokens); legacy keys resolve to
206-
* the unrestricted entitlement and never consult this table.
204+
* vocabulary in the task. A key may state them explicitly; a key naming none is granted all five
205+
* (the opt-in rule in `licenseTermsOf`); legacy keys resolve to the unrestricted entitlement and
206+
* never consult this table.
207207
*
208208
* The two add-on tokens, wired per the 2026-08-12 packages meeting: `spreadsheet` backs the
209209
* 'Spreadsheet Bundle' add-on and grants {@link FeatureId.Crud}, {@link FeatureId.UndoRedo},

0 commit comments

Comments
 (0)