Skip to content

Commit 02e3434

Browse files
HF-307/HF-306: teach the capability table both token dialects
The engine now resolves BOTH capability-token vocabularies in circulation, from one source of truth: the 21 function groups of the packaging doc (transcribed 1:1 from its §6, drift-checked by the published per-group counts) now live in capabilities.ts, and the functions_1..4 package slices are DERIVED from them as the doc's own cumulative group unions - so a function moved between groups moves in both dialects at once. New recognized tokens: fun:all (= the functions_4 grant), the 21 fun:<family>.<A|B|C> group tokens, and a fun:<CANONICAL_NAME> single- function token for every catalog entry. fun:info.a / fun:lookup.a / fun:offset / fun:version resolve to EMPTY grants on purpose - their members are the protected built-ins, which must never become table-covered. Token matching is now case-insensitive (the packaging doc states it outright for fun:*; the other tokens tolerate it for free). Accepting the superset is spec-clean (T7: an unrecognized token is a grant this version does not implement, so implementing more breaks nothing) and makes the engine robust to the still-open business choice between the dialects. The 18.08 'fun:* grants zero functions' pin is inverted BY DECISION (owner, 20.08), not by accident - see the test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019pxNP45obT2LZfjitaCv9o
1 parent 864ee26 commit 02e3434

4 files changed

Lines changed: 185 additions & 49 deletions

File tree

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

1717
- 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)
1818
- 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.
19+
- 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.
1920

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

src/license/CapabilityRegistry.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import {FeatureId, LicenseEntitlement} from './LicenseEntitlement'
7-
import {CAPABILITY_TABLE, CapabilityGrant} from './capabilities'
7+
import {CAPABILITY_TABLE, CapabilityGrant, normalizeCapabilityToken} from './capabilities'
88

99
/**
1010
* The capabilities a resolved {@link LicenseEntitlement} grants, ready for gate B (the
@@ -59,12 +59,13 @@ export class CapabilityRegistry {
5959
/**
6060
* Expands an entitlement's capability tokens into the concrete functions and features they
6161
* grant. An `unrestricted` entitlement short-circuits to an unrestricted result without
62-
* consulting the table at all. Expansion through `implies` is transitive and cycle-safe (a
63-
* visited set guards against a token implying itself, directly or through others); an
64-
* unrecognized token is skipped without an error.
62+
* consulting the table at all. Tokens are matched case-insensitively (the table is keyed by
63+
* the normalized spelling — see {@link normalizeCapabilityToken}). Expansion through `implies`
64+
* is transitive and cycle-safe (a visited set guards against a token implying itself, directly
65+
* or through others); an unrecognized token is skipped without an error.
6566
*
6667
* @param {LicenseEntitlement} entitlement - the entitlement to resolve, e.g. one built by
67-
* hand in a test or produced by PR 3's license-key payload adapter
68+
* hand in a test or produced by the license-key payload adapter
6869
*/
6970
public resolve(entitlement: LicenseEntitlement): ResolvedCapabilities {
7071
if (entitlement.unrestricted) {
@@ -77,7 +78,7 @@ export class CapabilityRegistry {
7778
const queue = [...entitlement.capabilities]
7879

7980
while (queue.length > 0) {
80-
const token = queue.shift() as string
81+
const token = normalizeCapabilityToken(queue.shift() as string)
8182
if (visited.has(token)) {
8283
continue
8384
}

src/license/capabilities.ts

Lines changed: 172 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export const FUNCTIONS_2_TOKEN = 'functions_2'
4444
export const FUNCTIONS_3_TOKEN = 'functions_3'
4545
/** Excel simulator package — the entire implemented catalog. */
4646
export const FUNCTIONS_4_TOKEN = 'functions_4'
47+
/**
48+
* The packaging doc's whole-catalog token — the excel-simulator package as that doc's own
49+
* vocabulary spells it. Grants exactly what {@link FUNCTIONS_4_TOKEN} grants.
50+
*/
51+
export const FUN_ALL_TOKEN = 'fun:all'
4752
/**
4853
* Spreadsheet Bundle add-on (2026-08-12 packages meeting). Grants {@link FeatureId.Crud},
4954
* {@link FeatureId.UndoRedo}, {@link FeatureId.Clipboard} and {@link FeatureId.Batching} — see
@@ -57,6 +62,21 @@ export const SPREADSHEET_ADDON_TOKEN = 'spreadsheet'
5762
*/
5863
export const IMPORT_EXPORT_ADDON_TOKEN = 'import_export'
5964

65+
/**
66+
* The canonical spelling of a capability token for table lookups.
67+
*
68+
* Token names are case-insensitive — the packaging doc states it outright for its `fun:*`
69+
* vocabulary, and tolerating case on the other tokens costs nothing since none of them collide
70+
* under lowercasing. Normalization happens at LOOKUP, never at storage: an entitlement carries
71+
* the key's own spellings (they are diagnostics), and {@link CAPABILITY_TABLE} is keyed by the
72+
* lowercase form.
73+
*
74+
* @param {string} token - a capability token as the key spells it
75+
*/
76+
export function normalizeCapabilityToken(token: string): string {
77+
return token.toLowerCase()
78+
}
79+
6080
/**
6181
* Describes what a capability token grants: a set of function ids, a set of {@link FeatureId}
6282
* values, and optionally other tokens it implies. `implies` is expanded recursively by
@@ -78,58 +98,120 @@ const OPERATOR_FUNCTIONS = [
7898
'HF.MULTIPLY', 'HF.NE', 'HF.POW', 'HF.UMINUS', 'HF.UNARY_PERCENT', 'HF.UPLUS',
7999
]
80100

101+
/**
102+
* The two protected built-ins. Both are named by the packaging doc (`fun:lookup.A`, `fun:info.A`)
103+
* but sit OUTSIDE the token system today — the interpreter never gate-checks a protected
104+
* function, so granting them would be dead weight that implies a restriction that does not exist.
105+
* The doc calls this a "technical limitation" on both; their tokens below are recognized but
106+
* grant nothing.
107+
*/
108+
const PROTECTED_BUILT_INS = ['OFFSET', 'VERSION']
109+
81110
// An earlier revision granted all five features from CORE_TOKEN, which made feature gating inert
82111
// by construction: no restricted key could ever lose an API area. Kuba's call (task comment, 12.08):
83112
// "Feature gating should work, but the legacy keys should grant all feat:* capabilities" — legacy
84113
// keys already resolve to the unrestricted entitlement, so the carve-out costs nothing, and the
85114
// five features moved onto their own `feat:*` tokens below.
86115

87116
/**
88-
* Package membership, as the LOWEST package that includes each function.
117+
* The 21 function groups of the packaging doc, keyed by their group tokens in normalized
118+
* (lowercase) spelling — the doc writes them `fun:<family>.<A|B|C>` and declares all token names
119+
* case-insensitive.
89120
*
90-
* Transcribed from Kuba's 12.08 packaging doc (CU doc `8cnjcyf-33175`, page `8cnjcyf-47835`, "HF
91-
* function groups and packages"), which supersedes the earlier evidence file this table was first
92-
* built from. The doc organizes the catalog into 21 group tokens (`fun:<family>.<A|B|C>`) and
93-
* states each package as the cumulative union of specific groups: Math engine = the `.A` groups,
94-
* Calculated fields = `.A` + `.B`, Spreadsheet = `.A` + `.B` + `.C`. Reproducing that union gives
95-
* 17 / 64 / 161 cumulative functions before the two protected built-ins below are removed;
96-
* `capability-table.spec.ts` pins the resulting counts so a later edit cannot drift from them
97-
* silently.
121+
* Transcribed 1:1 from §6 of Kuba's 12.08 packaging doc (CU doc `8cnjcyf-33175`, page
122+
* `8cnjcyf-47835`, "HF function groups and packages"), INCLUDING the members that resolve to no
123+
* grant here: the operators (granted by {@link CORE_TOKEN} instead) and the protected built-ins
124+
* (outside the token system, see {@link PROTECTED_BUILT_INS}). Keeping the doc's own membership
125+
* verbatim is what makes this map the SINGLE SOURCE OF TRUTH both token dialects read from — the
126+
* package slices below are DERIVED from these groups, so moving a function between groups moves
127+
* it in both dialects at once, and `capability-table.spec.ts` pins each group's size against the
128+
* doc's published counts so a re-transcription is a reviewable diff.
98129
*
99-
* `OFFSET` and `VERSION` are named by the doc (as `fun:lookup.A` and `fun:info.A`) but are
100-
* deliberately absent from every list below: both are protected built-ins that sit OUTSIDE the
101-
* token system today — the interpreter never gate-checks a protected function, so listing them
102-
* would be dead weight that implies a restriction that does not exist. The doc calls this a
103-
* "technical limitation" on both; see `hf-306-token-vocabulary-final` for the root cause of each
104-
* (registry protection for VERSION, parse-time resolution for OFFSET) and what closing it would
105-
* take.
106-
*/
107-
const MATH_ENGINE_FUNCTIONS = [
108-
'ABS', 'AVERAGE', 'COUNT', 'IF', 'LOG', 'MAX', 'MIN', 'MOD', 'POWER', 'PRODUCT', 'ROUND', 'ROUNDDOWN',
109-
'ROUNDUP', 'SQRT', 'SUM',
130+
* The doc freezes group names as API surface: once shipped inside license keys, a rename is a
131+
* breaking change.
132+
*/
133+
export const FUNCTION_GROUPS: ReadonlyMap<string, readonly string[]> = new Map([
134+
['fun:math.a', ['ABS', 'LOG', 'MOD', 'POWER', 'PRODUCT', 'ROUND', 'ROUNDDOWN', 'ROUNDUP', 'SQRT', 'SUM']],
135+
['fun:stat.a', ['AVERAGE', 'COUNT', 'MAX', 'MIN']],
136+
['fun:logic.a', ['IF']],
137+
['fun:operator.a', [...OPERATOR_FUNCTIONS]],
138+
['fun:info.a', ['VERSION']],
139+
['fun:lookup.a', ['OFFSET']],
140+
['fun:time.b', [
141+
'DATE', 'DATEDIF', 'DATEVALUE', 'DAY', 'DAYS', 'EOMONTH', 'HOUR', 'ISOWEEKNUM', 'MINUTE', 'MONTH',
142+
'NETWORKDAYS', 'SECOND', 'TODAY', 'WEEKDAY', 'WEEKNUM', 'WORKDAY', 'YEAR',
143+
]],
144+
['fun:text.b', [
145+
'CONCATENATE', 'EXACT', 'LEFT', 'LEN', 'LOWER', 'MID', 'REPLACE', 'REPT', 'RIGHT', 'SEARCH',
146+
'SUBSTITUTE', 'TEXT', 'TRIM', 'UPPER', 'VALUE',
147+
]],
148+
['fun:logic.b', ['AND', 'FALSE', 'IFS', 'NOT', 'OR', 'SWITCH', 'TRUE', 'XOR']],
149+
['fun:math.b', ['RAND', 'RANDBETWEEN', 'SUMIF', 'SUMIFS']],
150+
['fun:stat.b', ['AVERAGEIF', 'COUNTIF', 'STDEV.S']],
151+
['fun:lookup.c', [
152+
'ADDRESS', 'CHOOSE', 'COLUMN', 'COLUMNS', 'FILTER', 'HLOOKUP', 'HSTACK', 'HYPERLINK', 'INDEX', 'MATCH',
153+
'ROW', 'ROWS', 'SORT', 'TRANSPOSE', 'UNIQUE', 'VLOOKUP', 'VSTACK', 'XLOOKUP',
154+
]],
155+
['fun:math.c', [
156+
'ACOS', 'ASIN', 'ATAN', 'ATAN2', 'CEILING', 'COS', 'EVEN', 'EXP', 'FLOOR', 'INT', 'LN', 'MROUND', 'ODD',
157+
'PI', 'QUOTIENT', 'SEQUENCE', 'SIGN', 'SIN', 'SUBTOTAL', 'SUMPRODUCT', 'SUMSQ', 'SUMXMY2', 'TAN',
158+
]],
159+
['fun:stat.c', [
160+
'AVERAGEA', 'COUNTA', 'COUNTBLANK', 'COUNTIFS', 'LARGE', 'MAXIFS', 'MEDIAN', 'MINIFS', 'PERCENTILE.INC',
161+
'SMALL', 'STDEV.P', 'STDEVA', 'STDEVPA', 'VAR.P', 'VAR.S',
162+
]],
163+
['fun:time.c', ['DAYS360', 'EDATE', 'NOW', 'TIME', 'YEARFRAC']],
164+
['fun:text.c', ['CHAR', 'CLEAN', 'CODE', 'FIND', 'PROPER', 'T', 'TEXTJOIN', 'UNICHAR']],
165+
['fun:info.c', [
166+
'ISBLANK', 'ISERR', 'ISERROR', 'ISEVEN', 'ISLOGICAL', 'ISNA', 'ISNUMBER', 'ISODD', 'ISTEXT', 'N', 'NA',
167+
]],
168+
['fun:logic.c', ['IFERROR', 'IFNA']],
169+
['fun:finance.c', ['FV', 'IPMT', 'IRR', 'NPV', 'PMT', 'PPMT', 'PV', 'RATE', 'SLN', 'XIRR', 'XNPV']],
170+
['fun:engineer.c', ['DEC2HEX', 'HEX2DEC']],
171+
['fun:array.c', ['ARRAYFORMULA', 'ARRAY_CONSTRAIN']],
172+
])
173+
174+
/** The group tokens each package adds, exactly as the packaging doc's §4 table states them. */
175+
const MATH_ENGINE_GROUPS = ['fun:math.a', 'fun:stat.a', 'fun:logic.a', 'fun:operator.a', 'fun:info.a', 'fun:lookup.a']
176+
const CALCULATED_FIELDS_GROUPS = ['fun:time.b', 'fun:text.b', 'fun:logic.b', 'fun:math.b', 'fun:stat.b']
177+
const SPREADSHEET_GROUPS = [
178+
'fun:lookup.c', 'fun:math.c', 'fun:stat.c', 'fun:time.c', 'fun:text.c', 'fun:info.c', 'fun:logic.c',
179+
'fun:finance.c', 'fun:engineer.c', 'fun:array.c',
110180
]
111181

182+
/** The members of the given groups, concatenated. The groups are disjoint, so this is a union. */
183+
function membersOfGroups(groupTokens: string[]): string[] {
184+
return groupTokens.reduce<string[]>(
185+
(members, groupToken) => members.concat(FUNCTION_GROUPS.get(groupToken) ?? []),
186+
[],
187+
)
188+
}
189+
190+
/**
191+
* The members a group contributes to a package GRANT: the group verbatim, minus the operators
192+
* (granted by {@link CORE_TOKEN} in every package) and the protected built-ins (outside the token
193+
* system entirely).
194+
*/
195+
function gatableMembersOfGroups(groupTokens: string[]): string[] {
196+
return membersOfGroups(groupTokens).filter(
197+
(name) => OPERATOR_FUNCTIONS.indexOf(name) === -1 && PROTECTED_BUILT_INS.indexOf(name) === -1,
198+
)
199+
}
200+
201+
/**
202+
* Package membership, DERIVED from {@link FUNCTION_GROUPS} as the cumulative group unions the
203+
* packaging doc's §4 table states: Math engine = the `.A` groups, Calculated fields = `.A` + `.B`,
204+
* Spreadsheet = `.A` + `.B` + `.C`. Reproducing that union gives 17 / 64 / 161 cumulative
205+
* functions before the two protected built-ins are removed; `capability-table.spec.ts` pins the
206+
* resulting full memberships by name so a re-derivation is a reviewable diff.
207+
*/
208+
const MATH_ENGINE_FUNCTIONS = gatableMembersOfGroups(MATH_ENGINE_GROUPS)
209+
112210
/** Added by the calculated-fields package, on top of {@link MATH_ENGINE_FUNCTIONS}. */
113-
const CALCULATED_FIELDS_FUNCTIONS = [
114-
'AND', 'AVERAGEIF', 'CONCATENATE', 'COUNTIF', 'DATE', 'DATEDIF', 'DATEVALUE', 'DAY', 'DAYS', 'EOMONTH',
115-
'EXACT', 'FALSE', 'HOUR', 'IFS', 'ISOWEEKNUM', 'LEFT', 'LEN', 'LOWER', 'MID', 'MINUTE', 'MONTH',
116-
'NETWORKDAYS', 'NOT', 'OR', 'RAND', 'RANDBETWEEN', 'REPLACE', 'REPT', 'RIGHT', 'SEARCH', 'SECOND',
117-
'STDEV.S', 'SUBSTITUTE', 'SUMIF', 'SUMIFS', 'SWITCH', 'TEXT', 'TODAY', 'TRIM', 'TRUE', 'UPPER', 'VALUE',
118-
'WEEKDAY', 'WEEKNUM', 'WORKDAY', 'XOR', 'YEAR',
119-
]
211+
const CALCULATED_FIELDS_FUNCTIONS = gatableMembersOfGroups(CALCULATED_FIELDS_GROUPS)
120212

121213
/** Added by the spreadsheet package, on top of {@link CALCULATED_FIELDS_FUNCTIONS}. */
122-
const SPREADSHEET_FUNCTIONS = [
123-
'ACOS', 'ADDRESS', 'ARRAYFORMULA', 'ARRAY_CONSTRAIN', 'ASIN', 'ATAN', 'ATAN2', 'AVERAGEA', 'CEILING',
124-
'CHAR', 'CHOOSE', 'CLEAN', 'CODE', 'COLUMN', 'COLUMNS', 'COS', 'COUNTA', 'COUNTBLANK', 'COUNTIFS',
125-
'DAYS360', 'DEC2HEX', 'EDATE', 'EVEN', 'EXP', 'FILTER', 'FIND', 'FLOOR', 'FV', 'HEX2DEC', 'HLOOKUP',
126-
'HSTACK', 'HYPERLINK', 'IFERROR', 'IFNA', 'INDEX', 'INT', 'IPMT', 'IRR', 'ISBLANK', 'ISERR', 'ISERROR',
127-
'ISEVEN', 'ISLOGICAL', 'ISNA', 'ISNUMBER', 'ISODD', 'ISTEXT', 'LARGE', 'LN', 'MATCH', 'MAXIFS', 'MEDIAN',
128-
'MINIFS', 'MROUND', 'N', 'NA', 'NOW', 'NPV', 'ODD', 'PERCENTILE.INC', 'PI', 'PMT', 'PPMT', 'PROPER', 'PV',
129-
'QUOTIENT', 'RATE', 'ROW', 'ROWS', 'SEQUENCE', 'SIGN', 'SIN', 'SLN', 'SMALL', 'SORT', 'STDEV.P', 'STDEVA',
130-
'STDEVPA', 'SUBTOTAL', 'SUMPRODUCT', 'SUMSQ', 'SUMXMY2', 'T', 'TAN', 'TEXTJOIN', 'TIME', 'TRANSPOSE',
131-
'UNICHAR', 'UNIQUE', 'VAR.P', 'VAR.S', 'VLOOKUP', 'VSTACK', 'XIRR', 'XLOOKUP', 'XNPV', 'YEARFRAC',
132-
]
214+
const SPREADSHEET_FUNCTIONS = gatableMembersOfGroups(SPREADSHEET_GROUPS)
133215

134216
/**
135217
* Added by the excel-simulator package, on top of {@link SPREADSHEET_FUNCTIONS} — the rest of the
@@ -187,7 +269,50 @@ const functions4Grant: CapabilityGrant = {
187269
}
188270

189271
/**
190-
* The production capability table.
272+
* One table entry per group token: the group's gatable members, so a key may assemble a package
273+
* from groups instead of naming a `functions_N` slice. `fun:info.a` and `fun:lookup.a` resolve to
274+
* EMPTY grants on purpose — their members are the protected built-ins, which are always available
275+
* and must never become table-covered (a covered function is gated for every key not granting
276+
* it). The tokens stay recognized either way, so a key carrying them is never reported as
277+
* unrecognized: they are the doc's bookkeeping identifiers for functionality every key gets.
278+
*/
279+
const groupEntries: [string, CapabilityGrant][] = Array.from(FUNCTION_GROUPS.keys()).map((groupToken) => [
280+
groupToken,
281+
{functions: gatableMembersOfGroups([groupToken]), features: []},
282+
])
283+
284+
/**
285+
* One table entry per canonical function name: the packaging doc's single-function tokens
286+
* (`fun:<CANONICAL_FUNCTION_NAME>`), "for surgical grants: custom deals, previews, per-function
287+
* exceptions". One exists for EVERY canonical name — including the operators (harmless: core
288+
* grants them anyway) and the protected built-ins (empty grants, as above). Alias names get no
289+
* token of their own: tokens reference canonical names, and an alias travels with its canonical
290+
* function because the gates canonicalize before consulting the table.
291+
*/
292+
const singleFunctionEntries: [string, CapabilityGrant][] = functions4Grant.functions
293+
.concat(OPERATOR_FUNCTIONS, PROTECTED_BUILT_INS)
294+
.map((name) => [
295+
`fun:${normalizeCapabilityToken(name)}`,
296+
{functions: PROTECTED_BUILT_INS.indexOf(name) === -1 ? [name] : [], features: []},
297+
])
298+
299+
/**
300+
* The production capability table, keyed by NORMALIZED token spelling — look up through
301+
* {@link normalizeCapabilityToken}, never with a raw key string.
302+
*
303+
* The engine understands BOTH token dialects in circulation, resolved from the one group registry
304+
* above so they cannot drift apart:
305+
*
306+
* - the key spec's package slices (`functions_1..4`) plus the two add-on tokens — the vocabulary
307+
* the upstream generator's own schema mints today;
308+
* - the packaging doc's group vocabulary (`fun:all`, `fun:<family>.<A|B|C>`,
309+
* `fun:<CANONICAL_FUNCTION_NAME>`) — §6 of the 12.08 packaging doc.
310+
*
311+
* Accepting the superset is deliberate and spec-clean: an unrecognized token is defined as "a
312+
* grant this version does not implement" (strict-shape/lenient-vocabulary, T7), so implementing
313+
* more tokens than the generator currently mints breaks nothing — and it makes the engine robust
314+
* to the still-open business decision about which dialect keys will finally be worded in
315+
* (owner's call, 20.08). A key's function set is the UNION of everything recognized.
191316
*
192317
* The grants are stored FULLY EXPANDED rather than chained through `implies`: the packaging
193318
* design states the enforcement layer must not assume a hierarchy between tokens, and that the
@@ -212,6 +337,11 @@ const functions4Grant: CapabilityGrant = {
212337
* RESERVED grant, since nothing in the public API is gated on it yet: HF-107 hasn't shipped the
213338
* feature it would gate. Both tokens stay recognized either way, so an issued key carrying one is
214339
* never reported as unrecognized.
340+
*
341+
* Entry ORDER is load-bearing at one spot: `CapabilityRegistry`'s reverse index maps each
342+
* function id to the FIRST token that lists it, so the package slices stay ahead of the group and
343+
* single-function tokens, keeping `capabilityOf`'s answers what they were before the second
344+
* dialect existed.
215345
*/
216346
export const CAPABILITY_TABLE: ReadonlyMap<string, CapabilityGrant> = new Map([
217347
[CORE_TOKEN, coreGrant],
@@ -229,5 +359,7 @@ export const CAPABILITY_TABLE: ReadonlyMap<string, CapabilityGrant> = new Map([
229359
features: [FeatureId.Crud, FeatureId.UndoRedo, FeatureId.Clipboard, FeatureId.Batching],
230360
}],
231361
[IMPORT_EXPORT_ADDON_TOKEN, {functions: [], features: [FeatureId.ImportExport]}],
362+
[FUN_ALL_TOKEN, {functions: [...functions4Grant.functions], features: []}],
363+
...groupEntries,
364+
...singleFunctionEntries,
232365
])
233-

src/license/licenseResolution.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
notifyLicenseKeyNotice,
1010
notifyLicenseKeyState,
1111
} from '../helpers/licenseKeyValidator'
12-
import {ALL_FEATURE_TOKENS, CAPABILITY_TABLE, CORE_TOKEN} from './capabilities'
12+
import {ALL_FEATURE_TOKENS, CAPABILITY_TABLE, CORE_TOKEN, normalizeCapabilityToken} from './capabilities'
1313
import {LicenseEntitlement, LicenseExpiry, unrestrictedEntitlement} from './LicenseEntitlement'
1414
import {detectLicenseKeyFormat} from './vendor/detectFormat'
1515
import {EntitlementKeyData, EntitlementProductGrant, extractEntitlementKeyData} from './vendor/extractKeyData'
@@ -155,7 +155,7 @@ function licenseTermsOf(data: EntitlementKeyData): LicenseTerms {
155155
// Reading absence as denial instead would hand a dead public API to every key myHOT can mint
156156
// today, HyperFormula-only and Handsontable-only alike; both were verified doing exactly that
157157
// before this rule existed.
158-
if (!capabilityTokens.some((token) => token.indexOf(FEATURE_TOKEN_PREFIX) === 0)) {
158+
if (!capabilityTokens.some((token) => normalizeCapabilityToken(token).indexOf(FEATURE_TOKEN_PREFIX) === 0)) {
159159
capabilityTokens.push(...ALL_FEATURE_TOKENS)
160160
}
161161

@@ -272,7 +272,9 @@ function expiryWithinNoticeWindow(terms: LicenseTerms): Date | null {
272272
* @param {LicenseTerms} terms - the terms of the key
273273
*/
274274
function entitlementOf(terms: LicenseTerms): LicenseEntitlement {
275-
const unrecognizedCapabilities = terms.capabilityTokens.filter((token) => !CAPABILITY_TABLE.has(token))
275+
const unrecognizedCapabilities = terms.capabilityTokens.filter(
276+
(token) => !CAPABILITY_TABLE.has(normalizeCapabilityToken(token)),
277+
)
276278

277279
return {
278280
unrestricted: false,

0 commit comments

Comments
 (0)