License: Add new licensing mechanism#33206
License: Add new licensing mechanism#33206ajivanyandev wants to merge 47 commits intoDevExpress:26_1from
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a new DevExtreme licensing mechanism, adding a devextreme-license CLI and bundler plugin support to inject a generated license key into DevExtreme’s internal config during builds. It also adds a new LCP (product-only) key validation path and updates license warning/reporting behavior.
Changes:
- Add
devextreme-licenseCLI + supporting Node-side license key discovery/conversion utilities (LCX → LCP). - Add an
unplugin-based bundler plugin to replace a license-key placeholder inm_configat build time. - Add LCP key parsing/validation logic (product-kind bit flags, expiration checks) and update license warnings/tests.
Reviewed changes
Copilot reviewed 26 out of 28 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Locks unplugin@3.0.0 and related dependency snapshot updates. |
| packages/devextreme/package.json | Adds unplugin dependency and registers devextreme-license as an npm binary. |
| packages/devextreme/license/messages.js | Adds CLI/plugin message templates for licensing guidance. |
| packages/devextreme/license/dx-lcx-2-lcp.js | Implements LCX → LCP conversion + LCP parsing helpers (Node-side). |
| packages/devextreme/license/dx-get-lcx.js | Adds environment/file-based LCX key discovery logic (Node-side). |
| packages/devextreme/license/devextreme-license.js | Adds devextreme-license CLI for generating/printing an LCP key and updating .gitignore. |
| packages/devextreme/license/devextreme-license-plugin.js | Adds unplugin plugin to patch m_config license placeholder during bundling. |
| packages/devextreme/license/devextreme-license-plugin.d.ts | Type declaration for the bundler plugin export. |
| packages/devextreme/js/__internal/core/m_config.ts | Adds licenseKey default placeholder in global config. |
| packages/devextreme/js/__internal/core/license/types.ts | Extends token/error types and adds exported error token constants + warning metadata types. |
| packages/devextreme/js/__internal/core/license/rsa_bigint.ts | Refactors RSA bigint helpers to reuse bigIntFromBytes. |
| packages/devextreme/js/__internal/core/license/license_warnings.ts | Adds centralized console warning templates + logger for license warning types. |
| packages/devextreme/js/__internal/core/license/license_validation.ts | Switches parsing to LCP validation path and changes warning/logging behavior. |
| packages/devextreme/js/__internal/core/license/license_validation.test.ts | Updates tests to assert new warning behavior and adds placeholder coverage. |
| packages/devextreme/js/__internal/core/license/lcp_key_validation/utils.ts | Adds shared helpers for LCP parsing (RSA XML parsing, bit flags, tick conversion, signature verify). |
| packages/devextreme/js/__internal/core/license/lcp_key_validation/types.ts | Defines product-kind bit flags for DevExpress products. |
| packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts | Adds product info model helpers and bitwise product checks. |
| packages/devextreme/js/__internal/core/license/lcp_key_validation/license_payload.test.ts | Adds payload-level tests for product-kind / version behavior. |
| packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts | Adds license info helpers (validity, expiration, latest DevExtreme version). |
| packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts | Implements LCP parsing/verification into internal Token representation. |
| packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts | Adds LCP validator tests (including signature bypass harness). |
| packages/devextreme/js/__internal/core/license/lcp_key_validation/const.ts | Adds LCP constants (signature prefix, decode map, RSA key XML). |
| packages/devextreme/js/__internal/core/license/key.ts | Removes internal-usage ID constant (legacy/internal token behavior). |
| packages/devextreme/js/__internal/core/license/const.ts | Consolidates licensing constants (links, placeholder, formatting values). |
| packages/devextreme/js/__internal/core/license/byte_utils.ts | Adds bigIntFromBytes utility function. |
| packages/devextreme/eslint.config.mjs | Adds license/* to ignore list. |
| packages/devextreme/build/npm-bin/devextreme-license.js | Adds npm bin wrapper to execute the CLI from the distributed package. |
| packages/devextreme/build/gulp/npm.js | Ensures license/** is copied into the npm distribution. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
|
|
||
| function fail(msg) { | ||
| process.stderr.write(msg.endsWith('\n') ? msg : msg + '\n'); | ||
| process.exit(0); |
There was a problem hiding this comment.
fail() exits with code 0 even for unknown arguments / refusing to overwrite output files. That makes CI/scripts treat failures as success. Use a non-zero exit code for error paths (and keep 0 only for successful execution).
| process.exit(0); | |
| process.exit(1); |
| else if(a === '--cwd') out.cwd = args[++i] || process.cwd(); | ||
| else if(a.startsWith('--cwd=')) out.cwd = a.slice('--cwd='.length); |
There was a problem hiding this comment.
--cwd consumes the next argv token without validating it (e.g. --cwd --force will set cwd to "--force"). Handle missing/flag values the same way as --out (warn/fail when the next token is absent or starts with -).
| else if(a === '--cwd') out.cwd = args[++i] || process.cwd(); | |
| else if(a.startsWith('--cwd=')) out.cwd = a.slice('--cwd='.length); | |
| else if(a === '--cwd') { | |
| const next = args[i + 1]; | |
| if(!next || next.startsWith('-')) { | |
| logStderr(prefixed('Warning: --cwd requires a path argument but none was provided. Ignoring --cwd.')); | |
| } else { | |
| out.cwd = args[++i]; | |
| } | |
| } | |
| else if(a.startsWith('--cwd=')) { | |
| const val = a.slice('--cwd='.length); | |
| if(!val) { | |
| logStderr(prefixed('Warning: --cwd requires a path argument but none was provided. Ignoring --cwd.')); | |
| } else { | |
| out.cwd = val; | |
| } | |
| } |
| function warnLicenseIssue(ctx, source, licenseId, warning) { | ||
| try { | ||
| if(ctx && typeof ctx.warn === 'function') { | ||
| ctx.warn(`${PLUGIN_PREFIX} DevExpress license key (LCX) retrieved from: ${source}`); | ||
| if(licenseId) { | ||
| ctx.warn(`${PLUGIN_PREFIX} License ID: ${licenseId}`); | ||
| } | ||
| ctx.warn(`${PLUGIN_PREFIX} Warning: ${warning}`); | ||
| } | ||
| } catch{} |
There was a problem hiding this comment.
This warning helper interpolates source and warning directly into strings, but callers pass source as an object and warning can be an object from getLCPInfo(). This will log [object Object] and be hard to understand. Format these values explicitly (e.g. use source.type/source.path and convert warning into a human-readable message via templates).
| const lcp = resolveLcpSafe(this); | ||
| if(!lcp) return null; | ||
|
|
||
| return { code: code.split(PLACEHOLDER).join(lcp), map: null }; |
There was a problem hiding this comment.
The placeholder replacement injects the raw LCP string into the source code. Since the placeholder lives inside a string literal in m_config.ts, any '/\ characters that can occur in an LCP key will break the generated JS/TS (syntax error). Replace the whole string literal with a properly escaped literal (e.g. inject JSON.stringify(lcp) into an unquoted placeholder), rather than doing a raw text substitution.
| return { code: code.split(PLACEHOLDER).join(lcp), map: null }; | |
| const escapedLcp = JSON.stringify(lcp); | |
| const patchedCode = code | |
| .split(`'${PLACEHOLDER}'`).join(escapedLcp) | |
| .split(`"${PLACEHOLDER}"`).join(escapedLcp) | |
| .split(PLACEHOLDER).join(escapedLcp); | |
| return { code: patchedCode, map: null }; |
| useLegacyVisibleIndex: false, | ||
| versionAssertions: [], | ||
| copyStylesToShadowDom: true, | ||
| licenseKey: '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */', |
There was a problem hiding this comment.
licenseKey is initialized to a placeholder inside single quotes. The build-time plugin currently replaces only the placeholder text, so any quote/backslash characters in the real key can corrupt this string literal. Consider making the placeholder unquoted and having the plugin inject a properly escaped string literal (or otherwise ensure escaping).
| licenseKey: '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */', | |
| licenseKey: String.raw`/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */`, |
| export function parseLicenseKey(encodedKey: string | undefined): Token { | ||
| if (encodedKey === undefined) { | ||
| return GENERAL_ERROR; | ||
| } | ||
|
|
||
| const parts = encodedKey.split(KEY_SPLITTER); | ||
|
|
||
| if (parts.length !== 2 || parts[0].length === 0 || parts[1].length === 0) { | ||
| return GENERAL_ERROR; | ||
| } | ||
|
|
||
| if (!verifySignature({ text: parts[0], signature: parts[1] })) { | ||
| return VERIFICATION_ERROR; | ||
| if (isProductOnlyLicense(encodedKey)) { | ||
| return parseDevExpressProductKey(encodedKey); | ||
| } | ||
|
|
||
| let decodedPayload = ''; | ||
| try { | ||
| decodedPayload = atob(parts[0]); | ||
| } catch { | ||
| return DECODING_ERROR; | ||
| } | ||
|
|
||
| let payload: Payload = {}; | ||
| try { | ||
| payload = JSON.parse(decodedPayload); | ||
| } catch { | ||
| return DESERIALIZATION_ERROR; | ||
| } | ||
|
|
||
| const { | ||
| customerId, maxVersionAllowed, format, internalUsageId, ...rest | ||
| } = payload; | ||
|
|
||
| if (internalUsageId !== undefined) { | ||
| return { | ||
| kind: TokenKind.internal, | ||
| internalUsageId, | ||
| }; | ||
| } | ||
|
|
||
| if (customerId === undefined || maxVersionAllowed === undefined || format === undefined) { | ||
| return PAYLOAD_ERROR; | ||
| } | ||
|
|
||
| if (format !== FORMAT) { | ||
| return VERSION_ERROR; | ||
| } | ||
|
|
||
| return { | ||
| kind: TokenKind.verified, | ||
| payload: { | ||
| customerId, | ||
| maxVersionAllowed, | ||
| ...rest, | ||
| }, | ||
| }; | ||
| return GENERAL_ERROR; | ||
| } |
There was a problem hiding this comment.
parseLicenseKey() now only recognizes LCPv1 keys and returns GENERAL_ERROR for all legacy DevExtreme keys. Existing tests in this repo still expect legacy ("ewog....") and internal-usage tokens to be verified/parsed, so this change will break both runtime backward-compatibility and the Jest suite. Either keep legacy parsing/verification here (and only add LCP support), or update callers/tests to stop relying on legacy/internal tokens.
| @@ -189,20 +135,21 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer | |||
| } | |||
There was a problem hiding this comment.
validateLicense() still has an internal flow (versionsCompatible && internal check), but getLicenseCheckParams() no longer returns internal: true in any branch. This makes the internal-only behavior unreachable and likely changes behavior for internal/test builds. Either reintroduce internal token handling or remove the dead internal logic to avoid misleading control flow.
| 'js/viz/docs/*', | ||
| 'node_modules/*', | ||
| 'build/*', | ||
| 'license/*', |
There was a problem hiding this comment.
Adding license/* to ESLint ignores means the new CLI/plugin code won’t be linted/spellchecked at all (even though it ships in the npm package and runs in user environments). Prefer keeping lint enabled for this folder, or narrow the ignore to only generated/vendor files so issues (like exit codes/arg parsing) are caught automatically.
| 'license/*', |
|
|
||
| export function logLicenseWarning( | ||
| warningType: LicenseWarningType, | ||
| version: string, |
There was a problem hiding this comment.
The version parameter is unused, which will fail @typescript-eslint/no-unused-vars (enabled as an error for TS files). Either remove it or rename to _version (or use it if intended).
| version: string, | |
| _version: string, |
| /* eslint-disable */ | ||
|
|
||
| import { |
There was a problem hiding this comment.
This test file disables ESLint entirely (/* eslint-disable */). That makes it easy to miss real issues (unused vars, unsafe patterns) and is inconsistent with the rest of the codebase. Prefer targeted disables for specific rules/lines instead of disabling everything for the whole file.
No description provided.