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
12 changes: 4 additions & 8 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 @@ -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)
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
63 changes: 51 additions & 12 deletions src/helpers/licenseKeyValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type ConsoleMessages = {

type MessageDescriptor = {
template: LicenseKeyValidityState,
vars: TemplateVars,
expiryDate?: Date,
}

/**
Expand All @@ -42,6 +42,44 @@ const consoleMessages: ConsoleMessages = {

let _notified = false

/**
* Clears the once-per-page-load flag {@link notifyLicenseKeyState} keeps.
*
* Exists for tests only. The flag is module-level and never otherwise reset, so without this the
* whole console-message path is unobservable: the first spec to build any engine consumes the single
* warning and every later assertion sees silence regardless of what the code does. Making the reset
* explicit beats the alternatives — depending on spec-file order is flaky, and under Karma every
* spec shares one browser context, so order tricks do not work there at all.
*
* @internal
*/
export function resetLicenseKeyNotificationForTests(): void {
_notified = false
}

/**
* 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
* 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.
*
* @param {LicenseKeyValidityState} state - the state to report; `VALID` prints nothing
* @param {Date} [keyValidityDate] - the day the key stopped being valid, used by the `expired`
* message
*/
export function notifyLicenseKeyState(state: LicenseKeyValidityState, keyValidityDate?: Date): void {
if (_notified || state === LicenseKeyValidityState.VALID) {
return
}

const vars: TemplateVars = keyValidityDate === undefined ? {} : {keyValidityDate: formatDate(keyValidityDate)}

console.warn(consoleMessages[state](vars))
_notified = true
}

/**
* Checks if the provided license key is grammatically valid or not expired.
*
Expand All @@ -51,7 +89,6 @@ let _notified = false
export function checkLicenseKeyValidity(licenseKey: string): LicenseKeyValidityState {
const messageDescriptor: MessageDescriptor = {
template: LicenseKeyValidityState.MISSING,
vars: {},
}

if (licenseKey === 'gpl-v3' || licenseKey === 'internal-use-in-handsontable' || licenseKey === 'hftrial-0168e-1f2b7-47158-70b05-0842f') {
Expand All @@ -62,7 +99,7 @@ export function checkLicenseKeyValidity(licenseKey: string): LicenseKeyValidityS
const releaseDays = Math.floor(new Date(`${month}/${day}/${year}`).getTime() / 8.64e7)
const keyValidityDays = extractTime(licenseKey)

messageDescriptor.vars.keyValidityDate = formatDate(new Date((keyValidityDays + 1) * 8.64e7))
messageDescriptor.expiryDate = new Date((keyValidityDays + 1) * 8.64e7)

if (releaseDays > keyValidityDays) {
messageDescriptor.template = LicenseKeyValidityState.EXPIRED
Expand All @@ -74,27 +111,29 @@ export function checkLicenseKeyValidity(licenseKey: string): LicenseKeyValidityS
messageDescriptor.template = LicenseKeyValidityState.INVALID
}

if (!_notified && messageDescriptor.template !== LicenseKeyValidityState.VALID) {
console.warn(consoleMessages[messageDescriptor.template](messageDescriptor.vars))
_notified = true
}
notifyLicenseKeyState(messageDescriptor.template, messageDescriptor.expiryDate)

return messageDescriptor.template
}

/**
* Formats a Date instance to hard-coded format MMMM DD, YYYY.
*
* @param {Date} date The date to format.
* @returns {string}
* 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
* 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.
*
* @param {Date} date The date to format, at UTC midnight.
* @returns {string} The date as `MMMM DD, YYYY`.
*/
function formatDate(date: Date): string {
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
]
const month = monthNames[date.getMonth()]
const day = date.getDate()
const year = date.getFullYear()
const month = monthNames[date.getUTCMonth()]
const day = date.getUTCDate()
const year = date.getUTCFullYear()

return `${month} ${day}, ${year}`
}
5 changes: 1 addition & 4 deletions src/license/CapabilityRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import {FeatureId, LicenseEntitlement} from './LicenseEntitlement'
import {CAPABILITY_TABLE, CapabilityGrant, refreshCoreGrant} from './capabilities'
import {CAPABILITY_TABLE, CapabilityGrant} from './capabilities'

/**
* The capabilities a resolved {@link LicenseEntitlement} grants, ready for gate B (the
Expand Down Expand Up @@ -33,9 +33,6 @@ export class CapabilityRegistry {
* suite does not depend on its placeholder content.
*/
constructor(table?: ReadonlyMap<string, CapabilityGrant>) {
if (table === undefined) {
refreshCoreGrant()
}
this.table = table ?? CAPABILITY_TABLE
this.reverseIndex = CapabilityRegistry.buildReverseIndex(this.table)
}
Expand Down
25 changes: 16 additions & 9 deletions src/license/LicenseEntitlement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ export const enum FeatureId {
/**
* Describes when a license entitlement stops being valid.
*
* Per key-spec rev 3 §1.3, `date` is kept as a calendar string rather than an epoch, and is
* INCLUSIVE of its last valid day:
* - `kind === 'usage'`: `date` is compared against the client's LOCAL calendar date — deliberately
* not UTC, the date means the date, wherever the customer is.
* - `kind === 'release'`: `date` is compared LEXICOGRAPHICALLY, as text, against the library's
* build date; no clock is involved.
* `date` is kept as a calendar string rather than an epoch, and is INCLUSIVE of its last valid
* day:
* - `kind === 'usage'`: compared against the current instant in **UTC**. An earlier revision of
* the key spec called for the client's LOCAL calendar date; that was reversed, because the
* offline check and a future online check have to return the same verdict for the same key at
* the same instant, and any rule that reads a local clock breaks that parity. The practical
* cost is that a customer far west of UTC loses the tail of their last local day.
* - `kind === 'release'`: compared against the library's build date; no clock is involved, which
* is what keeps an air-gapped install with a wrong system clock working.
* - `kind === 'none'`: the entitlement does not expire.
*/
export interface LicenseExpiry {
Expand Down Expand Up @@ -63,9 +66,13 @@ export interface LicenseEntitlement {
expiry: LicenseExpiry,
/**
* When `true`, resolving this entitlement must not print a console message of any kind.
* HF-307 decision D3 (fail-closed, silent): a typed key with no recognized token resolves
* like an explicit `capabilities: []` — core and protected functions only, without a message,
* a warning, or a diagnostics getter.
*
* Set from the key's own flags ONLY — the key spec spells that flag three different ways across
* revisions and even within one revision, and all are honoured. An unrecognized token does NOT
* set it: HF-307 decision D3 makes the *grant* silent (an unknown token grants nothing, with no
* message and no diagnostics getter), which is a different thing from muting the key's console
* output. Coupling them suppressed expiry notices as a side effect of a vocabulary mismatch, and
* was confirmed an implementation error (Kuba, 12.08).
*/
silent: boolean,
isTrial: boolean,
Expand Down
Loading
Loading