Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### 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. [#1731](https://github.com/handsontable/hyperformula/pull/1731)
- 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)

## [3.4.0] - 2026-08-10

Expand Down
26 changes: 22 additions & 4 deletions docs/guide/license-key.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,31 @@ const options = {
}
```

### Proprietary license key formats

Your proprietary license key is in one of two formats, and both work the same way:

* A classic key: 25 characters in five dash-separated groups, for example
`1a2b3-4c5d6-7e8f9-0a1b2-3c4d5`.
* An entitlement key: a short, human-readable license text that ends with a machine-readable
block in square brackets. Assign the whole text to the `licenseKey` option, or just the
bracketed block — the block is the only part HyperFormula reads, so both work. The text around
the block may be re-wrapped on its way to you (for example, by an email client) without
affecting the key; the block itself has to arrive character for character.

### Proprietary license key validation

::: tip
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
version of the software, HyperFormula compares the date in your
proprietary license key against one of two references, depending on
the license you purchased:
* The HyperFormula build date, when the key ends maintenance on a set
date (versions released before that date keep working indefinitely)
* The current date (in UTC), when the key ends usage on a set date

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

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

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 src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,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 Down
29 changes: 17 additions & 12 deletions src/helpers/licenseKeyValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Copyright (c) 2025 Handsoncode. All rights reserved.
*/

import {ENTITLEMENT_KEY_CHECKSUM_LENGTH} from '../license/vendor/constants'
import {checkKeySchema, extractTime} from './licenseKeyHelper'

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

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

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

/**
Expand Down Expand Up @@ -178,7 +183,7 @@ export function checkLicenseKeyValidity(licenseKey: string): LicenseKeyValidityS
* Formats a Date instance to hard-coded format MMMM DD, YYYY.
*
* Read in UTC, not local time. Every date reaching this function is built at UTC midnight — the
* legacy path from a whole number of days since the epoch, the typed-key path from a calendar
* legacy path from a whole number of days since the epoch, the entitlement-key path from a calendar
* date in the payload — so local getters shifted the day backwards for anyone west of UTC and
* printed an expiry one day earlier than the one the key actually carries.
*
Expand Down
2 changes: 1 addition & 1 deletion src/license/LicenseEntitlement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export interface LicenseEntitlement {
/**
* The unrestricted entitlement: legacy keys and `gpl-v3` resolve to this today.
*
* HF-307 decision D3 (fail-closed, silent) means a typed key whose tokens this library version
* HF-307 decision D3 (fail-closed, silent) means an entitlement key whose tokens this library version
* does not recognize at all no longer maps here — it resolves to an entitlement with an empty,
* silent capability set instead of falling back to unrestricted access. Do not reuse this
* function for that case.
Expand Down
14 changes: 7 additions & 7 deletions src/license/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ export const NAMED_EXPRESSIONS_FEATURE_TOKEN = 'feat:named_expressions'
export const BATCHING_FEATURE_TOKEN = 'feat:batching'

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

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