Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6689397
HF-307 PR 2: public API guards (ensureCapability)
marcin-kordas-hoc Aug 11, 2026
8a26e2b
HF-307 PR 2: close the Crud gate on row/column reordering
marcin-kordas-hoc Aug 11, 2026
c29a9ba
HF-307 PR 2: ungate resumeEvaluation - gating it could brick an engine
marcin-kordas-hoc Aug 11, 2026
88feb29
HF-307 PR2: close a Crud bypass through cut+paste, fix docs and a pac…
marcin-kordas-hoc Aug 18, 2026
4ee01ae
Merge remote-tracking branch 'origin/hf-307-entitlement-gating-pr1' i…
marcin-kordas-hoc Sep 18, 2026
e88b96b
HF-307/HF-329/HF-306: license key reader, resolution and capability t…
marcin-kordas-hoc Sep 18, 2026
dfeb8f1
Merge PR1 (drop the unused implication step)
marcin-kordas-hoc Sep 22, 2026
bee030b
refactor(HF-307): let a clipboard grant cover pasting a cut
marcin-kordas-hoc Sep 22, 2026
b3e8c2f
Merge PR2 (clipboard covers pasting a cut)
marcin-kordas-hoc Sep 22, 2026
48ee653
refactor(HF-307): rename the vendored parser directory and trim its p…
marcin-kordas-hoc Sep 22, 2026
7d5ac2d
docs(HF-307): stop promising a Crud refusal paste() can no longer make
marcin-kordas-hoc Sep 22, 2026
27a672c
Merge PR2 (throws tag correction)
marcin-kordas-hoc Sep 22, 2026
14f1ed5
style(HF-307): close the gap the removed wrapper left
marcin-kordas-hoc Sep 22, 2026
f782d0e
Merge PR2
marcin-kordas-hoc Sep 22, 2026
dd731a9
docs(HF-307): name the parser change without describing the key format
marcin-kordas-hoc Sep 22, 2026
ce18576
fix(HF-307): stop blaming the installed version for a usage-based expiry
marcin-kordas-hoc Sep 22, 2026
690ef69
Merge branch 'hf307-fix/pr1' into hf307-fix/pr2
marcin-kordas-hoc Sep 22, 2026
fbb81a2
fix(HF-307): merge PR2's axis split forward, correct stale JSDoc
marcin-kordas-hoc Sep 22, 2026
8d5c39a
docs(HF-307): fix a JSDoc line missed in the previous merge commit
marcin-kordas-hoc Sep 22, 2026
258786e
refactor(HF-307): read capabilities only, never packages
marcin-kordas-hoc Sep 23, 2026
15d7231
docs(HF-307): drop the core token from the coverage invariant's descr…
marcin-kordas-hoc Sep 24, 2026
6b627a6
Merge branch 'hf-307-entitlement-gating-pr1' into hf-307-entitlement-…
sequba Sep 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Added

- 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)
- 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)
- 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)

### Changed

- 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)
- Changed the console message for an expired proprietary license key to stop naming the installed version when the key ran out on the usage axis rather than the maintenance axis. A maintenance key stops covering releases, so an older build keeps working; a usage-based key stops being valid at all, and there is no version to fall back to. Classic 25-character license keys have only the maintenance axis, so their message is unchanged. [#1730](https://github.com/handsontable/hyperformula/pull/1730)
- Changed the parser for the new proprietary license keys to the entitlement key format, 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)
- Changed the license capability tokens to be matched case-insensitively, and to grant functions through the packaging group-token vocabulary only: `fun:all`, `fun:<family>.<A|B|C>`, and per-function `fun:<FUNCTION_NAME>` tokens. A key names the capabilities it grants, and the engine grants their union; it keeps no notion of which tokens make up a package. The callable forms of the calculation operators, such as `HF.ADD`, are granted by `fun:operator.A`, while the infix operators themselves work under any key. [#1730](https://github.com/handsontable/hyperformula/pull/1730)

### Fixed

- Fixed the `AVERAGEIF` function returning a division-by-zero error when the calculated average was `0`. [#1733](https://github.com/handsontable/hyperformula/pull/1733)
Expand Down
63 changes: 57 additions & 6 deletions docs/guide/license-key.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,70 @@ const options = {
HyperFormula doesn't use an internet connection to validate your proprietary license key.
:::

To determine whether a user is still entitled to use a particular
version of the software, HyperFormula compares the time between
two dates:
* The HyperFormula build date
* The date in your proprietary license key
Which versions of HyperFormula a key covers, and for how long, follows from the
terms of your contract. Your key carries those terms, and HyperFormula applies
them locally, without any connection to a server.

## Feature packages and add-ons

A proprietary license key may grant the whole library, or only part of it. If your key covers
everything you buy nothing new to think about, and neither does the GPLv3 key `gpl-v3`, which
always grants everything.

If your key grants only part of the library, then:

* A function your key doesn't include evaluates to a `#LIC!` error, in the same way as any other
[error value](types-of-errors.md). Everything else in the sheet keeps calculating.
* An API method your key doesn't include throws a `LicenseCapabilityMissingError` when you call
it. Getters never throw; `copy()` and `cut()` do, because they belong to the clipboard feature.
* [`getAvailableFunctions()`](../api/classes/hyperformula.md#getavailablefunctions) and
[`getFunctionDetails()`](../api/classes/hyperformula.md#getfunctiondetails) describe only the
functions your key includes, so a function picker built from them never offers a function that
then fails.

Custom functions you register yourself are available whatever your key grants, as long as they use
an id of their own. The licence covers built-in ids, so a plugin registered under a built-in id your
key does not include is treated as that built-in and stays unavailable — it will not be described and
it evaluates to `#LIC!`. Pick an id the built-in catalogue does not use and this cannot happen.

Two commercial add-ons build on top of a package:

* **Spreadsheet Bundle** grants the CRUD API (adding, removing, and moving rows, columns, sheets,
and cell contents), undo/redo, clipboard operations, and batching (`batch()` /
`suspendEvaluation()`; `resumeEvaluation()` is deliberately never gated, so an engine can always
leave a suspended state). It does not grant named expressions, which stay outside both add-ons.
* **Import/export** is reserved for a future release. HyperFormula doesn't have an import/export
feature yet, so this add-on doesn't grant or restrict anything today.

In this release, not having either add-on doesn't restrict anything either: a key that names no
feature token at all is granted every feature area — CRUD, undo/redo, clipboard, named expressions
and batching — regardless of whether it names these add-ons. Every key issued today is of that
shape, so the add-on tokens describe what was sold rather than changing what the engine allows.

This process doesn't require any connection to the server.
::: tip
To find out which package your key includes, check your order confirmation or
[contact our team](contact.md). HyperFormula deliberately reports nothing about the contents of
your key at runtime.
:::

## License key notifications

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

In that case every licence-gated function call evaluates to a `#LIC!` error — but no API method
starts throwing, and `getAvailableFunctions()` still describes the full set of functions. A key
problem never narrows what the library reports it can do.

Arithmetic keeps working: operators such as `=A1+B1` are not function calls, so nothing gates them.
`VERSION()` and `OFFSET()` are function calls, but they are protected built-ins that sit outside the
licence system entirely, so they keep evaluating too. A sheet with a key problem therefore does not
go blank.

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

## License key support

If you have any issues with your license key, [contact our team](contact.md).
2 changes: 1 addition & 1 deletion docs/guide/types-of-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ according to the language settings.
| #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. |
| #CYCLE! | Circular reference | It occurs when a formula refers to its own cell, both directly and indirectly. |
| #ERROR! | An error occurred | It indicates that there is an unknown error in a formula. |
| #LIC! | Invalid license key | It occurs when the license key is invalid, expired, or missing. |
| #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. |
20 changes: 8 additions & 12 deletions src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ import {DateTime, instanceOfSimpleDate, SimpleDate, SimpleDateTime, SimpleTime}
import {AlwaysDense, ChooseAddressMapping} from './DependencyGraph/AddressMapping/ChooseAddressMappingPolicy'
import {ConfigValueEmpty, ExpectedValueOfTypeError} from './errors'
import {defaultStringifyCurrency, defaultStringifyDateTime, defaultStringifyDuration} from './format/format'
import {checkLicenseKeyValidity, LicenseKeyValidityState} from './helpers/licenseKeyValidator'
import {LicenseKeyValidityState} from './helpers/licenseKeyValidator'
import {HyperFormula} from './HyperFormula'
import {TranslationPackage} from './i18n'
import {FunctionPluginDefinition} from './interpreter'
import {CapabilityRegistry, ResolvedCapabilities} from './license/CapabilityRegistry'
import {unrestrictedEntitlement} from './license/LicenseEntitlement'
import {resolveLicense} from './license/licenseResolution'
import {Maybe} from './Maybe'
import {ParserConfig} from './parser/ParserConfig'
import {ConfigParams, ConfigParamsList} from './ConfigParams'
Expand Down Expand Up @@ -180,7 +180,7 @@ export class Config implements ConfigParams, ParserConfig {
/** @inheritDoc */
public readonly matchWholeCell: boolean

constructor(options: Partial<ConfigParams> = {}, showDeprecatedWarns: boolean = true) {
constructor(options: Partial<ConfigParams> = {}, showDeprecatedWarns: boolean = true, notifyLicenseMessages: boolean = true) {
const {
accentSensitive,
caseSensitive,
Expand Down Expand Up @@ -279,13 +279,9 @@ export class Config implements ConfigParams, ParserConfig {
validateNumberToBeAtLeast(this.maxColumns, 'maxColumns', 1)
this.context = context

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

privatePool.set(this, {
licenseKeyValidityState,
Expand Down Expand Up @@ -346,7 +342,7 @@ export class Config implements ConfigParams, ParserConfig {

/**
* Whether gate B (the entitlement check in the interpreter) needs to run at all for this
* config. `false` — the common case, for `gpl-v3`, legacy keys, and an unrestricted typed
* config. `false` — the common case, for `gpl-v3`, legacy keys, and an unrestricted entitlement
* key — is a single boolean read, cheaper than the string-enum comparison it replaces.
*
* @internal
Expand All @@ -370,12 +366,12 @@ export class Config implements ConfigParams, ParserConfig {
return getFullConfigFromPartial(this)
}

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

Config.warnDeprecatedOptions(init)

return new Config(mergedConfig, false)
return new Config(mergedConfig, false, notifyLicenseMessages)
}

private static warnDeprecatedOptions(options: Partial<ConfigParams>) {
Expand Down
Loading
Loading