Skip to content

Commit e88b96b

Browse files
HF-307/HF-329/HF-306: license key reader, resolution and capability table
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 4ee01ae commit e88b96b

17 files changed

Lines changed: 2004 additions & 102 deletions

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Added support for proprietary license keys that grant a subset of the library ("feature packages and add-ons"). A function your key does not include evaluates to a `#LIC!` error, and the corresponding parts of the API throw a `LicenseCapabilityMissingError`. Keys that grant everything, including `gpl-v3`, are unaffected. [#1728](https://github.com/handsontable/hyperformula/pull/1728) [#1729](https://github.com/handsontable/hyperformula/pull/1729) [#1730](https://github.com/handsontable/hyperformula/pull/1730)
13+
- Added a one-time console notice when a license key's usage-based expiry date falls within its configured notice period, naming the key's last covered day ("valid until … (UTC)"). The notice is silenced by the key's own silent flag, and never fires for a key expiring on the perpetual (`release_until`) axis. Blocking behavior at and after expiry is unchanged. [#1730](https://github.com/handsontable/hyperformula/pull/1730)
14+
- Added grants to the two commercial add-on tokens: `spreadsheet` (the Spreadsheet Bundle) now grants the CRUD, undo/redo, clipboard, and batching feature areas, and `import_export` grants the reserved import/export feature that nothing gates on until the feature ships. A key naming neither add-on keeps every feature area it has today. [#1730](https://github.com/handsontable/hyperformula/pull/1730)
15+
16+
### Changed
17+
18+
- 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. [#1730](https://github.com/handsontable/hyperformula/pull/1730)
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. [#1730](https://github.com/handsontable/hyperformula/pull/1730)
20+
- Changed the license capability tokens to be matched case-insensitively, and added support for the packaging group-token vocabulary (`fun:all`, `fun:<family>.<A|B|C>`, and per-function `fun:<FUNCTION_NAME>` tokens) alongside the existing package tokens (`functions_1``functions_4` and the add-ons). A key worded in either vocabulary grants the same functions. [#1730](https://github.com/handsontable/hyperformula/pull/1730)
21+
1022
### Fixed
1123

1224
- Fixed the `AVERAGEIF` function returning a division-by-zero error when the calculated average was `0`. [#1733](https://github.com/handsontable/hyperformula/pull/1733)

docs/guide/license-key.md

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,25 +40,94 @@ const options = {
4040
}
4141
```
4242

43+
### Proprietary license key formats
44+
45+
Your proprietary license key is in one of two formats, and both work the same way:
46+
47+
* A classic key: 25 characters in five dash-separated groups, for example
48+
`1a2b3-4c5d6-7e8f9-0a1b2-3c4d5`.
49+
* An entitlement key: a short, human-readable license text that ends with a machine-readable
50+
block in square brackets. Assign the whole text to the `licenseKey` option, or just the
51+
bracketed block — the block is the only part HyperFormula reads, so both work. The text around
52+
the block may be re-wrapped on its way to you (for example, by an email client) without
53+
affecting the key; the block itself has to arrive character for character.
54+
4355
### Proprietary license key validation
4456

4557
::: tip
4658
HyperFormula doesn't use an internet connection to validate your proprietary license key.
4759
:::
4860

4961
To determine whether a user is still entitled to use a particular
50-
version of the software, HyperFormula compares the time between
51-
two dates:
52-
* The HyperFormula build date
53-
* The date in your proprietary license key
62+
version of the software, HyperFormula compares the date in your
63+
proprietary license key against one of two references, depending on
64+
the license you purchased:
65+
* The HyperFormula build date, when the key ends maintenance on a set
66+
date (versions released before that date keep working indefinitely)
67+
* The current date (in UTC), when the key ends usage on a set date
5468

5569
This process doesn't require any connection to the server.
5670

71+
## Feature packages and add-ons
72+
73+
A proprietary license key may grant the whole library, or only part of it. If your key covers
74+
everything you buy nothing new to think about, and neither does the GPLv3 key `gpl-v3`, which
75+
always grants everything.
76+
77+
If your key grants only part of the library, then:
78+
79+
* A function your key doesn't include evaluates to a `#LIC!` error, in the same way as any other
80+
[error value](types-of-errors.md). Everything else in the sheet keeps calculating.
81+
* An API method your key doesn't include throws a `LicenseCapabilityMissingError` when you call
82+
it. Getters never throw; `copy()` and `cut()` do, because they belong to the clipboard feature.
83+
* [`getAvailableFunctions()`](../api/classes/hyperformula.md#getavailablefunctions) and
84+
[`getFunctionDetails()`](../api/classes/hyperformula.md#getfunctiondetails) describe only the
85+
functions your key includes, so a function picker built from them never offers a function that
86+
then fails.
87+
88+
Custom functions you register yourself are available whatever your key grants, as long as they use
89+
an id of their own. The licence covers built-in ids, so a plugin registered under a built-in id your
90+
key does not include is treated as that built-in and stays unavailable — it will not be described and
91+
it evaluates to `#LIC!`. Pick an id the built-in catalogue does not use and this cannot happen.
92+
93+
Two commercial add-ons build on top of a package:
94+
95+
* **Spreadsheet Bundle** grants the CRUD API (adding, removing, and moving rows, columns, sheets,
96+
and cell contents), undo/redo, clipboard operations, and batching (`batch()` /
97+
`suspendEvaluation()`; `resumeEvaluation()` is deliberately never gated, so an engine can always
98+
leave a suspended state). It does not grant named expressions, which stay outside both add-ons.
99+
* **Import/export** is reserved for a future release. HyperFormula doesn't have an import/export
100+
feature yet, so this add-on doesn't grant or restrict anything today.
101+
102+
In this release, not having either add-on doesn't restrict anything either: a key that names no
103+
feature token at all is granted every feature area — CRUD, undo/redo, clipboard, named expressions
104+
and batching — regardless of whether it names these add-ons. Every key issued today is of that
105+
shape, so the add-on tokens describe what was sold rather than changing what the engine allows.
106+
107+
::: tip
108+
To find out which package your key includes, check your order confirmation or
109+
[contact our team](contact.md). HyperFormula deliberately reports nothing about the contents of
110+
your key at runtime.
111+
:::
112+
57113
## License key notifications
58114

59115
If your license key is missing, invalid, or expired, you see a
60116
corresponding notification in the console.
61117

118+
In that case every licence-gated function call evaluates to a `#LIC!` error — but no API method
119+
starts throwing, and `getAvailableFunctions()` still describes the full set of functions. A key
120+
problem never narrows what the library reports it can do.
121+
122+
Arithmetic keeps working: operators such as `=A1+B1` are not function calls, so nothing gates them.
123+
`VERSION()` and `OFFSET()` are function calls, but they are protected built-ins that sit outside the
124+
licence system entirely, so they keep evaluating too. A sheet with a key problem therefore does not
125+
go blank.
126+
127+
A **valid** key can print one notification too: if it expires on a set date and that date is
128+
within the notice period your license carries, the console names the last day the key covers. It
129+
is a heads-up only — nothing is restricted while a key is valid, and the message appears once.
130+
62131
## License key support
63132

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

docs/guide/types-of-errors.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,4 @@ according to the language settings.
3737
| #VALUE! | Wrong type of argument | It occurs when a formula tries to improperly use different types of data. For example, you will see this error when you will try to add a string to a number. |
3838
| #CYCLE! | Circular reference | It occurs when a formula refers to its own cell, both directly and indirectly. |
3939
| #ERROR! | An error occurred | It indicates that there is an unknown error in a formula. |
40-
| #LIC! | Invalid license key | It occurs when the license key is invalid, expired, or missing. |
40+
| #LIC! | License key problem | It occurs when the license key is invalid, expired, or missing, or when the function is not included in the [feature package](license-key.md#feature-packages-and-add-ons) your license key grants. |

src/Config.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ import {DateTime, instanceOfSimpleDate, SimpleDate, SimpleDateTime, SimpleTime}
1616
import {AlwaysDense, ChooseAddressMapping} from './DependencyGraph/AddressMapping/ChooseAddressMappingPolicy'
1717
import {ConfigValueEmpty, ExpectedValueOfTypeError} from './errors'
1818
import {defaultStringifyCurrency, defaultStringifyDateTime, defaultStringifyDuration} from './format/format'
19-
import {checkLicenseKeyValidity, LicenseKeyValidityState} from './helpers/licenseKeyValidator'
19+
import {LicenseKeyValidityState} from './helpers/licenseKeyValidator'
2020
import {HyperFormula} from './HyperFormula'
2121
import {TranslationPackage} from './i18n'
2222
import {FunctionPluginDefinition} from './interpreter'
2323
import {CapabilityRegistry, ResolvedCapabilities} from './license/CapabilityRegistry'
24-
import {unrestrictedEntitlement} from './license/LicenseEntitlement'
24+
import {resolveLicense} from './license/licenseResolution'
2525
import {Maybe} from './Maybe'
2626
import {ParserConfig} from './parser/ParserConfig'
2727
import {ConfigParams, ConfigParamsList} from './ConfigParams'
@@ -180,7 +180,7 @@ export class Config implements ConfigParams, ParserConfig {
180180
/** @inheritDoc */
181181
public readonly matchWholeCell: boolean
182182

183-
constructor(options: Partial<ConfigParams> = {}, showDeprecatedWarns: boolean = true) {
183+
constructor(options: Partial<ConfigParams> = {}, showDeprecatedWarns: boolean = true, notifyLicenseMessages: boolean = true) {
184184
const {
185185
accentSensitive,
186186
caseSensitive,
@@ -279,13 +279,9 @@ export class Config implements ConfigParams, ParserConfig {
279279
validateNumberToBeAtLeast(this.maxColumns, 'maxColumns', 1)
280280
this.context = context
281281

282-
const licenseKeyValidityState = checkLicenseKeyValidity(this.licenseKey)
282+
const {validityState: licenseKeyValidityState, entitlement} = resolveLicense(this.licenseKey, notifyLicenseMessages)
283283
const capabilityRegistry = new CapabilityRegistry()
284-
// PR 1 (HF-307) ships the gate infrastructure without a real license-key payload adapter —
285-
// that lands in PR 3 as src/license/payloadAdapter.ts. Until then every entitlement resolves
286-
// as unrestricted, so isLicenseGateActive below reduces to today's licenseKeyValidityState
287-
// check and gate B in the interpreter never actually restricts a function.
288-
const licenseCapabilities = capabilityRegistry.resolve(unrestrictedEntitlement())
284+
const licenseCapabilities = capabilityRegistry.resolve(entitlement)
289285

290286
privatePool.set(this, {
291287
licenseKeyValidityState,
@@ -345,7 +341,7 @@ export class Config implements ConfigParams, ParserConfig {
345341

346342
/**
347343
* Whether gate B (the entitlement check in the interpreter) needs to run at all for this
348-
* 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
349345
* key — is a single boolean read, cheaper than the string-enum comparison it replaces.
350346
*
351347
* @internal
@@ -369,12 +365,12 @@ export class Config implements ConfigParams, ParserConfig {
369365
return getFullConfigFromPartial(this)
370366
}
371367

372-
public mergeConfig(init: Partial<ConfigParams>): Config {
368+
public mergeConfig(init: Partial<ConfigParams>, notifyLicenseMessages: boolean = true): Config {
373369
const mergedConfig: ConfigParams = Object.assign({}, this.getConfig(), init)
374370

375371
Config.warnDeprecatedOptions(init)
376372

377-
return new Config(mergedConfig, false)
373+
return new Config(mergedConfig, false, notifyLicenseMessages)
378374
}
379375

380376
private static warnDeprecatedOptions(options: Partial<ConfigParams>) {

0 commit comments

Comments
 (0)