Skip to content

License: Add new licensing mechanism#33206

Open
ajivanyandev wants to merge 47 commits intoDevExpress:26_1from
ajivanyandev:license/26-1-full-pipeline
Open

License: Add new licensing mechanism#33206
ajivanyandev wants to merge 47 commits intoDevExpress:26_1from
ajivanyandev:license/26-1-full-pipeline

Conversation

@ajivanyandev
Copy link
Copy Markdown
Contributor

No description provided.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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-license CLI + supporting Node-side license key discovery/conversion utilities (LCX → LCP).
  • Add an unplugin-based bundler plugin to replace a license-key placeholder in m_config at 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);
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
process.exit(0);
process.exit(1);

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +85
else if(a === '--cwd') out.cwd = args[++i] || process.cwd();
else if(a.startsWith('--cwd=')) out.cwd = a.slice('--cwd='.length);
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

--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 -).

Suggested change
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;
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +40
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{}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
const lcp = resolveLcpSafe(this);
if(!lcp) return null;

return { code: code.split(PLACEHOLDER).join(lcp), map: null };
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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 };

Copilot uses AI. Check for mistakes.
useLegacyVisibleIndex: false,
versionAssertions: [],
copyStylesToShadowDom: true,
licenseKey: '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */',
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
licenseKey: '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */',
licenseKey: String.raw`/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */`,

Copilot uses AI. Check for mistakes.
Comment on lines 29 to 39
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;
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines 124 to 135
@@ -189,20 +135,21 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
'js/viz/docs/*',
'node_modules/*',
'build/*',
'license/*',
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
'license/*',

Copilot uses AI. Check for mistakes.

export function logLicenseWarning(
warningType: LicenseWarningType,
version: string,
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
version: string,
_version: string,

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
/* eslint-disable */

import {
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 9, 2026 22:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants