Skip to content

HF-307: license key reader (entitlement key format, rev 6), resolution, and capability table - #1730

Open
marcin-kordas-hoc wants to merge 8 commits into
hf-307-entitlement-gating-pr2from
hf-307-entitlement-gating-pr3
Open

marcin-kordas-hoc wants to merge 8 commits into
hf-307-entitlement-gating-pr2from
hf-307-entitlement-gating-pr3

Conversation

@marcin-kordas-hoc

@marcin-kordas-hoc marcin-kordas-hoc commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Update 2026-09-22 — review round with @sequba. The sections below are the PR as written
before that round, kept for the reasoning they record. Where a sentence no longer matches the
code it is corrected inline and marked, rather than rewritten away.

Update 2026-09-18 — folded #1731, #1736, #1737, #1740, #1741 into this PR

So that no PR in this stack shows code a later PR rewrites, this PR now contains, squashed with
no loss of content: the entitlement reader (rev 6, was #1740), the notice-window warning (was
#1736), the two add-on tokens (was #1737), the function-metadata filtering (was #1731), and both
capability-token dialects (was #1741). Those five PRs are closed, folded here. The stack is now
#1728 → #1729 → this PR → #1743.

Two things below are stale as a result and are corrected here rather than edited in place, so the
history stays legible: the title above (fixed), and every PR-number reference in the body below
still says #1740/#1741/etc. — read those as "now #1730". The CHANGELOG entries in the diff are
already repointed to #1730.

Everything below this line is the PR as it was written across 2026-08-11 through 2026-08-26,
before the fold — the decisions and verification it documents did not change.


Third of four. Stacked on PR 2 (#1729), so review the commits on top of it.

Paired tests: handsontable/hyperformula-tests#32 — merge that first.

Why this PR exists at all

A genuine typed license key did not work before this. checkLicenseKeyValidity recognizes three fixed strings and the older 25-character format; a typed key matches none of them and falls through to INVALID. I verified this by building an engine with a real, unexpired [SUB] key before touching anything:

validityState = "invalid"
C1            = #LIC!   "License key is invalid."

So a paying customer pasting a valid key got #LIC! in every cell plus a console warning. An entitlement adapter alone would not have helped — gate A kills the formulas first.

What lands here

1. Vendor the typed-key reader into src/license/handsontable-license-key-parser/ (src/license/vendor/, renamed 22.09 at the reviewer's request so the directory names the repository it mirrors) as TypeScript (allowJs is off, strict is on, so this is a port, not a copy). This follows the key spec's own recommendation of a vendored copy plus a drift check rather than a shared package: a private dependency would break npm install for open-source users of this GPL package.

PROVENANCE.md records the upstream commit and a per-file sha256, so drift is detectable by re-cloning and re-hashing, and lists the deliberate divergences.

2. Resolve typed keys into both gates. resolveLicense reads the key once and answers both gates from that single reading, so they cannot disagree about what the string says. Anything that is not a typed key — gpl-v3, an older-format key, an empty string, a typed key with a broken checksum — falls through to checkLicenseKeyValidity with its verdict unchanged. That function is extracted from rather than rewritten, so both paths share one warning-per-page flag instead of each getting their own. Two additive edits to the file it lives in, both affecting the legacy path too and both deliberate: formatDate now reads its date in UTC (it printed the expiry a day early west of UTC — a latent bug on the legacy path as well), and an @internal reset for the warn-once flag was added because it made the console path untestable.

3. The real capability table, plus reading both payload shapes.

Please confirm two things

The gate-A/gate-B invariant. Only a VALID typed key resolves to a restricted entitlement; missing, invalid and expired all resolve to unrestrictedEntitlement(). Gate A already stops formula evaluation on its own — letting a bad key restrict the entitlement as well would make PR 2's ensureCapability throw from the CRUD API, turning today's "formulas fail, the API still works" into a silent breaking change for every user whose key lapsed. Mutation-tested.

The capability table is a DRAFT. Membership is transcribed from the packaging design's own per-function evidence file rather than invented, and the transcript is checked two ways: it reproduces that file's five published counts exactly (370 rows; 17 / 51 / 127 / 355 cumulative plus 15 operators), and all 423 registered function ids resolve into the 370 canonical entries once the 53 declared aliases are canonicalised, with zero uncovered. But the design is still under review — the free tier's exact contents and the placement of several function families are not settled. Landing it now was a deliberate call, not a claim that it is final.

Judgement calls worth a reviewer's eye

  • Both payload shapes are read, detected per product entry by the presence of capabilities: the shipped shape (tier/addons/exp) and the newer specified shape (capabilities/usage_until/release_until/notice/flags). They disagree about nearly every field, the newer one is still under review, and only the first can be minted today. The newer spec also contradicts itself on whether its dates are YYYY-MM-DD strings or numeric timestamps, so both are accepted.
  • Feature gating is REAL (updated 12.08, per Kuba's answers on the task). An earlier revision of this PR granted all five feature areas from the core token, which made feature gating inert by construction. The five features now live on their own feat:* tokens: a rev-5 key grants exactly the feat:* tokens it carries; the shipped shape — whose vocabulary predates feature tokens — is granted all five by the adapter, so a shipped-shape key's API behaviour is unchanged; legacy keys resolve unrestricted (Kuba's carve-out). The exits stay ungated (resumeEvaluation, teardown), per the never-gate-the-exit rule.
  • An unrecognized token no longer silences the key (updated 12.08). Silence comes solely from the key's flags; the earlier coupling (silent || unrecognized > 0) suppressed strictly more than D3 asks for and was confirmed an implementation error by Kuba.
  • The two add-on tokens are recognized but empty. spreadsheet has no agreed content — the packaging design names a package "Spreadsheet" and the pricing work names a "Spreadsheet Bundle" add-on, and whether those are the same set is unsettled; guessing either way silently sells an empty add-on or duplicates a whole tier. Pinned by a test so filling it has to be deliberate.
  • noticeDays is 0 for the shipped shape, which has no notice field. Inventing a default in the parser would put a product decision in the wrong place. The newer shape carries notice and it is used.
  • PROVENANCE.md names the upstream private repo and commit. That is what makes the documented drift check runnable. Ratified by Kuba on the task (D7-A, 12.08): kept as-is.

Update 13.08 — key spec rev 5 read against the code

Reading rev 5 (doc, updated 12.08) and running its own example payload through this branch turned up three things, all fixed in the last commit:

  • Feature tokens are now OPT-IN. §2.2 lists HyperFormula's entire token vocabulary as functions_1..4, spreadsheet, import_export — there is no feat:* entry at all. So a key that names no feature token cannot be saying "no features"; nothing in circulation can express one. Minting the spec's own §2 payload and running it: setCellContents, addRows, copy, undo, addNamedExpression and batch all threw. The same hole hit Handsontable-only keys and keys with hyperformula: null, which lost the API that core used to give them, silently (gate-A VALID = no console warning). A key that does name a feat:* token still gets exactly what it names, which is the gating Kuba asked for.
  • no-console-warns is honoured. rev 5 spells the flag three ways — §2.3 and the §2 example say no-console-warns, §4.3/§5.2 say silent-console, earlier revisions said silent. Only the last two were recognised, so a doc-conformant SaaS key printed console warnings it had explicitly asked to suppress.
  • An unreadable capabilities rejects the key. Present-but-not-an-array fell through to the shipped-shape branch, which was a free pass twice: every feature granted, and the rev-5 dates never read — a subscription expired in 2020 resolved as perpetual.

Still divergent from rev 5, deliberately: §4.1 says a non-trial key never hard-blocks, even past grace ("Trial: block. Non-trial: error only (18.1)"). This branch keeps EXPIRED → #LIC!, per Kuba's D5=A (hard blocking stays this release; the notice/soft-stop/hard-stop model is a follow-up).

Verification

Full unit suite green — 512 suites, 6344 tests (after the 13.08 update). Each of the three fixes above is mutation-verified: removing the opt-in rule fails 9 tests, removing no-console-warns fails 1, removing the capabilities guard fails 2, and cutting the console notification fails 1 (it used to fail none). Beyond that, the parts that matter were mutation-tested rather than trusted: a wrong SHA-512 round constant, a one-bit UTF-8 error, a broken invariant and a moved package boundary each fail the tests that are supposed to catch them.


Note

High Risk
Changes commercial licensing, formula evaluation (#LIC!), and API capability checks across the engine; mistakes affect every customer key and entitlement path.

Overview
This PR wires proprietary license keys to real feature packages and add-ons: entitlement keys (rev 6) are parsed via a vendored reader, resolved once in resolveLicense, and mapped through a static capability table (functions_1functions_4, fun:* group/per-function tokens, feat:* API areas, and spreadsheet / import_export add-ons).

Runtime behavior: For a valid restricted key, gated built-in functions evaluate to #LIC! and gated API areas throw LicenseCapabilityMissingError; getAvailableFunctions() / getFunctionDetails() only list functions the key actually grants. Invalid, missing, or expired keys still use an unrestricted entitlement for gate B (formulas fail via validity, but the public API is not further locked down). Legacy 25-character keys and gpl-v3 are unchanged.

Also added: one-time console messaging (invalid/expired plus “valid until … (UTC)” inside the usage notice window), optional silencing via config rebuild paths, UTC-correct expiry dates in warnings, and updated license/error docs.

Reviewed by Cursor Bugbot for commit dd731a9. Bugbot is set up for automated code reviews on this repo. Configure here.

⛔ Update 18.08 — upstream retired this key format the same day, and one claim above is wrong

A scoped spec-to-ship review of this PR (5 dimensions, every finding adversarially verified) confirmed 5 findings. Four are test-hardening and are fixed in the paired PR — see hyperformula-tests#32's "Update 18.08". The fifth needs a decision, not a patch, so nothing here has been changed for it yet.

The vendored reader cannot read the format upstream now ships. handsontable/license-key commit 01eae6530d4f (2026-08-18 08:44 UTC, DEV-2512, "Replace typed license keys with the entitlement key format (rev 6)"), released as 4.0.0, deletes src/typed-key/ outright — the exact five files PROVENANCE.md pins and hashes. Its own docs are explicit: "The typed key format was removed, not deprecated… this package cannot read a typed key." The replacement differs in three ways this reader gates on:

this branch entitlement format (rev 6 / 4.0.0)
format marker requires a leading [SUB]_/[FREE]_/[TRIAL]_/[PERP]_ tag (extractKeyData.ts:147) no tag — the key ends with a bracketed block, which is the only marker
payload version requires v: 1 (extractKeyData.ts:199) no v field at all — payload is exactly {"products":{…}}
checksum domain SHA-512 over the whole key body, prose included over the encoded payload only — an explicit reversal, so the prose becomes editable
unknown product invalidates the key (extractKeyData.ts:105) tolerated — "kept, and ignored"

Measured, not inferred: an authentic 4.0.0-format key built per upstream's own build-payload.js gives extractTypedKeyData(key)null, resolveLicense(key).validityStateinvalid, and C1#LIC! "License key is invalid.". Passing only the […] block behaves the same. The retired tagged format still resolves correctly on the same build — so this is a format gap, not a robustness one.

Independently, Budzio published the byte-level spec for that format this morning as a new page under key-spec rev 6 ("Technical implementation", sections T1T8, created 09:45 UTC). It matches upstream and confirms all four rows above. Three of its rules this branch already satisfies: checksum verified before any decode or parse ("order is normative"), uppercase hex rejected rather than normalised, and __proto__-safe result construction.

What is genuinely at stake, and what is not. The invariant holds throughout: a rev-6 key resolves INVALID and therefore to unrestrictedEntitlement(), so the CRUD API keeps working and only formulas fail (verified). And myHOT PR #73 (merged to master 12:37 UTC today, pinning license-key#4.0.0) states "the typed keys never reached production", with the legacy 25-character keys still generated in parallel and validating exactly as before — that path is untouched here. So the correction owed to this PR's own opening claim: no customer ever held a typed key, so "a paying customer pasting a valid key got #LIC!" describes a format that was never issued. The live exposure is narrower but real — per that PR's surfacing table, trial orders and the HubSpot trial webhook now surface the entitlement key, while commercial orders and transactional emails still show the legacy key only.

Why this is not fixed in this PR. Re-porting is the obvious move and upstream made it cheaper than the original port (sha512.js and utils.js are unchanged; the new extractEntitlementKeyData is deliberately schema-free and built to be vendored, so only the extractor, schema and constants need re-porting). But it is not a patch to slip in: adopting the new shape rules verbatim also makes capabilities mandatory and invalidates every payload shape this branch currently reads, rev 6 is still status "for review", and the work is arguably HF-329's (unassigned, due 21.08, untouched since 04.08 — it now has concrete content). Raised on the task as D9; the merge freeze until the spec owner returns means escalating is the action, not a delay.

Also worth a reviewer's eye, since it sharpens the still-unanswered D8: rev 6's own examples use capabilities: ["functions_1", "spreadsheet"] / ["core"] — i.e. exactly the token names this branch implements — while the packaging design published 12.08 states its vocabulary is fun:<family>.<A|B|C> and that "nothing else about packaging exists at the technical layer". The two documents disagree, and this branch conforms to the key spec rather than to the packaging doc. A key minted in the packaging vocabulary resolves VALID with the whole gated API and zero functions, silently (gate A says VALID, so nothing prints, and unrecognizedCapabilities is exposed nowhere). That outcome is now pinned by a test in the paired PR — pinned, not endorsed, so whichever way D8 lands has to change it.


Changed in the 2026-09-22 review round

  • The guide no longer describes the key formats, and the validation section points at the terms of the contract instead of enumerating the two date axes. The changelog entry for the parser change was reworded the same way, since it carried the same description.
  • PROVENANCE.md: two paragraphs about the superseded src/typed-key/ port removed; the divergence list corrected. Its item 3 described upstream's date check as if it were this port's, which has not been true since a type guard was added, and that guard — the only divergence that changes which keys are accepted — was missing from the list entirely.
  • The expired-key console message no longer blames the installed version when the key ran out on the usage axis. That wording predates this axis and was correct for every key develop can see; it is not for a subscription key.
  • The implies field and its recursive expansion are gone (see HF-307: entitlement model, capability registry, gate B (PR 1/4) #1728), so no capability token refers to another.

@qunabu

qunabu commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 11, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
hyperformula-docs ce18576 Sep 22 2026, 02:52 PM

Comment thread src/license/licenseResolution.ts Outdated
Comment thread src/license/capabilities.ts
Comment thread src/license/licenseResolution.ts Outdated
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

Performance comparison of head (ce18576) vs base (14f1ed5)

                                     testName |    base |    head | change
--------------------------------------------------------------------------
                                      Sheet A |  504.03 |  500.42 | -0.72%
                                      Sheet B |  161.36 |  161.34 | -0.01%
                                      Sheet T |  145.51 |  143.91 | -1.10%
                                Column ranges |  481.26 |  478.08 | -0.66%
                                Sorted lookup | 14509.3 | 14306.6 | -1.40%
Sheet A:  change value, add/remove row/column |   16.42 |   17.01 | +3.59%
 Sheet B: change value, add/remove row/column |  136.35 |  143.51 | +5.25%
                   Column ranges - add column |  152.11 |   154.5 | +1.57%
                Column ranges - without batch |   469.1 |  479.19 | +2.15%
                        Column ranges - batch |  119.52 |  116.89 | -2.20%

@marcin-kordas-hoc

Copy link
Copy Markdown
Collaborator Author

bugbot run

Comment thread src/license/capabilities.ts Outdated
Comment thread src/license/licenseResolution.ts Outdated
@marcin-kordas-hoc

Copy link
Copy Markdown
Collaborator Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/license/capabilities.ts
Comment thread src/license/licenseResolution.ts
@qunabu

qunabu commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Tobiadefami
Tobiadefami previously approved these changes Aug 24, 2026
@Tobiadefami
Tobiadefami dismissed their stale review August 24, 2026 15:38

Approval retracted because the repository requires obsolete PR descriptions to be updated. The code and paired tests remain reviewed with no scope-aligned finding; re-approval only requires confirming the refreshed PR description and unchanged head.

@Tobiadefami Tobiadefami left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at ab1bba1 together with the paired tests at 9977ea88. The typed-key reader, license resolution, capability mapping, legacy-key invariant, and current automated findings were checked. The six changed paired suites pass (131 tests), and the current engine checks are green. I found one material issue: the expired-key console date ignores the key's grace period, as noted inline.

Comment thread src/license/licenseResolution.ts Outdated
@marcin-kordas-hoc
marcin-kordas-hoc force-pushed the hf-307-entitlement-gating-pr3 branch from 8dcca66 to e88b96b Compare September 18, 2026 10:15

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread CHANGELOG.md
@marcin-kordas-hoc marcin-kordas-hoc changed the title HF-307 PR 3/4: make typed license keys work — vendored reader, resolution, capability table HF-307: license key reader (entitlement key format, rev 6), resolution, and capability table Sep 18, 2026
Comment thread docs/guide/license-key.md Outdated
Comment on lines +43 to +54
### 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't mention it in the public docs.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sequba Done — the whole "Proprietary license key formats" section is out of the guide.

I also applied the same reasoning to the changelog, since it is the same disclosure in another public file: the entry for this PR described the format in the same words ("a human-readable text ending with a machine-readable block in square brackets"). It now names the change and what is unaffected, without describing the format. Say the word if you would rather have that wording back.

Comment thread docs/guide/license-key.md
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section should say that the validation mechanism depends on the terms of your contract.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sequba Done. The section no longer enumerates the two date axes; it says that which versions a key covers, and for how long, follows from the terms of your contract, and that HyperFormula applies them locally. I kept the existing tip about no internet connection, since that was not what you objected to.

Worth knowing: this section predates HF-307 on develop and this PR had expanded it. The rewrite takes it back past the original wording rather than only undoing the expansion.

* events, and a page that swaps keys (or a test suite that builds one engine per key) must still
* warn for the second one even though the first already consumed a shared flag.
*/
const _noticedKeys = new Set<string>()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why it is per key? Why do we need to manage more keys in single instance of HF?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sequba Small correction to the premise first: _noticedKeys is a module-level const, not instance state. It is not one engine holding several keys — it is several engines on one page load.

The case it exists for is a page that swaps the key, or a suite that builds one engine per key. Two keys approaching their own expiry are two different events, and with a single boolean the second one is silently swallowed because the first consumed it. The flag above it stays a single boolean on purpose, because "a key is invalid" means the same thing whichever key caused it.

If you would rather have one notice per page load regardless, say so and it collapses to a boolean — it is a two-line change. I only wanted the reason on record either way.

@@ -0,0 +1,100 @@
# Vendored entitlement-key reader — provenance and drift control

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename the directory vendor to handsontable-license-key-parser

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sequba Done. src/license/vendor/ is now src/license/handsontable-license-key-parser/, moved with git mv so blame and --follow walk through it, and the paired test directory is renamed to match. Only paths changed; the prose about a "vendored reader" stays, since it describes the relationship rather than the location.

Comment thread src/license/vendor/PROVENANCE.md Outdated
Comment on lines +18 to +21
This replaces the earlier port of `src/typed-key/` at `7553d0d1` (2026-08-11). Upstream 4.0.0
(DEV-2512) **deleted** that directory and replaced the tagged key format with the entitlement key
format; the tagged format never reached customers, so the old reader was removed here rather than
kept alongside.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this paragraph

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sequba Done — removed.

Comment thread src/license/vendor/PROVENANCE.md Outdated
Comment on lines +36 to +38
`utils.js` and `sha512.js` are byte-identical between `src/typed-key/` at the old pin and
`src/entitlement-key/` at `4.0.0` (same hashes as the previous revision of this table), so their
ports carried over unchanged apart from this file's path references.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this paragraph

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sequba Done — removed.

Comment on lines +72 to +74
`allowJs` is off in HyperFormula's `tsconfig.json` and `strict` is on, so these files are a port
rather than a copy. Beyond adding types, the semantics were kept identical except for the
following, which a drift review should expect to see:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add exceptions to tsconfig so these files could be the byte-identical copies of the upstream?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sequba I would like this too, and it is worth more than tidiness: byte-identical files turn drift control from "re-read and re-port the changed file" into sha256sum, which is the whole point of the hash table in that document.

But it is not just allowJs. The files would have to be emitted and bundled as .js inside a strict build that also emits declarations, which means either checkJs: false plus a hand-written .d.ts per file, or @ts-check-clean upstream sources. I have not measured whether the rollup and declaration steps survive that.

Before I spend that: is the goal (a) a mechanical drift check, or (b) literally no local edits? If (a), there is a cheaper route — keep the port and add a CI job that clones the upstream at the pinned commit and compares. §7.1 of the spec asks for a drift check in CI anyway, and the document currently admits the check is manual.

One thing that collides either way: there is a deliberate local divergence in extractKeyData.ts (see my separate comment on the divergence list). A byte-identical copy cannot keep it.

Comment on lines +179 to +185
/** The group tokens each package adds, exactly as the packaging doc's §4 table states them. */
const MATH_ENGINE_GROUPS = ['fun:math.a', 'fun:stat.a', 'fun:logic.a', 'fun:operator.a', 'fun:info.a', 'fun:lookup.a']
const CALCULATED_FIELDS_GROUPS = ['fun:time.b', 'fun:text.b', 'fun:logic.b', 'fun:math.b', 'fun:stat.b']
const SPREADSHEET_GROUPS = [
'fun:lookup.c', 'fun:math.c', 'fun:stat.c', 'fun:time.c', 'fun:text.c', 'fun:info.c', 'fun:logic.c',
'fun:finance.c', 'fun:engineer.c', 'fun:array.c',
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These groups should not be hardcoded. The code should operate on a capabilities level and should know nothing about what capabilities are in what package.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sequba On the group constants you are simply right: MATH_ENGINE_GROUPS / CALCULATED_FIELDS_GROUPS / SPREADSHEET_GROUPS are the packaging table transcribed into the engine — the comment above them literally says "exactly as the packaging doc's §4 table states them". That belongs to whatever mints keys, not here.

Your broader point I want to scope before acting on, because it touches a decision we recorded in August (D8): the HyperFormula token registry belongs to us — rev 6 §2.2, and the shipped 4.0.0 schema declares hyperformula: [] with the whitelist off. Two readings:

  • (a) the engine knows token → functions but nothing about package → tokens. Then the three constants and the functions_1..4 grants go, and the fun:<family>.<tier> table stays.
  • (b) the engine knows nothing, and a key carries function ids directly. That retires the group-token vocabulary and reverses D8.

Which one? I would rather reopen D8 deliberately than by accident. This also gates the unrestricted reshape, since both rewrite the same file.

[FUN_ALL_TOKEN, {functions: [...functions4Grant.functions], features: []}],
...groupEntries,
...singleFunctionEntries,
])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this file there are a lot of things that don't belong in HF repo

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sequba On the group constants you are simply right: MATH_ENGINE_GROUPS / CALCULATED_FIELDS_GROUPS / SPREADSHEET_GROUPS are the packaging table transcribed into the engine — the comment above them literally says "exactly as the packaging doc's §4 table states them". That belongs to whatever mints keys, not here.

Your broader point I want to scope before acting on, because it touches a decision we recorded in August (D8): the HyperFormula token registry belongs to us — rev 6 §2.2, and the shipped 4.0.0 schema declares hyperformula: [] with the whitelist off. Two readings:

  • (a) the engine knows token → functions but nothing about package → tokens. Then the three constants and the functions_1..4 grants go, and the fun:<family>.<tier> table stays.
  • (b) the engine knows nothing, and a key carries function ids directly. That retires the group-token vocabulary and reverses D8.

Which one? I would rather reopen D8 deliberately than by accident. This also gates the unrestricted reshape, since both rewrite the same file.

marcin-kordas-hoc and others added 3 commits September 22, 2026 11:43
# Conflicts:
#	src/license/CapabilityRegistry.ts
…rovenance

Renames `src/license/vendor/` to `src/license/handsontable-license-key-parser/`
so the directory names the repository it mirrors, and renames the paired test
directory to match.

Drops two provenance paragraphs that only described the older, superseded port
rather than the code beside them.

Corrects the divergence list, which is the part a drift review reads. Item 3
described upstream's own date check as if it were this port's, which the code
has not matched since a type guard was added: upstream matches the stringified
value against the date shape, so a value that merely spells a date once
stringified is carried into a restricted entitlement, while here it takes the
invalid-key path. That is the only divergence which changes what a key is
worth, so it is now listed in its own right, with what the key specification
says about the field and the note that upstream has not adopted it.

The public guide no longer describes the key formats, and the validation
section now points at the terms of the contract instead of enumerating the
dates it compares.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 27a672c. Configure here.

Comment thread src/license/licenseResolution.ts Outdated
marcin-kordas-hoc and others added 2 commits September 22, 2026 13:39
The guide no longer documents what a proprietary key looks like, and the
changelog entry carried the same description in the same words. It says what
changed and what is unaffected instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@marcin-kordas-hoc

Copy link
Copy Markdown
Collaborator Author

@sequba One more thing in the vendored parser, found while acting on your comments rather than from your review — flagging it because it is the only divergence from upstream that changes which keys are accepted.

isIsoDate in extractKeyData.ts rejects a usage_until that is not a string. Upstream matches String(value) against the date shape, so a single-element array of the right string passes there and is carried into a RESTRICTED entitlement; here it takes the invalid-key path instead.

Two problems with how that was documented, both now fixed in PROVENANCE.md:

  • The "Deliberate divergences" list did not mention this guard at all, even though the document promises a drift review will find every divergence there.
  • Item 3 described upstream's behaviour as if it were this port's — it said a usage_until array "passes", which has not been true here since the guard was added. A false statement about the code in the same directory.

On the substance: the key spec's addendum (T7) makes that field a real calendar date, so the stricter reading is the specified one and it is upstream that deviates. Upstream has not adopted it — checked at c50ef40a, develop and master, and there is no PR or issue proposing it. Worth raising on that side; until then it is a live fork of behaviour rather than a re-port waiting to happen.

A key can run out along two axes and only one of them is about the build in
use. A maintenance key stops covering releases, so an older version keeps
working and installing one is the fix; a usage-based key stops being valid at
all, and telling its holder the key "is not valid for the installed version"
sends them to downgrade, which changes nothing.

Both went through one message, because the wording predates the usage axis this
work introduced. The axis now reaches the message, and the classic
25-character format keeps the old wording, being release-only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.33921% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.40%. Comparing base (14f1ed5) to head (ce18576).

Files with missing lines Patch % Lines
.../handsontable-license-key-parser/extractKeyData.ts 97.56% 2 Missing ⚠️
src/license/licenseResolution.ts 98.66% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                        Coverage Diff                        @@
##           hf-307-entitlement-gating-pr2    #1730      +/-   ##
=================================================================
+ Coverage                          97.34%   97.40%   +0.05%     
=================================================================
  Files                                198      204       +6     
  Lines                              15850    16272     +422     
  Branches                            3477     3602     +125     
=================================================================
+ Hits                               15429    15849     +420     
- Misses                               413      415       +2     
  Partials                               8        8              
Files with missing lines Coverage Δ
src/Config.ts 94.69% <100.00%> (-0.05%) ⬇️
src/HyperFormula.ts 99.76% <100.00%> (+<0.01%) ⬆️
src/helpers/licenseKeyValidator.ts 96.22% <100.00%> (+5.90%) ⬆️
src/interpreter/Interpreter.ts 95.49% <100.00%> (-0.02%) ⬇️
src/license/CapabilityRegistry.ts 100.00% <100.00%> (ø)
src/license/LicenseEntitlement.ts 100.00% <ø> (ø)
src/license/capabilities.ts 100.00% <100.00%> (ø)
...cense/handsontable-license-key-parser/constants.ts 100.00% <100.00%> (ø)
...se/handsontable-license-key-parser/detectFormat.ts 100.00% <100.00%> (ø)
.../license/handsontable-license-key-parser/sha512.ts 100.00% <100.00%> (ø)
... and 3 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants