From b136f46307beaf5b77cea2c21691a6c6bd499560 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 4 Feb 2026 16:25:22 +0400 Subject: [PATCH 01/48] add modules to retrieve lcx key and convert lcx to lcp --- packages/devextreme/build/npm-bin/get-lcx.js | 98 ++++++++++++++ packages/devextreme/build/npm-bin/lcx2lcp.js | 128 +++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 packages/devextreme/build/npm-bin/get-lcx.js create mode 100644 packages/devextreme/build/npm-bin/lcx2lcp.js diff --git a/packages/devextreme/build/npm-bin/get-lcx.js b/packages/devextreme/build/npm-bin/get-lcx.js new file mode 100644 index 000000000000..171133d40653 --- /dev/null +++ b/packages/devextreme/build/npm-bin/get-lcx.js @@ -0,0 +1,98 @@ +"use strict"; + +const fs = require("fs"); +const os = require("os"); +const path = require("path"); + +const LICENSE_ENV = "DevExpress_License"; +const LICENSE_PATH_ENV = "DevExpress_LicensePath"; +const LICENSE_FILE = "DevExpress_License.txt"; + +function isNonEmptyString(v) { + return typeof v === "string" && v.trim().length > 0; +} + +function readTextFileIfExists(filePath) { + try { + if (!filePath) return null; + if (!fs.existsSync(filePath)) return null; + const stat = fs.statSync(filePath); + if (!stat.isFile()) return null; + const raw = fs.readFileSync(filePath, "utf8"); + return isNonEmptyString(raw) ? raw : null; + } catch { + return null; + } +} + +function normalizeKey(raw) { + if (!isNonEmptyString(raw)) return null; + const lines = raw + .split(/\r?\n/) + .map((l) => l.trim()) + .filter(Boolean); + + if (lines.length === 0) return null; + const lcxLike = lines.find((l) => l.startsWith("LCX")); + return (lcxLike || lines[0]).trim(); +} + +function getDefaultLicenseFilePath() { + const home = os.homedir(); + + if (process.platform === "win32") { + const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming"); + return path.join(appData, "DevExpress", LICENSE_FILE); + } + + if (process.platform === "darwin") { + return path.join( + home, + "Library", + "Application Support", + "DevExpress", + LICENSE_FILE + ); + } + + return path.join(home, ".config", "DevExpress", LICENSE_FILE); +} + +function resolveFromLicensePathEnv(licensePathValue) { + if (!isNonEmptyString(licensePathValue)) return null; + + const p = licensePathValue.trim(); + + try { + if (fs.existsSync(p)) { + const stat = fs.statSync(p); + if (stat.isFile()) return p; + if (stat.isDirectory()) return path.join(p, LICENSE_FILE); + } + } catch {} + + if (p.toLowerCase().endsWith(".txt")) return p; + return path.join(p, LICENSE_FILE); +} + +function getDevExpressLCXKey() { + // 1) env DevExpress_License + const envKey = normalizeKey(process.env[LICENSE_ENV]); + if (envKey) return { key: envKey, source: `env:${LICENSE_ENV}` }; + + // 2) env DevExpress_LicensePath + const licensePath = resolveFromLicensePathEnv(process.env[LICENSE_PATH_ENV]); + const fromCustom = normalizeKey(readTextFileIfExists(licensePath)); + if (fromCustom) return { key: fromCustom, source: `file:${licensePath}` }; + + // 3) default OS location + const defaultPath = getDefaultLicenseFilePath(); + const fromDefault = normalizeKey(readTextFileIfExists(defaultPath)); + if (fromDefault) return { key: fromDefault, source: `file:${defaultPath}` }; + + return { key: null, source: null }; +} + +module.exports = { + getDevExpressLCXKey, +}; \ No newline at end of file diff --git a/packages/devextreme/build/npm-bin/lcx2lcp.js b/packages/devextreme/build/npm-bin/lcx2lcp.js new file mode 100644 index 000000000000..6a746c079992 --- /dev/null +++ b/packages/devextreme/build/npm-bin/lcx2lcp.js @@ -0,0 +1,128 @@ +"use strict"; + +const LCX_SIGNATURE = "LCXv1"; +const LCP_SIGNATURE = "LCPv1"; +const SIGN_LENGTH = 68 * 2; // 136 chars + +const ENCODE_MAP_STR = + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" + + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F" + + '\x20x\x220]qA\'u`U?.wOCLyJnz@$*DmsMhlW/T)dKHQ+jNEa6G:VZk9!p>%e7i3S5\\^=P&(Ic,2#rtgYojOv\\$]m)JncBVsi state.s.length) { + throw new Error("Invalid license data"); + } + state.pos = end; + return state.s.slice(start, end); +} + +function safeBase64ToUtf8(b64) { + try { + return Buffer.from(b64, "base64").toString("utf8"); + } catch { + throw new Error("Invalid license data"); + } +} + +function convertLCXtoLCP(licenseString) { + assertNonEmptyString(licenseString, "licenseString"); + const input = licenseString.trim(); + + if (!input.startsWith(LCX_SIGNATURE)) { + throw new Error("Unsupported license format"); + } + + const base64Part = input.slice(LCX_SIGNATURE.length); + const lcx = safeBase64ToUtf8(base64Part); + + if (lcx.length < SIGN_LENGTH) { + throw new Error("Invalid license data"); + } + + const lcxData = decode(lcx.slice(SIGN_LENGTH)); + const state = { s: lcxData, pos: 0 }; + const signProducts = readString(state, SIGN_LENGTH); + + void readString(state); + const productsString = readString(state); + + const payloadText = signProducts + productsString; + const payloadB64 = Buffer.from(payloadText, "utf8").toString("base64"); + const encoded = encode(payloadB64); + + return LCP_SIGNATURE + encoded; +} + +function tryConvertLCXtoLCP(licenseString) { + try { + return convertLCXtoLCP(licenseString); + } catch { + return null; + } +} + +module.exports = { + convertLCXtoLCP, + tryConvertLCXtoLCP, + LCX_SIGNATURE, + LCP_SIGNATURE, +}; From c886711af0c26570b5b7f10ee4643c3212a3c331 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 4 Feb 2026 22:49:23 +0400 Subject: [PATCH 02/48] Add CLI command to write LCP to the given output file --- .../build/npm-bin/devextreme-license.js | 167 ++++++++++++++++++ packages/devextreme/package.json | 3 +- 2 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 packages/devextreme/build/npm-bin/devextreme-license.js diff --git a/packages/devextreme/build/npm-bin/devextreme-license.js b/packages/devextreme/build/npm-bin/devextreme-license.js new file mode 100644 index 000000000000..ecab4acff18e --- /dev/null +++ b/packages/devextreme/build/npm-bin/devextreme-license.js @@ -0,0 +1,167 @@ +#!/usr/bin/env node +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +const { getDevExpressLCXKey } = require("./get-lcx"); +const { convertLCXtoLCP } = require("./lcx2lcp"); + +const EXPORT_NAME = "LICENSE_KEY"; + +function fail(msg, code = 1) { + process.stderr.write(msg.endsWith("\n") ? msg : msg + "\n"); + process.exit(code); +} + +function printHelp() { + process.stdout.write( + [ + "Usage:", + " devextreme-license --out [options]", + "", + "Options:", + " --out Output file path (required)", + " --no-gitignore Do not modify .gitignore", + " --force Overwrite existing output file", + " --cwd Project root (default: process.cwd())", + " -h, --help Show help", + "", + "Example:", + ' "prebuild": "devextreme-license --out src/.devextreme/license-key.ts"', + "", + ].join("\n") + ); +} + +function parseArgs(argv) { + const args = argv.slice(2); + const out = { + outPath: null, + gitignore: true, + force: false, + cwd: process.cwd(), + help: false, + }; + + for (let i = 0; i < args.length; i++) { + const a = args[i]; + + if (a === "-h" || a === "--help") out.help = true; + else if (a === "--out") out.outPath = args[++i] || null; + else if (a.startsWith("--out=")) out.outPath = a.slice("--out=".length); + else if (a === "--no-gitignore") out.gitignore = false; + else if (a === "--force") out.force = true; + else if (a === "--cwd") out.cwd = args[++i] || process.cwd(); + else if (a.startsWith("--cwd=")) out.cwd = a.slice("--cwd=".length); + else fail(`Unknown argument: ${a}\nRun devextreme-license --help`); + } + + return out; +} + +function ensureDirExists(dirPath) { + fs.mkdirSync(dirPath, { recursive: true }); +} + +function readTextIfExists(filePath) { + try { + if (!fs.existsSync(filePath)) return null; + return fs.readFileSync(filePath, "utf8"); + } catch { + return null; + } +} + +function writeFileAtomic(filePath, content) { + const dir = path.dirname(filePath); + const base = path.basename(filePath); + const tmp = path.join(dir, `.${base}.${process.pid}.${Date.now()}.tmp`); + fs.writeFileSync(tmp, content, "utf8"); + fs.renameSync(tmp, filePath); +} + +function toPosixPath(p) { + return p.split(path.sep).join("/"); +} + +function addToGitignore(projectRoot, outAbsPath) { + const gitignorePath = path.join(projectRoot, ".gitignore"); + + let rel = path.relative(projectRoot, outAbsPath); + if (rel.startsWith("..")) return; + + rel = toPosixPath(rel).trim(); + + const existing = readTextIfExists(gitignorePath); + if (existing == null) { + writeFileAtomic(gitignorePath, rel + "\n"); + return; + } + + const lines = existing.split(/\r?\n/).map((l) => l.trim()); + if (lines.includes(rel) || lines.includes("/" + rel)) return; + + const needsNewline = existing.length > 0 && !existing.endsWith("\n"); + fs.appendFileSync(gitignorePath, (needsNewline ? "\n" : "") + rel + "\n", "utf8"); +} + +function renderTsFile(lcpKey) { + return [ + "// Auto-generated by devextreme-license.", + "// Do not commit this file to source control.", + "", + `export const ${EXPORT_NAME} = ${JSON.stringify(lcpKey)} as const;`, + "", + ].join("\n"); +} + +function main() { + const opts = parseArgs(process.argv); + if (opts.help) { + printHelp(); + process.exit(0); + } + + if (!opts.outPath) { + fail("Missing required --out \nRun devextreme-license --help"); + } + + // Resolve LCX + const { key: lcx } = getDevExpressLCXKey(); + if (!lcx) { + fail( + "DevExpress license key (LCX) was not found on this machine.\n" + + "Set DevExpress_License env var or place DevExpress_License.txt in the standard location." + ); + } + + // Convert to LCP + let lcp; + try { + lcp = convertLCXtoLCP(lcx); + } catch { + fail("DevExpress license key was found but could not be converted to LCP."); + } + + const projectRoot = path.resolve(opts.cwd); + const outAbs = path.resolve(projectRoot, opts.outPath); + + ensureDirExists(path.dirname(outAbs)); + + if (!opts.force && fs.existsSync(outAbs)) { + fail(`Output file already exists: ${opts.outPath}\nUse --force to overwrite.`); + } + + writeFileAtomic(outAbs, renderTsFile(lcp)); + + if (opts.gitignore) { + try { + addToGitignore(projectRoot, outAbs); + } catch {} + } + + process.exit(0); +} + +main(); diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index a9568663760e..cd494208e908 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -254,7 +254,8 @@ }, "bin": { "devextreme-bundler-init": "bin/bundler-init.js", - "devextreme-bundler": "bin/bundler.js" + "devextreme-bundler": "bin/bundler.js", + "devextreme-license": "bin/devextreme-license.js" }, "browserslist": [ "last 2 Chrome versions", From 9d579ac98cfb936de89ed0fe75c3338f1ca5c106 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Wed, 4 Feb 2026 23:59:20 +0100 Subject: [PATCH 03/48] add LCP bundler-plugin --- packages/devextreme/build/gulp/npm.js | 2 +- .../npm-bin/bundler-plugin/plugin-dx.d.ts | 7 ++ .../build/npm-bin/bundler-plugin/plugin-dx.js | 74 +++++++++++++++++++ packages/devextreme/package.json | 3 +- pnpm-lock.yaml | 15 +++- 5 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.d.ts create mode 100644 packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.js diff --git a/packages/devextreme/build/gulp/npm.js b/packages/devextreme/build/gulp/npm.js index 9f7f9d5e67bd..024e709ce458 100644 --- a/packages/devextreme/build/gulp/npm.js +++ b/packages/devextreme/build/gulp/npm.js @@ -107,7 +107,7 @@ const sources = (src, dist, distGlob) => (() => merge( .pipe(gulp.dest(dist)), gulp - .src('build/npm-bin/*.js') + .src('build/npm-bin/**/*.js') .pipe(eol('\n')) .pipe(gulp.dest(`${dist}/bin`)), diff --git a/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.d.ts b/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.d.ts new file mode 100644 index 000000000000..a2c775ca7c76 --- /dev/null +++ b/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.d.ts @@ -0,0 +1,7 @@ +declare module './plugin-dx.js' { + const plugin: { + vite: (...args: any[]) => any; + }; + export default plugin; +} +export {}; diff --git a/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.js b/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.js new file mode 100644 index 000000000000..394efb3f5013 --- /dev/null +++ b/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.js @@ -0,0 +1,74 @@ +import path from 'node:path'; +import { createUnplugin } from 'unplugin'; + +const LICENSE_FILE_PATH = 'devextreme-license.js'; +const DEFAULT_PLACEHOLDER = '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */'; + +function normalizeFilePath(filePath) { + return path.resolve(filePath).replace(/\\/g, '/').toLowerCase(); +} + +export default createUnplugin((options = {}) => { + const placeholder = options.placeholder ?? DEFAULT_PLACEHOLDER; + + let cachedLcpKey; + let cachedLcpKeyPromise; + + async function resolveLcpKey() { + if (cachedLcpKey) { + return cachedLcpKey; + } + if (cachedLcpKeyPromise) { + return cachedLcpKeyPromise; + } + + cachedLcpKeyPromise = (async () => { + let lcpKey ='=================@@@@@@@@@@@_LCP_Key_@@@@@@@@@====================='; + + return lcpKey; + })(); + + return cachedLcpKeyPromise; + } + + return { + name: 'devextreme-bundler-plugin', + enforce: 'pre', + transformInclude(id) { + return typeof id === 'string' && id.endsWith(LICENSE_FILE_PATH); + }, + async transform(code, id) { + try { + const targetFile = path.resolve(process.cwd(), normalizeFilePath(LICENSE_FILE_PATH)); + + if (!targetFile || !placeholder) { + return null; + } + + const normalizedId = normalizeFilePath(id.split('?')[0]); + const normalizedTarget = normalizeFilePath(targetFile); + + if (normalizedId !== normalizedTarget) { + return null; + } + + if (!code.includes(placeholder)) { + return null; + } + + const lcpKey = await resolveLcpKey(); + if (!lcpKey) { + return code; + } + + const modifedCode = code.split(placeholder).join(String(lcpKey)); + + return modifedCode; + } catch (error) { + console.warn('[devextreme-bundler-plugin] Failed.', error); + } + + return code; + } + }; +}); diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index cd494208e908..cce8403fe77a 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -63,7 +63,8 @@ "inferno-create-element": "catalog:", "inferno-hydrate": "catalog:", "jszip": "^3.10.1", - "rrule": "^2.7.1" + "rrule": "^2.7.1", + "unplugin": "^3.0.0" }, "devDependencies": { "@babel/core": "7.29.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0c8481e9aa2..7d5eabb092c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1370,6 +1370,9 @@ importers: rrule: specifier: ^2.7.1 version: 2.8.1 + unplugin: + specifier: ^3.0.0 + version: 3.0.0 devDependencies: '@babel/core': specifier: 7.29.0 @@ -17655,6 +17658,10 @@ packages: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} + unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + unquote@1.1.1: resolution: {integrity: sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==} @@ -23558,7 +23565,7 @@ snapshots: dependencies: '@babel/core': 7.29.0 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.31 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 @@ -41887,6 +41894,12 @@ snapshots: picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 + unplugin@3.0.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + unquote@1.1.1: {} unrs-resolver@1.11.1: From 448a14a392ca837c32da69859a98cb7e4b828144 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 10 Feb 2026 14:30:42 +0400 Subject: [PATCH 04/48] redefined plugin structure --- packages/devextreme/build/gulp/npm.js | 2 +- .../license/devextreme-license-plugin.d.ts | 17 ++++ .../license/devextreme-license-plugin.js | 81 +++++++++++++++++++ .../{ => license}/devextreme-license.js | 11 +-- .../{get-lcx.js => license/dx-get-lcx.js} | 0 .../{lcx2lcp.js => license/dx-lcx-2-lcp.js} | 0 .../devextreme/js/__internal/core/m_config.ts | 1 + packages/devextreme/package.json | 8 +- 8 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts create mode 100644 packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js rename packages/devextreme/build/npm-bin/{ => license}/devextreme-license.js (94%) rename packages/devextreme/build/npm-bin/{get-lcx.js => license/dx-get-lcx.js} (100%) rename packages/devextreme/build/npm-bin/{lcx2lcp.js => license/dx-lcx-2-lcp.js} (100%) diff --git a/packages/devextreme/build/gulp/npm.js b/packages/devextreme/build/gulp/npm.js index 024e709ce458..6d5f65a119f9 100644 --- a/packages/devextreme/build/gulp/npm.js +++ b/packages/devextreme/build/gulp/npm.js @@ -107,7 +107,7 @@ const sources = (src, dist, distGlob) => (() => merge( .pipe(gulp.dest(dist)), gulp - .src('build/npm-bin/**/*.js') + .src(['build/npm-bin/**/*.js', 'build/npm-bin/**/*.d.ts']) .pipe(eol('\n')) .pipe(gulp.dest(`${dist}/bin`)), diff --git a/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts b/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts new file mode 100644 index 000000000000..65dcc92e95da --- /dev/null +++ b/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts @@ -0,0 +1,17 @@ +import type { UnpluginInstance } from 'unplugin'; + +export const DevExtremeLicensePlugin: UnpluginInstance; + +export const vite: UnpluginInstance['vite']; +export const rollup: UnpluginInstance['rollup']; +export const webpack: UnpluginInstance['webpack']; +export const esbuild: UnpluginInstance['esbuild']; + +declare const _default: { + vite: UnpluginInstance['vite']; + rollup: UnpluginInstance['rollup']; + webpack: UnpluginInstance['webpack']; + esbuild: UnpluginInstance['esbuild']; +}; + +export default _default; diff --git a/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js b/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js new file mode 100644 index 000000000000..959c4beee949 --- /dev/null +++ b/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js @@ -0,0 +1,81 @@ +"use strict"; + +const { createUnplugin } = require("unplugin"); +const { getDevExpressLCXKey } = require("./dx-get-lcx"); +const { tryConvertLCXtoLCP } = require("./dx-lcx-2-lcp"); + +const PLACEHOLDER = "/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */"; +// Target only the specific config file to avoid scanning all files during build +const TARGET_FILE_PATTERN = /[\/\\]__internal[\/\\]core[\/\\]m_config\.(ts|js)$/; + +const DevExtremeLicensePlugin = createUnplugin(() => { + let resolvedOnce = false; + let lcpCache = null; + let warnedOnce = false; + + function warnOnce(ctx, msg) { + if (warnedOnce) return; + warnedOnce = true; + try { + if (ctx && typeof ctx.warn === "function") ctx.warn(msg); + } catch {} + } + + function resolveLcpSafe(ctx) { + if (resolvedOnce) return lcpCache; + resolvedOnce = true; + + try { + const { key: lcx } = getDevExpressLCXKey(); + if (!lcx) { + warnOnce(ctx, "[devextreme-bundler-plugin] LCX not found. Placeholder will remain."); + return (lcpCache = null); + } + + const lcp = tryConvertLCXtoLCP(lcx); + if (!lcp) { + warnOnce(ctx, "[devextreme-bundler-plugin] LCX->LCP conversion failed. Placeholder will remain."); + return (lcpCache = null); + } + + return (lcpCache = lcp); + } catch { + warnOnce(ctx, "[devextreme-bundler-plugin] Failed to resolve license key. Placeholder will remain."); + return (lcpCache = null); + } + } + + return { + name: 'devextreme-bundler-plugin', + enforce: 'pre', + transform(code, id) { + try { + if (!TARGET_FILE_PATTERN.test(id)) return null; + if (typeof code !== "string") return null; + if (code.indexOf(PLACEHOLDER) === -1) return null; + + const lcp = resolveLcpSafe(this); + if (!lcp) return null; + + return { code: code.split(PLACEHOLDER).join(lcp), map: null }; + } catch { + warnOnce(this, "[devextreme-bundler-plugin] Patch error. Placeholder will remain."); + return null; + } + }, + }; +}); + +module.exports = { + DevExtremeLicensePlugin, + vite: DevExtremeLicensePlugin.vite, + rollup: DevExtremeLicensePlugin.rollup, + webpack: DevExtremeLicensePlugin.webpack, + esbuild: DevExtremeLicensePlugin.esbuild, + default: { + vite: DevExtremeLicensePlugin.vite, + rollup: DevExtremeLicensePlugin.rollup, + webpack: DevExtremeLicensePlugin.webpack, + esbuild: DevExtremeLicensePlugin.esbuild, + }, +}; diff --git a/packages/devextreme/build/npm-bin/devextreme-license.js b/packages/devextreme/build/npm-bin/license/devextreme-license.js similarity index 94% rename from packages/devextreme/build/npm-bin/devextreme-license.js rename to packages/devextreme/build/npm-bin/license/devextreme-license.js index ecab4acff18e..90a07bf6a687 100644 --- a/packages/devextreme/build/npm-bin/devextreme-license.js +++ b/packages/devextreme/build/npm-bin/license/devextreme-license.js @@ -4,14 +4,14 @@ const fs = require("fs"); const path = require("path"); -const { getDevExpressLCXKey } = require("./get-lcx"); -const { convertLCXtoLCP } = require("./lcx2lcp"); +const { getDevExpressLCXKey } = require("./dx-get-lcx"); +const { convertLCXtoLCP } = require("./dx-lcx-2-lcp"); const EXPORT_NAME = "LICENSE_KEY"; -function fail(msg, code = 1) { +function fail(msg) { process.stderr.write(msg.endsWith("\n") ? msg : msg + "\n"); - process.exit(code); + process.exit(0); } function printHelp() { @@ -54,7 +54,8 @@ function parseArgs(argv) { else if (a === "--force") out.force = true; else if (a === "--cwd") out.cwd = args[++i] || process.cwd(); else if (a.startsWith("--cwd=")) out.cwd = a.slice("--cwd=".length); - else fail(`Unknown argument: ${a}\nRun devextreme-license --help`); + else fai + l(`Unknown argument: ${a}\nRun devextreme-license --help`); } return out; diff --git a/packages/devextreme/build/npm-bin/get-lcx.js b/packages/devextreme/build/npm-bin/license/dx-get-lcx.js similarity index 100% rename from packages/devextreme/build/npm-bin/get-lcx.js rename to packages/devextreme/build/npm-bin/license/dx-get-lcx.js diff --git a/packages/devextreme/build/npm-bin/lcx2lcp.js b/packages/devextreme/build/npm-bin/license/dx-lcx-2-lcp.js similarity index 100% rename from packages/devextreme/build/npm-bin/lcx2lcp.js rename to packages/devextreme/build/npm-bin/license/dx-lcx-2-lcp.js diff --git a/packages/devextreme/js/__internal/core/m_config.ts b/packages/devextreme/js/__internal/core/m_config.ts index 9f556bad406b..74de307bb315 100644 --- a/packages/devextreme/js/__internal/core/m_config.ts +++ b/packages/devextreme/js/__internal/core/m_config.ts @@ -19,6 +19,7 @@ const config = { useLegacyVisibleIndex: false, versionAssertions: [], copyStylesToShadowDom: true, + licenseKey: '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */', floatingActionButtonConfig: { icon: 'add', diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index cce8403fe77a..55109cbc2100 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -256,7 +256,7 @@ "bin": { "devextreme-bundler-init": "bin/bundler-init.js", "devextreme-bundler": "bin/bundler.js", - "devextreme-license": "bin/devextreme-license.js" + "devextreme-license": "bin/license/devextreme-license.js" }, "browserslist": [ "last 2 Chrome versions", @@ -271,5 +271,11 @@ "publishConfig": { "directory": "artifacts/npm/devextreme", "linkDirectory": true + }, + "exports": { + "./license/devextreme-license-plugin": { + "types": "./bin/license/devextreme-license-plugin.d.ts", + "default": "./bin/license/devextreme-license-plugin.js" + } } } From b299af283c504c8fea18744aa90bafd5c0e1ab6e Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 10 Feb 2026 16:38:13 +0400 Subject: [PATCH 05/48] fix file structure --- packages/devextreme/build/gulp/npm.js | 7 +- .../npm-bin/bundler-plugin/plugin-dx.d.ts | 7 -- .../build/npm-bin/bundler-plugin/plugin-dx.js | 74 ------------------- .../npm-bin/license/devextreme-license.js | 3 +- packages/devextreme/package.json | 8 +- 5 files changed, 8 insertions(+), 91 deletions(-) delete mode 100644 packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.d.ts delete mode 100644 packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.js diff --git a/packages/devextreme/build/gulp/npm.js b/packages/devextreme/build/gulp/npm.js index 6d5f65a119f9..eef75928f29a 100644 --- a/packages/devextreme/build/gulp/npm.js +++ b/packages/devextreme/build/gulp/npm.js @@ -107,10 +107,15 @@ const sources = (src, dist, distGlob) => (() => merge( .pipe(gulp.dest(dist)), gulp - .src(['build/npm-bin/**/*.js', 'build/npm-bin/**/*.d.ts']) + .src(['build/npm-bin/**/*.js', '!build/npm-bin/license/**']) .pipe(eol('\n')) .pipe(gulp.dest(`${dist}/bin`)), + gulp + .src(['build/npm-bin/license/**']) + .pipe(eol('\n')) + .pipe(gulp.dest(`${dist}/license`)), + gulp .src('webpack.config.js') .pipe(gulp.dest(`${dist}/bin`)), diff --git a/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.d.ts b/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.d.ts deleted file mode 100644 index a2c775ca7c76..000000000000 --- a/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare module './plugin-dx.js' { - const plugin: { - vite: (...args: any[]) => any; - }; - export default plugin; -} -export {}; diff --git a/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.js b/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.js deleted file mode 100644 index 394efb3f5013..000000000000 --- a/packages/devextreme/build/npm-bin/bundler-plugin/plugin-dx.js +++ /dev/null @@ -1,74 +0,0 @@ -import path from 'node:path'; -import { createUnplugin } from 'unplugin'; - -const LICENSE_FILE_PATH = 'devextreme-license.js'; -const DEFAULT_PLACEHOLDER = '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */'; - -function normalizeFilePath(filePath) { - return path.resolve(filePath).replace(/\\/g, '/').toLowerCase(); -} - -export default createUnplugin((options = {}) => { - const placeholder = options.placeholder ?? DEFAULT_PLACEHOLDER; - - let cachedLcpKey; - let cachedLcpKeyPromise; - - async function resolveLcpKey() { - if (cachedLcpKey) { - return cachedLcpKey; - } - if (cachedLcpKeyPromise) { - return cachedLcpKeyPromise; - } - - cachedLcpKeyPromise = (async () => { - let lcpKey ='=================@@@@@@@@@@@_LCP_Key_@@@@@@@@@====================='; - - return lcpKey; - })(); - - return cachedLcpKeyPromise; - } - - return { - name: 'devextreme-bundler-plugin', - enforce: 'pre', - transformInclude(id) { - return typeof id === 'string' && id.endsWith(LICENSE_FILE_PATH); - }, - async transform(code, id) { - try { - const targetFile = path.resolve(process.cwd(), normalizeFilePath(LICENSE_FILE_PATH)); - - if (!targetFile || !placeholder) { - return null; - } - - const normalizedId = normalizeFilePath(id.split('?')[0]); - const normalizedTarget = normalizeFilePath(targetFile); - - if (normalizedId !== normalizedTarget) { - return null; - } - - if (!code.includes(placeholder)) { - return null; - } - - const lcpKey = await resolveLcpKey(); - if (!lcpKey) { - return code; - } - - const modifedCode = code.split(placeholder).join(String(lcpKey)); - - return modifedCode; - } catch (error) { - console.warn('[devextreme-bundler-plugin] Failed.', error); - } - - return code; - } - }; -}); diff --git a/packages/devextreme/build/npm-bin/license/devextreme-license.js b/packages/devextreme/build/npm-bin/license/devextreme-license.js index 90a07bf6a687..3b8a2085e20f 100644 --- a/packages/devextreme/build/npm-bin/license/devextreme-license.js +++ b/packages/devextreme/build/npm-bin/license/devextreme-license.js @@ -54,8 +54,7 @@ function parseArgs(argv) { else if (a === "--force") out.force = true; else if (a === "--cwd") out.cwd = args[++i] || process.cwd(); else if (a.startsWith("--cwd=")) out.cwd = a.slice("--cwd=".length); - else fai - l(`Unknown argument: ${a}\nRun devextreme-license --help`); + else fail(`Unknown argument: ${a}\nRun devextreme-license --help`); } return out; diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index 55109cbc2100..0078b0eba625 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -256,7 +256,7 @@ "bin": { "devextreme-bundler-init": "bin/bundler-init.js", "devextreme-bundler": "bin/bundler.js", - "devextreme-license": "bin/license/devextreme-license.js" + "devextreme-license": "license/devextreme-license.js" }, "browserslist": [ "last 2 Chrome versions", @@ -271,11 +271,5 @@ "publishConfig": { "directory": "artifacts/npm/devextreme", "linkDirectory": true - }, - "exports": { - "./license/devextreme-license-plugin": { - "types": "./bin/license/devextreme-license-plugin.d.ts", - "default": "./bin/license/devextreme-license-plugin.js" - } } } From ea372db761d77e9d97cc4ce56d63112a980dbc95 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 10 Feb 2026 16:41:20 +0400 Subject: [PATCH 06/48] remove unnecessary exports --- .../npm-bin/license/devextreme-license-plugin.d.ts | 13 ------------- .../npm-bin/license/devextreme-license-plugin.js | 10 ---------- 2 files changed, 23 deletions(-) diff --git a/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts b/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts index 65dcc92e95da..1f18fbe0b62b 100644 --- a/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts +++ b/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts @@ -2,16 +2,3 @@ import type { UnpluginInstance } from 'unplugin'; export const DevExtremeLicensePlugin: UnpluginInstance; -export const vite: UnpluginInstance['vite']; -export const rollup: UnpluginInstance['rollup']; -export const webpack: UnpluginInstance['webpack']; -export const esbuild: UnpluginInstance['esbuild']; - -declare const _default: { - vite: UnpluginInstance['vite']; - rollup: UnpluginInstance['rollup']; - webpack: UnpluginInstance['webpack']; - esbuild: UnpluginInstance['esbuild']; -}; - -export default _default; diff --git a/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js b/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js index 959c4beee949..c8b84950ce2d 100644 --- a/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js +++ b/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js @@ -68,14 +68,4 @@ const DevExtremeLicensePlugin = createUnplugin(() => { module.exports = { DevExtremeLicensePlugin, - vite: DevExtremeLicensePlugin.vite, - rollup: DevExtremeLicensePlugin.rollup, - webpack: DevExtremeLicensePlugin.webpack, - esbuild: DevExtremeLicensePlugin.esbuild, - default: { - vite: DevExtremeLicensePlugin.vite, - rollup: DevExtremeLicensePlugin.rollup, - webpack: DevExtremeLicensePlugin.webpack, - esbuild: DevExtremeLicensePlugin.esbuild, - }, }; From 12b87ee26800d1cd4c348983e984640d903eba2d Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 10 Feb 2026 16:54:25 +0400 Subject: [PATCH 07/48] change file structure --- packages/devextreme/build/gulp/npm.js | 4 +- .../license/devextreme-license-plugin.js | 71 -------- .../npm-bin/license/devextreme-license.js | 167 ------------------ .../build/npm-bin/license/dx-get-lcx.js | 98 ---------- .../build/npm-bin/license/dx-lcx-2-lcp.js | 128 -------------- packages/devextreme/eslint.config.mjs | 1 + .../license/devextreme-license-plugin.d.ts | 0 .../license/devextreme-license-plugin.js | 71 ++++++++ .../devextreme/license/devextreme-license.js | 167 ++++++++++++++++++ packages/devextreme/license/dx-get-lcx.js | 98 ++++++++++ packages/devextreme/license/dx-lcx-2-lcp.js | 128 ++++++++++++++ 11 files changed, 467 insertions(+), 466 deletions(-) delete mode 100644 packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js delete mode 100644 packages/devextreme/build/npm-bin/license/devextreme-license.js delete mode 100644 packages/devextreme/build/npm-bin/license/dx-get-lcx.js delete mode 100644 packages/devextreme/build/npm-bin/license/dx-lcx-2-lcp.js rename packages/devextreme/{build/npm-bin => }/license/devextreme-license-plugin.d.ts (100%) create mode 100644 packages/devextreme/license/devextreme-license-plugin.js create mode 100644 packages/devextreme/license/devextreme-license.js create mode 100644 packages/devextreme/license/dx-get-lcx.js create mode 100644 packages/devextreme/license/dx-lcx-2-lcp.js diff --git a/packages/devextreme/build/gulp/npm.js b/packages/devextreme/build/gulp/npm.js index eef75928f29a..142f2bb19a62 100644 --- a/packages/devextreme/build/gulp/npm.js +++ b/packages/devextreme/build/gulp/npm.js @@ -107,12 +107,12 @@ const sources = (src, dist, distGlob) => (() => merge( .pipe(gulp.dest(dist)), gulp - .src(['build/npm-bin/**/*.js', '!build/npm-bin/license/**']) + .src(['build/npm-bin/*.js']) .pipe(eol('\n')) .pipe(gulp.dest(`${dist}/bin`)), gulp - .src(['build/npm-bin/license/**']) + .src(['license/**']) .pipe(eol('\n')) .pipe(gulp.dest(`${dist}/license`)), diff --git a/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js b/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js deleted file mode 100644 index c8b84950ce2d..000000000000 --- a/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.js +++ /dev/null @@ -1,71 +0,0 @@ -"use strict"; - -const { createUnplugin } = require("unplugin"); -const { getDevExpressLCXKey } = require("./dx-get-lcx"); -const { tryConvertLCXtoLCP } = require("./dx-lcx-2-lcp"); - -const PLACEHOLDER = "/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */"; -// Target only the specific config file to avoid scanning all files during build -const TARGET_FILE_PATTERN = /[\/\\]__internal[\/\\]core[\/\\]m_config\.(ts|js)$/; - -const DevExtremeLicensePlugin = createUnplugin(() => { - let resolvedOnce = false; - let lcpCache = null; - let warnedOnce = false; - - function warnOnce(ctx, msg) { - if (warnedOnce) return; - warnedOnce = true; - try { - if (ctx && typeof ctx.warn === "function") ctx.warn(msg); - } catch {} - } - - function resolveLcpSafe(ctx) { - if (resolvedOnce) return lcpCache; - resolvedOnce = true; - - try { - const { key: lcx } = getDevExpressLCXKey(); - if (!lcx) { - warnOnce(ctx, "[devextreme-bundler-plugin] LCX not found. Placeholder will remain."); - return (lcpCache = null); - } - - const lcp = tryConvertLCXtoLCP(lcx); - if (!lcp) { - warnOnce(ctx, "[devextreme-bundler-plugin] LCX->LCP conversion failed. Placeholder will remain."); - return (lcpCache = null); - } - - return (lcpCache = lcp); - } catch { - warnOnce(ctx, "[devextreme-bundler-plugin] Failed to resolve license key. Placeholder will remain."); - return (lcpCache = null); - } - } - - return { - name: 'devextreme-bundler-plugin', - enforce: 'pre', - transform(code, id) { - try { - if (!TARGET_FILE_PATTERN.test(id)) return null; - if (typeof code !== "string") return null; - if (code.indexOf(PLACEHOLDER) === -1) return null; - - const lcp = resolveLcpSafe(this); - if (!lcp) return null; - - return { code: code.split(PLACEHOLDER).join(lcp), map: null }; - } catch { - warnOnce(this, "[devextreme-bundler-plugin] Patch error. Placeholder will remain."); - return null; - } - }, - }; -}); - -module.exports = { - DevExtremeLicensePlugin, -}; diff --git a/packages/devextreme/build/npm-bin/license/devextreme-license.js b/packages/devextreme/build/npm-bin/license/devextreme-license.js deleted file mode 100644 index 3b8a2085e20f..000000000000 --- a/packages/devextreme/build/npm-bin/license/devextreme-license.js +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env node -"use strict"; - -const fs = require("fs"); -const path = require("path"); - -const { getDevExpressLCXKey } = require("./dx-get-lcx"); -const { convertLCXtoLCP } = require("./dx-lcx-2-lcp"); - -const EXPORT_NAME = "LICENSE_KEY"; - -function fail(msg) { - process.stderr.write(msg.endsWith("\n") ? msg : msg + "\n"); - process.exit(0); -} - -function printHelp() { - process.stdout.write( - [ - "Usage:", - " devextreme-license --out [options]", - "", - "Options:", - " --out Output file path (required)", - " --no-gitignore Do not modify .gitignore", - " --force Overwrite existing output file", - " --cwd Project root (default: process.cwd())", - " -h, --help Show help", - "", - "Example:", - ' "prebuild": "devextreme-license --out src/.devextreme/license-key.ts"', - "", - ].join("\n") - ); -} - -function parseArgs(argv) { - const args = argv.slice(2); - const out = { - outPath: null, - gitignore: true, - force: false, - cwd: process.cwd(), - help: false, - }; - - for (let i = 0; i < args.length; i++) { - const a = args[i]; - - if (a === "-h" || a === "--help") out.help = true; - else if (a === "--out") out.outPath = args[++i] || null; - else if (a.startsWith("--out=")) out.outPath = a.slice("--out=".length); - else if (a === "--no-gitignore") out.gitignore = false; - else if (a === "--force") out.force = true; - else if (a === "--cwd") out.cwd = args[++i] || process.cwd(); - else if (a.startsWith("--cwd=")) out.cwd = a.slice("--cwd=".length); - else fail(`Unknown argument: ${a}\nRun devextreme-license --help`); - } - - return out; -} - -function ensureDirExists(dirPath) { - fs.mkdirSync(dirPath, { recursive: true }); -} - -function readTextIfExists(filePath) { - try { - if (!fs.existsSync(filePath)) return null; - return fs.readFileSync(filePath, "utf8"); - } catch { - return null; - } -} - -function writeFileAtomic(filePath, content) { - const dir = path.dirname(filePath); - const base = path.basename(filePath); - const tmp = path.join(dir, `.${base}.${process.pid}.${Date.now()}.tmp`); - fs.writeFileSync(tmp, content, "utf8"); - fs.renameSync(tmp, filePath); -} - -function toPosixPath(p) { - return p.split(path.sep).join("/"); -} - -function addToGitignore(projectRoot, outAbsPath) { - const gitignorePath = path.join(projectRoot, ".gitignore"); - - let rel = path.relative(projectRoot, outAbsPath); - if (rel.startsWith("..")) return; - - rel = toPosixPath(rel).trim(); - - const existing = readTextIfExists(gitignorePath); - if (existing == null) { - writeFileAtomic(gitignorePath, rel + "\n"); - return; - } - - const lines = existing.split(/\r?\n/).map((l) => l.trim()); - if (lines.includes(rel) || lines.includes("/" + rel)) return; - - const needsNewline = existing.length > 0 && !existing.endsWith("\n"); - fs.appendFileSync(gitignorePath, (needsNewline ? "\n" : "") + rel + "\n", "utf8"); -} - -function renderTsFile(lcpKey) { - return [ - "// Auto-generated by devextreme-license.", - "// Do not commit this file to source control.", - "", - `export const ${EXPORT_NAME} = ${JSON.stringify(lcpKey)} as const;`, - "", - ].join("\n"); -} - -function main() { - const opts = parseArgs(process.argv); - if (opts.help) { - printHelp(); - process.exit(0); - } - - if (!opts.outPath) { - fail("Missing required --out \nRun devextreme-license --help"); - } - - // Resolve LCX - const { key: lcx } = getDevExpressLCXKey(); - if (!lcx) { - fail( - "DevExpress license key (LCX) was not found on this machine.\n" + - "Set DevExpress_License env var or place DevExpress_License.txt in the standard location." - ); - } - - // Convert to LCP - let lcp; - try { - lcp = convertLCXtoLCP(lcx); - } catch { - fail("DevExpress license key was found but could not be converted to LCP."); - } - - const projectRoot = path.resolve(opts.cwd); - const outAbs = path.resolve(projectRoot, opts.outPath); - - ensureDirExists(path.dirname(outAbs)); - - if (!opts.force && fs.existsSync(outAbs)) { - fail(`Output file already exists: ${opts.outPath}\nUse --force to overwrite.`); - } - - writeFileAtomic(outAbs, renderTsFile(lcp)); - - if (opts.gitignore) { - try { - addToGitignore(projectRoot, outAbs); - } catch {} - } - - process.exit(0); -} - -main(); diff --git a/packages/devextreme/build/npm-bin/license/dx-get-lcx.js b/packages/devextreme/build/npm-bin/license/dx-get-lcx.js deleted file mode 100644 index 171133d40653..000000000000 --- a/packages/devextreme/build/npm-bin/license/dx-get-lcx.js +++ /dev/null @@ -1,98 +0,0 @@ -"use strict"; - -const fs = require("fs"); -const os = require("os"); -const path = require("path"); - -const LICENSE_ENV = "DevExpress_License"; -const LICENSE_PATH_ENV = "DevExpress_LicensePath"; -const LICENSE_FILE = "DevExpress_License.txt"; - -function isNonEmptyString(v) { - return typeof v === "string" && v.trim().length > 0; -} - -function readTextFileIfExists(filePath) { - try { - if (!filePath) return null; - if (!fs.existsSync(filePath)) return null; - const stat = fs.statSync(filePath); - if (!stat.isFile()) return null; - const raw = fs.readFileSync(filePath, "utf8"); - return isNonEmptyString(raw) ? raw : null; - } catch { - return null; - } -} - -function normalizeKey(raw) { - if (!isNonEmptyString(raw)) return null; - const lines = raw - .split(/\r?\n/) - .map((l) => l.trim()) - .filter(Boolean); - - if (lines.length === 0) return null; - const lcxLike = lines.find((l) => l.startsWith("LCX")); - return (lcxLike || lines[0]).trim(); -} - -function getDefaultLicenseFilePath() { - const home = os.homedir(); - - if (process.platform === "win32") { - const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming"); - return path.join(appData, "DevExpress", LICENSE_FILE); - } - - if (process.platform === "darwin") { - return path.join( - home, - "Library", - "Application Support", - "DevExpress", - LICENSE_FILE - ); - } - - return path.join(home, ".config", "DevExpress", LICENSE_FILE); -} - -function resolveFromLicensePathEnv(licensePathValue) { - if (!isNonEmptyString(licensePathValue)) return null; - - const p = licensePathValue.trim(); - - try { - if (fs.existsSync(p)) { - const stat = fs.statSync(p); - if (stat.isFile()) return p; - if (stat.isDirectory()) return path.join(p, LICENSE_FILE); - } - } catch {} - - if (p.toLowerCase().endsWith(".txt")) return p; - return path.join(p, LICENSE_FILE); -} - -function getDevExpressLCXKey() { - // 1) env DevExpress_License - const envKey = normalizeKey(process.env[LICENSE_ENV]); - if (envKey) return { key: envKey, source: `env:${LICENSE_ENV}` }; - - // 2) env DevExpress_LicensePath - const licensePath = resolveFromLicensePathEnv(process.env[LICENSE_PATH_ENV]); - const fromCustom = normalizeKey(readTextFileIfExists(licensePath)); - if (fromCustom) return { key: fromCustom, source: `file:${licensePath}` }; - - // 3) default OS location - const defaultPath = getDefaultLicenseFilePath(); - const fromDefault = normalizeKey(readTextFileIfExists(defaultPath)); - if (fromDefault) return { key: fromDefault, source: `file:${defaultPath}` }; - - return { key: null, source: null }; -} - -module.exports = { - getDevExpressLCXKey, -}; \ No newline at end of file diff --git a/packages/devextreme/build/npm-bin/license/dx-lcx-2-lcp.js b/packages/devextreme/build/npm-bin/license/dx-lcx-2-lcp.js deleted file mode 100644 index 6a746c079992..000000000000 --- a/packages/devextreme/build/npm-bin/license/dx-lcx-2-lcp.js +++ /dev/null @@ -1,128 +0,0 @@ -"use strict"; - -const LCX_SIGNATURE = "LCXv1"; -const LCP_SIGNATURE = "LCPv1"; -const SIGN_LENGTH = 68 * 2; // 136 chars - -const ENCODE_MAP_STR = - "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" + - "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F" + - '\x20x\x220]qA\'u`U?.wOCLyJnz@$*DmsMhlW/T)dKHQ+jNEa6G:VZk9!p>%e7i3S5\\^=P&(Ic,2#rtgYojOv\\$]m)JncBVsi state.s.length) { - throw new Error("Invalid license data"); - } - state.pos = end; - return state.s.slice(start, end); -} - -function safeBase64ToUtf8(b64) { - try { - return Buffer.from(b64, "base64").toString("utf8"); - } catch { - throw new Error("Invalid license data"); - } -} - -function convertLCXtoLCP(licenseString) { - assertNonEmptyString(licenseString, "licenseString"); - const input = licenseString.trim(); - - if (!input.startsWith(LCX_SIGNATURE)) { - throw new Error("Unsupported license format"); - } - - const base64Part = input.slice(LCX_SIGNATURE.length); - const lcx = safeBase64ToUtf8(base64Part); - - if (lcx.length < SIGN_LENGTH) { - throw new Error("Invalid license data"); - } - - const lcxData = decode(lcx.slice(SIGN_LENGTH)); - const state = { s: lcxData, pos: 0 }; - const signProducts = readString(state, SIGN_LENGTH); - - void readString(state); - const productsString = readString(state); - - const payloadText = signProducts + productsString; - const payloadB64 = Buffer.from(payloadText, "utf8").toString("base64"); - const encoded = encode(payloadB64); - - return LCP_SIGNATURE + encoded; -} - -function tryConvertLCXtoLCP(licenseString) { - try { - return convertLCXtoLCP(licenseString); - } catch { - return null; - } -} - -module.exports = { - convertLCXtoLCP, - tryConvertLCXtoLCP, - LCX_SIGNATURE, - LCP_SIGNATURE, -}; diff --git a/packages/devextreme/eslint.config.mjs b/packages/devextreme/eslint.config.mjs index 7922693e11fd..01944618ad23 100644 --- a/packages/devextreme/eslint.config.mjs +++ b/packages/devextreme/eslint.config.mjs @@ -34,6 +34,7 @@ export default [ 'js/viz/docs/*', 'node_modules/*', 'build/*', + 'license/*', '**/*.j.tsx', 'playground/*', 'themebuilder/data/metadata/*', diff --git a/packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts b/packages/devextreme/license/devextreme-license-plugin.d.ts similarity index 100% rename from packages/devextreme/build/npm-bin/license/devextreme-license-plugin.d.ts rename to packages/devextreme/license/devextreme-license-plugin.d.ts diff --git a/packages/devextreme/license/devextreme-license-plugin.js b/packages/devextreme/license/devextreme-license-plugin.js new file mode 100644 index 000000000000..5c0f21c92e41 --- /dev/null +++ b/packages/devextreme/license/devextreme-license-plugin.js @@ -0,0 +1,71 @@ + + +const { createUnplugin } = require('unplugin'); +const { getDevExpressLCXKey } = require('./dx-get-lcx'); +const { tryConvertLCXtoLCP } = require('./dx-lcx-2-lcp'); + +const PLACEHOLDER = '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */'; +// Target only the specific config file to avoid scanning all files during build +const TARGET_FILE_PATTERN = /[\/\\]__internal[\/\\]core[\/\\]m_config\.(ts|js)$/; + +const DevExtremeLicensePlugin = createUnplugin(() => { + let resolvedOnce = false; + let lcpCache = null; + let warnedOnce = false; + + function warnOnce(ctx, msg) { + if(warnedOnce) return; + warnedOnce = true; + try { + if(ctx && typeof ctx.warn === 'function') ctx.warn(msg); + } catch{} + } + + function resolveLcpSafe(ctx) { + if(resolvedOnce) return lcpCache; + resolvedOnce = true; + + try { + const { key: lcx } = getDevExpressLCXKey(); + if(!lcx) { + warnOnce(ctx, '[devextreme-bundler-plugin] LCX not found. Placeholder will remain.'); + return (lcpCache = null); + } + + const lcp = tryConvertLCXtoLCP(lcx); + if(!lcp) { + warnOnce(ctx, '[devextreme-bundler-plugin] LCX->LCP conversion failed. Placeholder will remain.'); + return (lcpCache = null); + } + + return (lcpCache = lcp); + } catch{ + warnOnce(ctx, '[devextreme-bundler-plugin] Failed to resolve license key. Placeholder will remain.'); + return (lcpCache = null); + } + } + + return { + name: 'devextreme-bundler-plugin', + enforce: 'pre', + transform(code, id) { + try { + if(!TARGET_FILE_PATTERN.test(id)) return null; + if(typeof code !== 'string') return null; + if(code.indexOf(PLACEHOLDER) === -1) return null; + + const lcp = resolveLcpSafe(this); + if(!lcp) return null; + + return { code: code.split(PLACEHOLDER).join(lcp), map: null }; + } catch{ + warnOnce(this, '[devextreme-bundler-plugin] Patch error. Placeholder will remain.'); + return null; + } + }, + }; +}); + +module.exports = { + DevExtremeLicensePlugin, +}; diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js new file mode 100644 index 000000000000..070ec351949b --- /dev/null +++ b/packages/devextreme/license/devextreme-license.js @@ -0,0 +1,167 @@ +#!/usr/bin/env node + + +const fs = require('fs'); +const path = require('path'); + +const { getDevExpressLCXKey } = require('./dx-get-lcx'); +const { convertLCXtoLCP } = require('./dx-lcx-2-lcp'); + +const EXPORT_NAME = 'LICENSE_KEY'; + +function fail(msg) { + process.stderr.write(msg.endsWith('\n') ? msg : msg + '\n'); + process.exit(0); +} + +function printHelp() { + process.stdout.write( + [ + 'Usage:', + ' devextreme-license --out [options]', + '', + 'Options:', + ' --out Output file path (required)', + ' --no-gitignore Do not modify .gitignore', + ' --force Overwrite existing output file', + ' --cwd Project root (default: process.cwd())', + ' -h, --help Show help', + '', + 'Example:', + ' "prebuild": "devextreme-license --out src/.devextreme/license-key.ts"', + '', + ].join('\n') + ); +} + +function parseArgs(argv) { + const args = argv.slice(2); + const out = { + outPath: null, + gitignore: true, + force: false, + cwd: process.cwd(), + help: false, + }; + + for(let i = 0; i < args.length; i++) { + const a = args[i]; + + if(a === '-h' || a === '--help') out.help = true; + else if(a === '--out') out.outPath = args[++i] || null; + else if(a.startsWith('--out=')) out.outPath = a.slice('--out='.length); + else if(a === '--no-gitignore') out.gitignore = false; + else if(a === '--force') out.force = true; + else if(a === '--cwd') out.cwd = args[++i] || process.cwd(); + else if(a.startsWith('--cwd=')) out.cwd = a.slice('--cwd='.length); + else fail(`Unknown argument: ${a}\nRun devextreme-license --help`); + } + + return out; +} + +function ensureDirExists(dirPath) { + fs.mkdirSync(dirPath, { recursive: true }); +} + +function readTextIfExists(filePath) { + try { + if(!fs.existsSync(filePath)) return null; + return fs.readFileSync(filePath, 'utf8'); + } catch{ + return null; + } +} + +function writeFileAtomic(filePath, content) { + const dir = path.dirname(filePath); + const base = path.basename(filePath); + const tmp = path.join(dir, `.${base}.${process.pid}.${Date.now()}.tmp`); + fs.writeFileSync(tmp, content, 'utf8'); + fs.renameSync(tmp, filePath); +} + +function toPosixPath(p) { + return p.split(path.sep).join('/'); +} + +function addToGitignore(projectRoot, outAbsPath) { + const gitignorePath = path.join(projectRoot, '.gitignore'); + + let rel = path.relative(projectRoot, outAbsPath); + if(rel.startsWith('..')) return; + + rel = toPosixPath(rel).trim(); + + const existing = readTextIfExists(gitignorePath); + if(existing == null) { + writeFileAtomic(gitignorePath, rel + '\n'); + return; + } + + const lines = existing.split(/\r?\n/).map((l) => l.trim()); + if(lines.includes(rel) || lines.includes('/' + rel)) return; + + const needsNewline = existing.length > 0 && !existing.endsWith('\n'); + fs.appendFileSync(gitignorePath, (needsNewline ? '\n' : '') + rel + '\n', 'utf8'); +} + +function renderTsFile(lcpKey) { + return [ + '// Auto-generated by devextreme-license.', + '// Do not commit this file to source control.', + '', + `export const ${EXPORT_NAME} = ${JSON.stringify(lcpKey)} as const;`, + '', + ].join('\n'); +} + +function main() { + const opts = parseArgs(process.argv); + if(opts.help) { + printHelp(); + process.exit(0); + } + + if(!opts.outPath) { + fail('Missing required --out \nRun devextreme-license --help'); + } + + // Resolve LCX + const { key: lcx } = getDevExpressLCXKey(); + if(!lcx) { + fail( + 'DevExpress license key (LCX) was not found on this machine.\n' + + 'Set DevExpress_License env var or place DevExpress_License.txt in the standard location.' + ); + } + + // Convert to LCP + let lcp; + try { + lcp = convertLCXtoLCP(lcx); + } catch{ + fail('DevExpress license key was found but could not be converted to LCP.'); + } + + const projectRoot = path.resolve(opts.cwd); + const outAbs = path.resolve(projectRoot, opts.outPath); + + ensureDirExists(path.dirname(outAbs)); + + if(!opts.force && fs.existsSync(outAbs)) { + fail(`Output file already exists: ${opts.outPath}\nUse --force to overwrite.`); + } + + writeFileAtomic(outAbs, renderTsFile(lcp)); + + if(opts.gitignore) { + try { + addToGitignore(projectRoot, outAbs); + } catch{} + } + + process.exit(0); +} + +main(); diff --git a/packages/devextreme/license/dx-get-lcx.js b/packages/devextreme/license/dx-get-lcx.js new file mode 100644 index 000000000000..3b81168ff900 --- /dev/null +++ b/packages/devextreme/license/dx-get-lcx.js @@ -0,0 +1,98 @@ + + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const LICENSE_ENV = 'DevExpress_License'; +const LICENSE_PATH_ENV = 'DevExpress_LicensePath'; +const LICENSE_FILE = 'DevExpress_License.txt'; + +function isNonEmptyString(v) { + return typeof v === 'string' && v.trim().length > 0; +} + +function readTextFileIfExists(filePath) { + try { + if(!filePath) return null; + if(!fs.existsSync(filePath)) return null; + const stat = fs.statSync(filePath); + if(!stat.isFile()) return null; + const raw = fs.readFileSync(filePath, 'utf8'); + return isNonEmptyString(raw) ? raw : null; + } catch{ + return null; + } +} + +function normalizeKey(raw) { + if(!isNonEmptyString(raw)) return null; + const lines = raw + .split(/\r?\n/) + .map((l) => l.trim()) + .filter(Boolean); + + if(lines.length === 0) return null; + const lcxLike = lines.find((l) => l.startsWith('LCX')); + return (lcxLike || lines[0]).trim(); +} + +function getDefaultLicenseFilePath() { + const home = os.homedir(); + + if(process.platform === 'win32') { + const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming'); + return path.join(appData, 'DevExpress', LICENSE_FILE); + } + + if(process.platform === 'darwin') { + return path.join( + home, + 'Library', + 'Application Support', + 'DevExpress', + LICENSE_FILE + ); + } + + return path.join(home, '.config', 'DevExpress', LICENSE_FILE); +} + +function resolveFromLicensePathEnv(licensePathValue) { + if(!isNonEmptyString(licensePathValue)) return null; + + const p = licensePathValue.trim(); + + try { + if(fs.existsSync(p)) { + const stat = fs.statSync(p); + if(stat.isFile()) return p; + if(stat.isDirectory()) return path.join(p, LICENSE_FILE); + } + } catch{} + + if(p.toLowerCase().endsWith('.txt')) return p; + return path.join(p, LICENSE_FILE); +} + +function getDevExpressLCXKey() { + // 1) env DevExpress_License + const envKey = normalizeKey(process.env[LICENSE_ENV]); + if(envKey) return { key: envKey, source: `env:${LICENSE_ENV}` }; + + // 2) env DevExpress_LicensePath + const licensePath = resolveFromLicensePathEnv(process.env[LICENSE_PATH_ENV]); + const fromCustom = normalizeKey(readTextFileIfExists(licensePath)); + if(fromCustom) return { key: fromCustom, source: `file:${licensePath}` }; + + // 3) default OS location + const defaultPath = getDefaultLicenseFilePath(); + const fromDefault = normalizeKey(readTextFileIfExists(defaultPath)); + if(fromDefault) return { key: fromDefault, source: `file:${defaultPath}` }; + + return { key: null, source: null }; +} + +module.exports = { + getDevExpressLCXKey, +}; diff --git a/packages/devextreme/license/dx-lcx-2-lcp.js b/packages/devextreme/license/dx-lcx-2-lcp.js new file mode 100644 index 000000000000..167dbb900805 --- /dev/null +++ b/packages/devextreme/license/dx-lcx-2-lcp.js @@ -0,0 +1,128 @@ + + +const LCX_SIGNATURE = 'LCXv1'; +const LCP_SIGNATURE = 'LCPv1'; +const SIGN_LENGTH = 68 * 2; // 136 chars + +const ENCODE_MAP_STR = + '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F' + + '\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F' + + '\x20x\x220]qA\'u`U?.wOCLyJnz@$*DmsMhlW/T)dKHQ+jNEa6G:VZk9!p>%e7i3S5\\^=P&(Ic,2#rtgYojOv\\$]m)JncBVsi state.s.length) { + throw new Error('Invalid license data'); + } + state.pos = end; + return state.s.slice(start, end); +} + +function safeBase64ToUtf8(b64) { + try { + return Buffer.from(b64, 'base64').toString('utf8'); + } catch{ + throw new Error('Invalid license data'); + } +} + +function convertLCXtoLCP(licenseString) { + assertNonEmptyString(licenseString, 'licenseString'); + const input = licenseString.trim(); + + if(!input.startsWith(LCX_SIGNATURE)) { + throw new Error('Unsupported license format'); + } + + const base64Part = input.slice(LCX_SIGNATURE.length); + const lcx = safeBase64ToUtf8(base64Part); + + if(lcx.length < SIGN_LENGTH) { + throw new Error('Invalid license data'); + } + + const lcxData = decode(lcx.slice(SIGN_LENGTH)); + const state = { s: lcxData, pos: 0 }; + const signProducts = readString(state, SIGN_LENGTH); + + void readString(state); + const productsString = readString(state); + + const payloadText = signProducts + productsString; + const payloadB64 = Buffer.from(payloadText, 'utf8').toString('base64'); + const encoded = encode(payloadB64); + + return LCP_SIGNATURE + encoded; +} + +function tryConvertLCXtoLCP(licenseString) { + try { + return convertLCXtoLCP(licenseString); + } catch{ + return null; + } +} + +module.exports = { + convertLCXtoLCP, + tryConvertLCXtoLCP, + LCX_SIGNATURE, + LCP_SIGNATURE, +}; From 642664df4e0e40c907f342c536739339a14f00ba Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 10 Feb 2026 16:56:00 +0400 Subject: [PATCH 08/48] small fix --- packages/devextreme/build/gulp/npm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/build/gulp/npm.js b/packages/devextreme/build/gulp/npm.js index 142f2bb19a62..271e204aeca9 100644 --- a/packages/devextreme/build/gulp/npm.js +++ b/packages/devextreme/build/gulp/npm.js @@ -107,7 +107,7 @@ const sources = (src, dist, distGlob) => (() => merge( .pipe(gulp.dest(dist)), gulp - .src(['build/npm-bin/*.js']) + .src('build/npm-bin/*.js') .pipe(eol('\n')) .pipe(gulp.dest(`${dist}/bin`)), From 281cbee6dbdd1121de7e66a7531f4b6d25bda168 Mon Sep 17 00:00:00 2001 From: Vasily Strelyaev Date: Tue, 18 Nov 2025 11:10:57 +0200 Subject: [PATCH 09/48] DX product key parsing for the client --- .../js/__internal/core/license/byte_utils.ts | 10 ++ .../js/__internal/core/license/const.ts | 9 ++ .../core/license/lcp_key_validation/const.ts | 4 + .../lcp_key_validation.test.ts | 40 +++++++ .../lcp_key_validation/lcp_key_validator.ts | 107 ++++++++++++++++++ .../lcp_key_validation/license_info.ts | 24 ++++ .../lcp_key_validation/product_info.ts | 23 ++++ .../core/license/lcp_key_validation/types.ts | 47 ++++++++ .../core/license/lcp_key_validation/utils.ts | 69 +++++++++++ .../core/license/license_validation.ts | 41 ++++--- .../js/__internal/core/license/rsa_bigint.ts | 8 +- .../js/__internal/core/license/types.ts | 26 ++++- 12 files changed, 374 insertions(+), 34 deletions(-) create mode 100644 packages/devextreme/js/__internal/core/license/const.ts create mode 100644 packages/devextreme/js/__internal/core/license/lcp_key_validation/const.ts create mode 100644 packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts create mode 100644 packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts create mode 100644 packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts create mode 100644 packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts create mode 100644 packages/devextreme/js/__internal/core/license/lcp_key_validation/types.ts create mode 100644 packages/devextreme/js/__internal/core/license/lcp_key_validation/utils.ts diff --git a/packages/devextreme/js/__internal/core/license/byte_utils.ts b/packages/devextreme/js/__internal/core/license/byte_utils.ts index d35693361dbf..8c34b0316984 100644 --- a/packages/devextreme/js/__internal/core/license/byte_utils.ts +++ b/packages/devextreme/js/__internal/core/license/byte_utils.ts @@ -48,6 +48,16 @@ export function leftRotate(x: number, n: number): number { return ((x << n) | (x >>> (32 - n))) >>> 0; } +export function bigIntFromBytes(bytes: Uint8Array): bigint { + const eight = BigInt(8); + const zero = BigInt(0); + + return bytes.reduce( + (acc, cur) => (acc << eight) + BigInt(cur), + zero, + ); +} + export function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array { const result = new Uint8Array(a.length + b.length); result.set(a, 0); diff --git a/packages/devextreme/js/__internal/core/license/const.ts b/packages/devextreme/js/__internal/core/license/const.ts new file mode 100644 index 000000000000..ad1ba4de7b11 --- /dev/null +++ b/packages/devextreme/js/__internal/core/license/const.ts @@ -0,0 +1,9 @@ +export const FORMAT = 1; +export const RTM_MIN_PATCH_VERSION = 3; +export const KEY_SPLITTER = '.'; + +export const BUY_NOW_LINK = 'https://go.devexpress.com/Licensing_Installer_Watermark_DevExtremeJQuery.aspx'; +export const LICENSING_DOC_LINK = 'https://go.devexpress.com/Licensing_Documentation_DevExtremeJQuery.aspx'; + +export const NBSP = '\u00A0'; +export const SUBSCRIPTION_NAMES = `Universal, DXperience, ASP.NET${NBSP}and${NBSP}Blazor, DevExtreme${NBSP}Complete`; diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/const.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/const.ts new file mode 100644 index 000000000000..4b0700026051 --- /dev/null +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/const.ts @@ -0,0 +1,4 @@ +export const LCP_SIGNATURE = 'LCPv1'; +export const SIGN_LENGTH = 68 * 2; // 136 characters +export const DECODE_MAP = '\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000a\u000b\u000c\u000d\u000e\u000f\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f\u0020R\u0022f6U`\'aA7Fdp,?#yeYx[KWwQMqk^T+5&r/8ItLDb2C0;H._ElZ@*N>ojOv\u005c$]m)JncBVsi { + it('serializer returns an invalid license for malformed input', () => { + const token = parseDevExpressProductKey('not-a-real-license'); + expect(token.kind).toBe(TokenKind.corrupted); + }); + + it('developer product license fixtures parse into valid LicenseInfo instances', () => { + const key = process.env.DX_PRODUCT_KEY ?? RAW_DEVELOPER_PRODUCT_LICENSE; + const token = parseDevExpressProductKey(key); + expect(token.kind).toBe(TokenKind.verified); + }); + + it('trial fallback does not grant product access', () => { + const trialLicense = getTrialLicense(); + expect(trialLicense.isValid).toBe(true); + + const version = trialLicense.findLatestDevExtremeVersion(); + + expect(version).toBe(undefined); + }); +}); diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts new file mode 100644 index 000000000000..4a2010107771 --- /dev/null +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts @@ -0,0 +1,107 @@ +import { FORMAT } from '../const'; +import { + DESERIALIZATION_ERROR, + type ErrorToken, + GENERAL_ERROR, + PRODUCT_KIND_ERROR, + type Token, + TokenKind, + VERIFICATION_ERROR, +} from '../types'; +import { + LCP_SIGNATURE, + RSA_PUBLIC_KEY_XML, + SIGN_LENGTH, +} from './const'; +import { LicenseInfo } from './license_info'; +import { ProductInfo } from './product_info'; +import { encodeString, shiftDecodeText, verifyHash } from './utils'; + +interface ParsedProducts { + products: ProductInfo[]; + errorToken?: ErrorToken; +} + +export function isProductOnlyLicense(license: string): boolean { + return typeof license === 'string' && license.startsWith(LCP_SIGNATURE); +} + +function productsFromString(encodedString: string): ParsedProducts { + if (!encodedString) { + return { + products: [], + errorToken: GENERAL_ERROR, + }; + } + + try { + const splitInfo = encodedString.split(';'); + const productTuples = splitInfo.slice(1).filter((entry) => entry.length > 0); + const products = productTuples.map((tuple) => { + const parts = tuple.split(','); + const version = Number.parseInt(parts[0], 10); + const productsValue = BigInt(parts[1]); + return new ProductInfo( + version, + productsValue, + ); + }); + + return { + products, + }; + } catch (error) { + return { + products: [], + errorToken: DESERIALIZATION_ERROR, + }; + } +} + +export function parseDevExpressProductKey(productsLicenseSource: string): Token { + if (isProductOnlyLicense(productsLicenseSource)) { + return GENERAL_ERROR; + } + + try { + const productsLicense = atob( + shiftDecodeText(productsLicenseSource.substring(LCP_SIGNATURE.length)), + ); + + const signature = productsLicense.substring(0, SIGN_LENGTH); + const productsPayload = productsLicense.substring(SIGN_LENGTH); + + if (!verifyHash(RSA_PUBLIC_KEY_XML, productsPayload, signature)) { + return VERIFICATION_ERROR; + } + + const { + products, + errorToken, + } = productsFromString( + encodeString(productsPayload, shiftDecodeText), + ); + + if (errorToken) { + return errorToken; + } + + const licenseInfo = new LicenseInfo(products); + const maxVersionAllowed = licenseInfo.findLatestDevExtremeVersion(); + + if (!maxVersionAllowed) { + return PRODUCT_KIND_ERROR; + } + + return { + kind: TokenKind.verified, + payload: { + customerId: '', + maxVersionAllowed, + format: FORMAT, + }, + }; + } catch (error) { + return GENERAL_ERROR; + } +} diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts new file mode 100644 index 000000000000..631704d01278 --- /dev/null +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts @@ -0,0 +1,24 @@ +import type { ProductInfo } from './product_info'; +import { ProductKind } from './types'; + +export class LicenseInfo { + public readonly products: ProductInfo[]; + + constructor(products: ProductInfo[] = []) { + this.products = products; + } + + get isValid(): boolean { + return Array.isArray(this.products) && this.products.length > 0; + } + + findLatestDevExtremeVersion(): number | undefined { + if (!this.isValid) { + return undefined; + } + + const sorted = [...this.products].sort((a, b) => b.version - a.version); + + return sorted.find((p) => p.isProduct(ProductKind.DevExtremeHtmlJs))?.version; + } +} diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts new file mode 100644 index 000000000000..6cd62ee47c12 --- /dev/null +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts @@ -0,0 +1,23 @@ +/* eslint-disable no-bitwise */ +export class ProductInfo { + public version: number; + + public products: bigint; + + constructor( + version: number, + products: bigint, + ) { + this.version = version; + this.products = BigInt(products); + } + + isProduct(...productIds: bigint[]): boolean { + if (productIds.length === 1) { + const flag = BigInt(productIds[0]); + return (this.products & flag) === flag; + } + + return productIds.some((id) => (this.products & BigInt(id)) === BigInt(id)); + } +} diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/types.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/types.ts new file mode 100644 index 000000000000..79918aa58294 --- /dev/null +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/types.ts @@ -0,0 +1,47 @@ +/* eslint-disable spellcheck/spell-checker, no-bitwise */ +import { bit } from './utils'; + +const productKind = { + Default: 0n, + DXperienceWin: bit(0), + XtraReports: bit(4), + XPO: bit(15), + DevExtremeAspNet: bit(17), + DXperienceASP: bit(25), + XAF: bit(28), + Blazor: bit(31), + DXperienceWPF: bit(38), + DocsBasic: bit(39), + Dashboard: bit(47), + Snap: bit(49), + DevExtremeHtmlJs: bit(54), + Docs: bit(55), + XtraReportsWpf: bit(57), + XtraReportsWeb: bit(59), + XtraReportsWin: bit(60), + XtraReportsBlazor: bit(41), + DXperienceEnt: bit(0), + DXperienceUni: bit(0), +}; + +productKind.DXperienceEnt = productKind.Blazor + | productKind.DXperienceWin + | productKind.XtraReports + | productKind.Snap + | productKind.XtraReportsWin + | productKind.XPO + | productKind.DXperienceASP + | productKind.DXperienceWPF + | productKind.XtraReportsWeb + | productKind.XtraReportsWpf + | productKind.XtraReportsBlazor + | productKind.DevExtremeAspNet + | productKind.DevExtremeHtmlJs; + +productKind.DXperienceUni = productKind.DXperienceEnt + | productKind.XAF + | productKind.DXperienceWPF + | productKind.Dashboard + | productKind.Docs; + +export const ProductKind = Object.freeze(productKind); diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/utils.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/utils.ts new file mode 100644 index 000000000000..a851f87fd9e2 --- /dev/null +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/utils.ts @@ -0,0 +1,69 @@ +/* eslint-disable spellcheck/spell-checker, no-bitwise */ +import { base64ToBytes, bigIntFromBytes } from '../byte_utils'; +import type { PublicKey } from '../key'; +import { pad } from '../pkcs1'; +import { compareSignatures } from '../rsa_bigint'; +import { sha1 } from '../sha1'; +import { DECODE_MAP } from './const'; + +export const bit = (shift: number): bigint => 1n << BigInt(shift); + +export const parseRsaXml = (xml: string): { modulus: Uint8Array; exponent: number } => { + const modulusMatch = /([^<]+)<\/Modulus>/.exec(xml); + const exponentMatch = /([^<]+)<\/Exponent>/.exec(xml); + + if (!modulusMatch || !exponentMatch) { + throw new Error('Invalid RSA XML key.'); + } + + return { + modulus: base64ToBytes(modulusMatch[1]), + exponent: Number(bigIntFromBytes(base64ToBytes(exponentMatch[1]))), + }; +}; + +export const encodeString = ( + text: string, + encode: (s: string) => string, +): string => ( + typeof encode === 'function' ? encode(text) : text +); + +export const shiftText = (text: string, map: string): string => { + if (!text) { + return text || ''; + } + + let result = ''; + + for (let i = 0; i < text.length; i += 1) { + const charCode = text.charCodeAt(i); + + if (charCode < map.length) { + result += map[charCode]; + } else { + result += text[i]; + } + } + + return result; +}; + +export const shiftDecodeText = (text: string): string => shiftText(text, DECODE_MAP); + +export const verifyHash = (xmlKey: string, data: string, signature: string): boolean => { + const { modulus, exponent } = parseRsaXml(xmlKey); + + const key: PublicKey = { + n: modulus, + e: exponent, + }; + + const sign = base64ToBytes(signature); + + return compareSignatures({ + key, + signature: sign, + actual: pad(sha1(data)), + }); +}; diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index d70b39e923be..b9add2c91f5a 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -9,7 +9,11 @@ import { parseVersion, } from '../../utils/version'; import { base64ToBytes } from './byte_utils'; +import { + BUY_NOW_LINK, FORMAT, KEY_SPLITTER, LICENSING_DOC_LINK, RTM_MIN_PATCH_VERSION, SUBSCRIPTION_NAMES, +} from './const'; import { INTERNAL_USAGE_ID, PUBLIC_KEY } from './key'; +import { isProductOnlyLicense, parseDevExpressProductKey } from './lcp_key_validation/lcp_key_validator'; import { pad } from './pkcs1'; import { compareSignatures } from './rsa_bigint'; import { sha1 } from './sha1'; @@ -19,30 +23,21 @@ import type { LicenseCheckParams, Token, } from './types'; -import { TokenKind } from './types'; +import { + DECODING_ERROR, + DESERIALIZATION_ERROR, + GENERAL_ERROR, + PAYLOAD_ERROR, + TokenKind, + VERIFICATION_ERROR, + VERSION_ERROR, +} from './types'; interface Payload extends Partial { readonly format?: number; readonly internalUsageId?: string; } -const FORMAT = 1; -const RTM_MIN_PATCH_VERSION = 3; -const KEY_SPLITTER = '.'; - -const BUY_NOW_LINK = 'https://go.devexpress.com/Licensing_Installer_Watermark_DevExtremeJQuery.aspx'; -const LICENSING_DOC_LINK = 'https://go.devexpress.com/Licensing_Documentation_DevExtremeJQuery.aspx'; - -const NBSP = '\u00A0'; -const SUBSCRIPTION_NAMES = `Universal, DXperience, ASP.NET${NBSP}and${NBSP}Blazor, DevExtreme${NBSP}Complete`; - -const GENERAL_ERROR: Token = { kind: TokenKind.corrupted, error: 'general' }; -const VERIFICATION_ERROR: Token = { kind: TokenKind.corrupted, error: 'verification' }; -const DECODING_ERROR: Token = { kind: TokenKind.corrupted, error: 'decoding' }; -const DESERIALIZATION_ERROR: Token = { kind: TokenKind.corrupted, error: 'deserialization' }; -const PAYLOAD_ERROR: Token = { kind: TokenKind.corrupted, error: 'payload' }; -const VERSION_ERROR: Token = { kind: TokenKind.corrupted, error: 'version' }; - let validationPerformed = false; // verifies RSASSA-PKCS1-v1.5 signature @@ -62,6 +57,10 @@ export function parseLicenseKey(encodedKey: string | undefined): Token { return GENERAL_ERROR; } + if (isProductOnlyLicense(encodedKey)) { + return parseDevExpressProductKey(encodedKey); + } + const parts = encodedKey.split(KEY_SPLITTER); if (parts.length !== 2 || parts[0].length === 0 || parts[1].length === 0) { @@ -119,8 +118,8 @@ function isPreview(patch: number): boolean { return isNaN(patch) || patch < RTM_MIN_PATCH_VERSION; } -function isDevExpressLicenseKey(licenseKey: string): boolean { - return licenseKey.startsWith('LCX') || licenseKey.startsWith('LCP'); +function isDevExpressDeveloperKey(licenseKey: string): boolean { + return licenseKey.startsWith('LCX'); } function getLicenseCheckParams({ @@ -141,7 +140,7 @@ function getLicenseCheckParams({ return { preview, error: 'W0019' }; } - if (isDevExpressLicenseKey(licenseKey)) { + if (isDevExpressDeveloperKey(licenseKey)) { return { preview, error: 'W0024' }; } diff --git a/packages/devextreme/js/__internal/core/license/rsa_bigint.ts b/packages/devextreme/js/__internal/core/license/rsa_bigint.ts index 0b830fdf3f23..df0c01650348 100644 --- a/packages/devextreme/js/__internal/core/license/rsa_bigint.ts +++ b/packages/devextreme/js/__internal/core/license/rsa_bigint.ts @@ -1,3 +1,4 @@ +import { bigIntFromBytes } from './byte_utils'; import type { PublicKey } from './key'; interface Args { @@ -7,9 +8,7 @@ interface Args { } export function compareSignatures(args: Args): boolean { try { - const zero = BigInt(0); const one = BigInt(1); - const eight = BigInt(8); const modExp = (base: bigint, exponent: bigint, modulus: bigint): bigint => { let result = one; @@ -27,11 +26,6 @@ export function compareSignatures(args: Args): boolean { return result; }; - const bigIntFromBytes = (bytes: Uint8Array): bigint => bytes.reduce( - (acc, cur) => (acc << eight) + BigInt(cur), // eslint-disable-line no-bitwise - zero, - ); - const actual = bigIntFromBytes(args.actual); const signature = bigIntFromBytes(args.signature); diff --git a/packages/devextreme/js/__internal/core/license/types.ts b/packages/devextreme/js/__internal/core/license/types.ts index c6212ba8eebf..4e38ccd812ee 100644 --- a/packages/devextreme/js/__internal/core/license/types.ts +++ b/packages/devextreme/js/__internal/core/license/types.ts @@ -10,19 +10,33 @@ export enum TokenKind { internal = 'internal', } -export type Token = { +export interface ErrorToken { + readonly kind: TokenKind.corrupted; + readonly error: 'general' | 'verification' | 'decoding' | 'deserialization' | 'payload' | 'version' | 'product-kind'; +} + +export interface VerifiedToken { readonly kind: TokenKind.verified; readonly payload: License; -} | { - readonly kind: TokenKind.corrupted; - readonly error: 'general' | 'verification' | 'decoding' | 'deserialization' | 'payload' | 'version'; -} | { +} + +export interface InternalToken { readonly kind: TokenKind.internal; readonly internalUsageId: string; -}; +} + +export type Token = ErrorToken | VerifiedToken | InternalToken; type LicenseVerifyResult = 'W0019' | 'W0020' | 'W0021' | 'W0022' | 'W0023' | 'W0024'; +export const GENERAL_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'general' }; +export const VERIFICATION_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'verification' }; +export const DECODING_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'decoding' }; +export const DESERIALIZATION_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'deserialization' }; +export const PAYLOAD_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'payload' }; +export const VERSION_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'version' }; +export const PRODUCT_KIND_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'product-kind' }; + export interface LicenseCheckParams { preview: boolean; internal?: true; From c5f719548fd9b2c3ddbcc4719580d5a90c576bbb Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 11 Feb 2026 14:31:22 +0400 Subject: [PATCH 10/48] validation logic fix --- .../core/license/lcp_key_validation/lcp_key_validator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts index 4a2010107771..afc1f00ce6aa 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts @@ -59,7 +59,7 @@ function productsFromString(encodedString: string): ParsedProducts { } export function parseDevExpressProductKey(productsLicenseSource: string): Token { - if (isProductOnlyLicense(productsLicenseSource)) { + if (!isProductOnlyLicense(productsLicenseSource)) { return GENERAL_ERROR; } From 8cb189e0942338256813826093e13ececaa67519 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Fri, 20 Feb 2026 12:33:26 +0400 Subject: [PATCH 11/48] add some errors, little change in key retrieval logic --- .../core/license/license_validation.ts | 27 +++++++++++++++-- packages/devextreme/license/dx-get-lcx.js | 30 +++++++++++-------- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index b9add2c91f5a..0eca24efdcbe 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -118,8 +118,25 @@ function isPreview(patch: number): boolean { return isNaN(patch) || patch < RTM_MIN_PATCH_VERSION; } -function isDevExpressDeveloperKey(licenseKey: string): boolean { - return licenseKey.startsWith('LCX'); +function hasLicensePrefix(licenseKey: string, prefix: string): boolean { + return licenseKey.trim().startsWith(prefix); +} + +export function isUnsupportedKeyFormat(licenseKey: string | undefined): boolean { + if (!licenseKey) { + return false; + } + + if (hasLicensePrefix(licenseKey, 'LCXv1')) { + errors.log('W0000', 'config', 'licenseKey', '0.0', 'LCXv1 is specified in the license key'); + return true; + } + if (hasLicensePrefix(licenseKey, 'egow')) { + errors.log('W0000', 'config', 'licenseKey', '0.0', 'DevExtreme key is specified in the license key'); + return true; + } + + return false; } function getLicenseCheckParams({ @@ -140,7 +157,7 @@ function getLicenseCheckParams({ return { preview, error: 'W0019' }; } - if (isDevExpressDeveloperKey(licenseKey)) { + if (hasLicensePrefix(licenseKey, 'LCX')) { return { preview, error: 'W0024' }; } @@ -174,6 +191,10 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer } validationPerformed = true; + if (isUnsupportedKeyFormat(licenseKey)) { + return; + } + const version = parseVersion(versionStr); const versionsCompatible = assertedVersionsCompatible(version); diff --git a/packages/devextreme/license/dx-get-lcx.js b/packages/devextreme/license/dx-get-lcx.js index 3b81168ff900..f8535e084e18 100644 --- a/packages/devextreme/license/dx-get-lcx.js +++ b/packages/devextreme/license/dx-get-lcx.js @@ -1,5 +1,3 @@ - - const fs = require('fs'); const os = require('os'); const path = require('path'); @@ -12,6 +10,10 @@ function isNonEmptyString(v) { return typeof v === 'string' && v.trim().length > 0; } +function hasEnvVar(name) { + return Object.prototype.hasOwnProperty.call(process.env, name); +} + function readTextFileIfExists(filePath) { try { if(!filePath) return null; @@ -20,7 +22,7 @@ function readTextFileIfExists(filePath) { if(!stat.isFile()) return null; const raw = fs.readFileSync(filePath, 'utf8'); return isNonEmptyString(raw) ? raw : null; - } catch{ + } catch { return null; } } @@ -69,26 +71,28 @@ function resolveFromLicensePathEnv(licensePathValue) { if(stat.isFile()) return p; if(stat.isDirectory()) return path.join(p, LICENSE_FILE); } - } catch{} + } catch {} if(p.toLowerCase().endsWith('.txt')) return p; return path.join(p, LICENSE_FILE); } function getDevExpressLCXKey() { - // 1) env DevExpress_License - const envKey = normalizeKey(process.env[LICENSE_ENV]); - if(envKey) return { key: envKey, source: `env:${LICENSE_ENV}` }; + if(hasEnvVar(LICENSE_ENV)) { + return { key: normalizeKey(process.env[LICENSE_ENV]), source: `env:${LICENSE_ENV}` }; + } - // 2) env DevExpress_LicensePath - const licensePath = resolveFromLicensePathEnv(process.env[LICENSE_PATH_ENV]); - const fromCustom = normalizeKey(readTextFileIfExists(licensePath)); - if(fromCustom) return { key: fromCustom, source: `file:${licensePath}` }; + if(hasEnvVar(LICENSE_PATH_ENV)) { + const licensePath = resolveFromLicensePathEnv(process.env[LICENSE_PATH_ENV]); + const key = normalizeKey(readTextFileIfExists(licensePath)); + return { key, source: key ? `file:${licensePath}` : `env:${LICENSE_PATH_ENV}` }; + } - // 3) default OS location const defaultPath = getDefaultLicenseFilePath(); const fromDefault = normalizeKey(readTextFileIfExists(defaultPath)); - if(fromDefault) return { key: fromDefault, source: `file:${defaultPath}` }; + if(fromDefault) { + return { key: fromDefault, source: `file:${defaultPath}` }; + } return { key: null, source: null }; } From e7e15b708c500f013cf20e2f5e8da0285f0e78e0 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Fri, 20 Feb 2026 12:55:18 +0400 Subject: [PATCH 12/48] change cli logic: make --out param optional --- .../devextreme/license/devextreme-license.js | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index 070ec351949b..ac8f4f5ca7c5 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -7,7 +7,8 @@ const path = require('path'); const { getDevExpressLCXKey } = require('./dx-get-lcx'); const { convertLCXtoLCP } = require('./dx-lcx-2-lcp'); -const EXPORT_NAME = 'LICENSE_KEY'; +const EXPORT_NAME = 'licenseKey'; +const TRIAL_VALUE = 'TRIAL'; function fail(msg) { process.stderr.write(msg.endsWith('\n') ? msg : msg + '\n'); @@ -21,7 +22,7 @@ function printHelp() { ' devextreme-license --out [options]', '', 'Options:', - ' --out Output file path (required)', + ' --out Output file path (optional)', ' --no-gitignore Do not modify .gitignore', ' --force Overwrite existing output file', ' --cwd Project root (default: process.cwd())', @@ -111,7 +112,7 @@ function renderTsFile(lcpKey) { '// Auto-generated by devextreme-license.', '// Do not commit this file to source control.', '', - `export const ${EXPORT_NAME} = ${JSON.stringify(lcpKey)} as const;`, + `export const ${EXPORT_NAME} = ${JSON.stringify(lcpKey)};`, '', ].join('\n'); } @@ -123,25 +124,31 @@ function main() { process.exit(0); } - if(!opts.outPath) { - fail('Missing required --out \nRun devextreme-license --help'); - } + const { key: lcx, source } = getDevExpressLCXKey() || {}; + + process.stdout.write( + `DevExpress license key (LCX) retrieved from: ${source || '(unknown source)'}\n` + ); + + let lcp = TRIAL_VALUE; - // Resolve LCX - const { key: lcx } = getDevExpressLCXKey(); - if(!lcx) { - fail( - 'DevExpress license key (LCX) was not found on this machine.\n' + - 'Set DevExpress_License env var or place DevExpress_License.txt in the standard location.' + if(lcx) { + try { + lcp = convertLCXtoLCP(lcx); + } catch{ + process.stderr.write( + 'DevExpress license key was found but could not be converted to LCP.\n' + ); + } + } else { + process.stderr.write( + 'DevExpress license key (LCX) was not found on this machine.\n' ); } - // Convert to LCP - let lcp; - try { - lcp = convertLCXtoLCP(lcx); - } catch{ - fail('DevExpress license key was found but could not be converted to LCP.'); + if(!opts.outPath) { + process.stdout.write(lcp + '\n'); + process.exit(0); } const projectRoot = path.resolve(opts.cwd); From 0867245c3a9f4c4b203cceb4e2ef859366b32e7a Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Fri, 20 Feb 2026 13:37:43 +0400 Subject: [PATCH 13/48] small fix in warning --- .../js/__internal/core/license/license_validation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index 0eca24efdcbe..5ff163eb7c0e 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -128,11 +128,11 @@ export function isUnsupportedKeyFormat(licenseKey: string | undefined): boolean } if (hasLicensePrefix(licenseKey, 'LCXv1')) { - errors.log('W0000', 'config', 'licenseKey', '0.0', 'LCXv1 is specified in the license key'); + errors.log('W0000', 'config', 'licenseKey', 'LCXv1 is specified in the license key'); return true; } if (hasLicensePrefix(licenseKey, 'egow')) { - errors.log('W0000', 'config', 'licenseKey', '0.0', 'DevExtreme key is specified in the license key'); + errors.log('W0000', 'config', 'licenseKey', 'DevExtreme key is specified in the license key'); return true; } From aad224e40254e194b9bee09b8a2d5b9bd0c4ec97 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Fri, 20 Feb 2026 13:46:52 +0400 Subject: [PATCH 14/48] change trial oanel logic to match new warnings logic --- .../js/__internal/core/license/license_validation.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index 5ff163eb7c0e..00ec7a15071d 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -139,6 +139,12 @@ export function isUnsupportedKeyFormat(licenseKey: string | undefined): boolean return false; } +function displayTrialPanel(): void { + const buyNowLink = config().buyNowLink ?? BUY_NOW_LINK; + const licensingDocLink = config().licensingDocLink ?? LICENSING_DOC_LINK; + showTrialPanel(buyNowLink, licensingDocLink, fullVersion, SUBSCRIPTION_NAMES); +} + function getLicenseCheckParams({ licenseKey, version, @@ -192,6 +198,7 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer validationPerformed = true; if (isUnsupportedKeyFormat(licenseKey)) { + displayTrialPanel(); return; } @@ -209,9 +216,7 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer } if (error && !internal) { - const buyNowLink = config().buyNowLink ?? BUY_NOW_LINK; - const licensingDocLink = config().licensingDocLink ?? LICENSING_DOC_LINK; - showTrialPanel(buyNowLink, licensingDocLink, fullVersion, SUBSCRIPTION_NAMES); + displayTrialPanel(); } const preview = isPreview(version.patch); From 649342dc621c8e609a645045cf7faed7df4cd884 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Fri, 20 Feb 2026 14:04:09 +0400 Subject: [PATCH 15/48] log source when running plugin --- packages/devextreme/license/devextreme-license-plugin.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/license/devextreme-license-plugin.js b/packages/devextreme/license/devextreme-license-plugin.js index 5c0f21c92e41..93d84ed2ece3 100644 --- a/packages/devextreme/license/devextreme-license-plugin.js +++ b/packages/devextreme/license/devextreme-license-plugin.js @@ -1,5 +1,3 @@ - - const { createUnplugin } = require('unplugin'); const { getDevExpressLCXKey } = require('./dx-get-lcx'); const { tryConvertLCXtoLCP } = require('./dx-lcx-2-lcp'); @@ -26,7 +24,11 @@ const DevExtremeLicensePlugin = createUnplugin(() => { resolvedOnce = true; try { - const { key: lcx } = getDevExpressLCXKey(); + const { key: lcx, source } = getDevExpressLCXKey() || {}; + + const sourceMessage = `[devextreme-bundler-plugin] DevExpress license key (LCX) retrieved from: ${source || '(unknown source)'}`; + process.stdout.write(sourceMessage + '\n'); + if(!lcx) { warnOnce(ctx, '[devextreme-bundler-plugin] LCX not found. Placeholder will remain.'); return (lcpCache = null); From 5e2cdaf9dc7c7ab737f0914e1ecc2200e84f538e Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 3 Mar 2026 19:09:41 +0400 Subject: [PATCH 16/48] Add validation on cli and plugin pipelines, minor fixes --- .../license/devextreme-license-plugin.js | 27 +++- .../devextreme/license/devextreme-license.js | 21 ++- packages/devextreme/license/dx-lcx-2-lcp.js | 120 ++++++++++++++++++ packages/devextreme/license/messages.js | 26 ++++ 4 files changed, 176 insertions(+), 18 deletions(-) create mode 100644 packages/devextreme/license/messages.js diff --git a/packages/devextreme/license/devextreme-license-plugin.js b/packages/devextreme/license/devextreme-license-plugin.js index 93d84ed2ece3..c594cc907f3f 100644 --- a/packages/devextreme/license/devextreme-license-plugin.js +++ b/packages/devextreme/license/devextreme-license-plugin.js @@ -1,6 +1,7 @@ const { createUnplugin } = require('unplugin'); const { getDevExpressLCXKey } = require('./dx-get-lcx'); -const { tryConvertLCXtoLCP } = require('./dx-lcx-2-lcp'); +const { tryConvertLCXtoLCP, getLCPWarning } = require('./dx-lcx-2-lcp'); +const { MESSAGES } = require('./messages'); const PLACEHOLDER = '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */'; // Target only the specific config file to avoid scanning all files during build @@ -26,23 +27,35 @@ const DevExtremeLicensePlugin = createUnplugin(() => { try { const { key: lcx, source } = getDevExpressLCXKey() || {}; - const sourceMessage = `[devextreme-bundler-plugin] DevExpress license key (LCX) retrieved from: ${source || '(unknown source)'}`; - process.stdout.write(sourceMessage + '\n'); - if(!lcx) { - warnOnce(ctx, '[devextreme-bundler-plugin] LCX not found. Placeholder will remain.'); + warnOnce(ctx, `[devextreme-bundler-plugin] Warning: ${MESSAGES.keyNotFound}`); return (lcpCache = null); } const lcp = tryConvertLCXtoLCP(lcx); if(!lcp) { - warnOnce(ctx, '[devextreme-bundler-plugin] LCX->LCP conversion failed. Placeholder will remain.'); + try { + if(ctx && typeof ctx.warn === 'function') { + ctx.warn(`[devextreme-bundler-plugin] DevExpress license key (LCX) retrieved from: ${source}`); + ctx.warn(`[devextreme-bundler-plugin] Warning: ${MESSAGES.keyNotFound}`); + } + } catch{} return (lcpCache = null); } + const warning = getLCPWarning(lcp); + if(warning) { + try { + if(ctx && typeof ctx.warn === 'function') { + ctx.warn(`[devextreme-bundler-plugin] DevExpress license key (LCX) retrieved from: ${source}`); + ctx.warn(`[devextreme-bundler-plugin] Warning: ${warning}`); + } + } catch{} + } + return (lcpCache = lcp); } catch{ - warnOnce(ctx, '[devextreme-bundler-plugin] Failed to resolve license key. Placeholder will remain.'); + warnOnce(ctx, `[devextreme-bundler-plugin] Warning: ${MESSAGES.resolveFailed}`); return (lcpCache = null); } } diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index ac8f4f5ca7c5..c2f5953a90d8 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -5,7 +5,8 @@ const fs = require('fs'); const path = require('path'); const { getDevExpressLCXKey } = require('./dx-get-lcx'); -const { convertLCXtoLCP } = require('./dx-lcx-2-lcp'); +const { convertLCXtoLCP, getLCPWarning } = require('./dx-lcx-2-lcp'); +const { MESSAGES } = require('./messages'); const EXPORT_NAME = 'licenseKey'; const TRIAL_VALUE = 'TRIAL'; @@ -126,24 +127,22 @@ function main() { const { key: lcx, source } = getDevExpressLCXKey() || {}; - process.stdout.write( - `DevExpress license key (LCX) retrieved from: ${source || '(unknown source)'}\n` - ); - let lcp = TRIAL_VALUE; if(lcx) { try { lcp = convertLCXtoLCP(lcx); + const warning = getLCPWarning(lcp); + if(warning) { + process.stderr.write(`DevExpress license key (LCX) retrieved from: ${source}\n`); + process.stderr.write(`[devextreme-license] Warning: ${warning}\n`); + } } catch{ - process.stderr.write( - 'DevExpress license key was found but could not be converted to LCP.\n' - ); + process.stderr.write(`DevExpress license key (LCX) retrieved from: ${source}\n`); + process.stderr.write(`[devextreme-license] Warning: ${MESSAGES.keyNotFound}\n`); } } else { - process.stderr.write( - 'DevExpress license key (LCX) was not found on this machine.\n' - ); + process.stderr.write(`[devextreme-license] Warning: ${MESSAGES.keyNotFound}\n`); } if(!opts.outPath) { diff --git a/packages/devextreme/license/dx-lcx-2-lcp.js b/packages/devextreme/license/dx-lcx-2-lcp.js index 167dbb900805..342dfaeca67d 100644 --- a/packages/devextreme/license/dx-lcx-2-lcp.js +++ b/packages/devextreme/license/dx-lcx-2-lcp.js @@ -1,5 +1,6 @@ +const { MESSAGES } = require('./messages'); const LCX_SIGNATURE = 'LCXv1'; const LCP_SIGNATURE = 'LCPv1'; const SIGN_LENGTH = 68 * 2; // 136 chars @@ -120,9 +121,128 @@ function tryConvertLCXtoLCP(licenseString) { } } +const DEVEXTREME_HTMLJS_BIT = 1n << 54n; // ProductKind.DevExtremeHtmlJs from types.ts + +const TokenKind = Object.freeze({ + corrupted: 'corrupted', + verified: 'verified', + internal: 'internal', +}); + +const GENERAL_ERROR = { kind: TokenKind.corrupted, error: 'general' }; +const DESERIALIZATION_ERROR = { kind: TokenKind.corrupted, error: 'deserialization' }; +const PRODUCT_KIND_ERROR = { kind: TokenKind.corrupted, error: 'product-kind' }; + +function productsFromString(encodedString) { + if(!encodedString) { + return { products: [], errorToken: GENERAL_ERROR }; + } + try { + const productTuples = encodedString.split(';').slice(1).filter(e => e.length > 0); + const products = productTuples.map(tuple => { + const parts = tuple.split(','); + return { + version: Number.parseInt(parts[0], 10), + products: BigInt(parts[1]), + }; + }); + return { products }; + } catch{ + return { products: [], errorToken: DESERIALIZATION_ERROR }; + } +} + +function findLatestDevExtremeVersion(products) { + if(!Array.isArray(products) || products.length === 0) return undefined; + const sorted = [...products].sort((a, b) => b.version - a.version); + const match = sorted.find(p => (p.products & DEVEXTREME_HTMLJS_BIT) === DEVEXTREME_HTMLJS_BIT); + return match?.version; +} + +function parseLCP(lcpString) { + if(typeof lcpString !== 'string' || !lcpString.startsWith(LCP_SIGNATURE)) { + return GENERAL_ERROR; + } + + try { + const b64 = decode(lcpString.slice(LCP_SIGNATURE.length)); + const decoded = Buffer.from(b64, 'base64').toString('binary'); + + if(decoded.length < SIGN_LENGTH) { + return GENERAL_ERROR; + } + + const productsPayload = decoded.slice(SIGN_LENGTH); + const decodedPayload = mapString(productsPayload, DECODE_MAP); + const { products, errorToken } = productsFromString(decodedPayload); + if(errorToken) { + return errorToken; + } + + const maxVersionAllowed = findLatestDevExtremeVersion(products); + if(!maxVersionAllowed) { + return PRODUCT_KIND_ERROR; + } + + return { + kind: TokenKind.verified, + payload: { customerId: '', maxVersionAllowed }, + }; + } catch{ + return GENERAL_ERROR; + } +} + +function formatVersionCode(versionCode) { + return `v${Math.floor(versionCode / 10)}.${versionCode % 10}`; +} + +function readDevExtremeVersion() { + try { + const pkgPath = require('path').join(__dirname, '..', 'package.json'); + const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf8')); + const parts = String(pkg.version || '').split('.'); + const major = parseInt(parts[0], 10); + const minor = parseInt(parts[1], 10); + if(!isNaN(major) && !isNaN(minor)) { + return { major, minor, code: major * 10 + minor }; + } + } catch{} + return null; +} + +function getLCPWarning(lcpString) { + const token = parseLCP(lcpString); + + if(token.kind === TokenKind.corrupted) { + if(token.error === 'product-kind') { + return MESSAGES.trial; + } + return null; + } + + // token.kind === TokenKind.verified — check version compatibility + const devExtremeVersion = readDevExtremeVersion(); + if(devExtremeVersion) { + const { major, minor, code: currentCode } = devExtremeVersion; + const { maxVersionAllowed } = token.payload; + if(maxVersionAllowed < currentCode) { + return MESSAGES.versionIncompatible( + formatVersionCode(maxVersionAllowed), + `v${major}.${minor}`, + ); + } + } + + return null; +} + module.exports = { convertLCXtoLCP, tryConvertLCXtoLCP, + parseLCP, + getLCPWarning, + TokenKind, LCX_SIGNATURE, LCP_SIGNATURE, }; diff --git a/packages/devextreme/license/messages.js b/packages/devextreme/license/messages.js new file mode 100644 index 000000000000..e110da4e5d17 --- /dev/null +++ b/packages/devextreme/license/messages.js @@ -0,0 +1,26 @@ +'use strict'; + +const MESSAGES = Object.freeze({ + keyNotFound: + 'For evaluation purposes only. Redistribution prohibited. ' + + 'If you own a licensed/registered version or if you are using a 30-day trial version of DevExpress product libraries on a development machine, ' + + 'download your personal license key (devexpress.com/DX1001) and place DevExpress_License.txt in the following folder: ' + + '"%AppData%/DevExpress" (Windows) or "$HOME/Library/Application Support/DevExpress" (MacOS) or "$HOME/.config/DevExpress" (Linux). ' + + 'Alternatively, download and run the DevExpress Unified Component Installer to automatically activate your license.', + + trial: + 'For evaluation purposes only. Redistribution prohibited. ' + + 'Please purchase a license to continue use of the following DevExpress product libraries: ' + + 'Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete.', + + versionIncompatible: (keyVersion, requiredVersion) => + 'For evaluation purposes only. Redistribution prohibited. ' + + `Incompatible DevExpress license key version (${keyVersion}). ` + + `Download and register an updated DevExpress license key (${requiredVersion}+). ` + + 'Clear IDE/NuGet cache and rebuild your project (devexpress.com/DX1002).', + + resolveFailed: + 'Failed to resolve license key. Placeholder will remain.', +}); + +module.exports = { MESSAGES }; From 6215d5ed614ac7a9d45b9e6ffc850bea470194b4 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Thu, 12 Mar 2026 02:02:54 +0400 Subject: [PATCH 17/48] fix non modular behaviour --- .../devextreme/license/devextreme-license.js | 47 +++++++++++++++---- packages/devextreme/package.json | 2 +- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index c2f5953a90d8..cc009a865427 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -20,17 +20,19 @@ function printHelp() { process.stdout.write( [ 'Usage:', - ' devextreme-license --out [options]', + ' devextreme-license-verify --out [options]', '', 'Options:', ' --out Output file path (optional)', + ' --non-modular Generate a non-modular JS file (only with .js extension)', ' --no-gitignore Do not modify .gitignore', ' --force Overwrite existing output file', ' --cwd Project root (default: process.cwd())', ' -h, --help Show help', '', - 'Example:', - ' "prebuild": "devextreme-license --out src/.devextreme/license-key.ts"', + 'Examples:', + ' "prebuild": "devextreme-license-verify --out src/.devextreme/license-key.ts"', + ' "prebuild": "devextreme-license-verify --non-modular --out src/.devextreme/license-key.js"', '', ].join('\n') ); @@ -40,6 +42,7 @@ function parseArgs(argv) { const args = argv.slice(2); const out = { outPath: null, + nonModular: false, gitignore: true, force: false, cwd: process.cwd(), @@ -50,13 +53,28 @@ function parseArgs(argv) { const a = args[i]; if(a === '-h' || a === '--help') out.help = true; - else if(a === '--out') out.outPath = args[++i] || null; - else if(a.startsWith('--out=')) out.outPath = a.slice('--out='.length); + else if(a === '--out') { + const next = args[i + 1]; + if(!next || next.startsWith('-')) { + process.stderr.write('[devextreme-license-verify] Warning: --out requires a path argument but none was provided. Ignoring --out.\n'); + } else { + out.outPath = args[++i]; + } + } + else if(a.startsWith('--out=')) { + const val = a.slice('--out='.length); + if(!val) { + process.stderr.write('[devextreme-license-verify] Warning: --out requires a path argument but none was provided. Ignoring --out.\n'); + } else { + out.outPath = val; + } + } + else if(a === '--non-modular') out.nonModular = true; else if(a === '--no-gitignore') out.gitignore = false; else if(a === '--force') out.force = true; else if(a === '--cwd') out.cwd = args[++i] || process.cwd(); else if(a.startsWith('--cwd=')) out.cwd = a.slice('--cwd='.length); - else fail(`Unknown argument: ${a}\nRun devextreme-license --help`); + else fail(`Unknown argument: ${a}\nRun devextreme-license-verify --help`); } return out; @@ -108,9 +126,9 @@ function addToGitignore(projectRoot, outAbsPath) { fs.appendFileSync(gitignorePath, (needsNewline ? '\n' : '') + rel + '\n', 'utf8'); } -function renderTsFile(lcpKey) { +function renderFile(lcpKey) { return [ - '// Auto-generated by devextreme-license.', + '// Auto-generated by devextreme-license-verify.', '// Do not commit this file to source control.', '', `export const ${EXPORT_NAME} = ${JSON.stringify(lcpKey)};`, @@ -118,6 +136,16 @@ function renderTsFile(lcpKey) { ].join('\n'); } +function renderNonModularFile(lcpKey) { + return [ + '// Auto-generated by devextreme-license-verify.', + '// Do not commit this file to source control.', + '', + `DevExpress.config({ licenseKey: ${JSON.stringify(lcpKey)} });`, + '', + ].join('\n'); +} + function main() { const opts = parseArgs(process.argv); if(opts.help) { @@ -159,7 +187,8 @@ function main() { fail(`Output file already exists: ${opts.outPath}\nUse --force to overwrite.`); } - writeFileAtomic(outAbs, renderTsFile(lcp)); + const useNonModular = opts.nonModular && outAbs.endsWith('.js'); + writeFileAtomic(outAbs, useNonModular ? renderNonModularFile(lcp) : renderFile(lcp)); if(opts.gitignore) { try { diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index 0078b0eba625..bd68c26df084 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -256,7 +256,7 @@ "bin": { "devextreme-bundler-init": "bin/bundler-init.js", "devextreme-bundler": "bin/bundler.js", - "devextreme-license": "license/devextreme-license.js" + "devextreme-license-verify": "license/devextreme-license.js" }, "browserslist": [ "last 2 Chrome versions", From 842fd3fc578e301988eee3fd411c3d1bbfc27526 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Thu, 12 Mar 2026 02:23:36 +0400 Subject: [PATCH 18/48] cli wrapper to call from bin --- .../devextreme/build/npm-bin/devextreme-license-verify.js | 4 ++++ packages/devextreme/package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 packages/devextreme/build/npm-bin/devextreme-license-verify.js diff --git a/packages/devextreme/build/npm-bin/devextreme-license-verify.js b/packages/devextreme/build/npm-bin/devextreme-license-verify.js new file mode 100644 index 000000000000..146a83b3c95c --- /dev/null +++ b/packages/devextreme/build/npm-bin/devextreme-license-verify.js @@ -0,0 +1,4 @@ +#!/usr/bin/env node +'use strict'; + +require('../license/devextreme-license'); diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index bd68c26df084..87c3a572b5c5 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -256,7 +256,7 @@ "bin": { "devextreme-bundler-init": "bin/bundler-init.js", "devextreme-bundler": "bin/bundler.js", - "devextreme-license-verify": "license/devextreme-license.js" + "devextreme-license-verify": "bin/devextreme-license-verify.js" }, "browserslist": [ "last 2 Chrome versions", From 0aa849420dcc584410d8915b924e9449ab055b3a Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Thu, 12 Mar 2026 02:43:38 +0400 Subject: [PATCH 19/48] get rid of class based implementations for cleanup --- .../lcp_key_validation.test.ts | 14 ++++----- .../lcp_key_validation/lcp_key_validator.ts | 9 +++--- .../lcp_key_validation/license_info.ts | 28 ++++++++---------- .../lcp_key_validation/product_info.ts | 29 ++++++++----------- 4 files changed, 35 insertions(+), 45 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts index 720efd5a2679..c2e4c519acc5 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts @@ -4,17 +4,17 @@ import { version as currentVersion } from '@js/core/version'; import { parseVersion } from '../../../utils/version'; import { TokenKind } from '../types'; import { parseDevExpressProductKey } from './lcp_key_validator'; -import { LicenseInfo } from './license_info'; -import { ProductInfo } from './product_info'; +import { findLatestDevExtremeVersion, isLicenseValid } from './license_info'; +import { createProductInfo } from './product_info'; const RAW_DEVELOPER_PRODUCT_LICENSE = 'LCPv1EK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEQtEpQtEpQtEpQtEpQtEpQtEpQtEpQtEpQtEpQtEpQtE>7yFIp@@I%-QpbXN-v>K-@2K@v2%d)Ig-QIp-)I7yFI7yFI7yF'; -function getTrialLicense(): LicenseInfo { +function getTrialLicense() { const { major, minor } = parseVersion(currentVersion); const products = [ - new ProductInfo(parseInt(`${major}${minor}`, 10), 0n), + createProductInfo(parseInt(`${major}${minor}`, 10), 0n), ]; - return new LicenseInfo(products); + return { products }; } describe('LCP key validation', () => { @@ -31,9 +31,9 @@ describe('LCP key validation', () => { it('trial fallback does not grant product access', () => { const trialLicense = getTrialLicense(); - expect(trialLicense.isValid).toBe(true); + expect(isLicenseValid(trialLicense)).toBe(true); - const version = trialLicense.findLatestDevExtremeVersion(); + const version = findLatestDevExtremeVersion(trialLicense); expect(version).toBe(undefined); }); diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts index afc1f00ce6aa..8d5f223f95f3 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts @@ -13,8 +13,8 @@ import { RSA_PUBLIC_KEY_XML, SIGN_LENGTH, } from './const'; -import { LicenseInfo } from './license_info'; -import { ProductInfo } from './product_info'; +import { findLatestDevExtremeVersion } from './license_info'; +import { createProductInfo, type ProductInfo } from './product_info'; import { encodeString, shiftDecodeText, verifyHash } from './utils'; interface ParsedProducts { @@ -41,7 +41,7 @@ function productsFromString(encodedString: string): ParsedProducts { const parts = tuple.split(','); const version = Number.parseInt(parts[0], 10); const productsValue = BigInt(parts[1]); - return new ProductInfo( + return createProductInfo( version, productsValue, ); @@ -86,8 +86,7 @@ export function parseDevExpressProductKey(productsLicenseSource: string): Token return errorToken; } - const licenseInfo = new LicenseInfo(products); - const maxVersionAllowed = licenseInfo.findLatestDevExtremeVersion(); + const maxVersionAllowed = findLatestDevExtremeVersion({ products }); if (!maxVersionAllowed) { return PRODUCT_KIND_ERROR; diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts index 631704d01278..66e40a5624e1 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts @@ -1,24 +1,20 @@ -import type { ProductInfo } from './product_info'; +import { isProduct, type ProductInfo } from './product_info'; import { ProductKind } from './types'; -export class LicenseInfo { - public readonly products: ProductInfo[]; +export interface LicenseInfo { + readonly products: ProductInfo[]; +} - constructor(products: ProductInfo[] = []) { - this.products = products; - } +export function isLicenseValid(info: LicenseInfo): boolean { + return Array.isArray(info.products) && info.products.length > 0; +} - get isValid(): boolean { - return Array.isArray(this.products) && this.products.length > 0; +export function findLatestDevExtremeVersion(info: LicenseInfo): number | undefined { + if (!isLicenseValid(info)) { + return undefined; } - findLatestDevExtremeVersion(): number | undefined { - if (!this.isValid) { - return undefined; - } + const sorted = [...info.products].sort((a, b) => b.version - a.version); - const sorted = [...this.products].sort((a, b) => b.version - a.version); - - return sorted.find((p) => p.isProduct(ProductKind.DevExtremeHtmlJs))?.version; - } + return sorted.find((p) => isProduct(p, ProductKind.DevExtremeHtmlJs))?.version; } diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts index 6cd62ee47c12..0d051610cdb0 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts @@ -1,23 +1,18 @@ /* eslint-disable no-bitwise */ -export class ProductInfo { - public version: number; +export interface ProductInfo { + readonly version: number; + readonly products: bigint; +} - public products: bigint; +export function createProductInfo(version: number, products: bigint): ProductInfo { + return { version, products: BigInt(products) }; +} - constructor( - version: number, - products: bigint, - ) { - this.version = version; - this.products = BigInt(products); +export function isProduct(info: ProductInfo, ...productIds: bigint[]): boolean { + if (productIds.length === 1) { + const flag = BigInt(productIds[0]); + return (info.products & flag) === flag; } - isProduct(...productIds: bigint[]): boolean { - if (productIds.length === 1) { - const flag = BigInt(productIds[0]); - return (this.products & flag) === flag; - } - - return productIds.some((id) => (this.products & BigInt(id)) === BigInt(id)); - } + return productIds.some((id) => (info.products & BigInt(id)) === BigInt(id)); } From 9b4fc950215e1191b1f89810537dba65fd8aa367 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Thu, 12 Mar 2026 03:05:18 +0400 Subject: [PATCH 20/48] add payload tests, fix key validation test logic --- .../lcp_key_validation.test.ts | 7 +- .../license_payload.test.ts | 298 ++++++++++++++++++ 2 files changed, 300 insertions(+), 5 deletions(-) create mode 100644 packages/devextreme/js/__internal/core/license/lcp_key_validation/license_payload.test.ts diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts index c2e4c519acc5..c356ced5b2cd 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts @@ -7,8 +7,6 @@ import { parseDevExpressProductKey } from './lcp_key_validator'; import { findLatestDevExtremeVersion, isLicenseValid } from './license_info'; import { createProductInfo } from './product_info'; -const RAW_DEVELOPER_PRODUCT_LICENSE = 'LCPv1EK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEK)rEQtEpQtEpQtEpQtEpQtEpQtEpQtEpQtEpQtEpQtEpQtE>7yFIp@@I%-QpbXN-v>K-@2K@v2%d)Ig-QIp-)I7yFI7yFI7yF'; - function getTrialLicense() { const { major, minor } = parseVersion(currentVersion); const products = [ @@ -23,9 +21,8 @@ describe('LCP key validation', () => { expect(token.kind).toBe(TokenKind.corrupted); }); - it('developer product license fixtures parse into valid LicenseInfo instances', () => { - const key = process.env.DX_PRODUCT_KEY ?? RAW_DEVELOPER_PRODUCT_LICENSE; - const token = parseDevExpressProductKey(key); + (process.env.DX_PRODUCT_KEY ? it : it.skip)('developer product license fixtures parse into valid LicenseInfo instances', () => { + const token = parseDevExpressProductKey(process.env.DX_PRODUCT_KEY as string); expect(token.kind).toBe(TokenKind.verified); }); diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_payload.test.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_payload.test.ts new file mode 100644 index 000000000000..049004ff44db --- /dev/null +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_payload.test.ts @@ -0,0 +1,298 @@ +/** + * Payload-level license tests — analogous to dxvcs LicenseTestHelperTests. + * + * These tests exercise ProductInfo / LicenseInfo directly + * (no full LCP key encoding / signature verification involved) + * so we can validate product-kind bit-flag logic in isolation. + */ +/* eslint-disable spellcheck/spell-checker, no-bitwise */ + +import { describe, expect, it } from '@jest/globals'; +import { version as currentVersion } from '@js/core/version'; + +import { parseVersion } from '../../../utils/version'; +import { findLatestDevExtremeVersion, isLicenseValid } from './license_info'; +import { createProductInfo, isProduct } from './product_info'; +import { ProductKind } from './types'; + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +/** Build a numeric version id that matches the current DevExtreme build (e.g. 251). */ +function currentVersionId(): number { + const { major, minor } = parseVersion(currentVersion); + return parseInt(`${major}${minor}`, 10); +} + +/** Shortcut: create a LicenseInfo with a single ProductInfo entry. */ +function makeLicense(products: bigint, version?: number) { + const v = version ?? currentVersionId(); + return { products: [createProductInfo(v, products)] }; +} + +// --------------------------------------------------------------------------- +// ProductInfo.isProduct +// --------------------------------------------------------------------------- + +describe('ProductInfo.isProduct – product-kind bit flags', () => { + it.each([ + { name: 'DXperienceASP', kind: ProductKind.DXperienceASP }, + { name: 'DXperienceWPF', kind: ProductKind.DXperienceWPF }, + { name: 'DXperienceWin', kind: ProductKind.DXperienceWin }, + { name: 'Blazor', kind: ProductKind.Blazor }, + { name: 'XAF', kind: ProductKind.XAF }, + { name: 'DevExtremeHtmlJs', kind: ProductKind.DevExtremeHtmlJs }, + { name: 'Dashboard', kind: ProductKind.Dashboard }, + { name: 'Docs', kind: ProductKind.Docs }, + { name: 'DocsBasic', kind: ProductKind.DocsBasic }, + { name: 'XtraReports', kind: ProductKind.XtraReports }, + ])('single flag $name is detected', ({ kind }) => { + const pi = createProductInfo(currentVersionId(), kind); + expect(isProduct(pi, kind)).toBe(true); + }); + + it('does not match a flag that was NOT set', () => { + const pi = createProductInfo(currentVersionId(), ProductKind.Blazor); + expect(isProduct(pi, ProductKind.Docs)).toBe(false); + expect(isProduct(pi, ProductKind.DXperienceWin)).toBe(false); + }); + + it('DXperienceUni includes every individual product', () => { + const pi = createProductInfo(currentVersionId(), ProductKind.DXperienceUni); + + expect(isProduct(pi, ProductKind.DXperienceWin)).toBe(true); + expect(isProduct(pi, ProductKind.DXperienceASP)).toBe(true); + expect(isProduct(pi, ProductKind.DXperienceWPF)).toBe(true); + expect(isProduct(pi, ProductKind.Blazor)).toBe(true); + expect(isProduct(pi, ProductKind.XAF)).toBe(true); + expect(isProduct(pi, ProductKind.Dashboard)).toBe(true); + expect(isProduct(pi, ProductKind.Docs)).toBe(true); + expect(isProduct(pi, ProductKind.DevExtremeHtmlJs)).toBe(true); + expect(isProduct(pi, ProductKind.XtraReports)).toBe(true); + }); + + it('DXperienceEnt includes its constituent products but not XAF/Dashboard/Docs', () => { + const pi = createProductInfo(currentVersionId(), ProductKind.DXperienceEnt); + + expect(isProduct(pi, ProductKind.DXperienceWin)).toBe(true); + expect(isProduct(pi, ProductKind.DXperienceASP)).toBe(true); + expect(isProduct(pi, ProductKind.DXperienceWPF)).toBe(true); + expect(isProduct(pi, ProductKind.Blazor)).toBe(true); + expect(isProduct(pi, ProductKind.DevExtremeHtmlJs)).toBe(true); + expect(isProduct(pi, ProductKind.XtraReports)).toBe(true); + + // Not included in DXperienceEnt + expect(isProduct(pi, ProductKind.XAF)).toBe(false); + expect(isProduct(pi, ProductKind.Dashboard)).toBe(false); + // Note: Docs IS included in DXperienceUni but NOT in DXperienceEnt + expect(isProduct(pi, ProductKind.Docs)).toBe(false); + }); + + it('isProduct returns true when ANY of multiple flags matches', () => { + const pi = createProductInfo(currentVersionId(), ProductKind.Docs); + expect(isProduct(pi, ProductKind.Docs, ProductKind.DocsBasic)).toBe(true); + expect(isProduct(pi, ProductKind.DocsBasic, ProductKind.Docs)).toBe(true); + expect(isProduct(pi, ProductKind.Blazor, ProductKind.Docs)).toBe(true); + }); + + it('isProduct returns false when NONE of multiple flags matches', () => { + const pi = createProductInfo(currentVersionId(), ProductKind.Blazor); + expect(isProduct(pi, ProductKind.Docs, ProductKind.DocsBasic)).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// LicenseInfo – validity & findLatestDevExtremeVersion +// --------------------------------------------------------------------------- + +describe('LicenseInfo – payload-level behaviour (analogous to dxvcs LicenseTestHelperTests)', () => { + const versionId = currentVersionId(); + + // -- trial / no-product license ------------------------------------------ + + it('trial license (products = 0n) is valid but has no DevExtreme product', () => { + const trial = makeLicense(ProductKind.Default); + expect(isLicenseValid(trial)).toBe(true); + expect(findLatestDevExtremeVersion(trial)).toBeUndefined(); + }); + + it('empty LicenseInfo is invalid', () => { + const empty = { products: [] }; + expect(isLicenseValid(empty)).toBe(false); + expect(findLatestDevExtremeVersion(empty)).toBeUndefined(); + }); + + it('no-arg LicenseInfo is invalid', () => { + const noLicense = { products: [] }; + expect(isLicenseValid(noLicense)).toBe(false); + expect(findLatestDevExtremeVersion(noLicense)).toBeUndefined(); + }); + + // -- licensed with DevExtremeHtmlJs -------------------------------------- + + it.each([ + { name: 'DevExtremeHtmlJs', kind: ProductKind.DevExtremeHtmlJs }, + { name: 'DXperienceEnt', kind: ProductKind.DXperienceEnt }, + { name: 'DXperienceUni', kind: ProductKind.DXperienceUni }, + ])('license with $name grants DevExtreme access at current version', ({ kind }) => { + const lic = makeLicense(kind); + expect(isLicenseValid(lic)).toBe(true); + expect(findLatestDevExtremeVersion(lic)).toBe(versionId); + }); + + // -- licensed WITHOUT DevExtremeHtmlJs flag ------------------------------ + + it.each([ + { name: 'DXperienceWin', kind: ProductKind.DXperienceWin }, + { name: 'Blazor', kind: ProductKind.Blazor }, + { name: 'XAF', kind: ProductKind.XAF }, + { name: 'Docs', kind: ProductKind.Docs }, + { name: 'Dashboard', kind: ProductKind.Dashboard }, + { name: 'XtraReports', kind: ProductKind.XtraReports }, + ])('license with only $name does NOT grant DevExtreme access', ({ kind }) => { + const lic = makeLicense(kind); + expect(isLicenseValid(lic)).toBe(true); + expect(findLatestDevExtremeVersion(lic)).toBeUndefined(); + }); + + // -- version matching ---------------------------------------------------- + + it('findLatestDevExtremeVersion returns the highest matching version', () => { + const lic = { + products: [ + createProductInfo(240, ProductKind.DevExtremeHtmlJs), + createProductInfo(250, ProductKind.DevExtremeHtmlJs), + ], + }; + expect(findLatestDevExtremeVersion(lic)).toBe(250); + }); + + it('products on older version do not appear at newer version', () => { + const oldVersion = versionId - 10; + const lic = makeLicense(ProductKind.DevExtremeHtmlJs, oldVersion); + expect(isLicenseValid(lic)).toBe(true); + expect(findLatestDevExtremeVersion(lic)).toBe(oldVersion); + }); + + // -- combining product kinds -------------------------------------------- + + it('combined flags DXperienceASP | DevExtremeHtmlJs grant DevExtreme', () => { + const combined = ProductKind.DXperienceASP | ProductKind.DevExtremeHtmlJs; + const lic = makeLicense(combined); + const pi = lic.products[0]; + + expect(isProduct(pi, ProductKind.DXperienceASP)).toBe(true); + expect(isProduct(pi, ProductKind.DevExtremeHtmlJs)).toBe(true); + expect(findLatestDevExtremeVersion(lic)).toBe(versionId); + }); + + it('individual non-DevExtreme kind does NOT grant DevExtreme even when combined with other non-DevExtreme', () => { + const combined = ProductKind.DXperienceWin | ProductKind.Docs; + const lic = makeLicense(combined); + + expect(isProduct(lic.products[0], ProductKind.DXperienceWin)).toBe(true); + expect(isProduct(lic.products[0], ProductKind.Docs)).toBe(true); + expect(findLatestDevExtremeVersion(lic)).toBeUndefined(); + }); +}); + +// --------------------------------------------------------------------------- +// Smoke tests – mirrors dxvcs LicenseTestHelperTests.Smoke per product kind +// --------------------------------------------------------------------------- + +describe('Smoke tests per ProductKind (dxvcs-style)', () => { + it.each([ + { name: 'DXperienceASP', kind: ProductKind.DXperienceASP }, + { name: 'DXperienceWPF', kind: ProductKind.DXperienceWPF }, + { name: 'DXperienceWin', kind: ProductKind.DXperienceWin }, + { name: 'Blazor', kind: ProductKind.Blazor }, + { name: 'XAF', kind: ProductKind.XAF }, + ])('$name – trial / licensed / universal / no-license states', ({ kind }) => { + // 1. Trial (products = Default = 0n --> no product flags) + const trial = makeLicense(ProductKind.Default); + expect(isLicenseValid(trial)).toBe(true); + expect(isProduct(trial.products[0], kind)).toBe(false); + + // 2. Licensed with DXperienceUni --> every kind is included + const uniLic = makeLicense(ProductKind.DXperienceUni); + expect(isProduct(uniLic.products[0], kind)).toBe(true); + // DevExtremeHtmlJs should also be present in Uni + expect(isProduct(uniLic.products[0], ProductKind.DevExtremeHtmlJs)).toBe(true); + + // 3. Licensed with specific kind --> only that kind + const specificLic = makeLicense(kind); + expect(isProduct(specificLic.products[0], kind)).toBe(true); + + // Docs should NOT be present when only 'kind' is specified + // (unless kind itself is Docs or encompasses Docs) + if ((kind & ProductKind.Docs) !== ProductKind.Docs) { + expect(isProduct(specificLic.products[0], ProductKind.Docs)).toBe(false); + } + + // 4. No license + const noLicense = { products: [] }; + expect(isLicenseValid(noLicense)).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// DocsBasic vs Docs (analogous to dxvcs LicenseInfoTests.HasLicenseTests) +// --------------------------------------------------------------------------- + +describe('HasLicense-style tests (DocsBasic vs Docs)', () => { + it('DocsBasic flag set → isProduct(DocsBasic) true, isProduct(Docs) false', () => { + const pi = createProductInfo(currentVersionId(), ProductKind.DocsBasic); + expect(isProduct(pi, ProductKind.DocsBasic)).toBe(true); + expect(isProduct(pi, ProductKind.Docs)).toBe(false); + }); + + it('Docs flag set → isProduct(Docs) true, isProduct(DocsBasic) false', () => { + const pi = createProductInfo(currentVersionId(), ProductKind.Docs); + expect(isProduct(pi, ProductKind.Docs)).toBe(true); + expect(isProduct(pi, ProductKind.DocsBasic)).toBe(false); + }); + + it('isProduct with multiple alternatives works like HasLicense(version, kind1, kind2)', () => { + const pi = createProductInfo(currentVersionId(), ProductKind.DocsBasic); + expect(isProduct(pi, ProductKind.Docs, ProductKind.DocsBasic)).toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +// Multi-version license (analogous to dxvcs LicenseInfoTests.FindBestLicense) +// --------------------------------------------------------------------------- + +describe('Multi-version license scenarios', () => { + const versionId = currentVersionId(); + + it('finds the latest DevExtreme version from multiple product entries', () => { + const lic = { + products: [ + createProductInfo(versionId - 1, ProductKind.DevExtremeHtmlJs), + createProductInfo(versionId, ProductKind.DevExtremeHtmlJs), + ], + }; + expect(findLatestDevExtremeVersion(lic)).toBe(versionId); + }); + + it('returns older version when newer does not include DevExtreme', () => { + const lic = { + products: [ + createProductInfo(versionId, ProductKind.DXperienceWin), // no DevExtreme + createProductInfo(versionId - 1, ProductKind.DevExtremeHtmlJs), + ], + }; + expect(findLatestDevExtremeVersion(lic)).toBe(versionId - 1); + }); + + it('returns undefined when no entry has DevExtremeHtmlJs', () => { + const lic = { + products: [ + createProductInfo(versionId, ProductKind.DXperienceWin), + createProductInfo(versionId - 1, ProductKind.Blazor), + ], + }; + expect(findLatestDevExtremeVersion(lic)).toBeUndefined(); + }); +}); From 701dab45cc065aed9eb44c7c1fb28cb7ebf4d6cd Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Thu, 12 Mar 2026 03:37:24 +0400 Subject: [PATCH 21/48] fix type error --- .../js/__internal/core/license/license_validation_internal.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/devextreme/js/__internal/core/license/license_validation_internal.ts b/packages/devextreme/js/__internal/core/license/license_validation_internal.ts index df552c7a8878..a61f9cc003d5 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation_internal.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation_internal.ts @@ -4,6 +4,9 @@ import type { Token } from './types'; // @ts-expect-error - only for internal usage export function parseLicenseKey(encodedKey: string | undefined): Token {} +// @ts-expect-error - only for internal usage +export function isUnsupportedKeyFormat(licenseKey: string | undefined): boolean {} + export function validateLicense(licenseKey: string, version?: string): void {} // @ts-expect-error - only for internal usage From 4d2d8000c22c6db367e27a151275fc8c12384448 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 18 Mar 2026 12:07:07 +0400 Subject: [PATCH 22/48] isolate warning logic into a helper function --- .../license/devextreme-license-plugin.js | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/devextreme/license/devextreme-license-plugin.js b/packages/devextreme/license/devextreme-license-plugin.js index c594cc907f3f..7777037fb422 100644 --- a/packages/devextreme/license/devextreme-license-plugin.js +++ b/packages/devextreme/license/devextreme-license-plugin.js @@ -3,6 +3,8 @@ const { getDevExpressLCXKey } = require('./dx-get-lcx'); const { tryConvertLCXtoLCP, getLCPWarning } = require('./dx-lcx-2-lcp'); const { MESSAGES } = require('./messages'); +const PLUGIN_NAME = 'devextreme-bundler-plugin'; +const PLUGIN_PREFIX = `[${PLUGIN_NAME}]`; const PLACEHOLDER = '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */'; // Target only the specific config file to avoid scanning all files during build const TARGET_FILE_PATTERN = /[\/\\]__internal[\/\\]core[\/\\]m_config\.(ts|js)$/; @@ -12,11 +14,26 @@ const DevExtremeLicensePlugin = createUnplugin(() => { let lcpCache = null; let warnedOnce = false; + function warn(ctx, msg) { + try { + if(ctx && typeof ctx.warn === 'function') { + ctx.warn(msg); + } + } catch{} + } + function warnOnce(ctx, msg) { if(warnedOnce) return; warnedOnce = true; + warn(ctx, msg); + } + + function warnLicenseIssue(ctx, source, warning) { try { - if(ctx && typeof ctx.warn === 'function') ctx.warn(msg); + if(ctx && typeof ctx.warn === 'function') { + ctx.warn(`${PLUGIN_PREFIX} DevExpress license key (LCX) retrieved from: ${source}`); + ctx.warn(`${PLUGIN_PREFIX} Warning: ${warning}`); + } } catch{} } @@ -28,53 +45,43 @@ const DevExtremeLicensePlugin = createUnplugin(() => { const { key: lcx, source } = getDevExpressLCXKey() || {}; if(!lcx) { - warnOnce(ctx, `[devextreme-bundler-plugin] Warning: ${MESSAGES.keyNotFound}`); + warnOnce(ctx, `${PLUGIN_PREFIX} Warning: ${MESSAGES.keyNotFound}`); return (lcpCache = null); } const lcp = tryConvertLCXtoLCP(lcx); if(!lcp) { - try { - if(ctx && typeof ctx.warn === 'function') { - ctx.warn(`[devextreme-bundler-plugin] DevExpress license key (LCX) retrieved from: ${source}`); - ctx.warn(`[devextreme-bundler-plugin] Warning: ${MESSAGES.keyNotFound}`); - } - } catch{} + warnLicenseIssue(ctx, source, MESSAGES.keyNotFound); return (lcpCache = null); } const warning = getLCPWarning(lcp); if(warning) { - try { - if(ctx && typeof ctx.warn === 'function') { - ctx.warn(`[devextreme-bundler-plugin] DevExpress license key (LCX) retrieved from: ${source}`); - ctx.warn(`[devextreme-bundler-plugin] Warning: ${warning}`); - } - } catch{} + warnLicenseIssue(ctx, source, warning); } return (lcpCache = lcp); } catch{ - warnOnce(ctx, `[devextreme-bundler-plugin] Warning: ${MESSAGES.resolveFailed}`); + warnOnce(ctx, `${PLUGIN_PREFIX} Warning: ${MESSAGES.resolveFailed}`); return (lcpCache = null); } } return { - name: 'devextreme-bundler-plugin', + name: PLUGIN_NAME, enforce: 'pre', transform(code, id) { try { if(!TARGET_FILE_PATTERN.test(id)) return null; if(typeof code !== 'string') return null; - if(code.indexOf(PLACEHOLDER) === -1) return null; + if(!code.includes(PLACEHOLDER)) return null; const lcp = resolveLcpSafe(this); if(!lcp) return null; return { code: code.split(PLACEHOLDER).join(lcp), map: null }; } catch{ - warnOnce(this, '[devextreme-bundler-plugin] Patch error. Placeholder will remain.'); + warnOnce(this, `${PLUGIN_PREFIX} Patch error. Placeholder will remain.`); return null; } }, From 237859ebd041b1825a37d44a6e7b5adea5d96ee0 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 18 Mar 2026 12:09:55 +0400 Subject: [PATCH 23/48] remove egow check for 25_2 --- .../js/__internal/core/license/license_validation.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index 00ec7a15071d..ed8540df4ab9 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -131,10 +131,6 @@ export function isUnsupportedKeyFormat(licenseKey: string | undefined): boolean errors.log('W0000', 'config', 'licenseKey', 'LCXv1 is specified in the license key'); return true; } - if (hasLicensePrefix(licenseKey, 'egow')) { - errors.log('W0000', 'config', 'licenseKey', 'DevExtreme key is specified in the license key'); - return true; - } return false; } From 867043ec7f1c00c8619b249b49f58224ea8caa64 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 18 Mar 2026 14:09:00 +0400 Subject: [PATCH 24/48] rename license command --- ...e-license-verify.js => devextreme-license.js} | 0 .../devextreme/license/devextreme-license.js | 16 ++++++++-------- packages/devextreme/package.json | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) rename packages/devextreme/build/npm-bin/{devextreme-license-verify.js => devextreme-license.js} (100%) diff --git a/packages/devextreme/build/npm-bin/devextreme-license-verify.js b/packages/devextreme/build/npm-bin/devextreme-license.js similarity index 100% rename from packages/devextreme/build/npm-bin/devextreme-license-verify.js rename to packages/devextreme/build/npm-bin/devextreme-license.js diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index cc009a865427..face0c2824e3 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -20,7 +20,7 @@ function printHelp() { process.stdout.write( [ 'Usage:', - ' devextreme-license-verify --out [options]', + ' devextreme-license --out [options]', '', 'Options:', ' --out Output file path (optional)', @@ -31,8 +31,8 @@ function printHelp() { ' -h, --help Show help', '', 'Examples:', - ' "prebuild": "devextreme-license-verify --out src/.devextreme/license-key.ts"', - ' "prebuild": "devextreme-license-verify --non-modular --out src/.devextreme/license-key.js"', + ' "prebuild": "devextreme-license --out src/.devextreme/license-key.ts"', + ' "prebuild": "devextreme-license --non-modular --out src/.devextreme/license-key.js"', '', ].join('\n') ); @@ -56,7 +56,7 @@ function parseArgs(argv) { else if(a === '--out') { const next = args[i + 1]; if(!next || next.startsWith('-')) { - process.stderr.write('[devextreme-license-verify] Warning: --out requires a path argument but none was provided. Ignoring --out.\n'); + process.stderr.write('[devextreme-license] Warning: --out requires a path argument but none was provided. Ignoring --out.\n'); } else { out.outPath = args[++i]; } @@ -64,7 +64,7 @@ function parseArgs(argv) { else if(a.startsWith('--out=')) { const val = a.slice('--out='.length); if(!val) { - process.stderr.write('[devextreme-license-verify] Warning: --out requires a path argument but none was provided. Ignoring --out.\n'); + process.stderr.write('[devextreme-license] Warning: --out requires a path argument but none was provided. Ignoring --out.\n'); } else { out.outPath = val; } @@ -74,7 +74,7 @@ function parseArgs(argv) { else if(a === '--force') out.force = true; else if(a === '--cwd') out.cwd = args[++i] || process.cwd(); else if(a.startsWith('--cwd=')) out.cwd = a.slice('--cwd='.length); - else fail(`Unknown argument: ${a}\nRun devextreme-license-verify --help`); + else fail(`Unknown argument: ${a}\nRun devextreme-license --help`); } return out; @@ -128,7 +128,7 @@ function addToGitignore(projectRoot, outAbsPath) { function renderFile(lcpKey) { return [ - '// Auto-generated by devextreme-license-verify.', + '// Auto-generated by devextreme-license.', '// Do not commit this file to source control.', '', `export const ${EXPORT_NAME} = ${JSON.stringify(lcpKey)};`, @@ -138,7 +138,7 @@ function renderFile(lcpKey) { function renderNonModularFile(lcpKey) { return [ - '// Auto-generated by devextreme-license-verify.', + '// Auto-generated by devextreme-license.', '// Do not commit this file to source control.', '', `DevExpress.config({ licenseKey: ${JSON.stringify(lcpKey)} });`, diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index 87c3a572b5c5..cce8403fe77a 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -256,7 +256,7 @@ "bin": { "devextreme-bundler-init": "bin/bundler-init.js", "devextreme-bundler": "bin/bundler.js", - "devextreme-license-verify": "bin/devextreme-license-verify.js" + "devextreme-license": "bin/devextreme-license.js" }, "browserslist": [ "last 2 Chrome versions", From 3d1f90ee4b753ec529d9baef14f05e9de36cd366 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Fri, 27 Mar 2026 01:27:47 +0400 Subject: [PATCH 25/48] Add license id to the generated license file and license related warnings --- .../lcp_key_validation/lcp_key_validator.ts | 5 ++ .../core/license/license_validation.ts | 10 +++- .../js/__internal/core/license/types.ts | 2 + .../license/devextreme-license-plugin.js | 13 +++-- .../devextreme/license/devextreme-license.js | 39 ++++++++----- packages/devextreme/license/dx-lcx-2-lcp.js | 56 +++++++++++-------- 6 files changed, 78 insertions(+), 47 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts index 8d5f223f95f3..908000d861d6 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts @@ -19,6 +19,7 @@ import { encodeString, shiftDecodeText, verifyHash } from './utils'; interface ParsedProducts { products: ProductInfo[]; + licenseId?: string; errorToken?: ErrorToken; } @@ -36,6 +37,7 @@ function productsFromString(encodedString: string): ParsedProducts { try { const splitInfo = encodedString.split(';'); + const licenseId = splitInfo[0] || undefined; const productTuples = splitInfo.slice(1).filter((entry) => entry.length > 0); const products = productTuples.map((tuple) => { const parts = tuple.split(','); @@ -49,6 +51,7 @@ function productsFromString(encodedString: string): ParsedProducts { return { products, + licenseId, }; } catch (error) { return { @@ -77,6 +80,7 @@ export function parseDevExpressProductKey(productsLicenseSource: string): Token const { products, + licenseId, errorToken, } = productsFromString( encodeString(productsPayload, shiftDecodeText), @@ -98,6 +102,7 @@ export function parseDevExpressProductKey(productsLicenseSource: string): Token customerId: '', maxVersionAllowed, format: FORMAT, + licenseId, }, }; } catch (error) { diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index ed8540df4ab9..f5a2da07aa79 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -178,10 +178,10 @@ function getLicenseCheckParams({ } if (major * 10 + minor > license.payload.maxVersionAllowed) { - return { preview, error: 'W0020' }; + return { preview, error: 'W0020', licenseId: license.payload.licenseId }; } - return { preview, error: undefined }; + return { preview, error: undefined, licenseId: license.payload.licenseId }; } catch { return { preview, error: 'W0021' }; } @@ -202,7 +202,7 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer const versionsCompatible = assertedVersionsCompatible(version); - const { internal, error } = getLicenseCheckParams({ + const { internal, error, licenseId } = getLicenseCheckParams({ licenseKey, version, }); @@ -218,6 +218,10 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer const preview = isPreview(version.patch); if (error) { + if (licenseId) { + // eslint-disable-next-line no-console + console.warn(`DevExtreme: License ID: ${licenseId}`); + } errors.log(preview ? 'W0022' : error); return; } diff --git a/packages/devextreme/js/__internal/core/license/types.ts b/packages/devextreme/js/__internal/core/license/types.ts index 4e38ccd812ee..4eeb6028ee30 100644 --- a/packages/devextreme/js/__internal/core/license/types.ts +++ b/packages/devextreme/js/__internal/core/license/types.ts @@ -2,6 +2,7 @@ export interface License { readonly [k: string]: unknown; readonly customerId: string; readonly maxVersionAllowed: number; + readonly licenseId?: string; } export enum TokenKind { @@ -41,4 +42,5 @@ export interface LicenseCheckParams { preview: boolean; internal?: true; error: LicenseVerifyResult | undefined; + licenseId?: string; } diff --git a/packages/devextreme/license/devextreme-license-plugin.js b/packages/devextreme/license/devextreme-license-plugin.js index 7777037fb422..f84ecbb7711f 100644 --- a/packages/devextreme/license/devextreme-license-plugin.js +++ b/packages/devextreme/license/devextreme-license-plugin.js @@ -1,6 +1,6 @@ const { createUnplugin } = require('unplugin'); const { getDevExpressLCXKey } = require('./dx-get-lcx'); -const { tryConvertLCXtoLCP, getLCPWarning } = require('./dx-lcx-2-lcp'); +const { tryConvertLCXtoLCP, getLCPInfo } = require('./dx-lcx-2-lcp'); const { MESSAGES } = require('./messages'); const PLUGIN_NAME = 'devextreme-bundler-plugin'; @@ -28,10 +28,13 @@ const DevExtremeLicensePlugin = createUnplugin(() => { warn(ctx, msg); } - function warnLicenseIssue(ctx, source, warning) { + 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{} @@ -51,13 +54,13 @@ const DevExtremeLicensePlugin = createUnplugin(() => { const lcp = tryConvertLCXtoLCP(lcx); if(!lcp) { - warnLicenseIssue(ctx, source, MESSAGES.keyNotFound); + warnLicenseIssue(ctx, source, null, MESSAGES.keyNotFound); return (lcpCache = null); } - const warning = getLCPWarning(lcp); + const { warning, licenseId } = getLCPInfo(lcp); if(warning) { - warnLicenseIssue(ctx, source, warning); + warnLicenseIssue(ctx, source, licenseId, warning); } return (lcpCache = lcp); diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index face0c2824e3..ae0bba7a7457 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -5,7 +5,7 @@ const fs = require('fs'); const path = require('path'); const { getDevExpressLCXKey } = require('./dx-get-lcx'); -const { convertLCXtoLCP, getLCPWarning } = require('./dx-lcx-2-lcp'); +const { convertLCXtoLCP, getLCPInfo } = require('./dx-lcx-2-lcp'); const { MESSAGES } = require('./messages'); const EXPORT_NAME = 'licenseKey'; @@ -126,24 +126,28 @@ function addToGitignore(projectRoot, outAbsPath) { fs.appendFileSync(gitignorePath, (needsNewline ? '\n' : '') + rel + '\n', 'utf8'); } -function renderFile(lcpKey) { - return [ +function renderFile(lcpKey, licenseId) { + const lines = [ '// Auto-generated by devextreme-license.', '// Do not commit this file to source control.', - '', - `export const ${EXPORT_NAME} = ${JSON.stringify(lcpKey)};`, - '', - ].join('\n'); + ]; + if(licenseId) { + lines.push(`// License ID: ${licenseId}`); + } + lines.push('', `export const ${EXPORT_NAME} = ${JSON.stringify(lcpKey)};`, ''); + return lines.join('\n'); } -function renderNonModularFile(lcpKey) { - return [ +function renderNonModularFile(lcpKey, licenseId) { + const lines = [ '// Auto-generated by devextreme-license.', '// Do not commit this file to source control.', - '', - `DevExpress.config({ licenseKey: ${JSON.stringify(lcpKey)} });`, - '', - ].join('\n'); + ]; + if(licenseId) { + lines.push(`// License ID: ${licenseId}`); + } + lines.push('', `DevExpress.config({ licenseKey: ${JSON.stringify(lcpKey)} });`, ''); + return lines.join('\n'); } function main() { @@ -156,13 +160,18 @@ function main() { const { key: lcx, source } = getDevExpressLCXKey() || {}; let lcp = TRIAL_VALUE; + let licenseId = null; if(lcx) { try { lcp = convertLCXtoLCP(lcx); - const warning = getLCPWarning(lcp); + const { warning, licenseId: id } = getLCPInfo(lcp); + licenseId = id; if(warning) { process.stderr.write(`DevExpress license key (LCX) retrieved from: ${source}\n`); + if(licenseId) { + process.stderr.write(`License ID: ${licenseId}\n`); + } process.stderr.write(`[devextreme-license] Warning: ${warning}\n`); } } catch{ @@ -188,7 +197,7 @@ function main() { } const useNonModular = opts.nonModular && outAbs.endsWith('.js'); - writeFileAtomic(outAbs, useNonModular ? renderNonModularFile(lcp) : renderFile(lcp)); + writeFileAtomic(outAbs, useNonModular ? renderNonModularFile(lcp, licenseId) : renderFile(lcp, licenseId)); if(opts.gitignore) { try { diff --git a/packages/devextreme/license/dx-lcx-2-lcp.js b/packages/devextreme/license/dx-lcx-2-lcp.js index 342dfaeca67d..3d6f1e836aa6 100644 --- a/packages/devextreme/license/dx-lcx-2-lcp.js +++ b/packages/devextreme/license/dx-lcx-2-lcp.js @@ -138,15 +138,16 @@ function productsFromString(encodedString) { return { products: [], errorToken: GENERAL_ERROR }; } try { - const productTuples = encodedString.split(';').slice(1).filter(e => e.length > 0); + const splitInfo = encodedString.split(';'); + const licenseId = splitInfo[0]; + const productTuples = splitInfo.slice(1).filter((entry) => entry.length > 0); const products = productTuples.map(tuple => { const parts = tuple.split(','); - return { - version: Number.parseInt(parts[0], 10), - products: BigInt(parts[1]), - }; + const version = Number.parseInt(parts[0], 10); + const products = BigInt(parts[1]); + return { version, products }; }); - return { products }; + return { products, licenseId }; } catch{ return { products: [], errorToken: DESERIALIZATION_ERROR }; } @@ -174,7 +175,7 @@ function parseLCP(lcpString) { const productsPayload = decoded.slice(SIGN_LENGTH); const decodedPayload = mapString(productsPayload, DECODE_MAP); - const { products, errorToken } = productsFromString(decodedPayload); + const { products, errorToken, licenseId } = productsFromString(decodedPayload); if(errorToken) { return errorToken; } @@ -186,7 +187,7 @@ function parseLCP(lcpString) { return { kind: TokenKind.verified, - payload: { customerId: '', maxVersionAllowed }, + payload: { customerId: '', maxVersionAllowed, licenseId }, }; } catch{ return GENERAL_ERROR; @@ -211,36 +212,43 @@ function readDevExtremeVersion() { return null; } -function getLCPWarning(lcpString) { +function getLCPInfo(lcpString) { const token = parseLCP(lcpString); + let warning = null; + let licenseId = null; if(token.kind === TokenKind.corrupted) { if(token.error === 'product-kind') { - return MESSAGES.trial; + warning = MESSAGES.trial; } - return null; - } - - // token.kind === TokenKind.verified — check version compatibility - const devExtremeVersion = readDevExtremeVersion(); - if(devExtremeVersion) { - const { major, minor, code: currentCode } = devExtremeVersion; - const { maxVersionAllowed } = token.payload; - if(maxVersionAllowed < currentCode) { - return MESSAGES.versionIncompatible( - formatVersionCode(maxVersionAllowed), - `v${major}.${minor}`, - ); + } else { + // token.kind === TokenKind.verified — check version compatibility + licenseId = token.payload.licenseId || null; + const devExtremeVersion = readDevExtremeVersion(); + if(devExtremeVersion) { + const { major, minor, code: currentCode } = devExtremeVersion; + const { maxVersionAllowed } = token.payload; + if(maxVersionAllowed < currentCode) { + warning = MESSAGES.versionIncompatible( + formatVersionCode(maxVersionAllowed), + `v${major}.${minor}`, + ); + } } } - return null; + return { warning, licenseId }; +} + +function getLCPWarning(lcpString) { + return getLCPInfo(lcpString).warning; } module.exports = { convertLCXtoLCP, tryConvertLCXtoLCP, parseLCP, + getLCPInfo, getLCPWarning, TokenKind, LCX_SIGNATURE, From bdec4ec2762ec0fa69187ee19d4211ac64b18edd Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Mon, 30 Mar 2026 12:28:40 +0400 Subject: [PATCH 26/48] revert license id warnings in runtime --- .../license/lcp_key_validation/lcp_key_validator.ts | 5 ----- .../js/__internal/core/license/license_validation.ts | 10 +++------- .../devextreme/js/__internal/core/license/types.ts | 2 -- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts index 908000d861d6..8d5f223f95f3 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts @@ -19,7 +19,6 @@ import { encodeString, shiftDecodeText, verifyHash } from './utils'; interface ParsedProducts { products: ProductInfo[]; - licenseId?: string; errorToken?: ErrorToken; } @@ -37,7 +36,6 @@ function productsFromString(encodedString: string): ParsedProducts { try { const splitInfo = encodedString.split(';'); - const licenseId = splitInfo[0] || undefined; const productTuples = splitInfo.slice(1).filter((entry) => entry.length > 0); const products = productTuples.map((tuple) => { const parts = tuple.split(','); @@ -51,7 +49,6 @@ function productsFromString(encodedString: string): ParsedProducts { return { products, - licenseId, }; } catch (error) { return { @@ -80,7 +77,6 @@ export function parseDevExpressProductKey(productsLicenseSource: string): Token const { products, - licenseId, errorToken, } = productsFromString( encodeString(productsPayload, shiftDecodeText), @@ -102,7 +98,6 @@ export function parseDevExpressProductKey(productsLicenseSource: string): Token customerId: '', maxVersionAllowed, format: FORMAT, - licenseId, }, }; } catch (error) { diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index f5a2da07aa79..ed8540df4ab9 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -178,10 +178,10 @@ function getLicenseCheckParams({ } if (major * 10 + minor > license.payload.maxVersionAllowed) { - return { preview, error: 'W0020', licenseId: license.payload.licenseId }; + return { preview, error: 'W0020' }; } - return { preview, error: undefined, licenseId: license.payload.licenseId }; + return { preview, error: undefined }; } catch { return { preview, error: 'W0021' }; } @@ -202,7 +202,7 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer const versionsCompatible = assertedVersionsCompatible(version); - const { internal, error, licenseId } = getLicenseCheckParams({ + const { internal, error } = getLicenseCheckParams({ licenseKey, version, }); @@ -218,10 +218,6 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer const preview = isPreview(version.patch); if (error) { - if (licenseId) { - // eslint-disable-next-line no-console - console.warn(`DevExtreme: License ID: ${licenseId}`); - } errors.log(preview ? 'W0022' : error); return; } diff --git a/packages/devextreme/js/__internal/core/license/types.ts b/packages/devextreme/js/__internal/core/license/types.ts index 4eeb6028ee30..4e38ccd812ee 100644 --- a/packages/devextreme/js/__internal/core/license/types.ts +++ b/packages/devextreme/js/__internal/core/license/types.ts @@ -2,7 +2,6 @@ export interface License { readonly [k: string]: unknown; readonly customerId: string; readonly maxVersionAllowed: number; - readonly licenseId?: string; } export enum TokenKind { @@ -42,5 +41,4 @@ export interface LicenseCheckParams { preview: boolean; internal?: true; error: LicenseVerifyResult | undefined; - licenseId?: string; } From 86939cb916ccb10ac9da007a053e1227347c3c68 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Mon, 30 Mar 2026 13:01:33 +0400 Subject: [PATCH 27/48] remove id from the generated file --- .../devextreme/license/devextreme-license.js | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index ae0bba7a7457..362d1c945352 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -126,28 +126,24 @@ function addToGitignore(projectRoot, outAbsPath) { fs.appendFileSync(gitignorePath, (needsNewline ? '\n' : '') + rel + '\n', 'utf8'); } -function renderFile(lcpKey, licenseId) { - const lines = [ +function renderFile(lcpKey) { + return [ '// Auto-generated by devextreme-license.', '// Do not commit this file to source control.', - ]; - if(licenseId) { - lines.push(`// License ID: ${licenseId}`); - } - lines.push('', `export const ${EXPORT_NAME} = ${JSON.stringify(lcpKey)};`, ''); - return lines.join('\n'); + '', + `export const ${EXPORT_NAME} = ${JSON.stringify(lcpKey)};`, + '', + ].join('\n'); } -function renderNonModularFile(lcpKey, licenseId) { - const lines = [ +function renderNonModularFile(lcpKey) { + return [ '// Auto-generated by devextreme-license.', '// Do not commit this file to source control.', - ]; - if(licenseId) { - lines.push(`// License ID: ${licenseId}`); - } - lines.push('', `DevExpress.config({ licenseKey: ${JSON.stringify(lcpKey)} });`, ''); - return lines.join('\n'); + '', + `DevExpress.config({ licenseKey: ${JSON.stringify(lcpKey)} });`, + '', + ].join('\n'); } function main() { @@ -197,7 +193,7 @@ function main() { } const useNonModular = opts.nonModular && outAbs.endsWith('.js'); - writeFileAtomic(outAbs, useNonModular ? renderNonModularFile(lcp, licenseId) : renderFile(lcp, licenseId)); + writeFileAtomic(outAbs, useNonModular ? renderNonModularFile(lcp) : renderFile(lcp)); if(opts.gitignore) { try { From 5c807fe1df18ec9f98339756dc6cad8e2526c883 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Mon, 30 Mar 2026 18:52:03 +0400 Subject: [PATCH 28/48] new warnings for devextreme-license (CLI tool) --- .../devextreme/license/devextreme-license.js | 32 +++++++++---- packages/devextreme/license/dx-get-lcx.js | 34 +++++++++++-- packages/devextreme/license/dx-lcx-2-lcp.js | 42 ++++++++-------- packages/devextreme/license/messages.js | 48 ++++++++++++++++--- 4 files changed, 114 insertions(+), 42 deletions(-) diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index 362d1c945352..24b464c68d68 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -6,10 +6,11 @@ const path = require('path'); const { getDevExpressLCXKey } = require('./dx-get-lcx'); const { convertLCXtoLCP, getLCPInfo } = require('./dx-lcx-2-lcp'); -const { MESSAGES } = require('./messages'); +const { TEMPLATES } = require('./messages'); const EXPORT_NAME = 'licenseKey'; const TRIAL_VALUE = 'TRIAL'; +const CLI_PREFIX = '[devextreme-license]'; function fail(msg) { process.stderr.write(msg.endsWith('\n') ? msg : msg + '\n'); @@ -153,7 +154,7 @@ function main() { process.exit(0); } - const { key: lcx, source } = getDevExpressLCXKey() || {}; + const { key: lcx, source, currentVersion } = getDevExpressLCXKey() || {}; let lcp = TRIAL_VALUE; let licenseId = null; @@ -164,18 +165,33 @@ function main() { const { warning, licenseId: id } = getLCPInfo(lcp); licenseId = id; if(warning) { - process.stderr.write(`DevExpress license key (LCX) retrieved from: ${source}\n`); if(licenseId) { - process.stderr.write(`License ID: ${licenseId}\n`); + process.stderr.write(`${CLI_PREFIX} ${TEMPLATES.licenseId(licenseId)}\n\n`); } - process.stderr.write(`[devextreme-license] Warning: ${warning}\n`); + process.stderr.write(` + ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense(currentVersion)}\n\n + ${TEMPLATES.keyWasFound(source.type, source.path)}\n + `); + if(warning.type !== 'trial') { + process.stderr.write(` + ${TEMPLATES.keyVerificationFailed(warning.type, warning.keyVersion, warning.requiredVersion)}\n\n + ${CLI_PREFIX} ${TEMPLATES.warningPrefix(TEMPLATES.warningCodeByType(warning.type))} ${TEMPLATES.installationInstructions} + `); + } } } catch{ - process.stderr.write(`DevExpress license key (LCX) retrieved from: ${source}\n`); - process.stderr.write(`[devextreme-license] Warning: ${MESSAGES.keyNotFound}\n`); + process.stderr.write(` + ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense(currentVersion)}\n\n + ${TEMPLATES.keyNotFound}\n\n + ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1001)} ${TEMPLATES.installationInstructions} + `); } } else { - process.stderr.write(`[devextreme-license] Warning: ${MESSAGES.keyNotFound}\n`); + process.stderr.write(` + ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense(currentVersion)}\n\n + ${TEMPLATES.keyNotFound}\n\n + ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1001)} ${TEMPLATES.installationInstructions} + `); } if(!opts.outPath) { diff --git a/packages/devextreme/license/dx-get-lcx.js b/packages/devextreme/license/dx-get-lcx.js index f8535e084e18..570895982abd 100644 --- a/packages/devextreme/license/dx-get-lcx.js +++ b/packages/devextreme/license/dx-get-lcx.js @@ -77,26 +77,52 @@ function resolveFromLicensePathEnv(licensePathValue) { return path.join(p, LICENSE_FILE); } +function readDevExtremeVersion() { + try { + const pkgPath = require('path').join(__dirname, '..', 'package.json'); + const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf8')); + const parts = String(pkg.version || '').split('.'); + const major = parseInt(parts[0], 10); + const minor = parseInt(parts[1], 10); + if(!isNaN(major) && !isNaN(minor)) { + return { major, minor, code: major * 10 + minor }; + } + } catch{} + return null; +} + +function buildVersionString(devExtremeVersion){ + const { major, minor, code: currentCode } = devExtremeVersion; + return `${major}.${minor}`; +} + function getDevExpressLCXKey() { + const devExtremeVersion = readDevExtremeVersion(); + let currentVersion = ''; + if(devExtremeVersion) { + currentVersion = buildVersionString(devExtremeVersion); + } if(hasEnvVar(LICENSE_ENV)) { - return { key: normalizeKey(process.env[LICENSE_ENV]), source: `env:${LICENSE_ENV}` }; + return { key: normalizeKey(process.env[LICENSE_ENV]), source: { type: 'envVariable' }, currentVersion }; } if(hasEnvVar(LICENSE_PATH_ENV)) { const licensePath = resolveFromLicensePathEnv(process.env[LICENSE_PATH_ENV]); const key = normalizeKey(readTextFileIfExists(licensePath)); - return { key, source: key ? `file:${licensePath}` : `env:${LICENSE_PATH_ENV}` }; + return { key, source: { type: 'envPath' }, currentVersion }; } const defaultPath = getDefaultLicenseFilePath(); const fromDefault = normalizeKey(readTextFileIfExists(defaultPath)); if(fromDefault) { - return { key: fromDefault, source: `file:${defaultPath}` }; + return { key: fromDefault, source: { type: 'file', path: defaultPath }, currentVersion }; } - return { key: null, source: null }; + return { key: null, source: null, currentVersion }; } module.exports = { getDevExpressLCXKey, + readDevExtremeVersion, + buildVersionString }; diff --git a/packages/devextreme/license/dx-lcx-2-lcp.js b/packages/devextreme/license/dx-lcx-2-lcp.js index 3d6f1e836aa6..d55d82b182c7 100644 --- a/packages/devextreme/license/dx-lcx-2-lcp.js +++ b/packages/devextreme/license/dx-lcx-2-lcp.js @@ -1,5 +1,5 @@ - +import { readDevExtremeVersion, buildVersionString } from './dx-get-lcx'; const { MESSAGES } = require('./messages'); const LCX_SIGNATURE = 'LCXv1'; const LCP_SIGNATURE = 'LCPv1'; @@ -198,46 +198,42 @@ function formatVersionCode(versionCode) { return `v${Math.floor(versionCode / 10)}.${versionCode % 10}`; } -function readDevExtremeVersion() { - try { - const pkgPath = require('path').join(__dirname, '..', 'package.json'); - const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf8')); - const parts = String(pkg.version || '').split('.'); - const major = parseInt(parts[0], 10); - const minor = parseInt(parts[1], 10); - if(!isNaN(major) && !isNaN(minor)) { - return { major, minor, code: major * 10 + minor }; - } - } catch{} - return null; -} - function getLCPInfo(lcpString) { const token = parseLCP(lcpString); let warning = null; let licenseId = null; + let currentVersion = ''; if(token.kind === TokenKind.corrupted) { - if(token.error === 'product-kind') { - warning = MESSAGES.trial; + switch(token.error) { + case 'general': + warning = { type: 'general' }; + break; + case 'deserialization': + warning = { type: 'corrupted' }; + break; + case 'product-kind': + warning = { type: 'trial' }; + break; } } else { // token.kind === TokenKind.verified — check version compatibility licenseId = token.payload.licenseId || null; const devExtremeVersion = readDevExtremeVersion(); if(devExtremeVersion) { - const { major, minor, code: currentCode } = devExtremeVersion; + currentVersion = buildVersionString(devExtremeVersion); const { maxVersionAllowed } = token.payload; if(maxVersionAllowed < currentCode) { - warning = MESSAGES.versionIncompatible( - formatVersionCode(maxVersionAllowed), - `v${major}.${minor}`, - ); + warning = { + type:'incompatibleVersion', + keyVersion: formatVersionCode(maxVersionAllowed), + currentVersion + }; } } } - return { warning, licenseId }; + return { warning, licenseId, currentVersion }; } function getLCPWarning(lcpString) { diff --git a/packages/devextreme/license/messages.js b/packages/devextreme/license/messages.js index e110da4e5d17..f8b0db3fd2f6 100644 --- a/packages/devextreme/license/messages.js +++ b/packages/devextreme/license/messages.js @@ -13,14 +13,48 @@ const MESSAGES = Object.freeze({ 'Please purchase a license to continue use of the following DevExpress product libraries: ' + 'Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete.', - versionIncompatible: (keyVersion, requiredVersion) => - 'For evaluation purposes only. Redistribution prohibited. ' + - `Incompatible DevExpress license key version (${keyVersion}). ` + - `Download and register an updated DevExpress license key (${requiredVersion}+). ` + - 'Clear IDE/NuGet cache and rebuild your project (devexpress.com/DX1002).', - resolveFailed: 'Failed to resolve license key. Placeholder will remain.', }); -module.exports = { MESSAGES }; +const TEMPLATES = Object.freeze({ + warningPrefix: (number) => `Warning number: DX${number}. For evaluation purposes only. Redistribution prohibited.`, + keyNotFound: 'No valid DevExpress license key was found on this machine.', + keyWasFound: (type, path) => { + switch(type) { + case 'envVariable': + return 'The DevExpress license key was retrieved from the "DevExpress_License" environment variable.'; + case 'envPath': + return 'The DevExpress license key was retrieved from the "DevExpress_LicensePath" environment variable.'; + case 'file': + return `The DevExpress license key was retrieved from file: "${path}".`; + default: + return 'The DevExpress license key was retrieved.'; + } + }, + keyVerificationFailed: (type, keyVersion, requiredVersion) => { + switch(type) { + case 'incompatibleVersion': + return `Incompatible DevExpress license key version (${keyVersion}). Download and register an updated DevExpress license key (${requiredVersion}+). Clear npm/IDE/NuGet cache and rebuild your project.`; + default: + return 'License key verification has failed.'; + } + }, + warningCodeByType: (type) => { + switch(code) { + case 'general': + return 1001; + case 'incompatibleVersion': + return 1002; + default: + return 1001; + } + } + purchaseLicense: (version) => + `Purchase a license to continue use of DevExtreme (v${version}). Included in subscriptions: Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete. To purchase a license, visit https://js.devexpress.com/Buy/`, + installationInstructions: 'If you own a licensed/registered version or if you are using a 30-day trial version of DevExpress product libraries on a development machine, download your personal license key and verify it with the devextreme-licensing tools. Setup instructions: https://js.devexpress.com/Documentation/Guide/Common/Licensing', + oldDevExtremeKey: 'The invalid/old DevExtreme key is used instead of the DevExpress license key.', + licenseId: (id) => `License ID: ${id}`, +}); + +module.exports = { MESSAGES, TEMPLATES }; From 544ede9e4612e30a603e1a47b428f7e5a26428c4 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 1 Apr 2026 10:27:15 +0400 Subject: [PATCH 29/48] cli fixes --- .../devextreme/license/devextreme-license.js | 34 +++++++------------ packages/devextreme/license/dx-lcx-2-lcp.js | 20 ++++++++++- packages/devextreme/license/messages.js | 2 +- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index 24b464c68d68..586b43116164 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -160,31 +160,23 @@ function main() { let licenseId = null; if(lcx) { - try { - lcp = convertLCXtoLCP(lcx); - const { warning, licenseId: id } = getLCPInfo(lcp); - licenseId = id; - if(warning) { - if(licenseId) { - process.stderr.write(`${CLI_PREFIX} ${TEMPLATES.licenseId(licenseId)}\n\n`); - } - process.stderr.write(` - ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense(currentVersion)}\n\n - ${TEMPLATES.keyWasFound(source.type, source.path)}\n - `); - if(warning.type !== 'trial') { - process.stderr.write(` - ${TEMPLATES.keyVerificationFailed(warning.type, warning.keyVersion, warning.requiredVersion)}\n\n - ${CLI_PREFIX} ${TEMPLATES.warningPrefix(TEMPLATES.warningCodeByType(warning.type))} ${TEMPLATES.installationInstructions} - `); - } + lcp = convertLCXtoLCP(lcx); + const { warning, licenseId: id } = getLCPInfo(lcp); + licenseId = id; + if(warning) { + if(licenseId) { + process.stderr.write(`${CLI_PREFIX} ${TEMPLATES.licenseId(licenseId)}\n\n`); } - } catch{ process.stderr.write(` ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense(currentVersion)}\n\n - ${TEMPLATES.keyNotFound}\n\n - ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1001)} ${TEMPLATES.installationInstructions} + ${TEMPLATES.keyWasFound(source.type, source.path)}\n `); + if(warning.type !== 'trial') { + process.stderr.write(` + ${TEMPLATES.keyVerificationFailed(warning.type, warning.keyVersion, warning.requiredVersion)}\n\n + ${CLI_PREFIX} ${TEMPLATES.warningPrefix(TEMPLATES.warningCodeByType(warning.type))} ${TEMPLATES.installationInstructions} + `); + } } } else { process.stderr.write(` diff --git a/packages/devextreme/license/dx-lcx-2-lcp.js b/packages/devextreme/license/dx-lcx-2-lcp.js index d55d82b182c7..737a7e937adb 100644 --- a/packages/devextreme/license/dx-lcx-2-lcp.js +++ b/packages/devextreme/license/dx-lcx-2-lcp.js @@ -1,5 +1,4 @@ -import { readDevExtremeVersion, buildVersionString } from './dx-get-lcx'; const { MESSAGES } = require('./messages'); const LCX_SIGNATURE = 'LCXv1'; const LCP_SIGNATURE = 'LCPv1'; @@ -133,6 +132,25 @@ const GENERAL_ERROR = { kind: TokenKind.corrupted, error: 'general' }; const DESERIALIZATION_ERROR = { kind: TokenKind.corrupted, error: 'deserialization' }; const PRODUCT_KIND_ERROR = { kind: TokenKind.corrupted, error: 'product-kind' }; +function readDevExtremeVersion() { + try { + const pkgPath = require('path').join(__dirname, '..', 'package.json'); + const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf8')); + const parts = String(pkg.version || '').split('.'); + const major = parseInt(parts[0], 10); + const minor = parseInt(parts[1], 10); + if(!isNaN(major) && !isNaN(minor)) { + return { major, minor, code: major * 10 + minor }; + } + } catch{} + return null; +} + +function buildVersionString(devExtremeVersion){ + const { major, minor, code: currentCode } = devExtremeVersion; + return `${major}.${minor}`; +} + function productsFromString(encodedString) { if(!encodedString) { return { products: [], errorToken: GENERAL_ERROR }; diff --git a/packages/devextreme/license/messages.js b/packages/devextreme/license/messages.js index f8b0db3fd2f6..bcc109f55423 100644 --- a/packages/devextreme/license/messages.js +++ b/packages/devextreme/license/messages.js @@ -52,7 +52,7 @@ const TEMPLATES = Object.freeze({ } purchaseLicense: (version) => `Purchase a license to continue use of DevExtreme (v${version}). Included in subscriptions: Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete. To purchase a license, visit https://js.devexpress.com/Buy/`, - installationInstructions: 'If you own a licensed/registered version or if you are using a 30-day trial version of DevExpress product libraries on a development machine, download your personal license key and verify it with the devextreme-licensing tools. Setup instructions: https://js.devexpress.com/Documentation/Guide/Common/Licensing', + installationInstructions: 'If you own a licensed/registered version or if you are using a 30-day trial version of DevExpress product libraries on a development machine, download your personal license key and verify it with the devextreme-license tools. Setup instructions: https://js.devexpress.com/Documentation/Guide/Common/Licensing', oldDevExtremeKey: 'The invalid/old DevExtreme key is used instead of the DevExpress license key.', licenseId: (id) => `License ID: ${id}`, }); From 0a614d4c2964919b5246790328ac589268e2a273 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 1 Apr 2026 11:36:59 +0400 Subject: [PATCH 30/48] add browser warnings for license --- .../core/license/license_validation.test.ts | 41 +++++---- .../core/license/license_validation.ts | 49 ++++++++--- .../core/license/license_warnings.ts | 88 +++++++++++++++++++ .../js/__internal/core/license/types.ts | 9 ++ 4 files changed, 158 insertions(+), 29 deletions(-) create mode 100644 packages/devextreme/js/__internal/core/license/license_warnings.ts diff --git a/packages/devextreme/js/__internal/core/license/license_validation.test.ts b/packages/devextreme/js/__internal/core/license/license_validation.test.ts index daa32510246a..4b10228d005c 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.test.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.test.ts @@ -294,9 +294,11 @@ describe('license check', () => { const TOKEN_UNSUPPORTED_VERSION = 'ewogICJmb3JtYXQiOiAyLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxCn0=.tTBymZMROsYyMiP6ldXFqGurbzqjhSQIu/pjyEUJA3v/57VgToomYl7FVzBj1asgHpadvysyTUiX3nFvPxbp166L3+LB3Jybw9ueMnwePu5vQOO0krqKLBqRq+TqHKn7k76uYRbkCIo5UajNfzetHhlkin3dJf3x2K/fcwbPW5A='; let trialPanelSpy = jest.spyOn(trialPanel, 'renderTrialPanel'); + let consoleWarnSpy = jest.spyOn(console, 'warn'); beforeEach(() => { jest.spyOn(errors, 'log').mockImplementation(() => {}); + consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); trialPanelSpy = jest.spyOn(trialPanel, 'renderTrialPanel'); setLicenseCheckSkipCondition(false); }); @@ -309,10 +311,10 @@ describe('license check', () => { { token: '', version: '1.0.3' }, { token: null, version: '1.0.4' }, { token: undefined, version: '1.0.50' }, - ])('W0019 error should be logged if license is empty', ({ token, version }) => { + ])('Warning should be logged with no-key message if license is empty', ({ token, version }) => { validateLicense(token as string, version); - expect(errors.log).toHaveBeenCalledTimes(1); - expect(errors.log).toHaveBeenCalledWith('W0019'); + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('No valid DevExpress license key was found')); }); test.each([ @@ -357,9 +359,10 @@ describe('license check', () => { { token: TOKEN_23_1, version: '12.3.4' }, { token: TOKEN_23_2, version: '23.1.5' }, { token: TOKEN_23_2, version: '23.2.6' }, - ])('No messages should be logged if license is valid', ({ token, version }) => { + ])('Old format license should trigger old-key warning', ({ token, version }) => { validateLicense(token, version); - expect(errors.log).not.toHaveBeenCalled(); + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('invalid/old DevExtreme key')); }); test.each([ @@ -367,9 +370,9 @@ describe('license check', () => { { token: TOKEN_23_1, version: '12.3.4' }, { token: TOKEN_23_2, version: '23.1.5' }, { token: TOKEN_23_2, version: '23.2.6' }, - ])('Trial panel should not be displayed if license is valid', ({ token, version }) => { + ])('Trial panel should be displayed for old format license keys', ({ token, version }) => { validateLicense(token, version); - expect(trialPanelSpy).not.toHaveBeenCalled(); + expect(trialPanelSpy).toHaveBeenCalledTimes(1); }); test('Trial panel "Buy Now" link must use the jQuery link if no config has been set', () => { @@ -399,15 +402,16 @@ describe('license check', () => { setLicenseCheckSkipCondition(); validateLicense('', '1.0'); expect(errors.log).not.toHaveBeenCalled(); + expect(consoleWarnSpy).not.toHaveBeenCalled(); }); test.each([ { token: TOKEN_23_1, version: '23.2.3' }, { token: TOKEN_23_2, version: '42.4.5' }, - ])('W0020 error should be logged if license is outdated', ({ token, version }) => { + ])('Old format license should trigger old-key warning when outdated', ({ token, version }) => { validateLicense(token, version); - expect(errors.log).toHaveBeenCalledTimes(1); - expect(errors.log).toHaveBeenCalledWith('W0020'); + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('invalid/old DevExtreme key')); }); test.each([ @@ -425,9 +429,9 @@ describe('license check', () => { { token: TOKEN_23_1, version: '23.2.3-alpha' }, { token: TOKEN_23_2, version: '24.1.0' }, { token: TOKEN_23_2, version: '24.1.abc' }, - ])('Trial panel should not be displayed in previews if the license is for the previous RTM', ({ token, version }) => { + ])('Trial panel should be displayed in previews for old format license keys', ({ token, version }) => { validateLicense(token, version); - expect(trialPanelSpy).not.toHaveBeenCalled(); + expect(trialPanelSpy).toHaveBeenCalledTimes(1); }); test.each([ @@ -440,9 +444,10 @@ describe('license check', () => { { token: TOKEN_UNSUPPORTED_VERSION, version: '1.2.3' }, { token: 'str@nge in.put', version: '1.2.3' }, { token: '3.2.1', version: '1.2.3' }, - ])('W0021 error should be logged if license is corrupted/invalid [%#]', ({ token, version }) => { + ])('License verification warning should be logged if license is corrupted/invalid [%#]', ({ token, version }) => { validateLicense(token, version); - expect(errors.log).toHaveBeenCalledWith('W0021'); + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('License key verification has failed')); }); test.each([ @@ -514,9 +519,11 @@ describe('internal license check', () => { describe('DevExpress license check', () => { let trialPanelSpy = jest.spyOn(trialPanel, 'renderTrialPanel'); + let consoleWarnSpy = jest.spyOn(console, 'warn'); beforeEach(() => { jest.spyOn(errors, 'log').mockImplementation(() => {}); + consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); trialPanelSpy = jest.spyOn(trialPanel, 'renderTrialPanel'); setLicenseCheckSkipCondition(false); }); @@ -528,14 +535,16 @@ describe('DevExpress license check', () => { test('DevExpress License Key copied from Download Manager (incorrect)', () => { const token = 'LCXv1therestofthekey'; validateLicense(token, '25.1.3'); - expect(errors.log).toHaveBeenCalled(); + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('.NET license key (LCX)')); expect(trialPanelSpy).toHaveBeenCalled(); }); test('DevExpress License Key generated from LCX key (incorrect)', () => { const token = 'LCPtherestofthekey'; validateLicense(token, '25.1.3'); - expect(errors.log).toHaveBeenCalled(); + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('License key verification has failed')); expect(trialPanelSpy).toHaveBeenCalled(); }); }); diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index ed8540df4ab9..746ffa3e5002 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -14,6 +14,7 @@ import { } from './const'; import { INTERNAL_USAGE_ID, PUBLIC_KEY } from './key'; import { isProductOnlyLicense, parseDevExpressProductKey } from './lcp_key_validation/lcp_key_validator'; +import { logLicenseWarning } from './license_warnings'; import { pad } from './pkcs1'; import { compareSignatures } from './rsa_bigint'; import { sha1 } from './sha1'; @@ -156,17 +157,21 @@ function getLicenseCheckParams({ const { major, minor } = preview ? getPreviousMajorVersion(version) : version; if (!licenseKey) { - return { preview, error: 'W0019' }; + return { preview, error: 'W0019', warningType: 'no-key' }; } if (hasLicensePrefix(licenseKey, 'LCX')) { - return { preview, error: 'W0024' }; + return { preview, error: 'W0021', warningType: 'lcx-used' }; } + const isProductKey = isProductOnlyLicense(licenseKey); const license = parseLicenseKey(licenseKey); if (license.kind === TokenKind.corrupted) { - return { preview, error: 'W0021' }; + if (license.error === 'product-kind') { + return { preview, error: 'W0021', warningType: 'no-devextreme-license' }; + } + return { preview, error: 'W0021', warningType: 'invalid-key' }; } if (license.kind === TokenKind.internal) { @@ -174,16 +179,25 @@ function getLicenseCheckParams({ } if (!(major && minor)) { - return { preview, error: 'W0021' }; + return { preview, error: 'W0021', warningType: 'invalid-key' }; + } + + if (!isProductKey) { + return { preview, error: 'W0021', warningType: 'old-devextreme-key' }; } if (major * 10 + minor > license.payload.maxVersionAllowed) { - return { preview, error: 'W0020' }; + return { + preview, + error: 'W0020', + warningType: 'version-mismatch', + maxVersionAllowed: license.payload.maxVersionAllowed, + }; } return { preview, error: undefined }; } catch { - return { preview, error: 'W0021' }; + return { preview, error: 'W0021', warningType: 'invalid-key' }; } } @@ -193,16 +207,13 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer } validationPerformed = true; - if (isUnsupportedKeyFormat(licenseKey)) { - displayTrialPanel(); - return; - } - const version = parseVersion(versionStr); const versionsCompatible = assertedVersionsCompatible(version); - const { internal, error } = getLicenseCheckParams({ + const { + internal, error, warningType, maxVersionAllowed, + } = getLicenseCheckParams({ licenseKey, version, }); @@ -218,7 +229,19 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer const preview = isPreview(version.patch); if (error) { - errors.log(preview ? 'W0022' : error); + if (preview) { + errors.log('W0022'); + } else if (warningType) { + const versionInfo = warningType === 'version-mismatch' && maxVersionAllowed !== undefined + ? { + keyVersion: `${Math.floor(maxVersionAllowed / 10)}.${maxVersionAllowed % 10}`, + requiredVersion: `${version.major}.${version.minor}`, + } + : undefined; + logLicenseWarning(warningType, versionStr, versionInfo); + } else { + errors.log(error); + } return; } diff --git a/packages/devextreme/js/__internal/core/license/license_warnings.ts b/packages/devextreme/js/__internal/core/license/license_warnings.ts new file mode 100644 index 000000000000..c4b73072b225 --- /dev/null +++ b/packages/devextreme/js/__internal/core/license/license_warnings.ts @@ -0,0 +1,88 @@ +import type { LicenseWarningType } from './types'; + +const SOURCE = '[devextreme-license]'; + +export const TEMPLATES = Object.freeze({ + warningPrefix: (code: string | number): string => `Warning number: ${code}. For evaluation purposes only. Redistribution prohibited.`, + + keyNotFound: 'No valid DevExpress license key was found on this machine.', + + keyWasFound: (type: string, path?: string): string => { + switch (type) { + case 'envVariable': + return 'The DevExpress license key was retrieved from the "DevExpress_License" environment variable.'; + case 'envPath': + return 'The DevExpress license key was retrieved from the "DevExpress_LicensePath" environment variable.'; + case 'file': + return `The DevExpress license key was retrieved from file: "${path}".`; + default: + return 'The DevExpress license key was retrieved.'; + } + }, + + keyVerificationFailed: (type?: string, keyVersion?: string, requiredVersion?: string): string => { + switch (type) { + case 'incompatibleVersion': + return `Incompatible DevExpress license key version (${keyVersion}). Download and register an updated DevExpress license key (${requiredVersion}+). Clear npm/IDE/NuGet cache and rebuild your project.`; + default: + return 'License key verification has failed.'; + } + }, + + purchaseLicense: (version: string): string => `Purchase a license to continue use of DevExtreme (v${version}). Included in subscriptions: Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete. To purchase a license, visit https://js.devexpress.com/Buy/`, + + installationInstructions: 'If you own a licensed/registered version or if you are using a 30-day trial version of DevExpress product libraries on a development machine, download your personal license key and verify it with the devextreme-license tools. Setup instructions: https://js.devexpress.com/Documentation/Guide/Common/Licensing', + + // eslint-disable-next-line spellcheck/spell-checker + lcxUsedInsteadOfLcp: 'The .NET license key (LCX) is used instead of the DevExpress license key (LCP).', + + oldDevExtremeKey: 'The invalid/old DevExtreme key is used instead of the DevExpress license key.', + + licenseId: (id: string): string => `License ID: ${id}`, +}); + +export function logLicenseWarning( + warningType: LicenseWarningType, + version: string, + versionInfo?: { keyVersion: string; requiredVersion: string }, +): void { + const T = TEMPLATES; + + const purchaseLine = `${SOURCE} ${T.warningPrefix('W0019')} ${T.purchaseLicense(version)}`; + const installLine = `${SOURCE} ${T.warningPrefix('W0021')} ${T.installationInstructions}`; + + const lines: string[] = [purchaseLine]; + + switch (warningType) { + case 'no-key': + lines.push('', T.keyNotFound, '', installLine); + break; + + case 'invalid-key': + lines.push('', T.keyVerificationFailed(), '', installLine); + break; + + case 'lcx-used': + // eslint-disable-next-line spellcheck/spell-checker + lines.push('', T.keyVerificationFailed(), T.lcxUsedInsteadOfLcp, '', installLine); + break; + + case 'old-devextreme-key': + lines.push('', T.keyVerificationFailed(), T.oldDevExtremeKey, '', installLine); + break; + + case 'version-mismatch': { + const incompatibleLine = `${SOURCE} ${T.warningPrefix('W0020')} ${T.keyVerificationFailed('incompatibleVersion', versionInfo?.keyVersion, versionInfo?.requiredVersion)}`; + lines.push('', T.keyVerificationFailed(), '', incompatibleLine); + break; + } + + case 'no-devextreme-license': + // Only the purchase line, no additional details + break; + default: + break; + } + + console.warn(lines.join('\n')); +} diff --git a/packages/devextreme/js/__internal/core/license/types.ts b/packages/devextreme/js/__internal/core/license/types.ts index 4e38ccd812ee..104340d3584f 100644 --- a/packages/devextreme/js/__internal/core/license/types.ts +++ b/packages/devextreme/js/__internal/core/license/types.ts @@ -37,8 +37,17 @@ export const PAYLOAD_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'pa export const VERSION_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'version' }; export const PRODUCT_KIND_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'product-kind' }; +export type LicenseWarningType = 'no-key' + | 'invalid-key' + | 'lcx-used' + | 'old-devextreme-key' + | 'version-mismatch' + | 'no-devextreme-license'; + export interface LicenseCheckParams { preview: boolean; internal?: true; error: LicenseVerifyResult | undefined; + warningType?: LicenseWarningType; + maxVersionAllowed?: number; } From b1e31ec6c03c0e4ff84eb85dc02a9e89ccb4990c Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 1 Apr 2026 15:13:45 +0400 Subject: [PATCH 31/48] fix typo --- packages/devextreme/license/messages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/license/messages.js b/packages/devextreme/license/messages.js index bcc109f55423..cb7f14875f76 100644 --- a/packages/devextreme/license/messages.js +++ b/packages/devextreme/license/messages.js @@ -49,7 +49,7 @@ const TEMPLATES = Object.freeze({ default: return 1001; } - } + }, purchaseLicense: (version) => `Purchase a license to continue use of DevExtreme (v${version}). Included in subscriptions: Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete. To purchase a license, visit https://js.devexpress.com/Buy/`, installationInstructions: 'If you own a licensed/registered version or if you are using a 30-day trial version of DevExpress product libraries on a development machine, download your personal license key and verify it with the devextreme-license tools. Setup instructions: https://js.devexpress.com/Documentation/Guide/Common/Licensing', From 25fe95b5781a79ab4101ad99c692fda0989a7683 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 1 Apr 2026 15:54:28 +0400 Subject: [PATCH 32/48] fix small issues --- packages/devextreme/license/devextreme-license.js | 4 ++-- packages/devextreme/license/dx-lcx-2-lcp.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index 586b43116164..029217eda35a 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -5,7 +5,7 @@ const fs = require('fs'); const path = require('path'); const { getDevExpressLCXKey } = require('./dx-get-lcx'); -const { convertLCXtoLCP, getLCPInfo } = require('./dx-lcx-2-lcp'); +const { tryConvertLCXtoLCP, getLCPInfo } = require('./dx-lcx-2-lcp'); const { TEMPLATES } = require('./messages'); const EXPORT_NAME = 'licenseKey'; @@ -160,7 +160,7 @@ function main() { let licenseId = null; if(lcx) { - lcp = convertLCXtoLCP(lcx); + lcp = tryConvertLCXtoLCP(lcx); const { warning, licenseId: id } = getLCPInfo(lcp); licenseId = id; if(warning) { diff --git a/packages/devextreme/license/dx-lcx-2-lcp.js b/packages/devextreme/license/dx-lcx-2-lcp.js index 737a7e937adb..1e863481e20f 100644 --- a/packages/devextreme/license/dx-lcx-2-lcp.js +++ b/packages/devextreme/license/dx-lcx-2-lcp.js @@ -241,7 +241,7 @@ function getLCPInfo(lcpString) { if(devExtremeVersion) { currentVersion = buildVersionString(devExtremeVersion); const { maxVersionAllowed } = token.payload; - if(maxVersionAllowed < currentCode) { + if(maxVersionAllowed < devExtremeVersion.code) { warning = { type:'incompatibleVersion', keyVersion: formatVersionCode(maxVersionAllowed), From 339127411b3b5e13ceb69cd987644d15d33d4e14 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 1 Apr 2026 16:11:16 +0400 Subject: [PATCH 33/48] type fix --- packages/devextreme/license/messages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/license/messages.js b/packages/devextreme/license/messages.js index cb7f14875f76..b619c72838fb 100644 --- a/packages/devextreme/license/messages.js +++ b/packages/devextreme/license/messages.js @@ -41,7 +41,7 @@ const TEMPLATES = Object.freeze({ } }, warningCodeByType: (type) => { - switch(code) { + switch(type) { case 'general': return 1001; case 'incompatibleVersion': From 1b0c8d009d97ecf9524e3f173f6ac96dbeef2083 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 1 Apr 2026 16:16:09 +0400 Subject: [PATCH 34/48] minor fixes --- packages/devextreme/license/devextreme-license.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index 029217eda35a..759e8dd515d9 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -160,7 +160,7 @@ function main() { let licenseId = null; if(lcx) { - lcp = tryConvertLCXtoLCP(lcx); + lcp = tryConvertLCXtoLCP(lcx) || TRIAL_VALUE; const { warning, licenseId: id } = getLCPInfo(lcp); licenseId = id; if(warning) { @@ -173,7 +173,7 @@ function main() { `); if(warning.type !== 'trial') { process.stderr.write(` - ${TEMPLATES.keyVerificationFailed(warning.type, warning.keyVersion, warning.requiredVersion)}\n\n + ${TEMPLATES.keyVerificationFailed(warning.type, warning.keyVersion, warning.currentVersion)}\n\n ${CLI_PREFIX} ${TEMPLATES.warningPrefix(TEMPLATES.warningCodeByType(warning.type))} ${TEMPLATES.installationInstructions} `); } From f6f5d27ce7ec4ed306ded49ba2076602bf7e5c7a Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Mon, 6 Apr 2026 15:57:26 +0400 Subject: [PATCH 35/48] browser warnings update --- .../js/__internal/core/license/const.ts | 2 + .../core/license/license_validation.test.ts | 36 ++--------------- .../core/license/license_validation.ts | 16 ++------ .../core/license/license_warnings.ts | 39 +++++++++++++------ 4 files changed, 37 insertions(+), 56 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/const.ts b/packages/devextreme/js/__internal/core/license/const.ts index ad1ba4de7b11..18a3cd0247eb 100644 --- a/packages/devextreme/js/__internal/core/license/const.ts +++ b/packages/devextreme/js/__internal/core/license/const.ts @@ -5,5 +5,7 @@ export const KEY_SPLITTER = '.'; export const BUY_NOW_LINK = 'https://go.devexpress.com/Licensing_Installer_Watermark_DevExtremeJQuery.aspx'; export const LICENSING_DOC_LINK = 'https://go.devexpress.com/Licensing_Documentation_DevExtremeJQuery.aspx'; +export const LICENSE_KEY_PLACEHOLDER = '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */'; + export const NBSP = '\u00A0'; export const SUBSCRIPTION_NAMES = `Universal, DXperience, ASP.NET${NBSP}and${NBSP}Blazor, DevExtreme${NBSP}Complete`; diff --git a/packages/devextreme/js/__internal/core/license/license_validation.test.ts b/packages/devextreme/js/__internal/core/license/license_validation.test.ts index 4b10228d005c..de13ecb07eff 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.test.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.test.ts @@ -10,6 +10,7 @@ import { assertedVersionsCompatible, clearAssertedVersions, } from '../../utils/version'; +import { LICENSE_KEY_PLACEHOLDER } from './const'; import { parseLicenseKey, setLicenseCheckSkipCondition, @@ -311,6 +312,7 @@ describe('license check', () => { { token: '', version: '1.0.3' }, { token: null, version: '1.0.4' }, { token: undefined, version: '1.0.50' }, + { token: LICENSE_KEY_PLACEHOLDER, version: '1.0.3' }, ])('Warning should be logged with no-key message if license is empty', ({ token, version }) => { validateLicense(token as string, version); expect(consoleWarnSpy).toHaveBeenCalledTimes(1); @@ -324,36 +326,13 @@ describe('license check', () => { { token: '', version: '1.0.0' }, { token: null, version: '1.2.4-preview' }, { token: undefined, version: '1.2' }, + { token: LICENSE_KEY_PLACEHOLDER, version: '1.0.3' }, + { token: LICENSE_KEY_PLACEHOLDER, version: '1.0.0' }, ])('trial panel should be displayed if license is empty, preview or not', ({ token, version }) => { validateLicense(token as string, version); expect(trialPanelSpy).toHaveBeenCalledTimes(1); }); - test.each([ - { token: '', version: '1.0' }, - { token: null, version: '1.0.' }, - { token: undefined, version: '1.0.0' }, - { token: TOKEN_23_1, version: '23.1.0' }, - { token: TOKEN_23_1, version: '12.3.1' }, - { token: TOKEN_23_2, version: '23.1.2' }, - { token: TOKEN_23_2, version: '23.2.3-preview' }, - { token: TOKEN_23_1, version: '23.2.0' }, - { token: TOKEN_23_2, version: '42.4.3-alfa' }, - { token: TOKEN_UNVERIFIED, version: '1.2.0' }, - { token: TOKEN_INVALID_JSON, version: '1.2.1' }, - { token: TOKEN_INVALID_BASE64, version: '1.2.2' }, - { token: TOKEN_MISSING_FIELD_1, version: '1.2' }, - { token: TOKEN_MISSING_FIELD_2, version: '1.2.4-preview' }, - { token: TOKEN_MISSING_FIELD_3, version: '1.2.' }, - { token: TOKEN_UNSUPPORTED_VERSION, version: '1.2.abc' }, - { token: 'Another', version: '1.2.0' }, - { token: '3.2.1', version: '1.2.1' }, - { token: TOKEN_23_1, version: '123' }, - ])('W0022 error should be logged if version is preview [%#]', ({ token, version }) => { - validateLicense(token as string, version); - expect(errors.log).toHaveBeenCalledWith('W0022'); - }); - test.each([ { token: TOKEN_23_1, version: '23.1.3' }, { token: TOKEN_23_1, version: '12.3.4' }, @@ -508,13 +487,6 @@ describe('internal license check', () => { expect(errors.log).toHaveBeenCalledWith('W0020'); expect(trialPanelSpy).not.toHaveBeenCalled(); }); - - test('internal usage token (incorrect, pre-release)', () => { - const token = 'ewogICJpbnRlcm5hbFVzYWdlSWQiOiAiUDdmNU5icU9WMDZYRFVpa3Q1bkRyQSIsCiAgImZvcm1hdCI6IDEKfQ==.ox52WAqudazQ0ZKdnJqvh/RmNNNX+IB9cmun97irvSeZK2JMf9sbBXC1YCrSZNIPBjQapyIV8Ctv9z2wzb3BkWy+R9CEh+ev7purq7Lk0ugpwDye6GaCzqlDg+58EHwPCNaasIuBiQC3ztvOItrGwWSu0aEFooiajk9uAWwzWeM='; - validateLicense(token, '1.2.1'); - expect(errors.log).toHaveBeenCalledWith('W0022'); - expect(trialPanelSpy).not.toHaveBeenCalled(); - }); }); describe('DevExpress license check', () => { diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index 746ffa3e5002..bbf7c87c815a 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -10,7 +10,8 @@ import { } from '../../utils/version'; import { base64ToBytes } from './byte_utils'; import { - BUY_NOW_LINK, FORMAT, KEY_SPLITTER, LICENSING_DOC_LINK, RTM_MIN_PATCH_VERSION, SUBSCRIPTION_NAMES, + BUY_NOW_LINK, FORMAT, KEY_SPLITTER, LICENSE_KEY_PLACEHOLDER, + LICENSING_DOC_LINK, RTM_MIN_PATCH_VERSION, SUBSCRIPTION_NAMES, } from './const'; import { INTERNAL_USAGE_ID, PUBLIC_KEY } from './key'; import { isProductOnlyLicense, parseDevExpressProductKey } from './lcp_key_validation/lcp_key_validator'; @@ -156,7 +157,7 @@ function getLicenseCheckParams({ const { major, minor } = preview ? getPreviousMajorVersion(version) : version; - if (!licenseKey) { + if (!licenseKey || licenseKey === LICENSE_KEY_PLACEHOLDER) { return { preview, error: 'W0019', warningType: 'no-key' }; } @@ -226,12 +227,8 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer displayTrialPanel(); } - const preview = isPreview(version.patch); - if (error) { - if (preview) { - errors.log('W0022'); - } else if (warningType) { + if (warningType) { const versionInfo = warningType === 'version-mismatch' && maxVersionAllowed !== undefined ? { keyVersion: `${Math.floor(maxVersionAllowed / 10)}.${maxVersionAllowed % 10}`, @@ -242,11 +239,6 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer } else { errors.log(error); } - return; - } - - if (preview && !internal) { - errors.log('W0022'); } } diff --git a/packages/devextreme/js/__internal/core/license/license_warnings.ts b/packages/devextreme/js/__internal/core/license/license_warnings.ts index c4b73072b225..cb9a68ad5cf6 100644 --- a/packages/devextreme/js/__internal/core/license/license_warnings.ts +++ b/packages/devextreme/js/__internal/core/license/license_warnings.ts @@ -1,11 +1,26 @@ import type { LicenseWarningType } from './types'; -const SOURCE = '[devextreme-license]'; - export const TEMPLATES = Object.freeze({ - warningPrefix: (code: string | number): string => `Warning number: ${code}. For evaluation purposes only. Redistribution prohibited.`, + warningPrefix: (code: string | number): string => { + let warningDescription = ''; + switch (code) { + case 'W0019': + warningDescription = 'DevExtreme: You are using a trial (evaluation) version of DevExtreme.'; + break; + case 'W0020': + warningDescription = 'DevExtreme: License Key Has Expired.'; + break; + case 'W0021': + warningDescription = 'DevExtreme: License Key Verification Has Failed.'; + break; + default: + warningDescription = 'DevExtreme: For evaluation purposes only. Redistribution prohibited.'; + break; + } + return `${code} - ${warningDescription}`; + }, - keyNotFound: 'No valid DevExpress license key was found on this machine.', + keyNotFound: 'A devextreme-license generated key has not been specified in the GlobalConfig.', keyWasFound: (type: string, path?: string): string => { switch (type) { @@ -48,32 +63,32 @@ export function logLicenseWarning( ): void { const T = TEMPLATES; - const purchaseLine = `${SOURCE} ${T.warningPrefix('W0019')} ${T.purchaseLicense(version)}`; - const installLine = `${SOURCE} ${T.warningPrefix('W0021')} ${T.installationInstructions}`; + const purchaseLine = `${T.warningPrefix('W0019')} ${T.purchaseLicense(version)}`; + const installLine = `${T.warningPrefix('W0021')} ${T.installationInstructions}`; const lines: string[] = [purchaseLine]; switch (warningType) { case 'no-key': - lines.push('', T.keyNotFound, '', installLine); + lines.push(T.keyNotFound, installLine); break; case 'invalid-key': - lines.push('', T.keyVerificationFailed(), '', installLine); + lines.push(T.keyVerificationFailed(), installLine); break; case 'lcx-used': // eslint-disable-next-line spellcheck/spell-checker - lines.push('', T.keyVerificationFailed(), T.lcxUsedInsteadOfLcp, '', installLine); + lines.push(T.keyVerificationFailed(), T.lcxUsedInsteadOfLcp, installLine); break; case 'old-devextreme-key': - lines.push('', T.keyVerificationFailed(), T.oldDevExtremeKey, '', installLine); + lines.push(T.keyVerificationFailed(), T.oldDevExtremeKey, installLine); break; case 'version-mismatch': { - const incompatibleLine = `${SOURCE} ${T.warningPrefix('W0020')} ${T.keyVerificationFailed('incompatibleVersion', versionInfo?.keyVersion, versionInfo?.requiredVersion)}`; - lines.push('', T.keyVerificationFailed(), '', incompatibleLine); + const incompatibleLine = `${T.warningPrefix('W0020')} ${T.keyVerificationFailed('incompatibleVersion', versionInfo?.keyVersion, versionInfo?.requiredVersion)}`; + lines.push(T.keyVerificationFailed(), incompatibleLine); break; } From d06ccd9986e0240e6f07fd7a609536c287dd117a Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Mon, 6 Apr 2026 16:19:39 +0400 Subject: [PATCH 36/48] part of cli warning fixes --- .../devextreme/license/devextreme-license.js | 28 +++++++++---------- packages/devextreme/license/messages.js | 6 ++-- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index 759e8dd515d9..bfd936f3f75a 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -164,26 +164,24 @@ function main() { const { warning, licenseId: id } = getLCPInfo(lcp); licenseId = id; if(warning) { + const parts = []; if(licenseId) { - process.stderr.write(`${CLI_PREFIX} ${TEMPLATES.licenseId(licenseId)}\n\n`); + parts.push(`${CLI_PREFIX} ${TEMPLATES.licenseId(licenseId)}`); } - process.stderr.write(` - ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense(currentVersion)}\n\n - ${TEMPLATES.keyWasFound(source.type, source.path)}\n - `); + parts.push(`${CLI_PREFIX} ${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense(currentVersion)}`); + parts.push(TEMPLATES.keyWasFound(source.type, source.path)); if(warning.type !== 'trial') { - process.stderr.write(` - ${TEMPLATES.keyVerificationFailed(warning.type, warning.keyVersion, warning.currentVersion)}\n\n - ${CLI_PREFIX} ${TEMPLATES.warningPrefix(TEMPLATES.warningCodeByType(warning.type))} ${TEMPLATES.installationInstructions} - `); - } + parts.push(TEMPLATES.keyVerificationFailed(warning.type, warning.keyVersion, warning.currentVersion)); + parts.push(`${CLI_PREFIX} ${TEMPLATES.warningPrefix(TEMPLATES.warningCodeByType(warning.type))} ${TEMPLATES.installationInstructions}`); + } + process.stderr.write(parts.join('\n') + '\n\n'); } } else { - process.stderr.write(` - ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense(currentVersion)}\n\n - ${TEMPLATES.keyNotFound}\n\n - ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1001)} ${TEMPLATES.installationInstructions} - `); + process.stderr.write( + `${CLI_PREFIX} ${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense(currentVersion)}\n` + + `${TEMPLATES.keyNotFound}\n` + + `${CLI_PREFIX} ${TEMPLATES.warningPrefix(1001)} ${TEMPLATES.installationInstructions}\n\n` + ); } if(!opts.outPath) { diff --git a/packages/devextreme/license/messages.js b/packages/devextreme/license/messages.js index b619c72838fb..6ce04451fba6 100644 --- a/packages/devextreme/license/messages.js +++ b/packages/devextreme/license/messages.js @@ -19,7 +19,7 @@ const MESSAGES = Object.freeze({ const TEMPLATES = Object.freeze({ warningPrefix: (number) => `Warning number: DX${number}. For evaluation purposes only. Redistribution prohibited.`, - keyNotFound: 'No valid DevExpress license key was found on this machine.', + keyNotFound: 'A valid DevExpress license key was not found on this machine.', keyWasFound: (type, path) => { switch(type) { case 'envVariable': @@ -52,8 +52,8 @@ const TEMPLATES = Object.freeze({ }, purchaseLicense: (version) => `Purchase a license to continue use of DevExtreme (v${version}). Included in subscriptions: Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete. To purchase a license, visit https://js.devexpress.com/Buy/`, - installationInstructions: 'If you own a licensed/registered version or if you are using a 30-day trial version of DevExpress product libraries on a development machine, download your personal license key and verify it with the devextreme-license tools. Setup instructions: https://js.devexpress.com/Documentation/Guide/Common/Licensing', - oldDevExtremeKey: 'The invalid/old DevExtreme key is used instead of the DevExpress license key.', + installationInstructions: 'If you own a licensed/registered version or if you are using a 30-day trial version of DevExpress product libraries on a development machine, download your personal license key and verify it with the devextreme-license tool (https://devexpress.com/DX1001).', + oldDevExtremeKey: (version) => `A DevExtreme key (v25_1 or earlier) has been detected. Use DevExpress license key (v${version}+) instead.`, licenseId: (id) => `License ID: ${id}`, }); From d0ffcc0c43d80f4e83ad6e846829a8d2ed6051a1 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Mon, 6 Apr 2026 16:25:14 +0400 Subject: [PATCH 37/48] browser warning messages update --- .../js/__internal/core/license/license_warnings.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/license_warnings.ts b/packages/devextreme/js/__internal/core/license/license_warnings.ts index cb9a68ad5cf6..a16ee3dc9d48 100644 --- a/packages/devextreme/js/__internal/core/license/license_warnings.ts +++ b/packages/devextreme/js/__internal/core/license/license_warnings.ts @@ -46,12 +46,12 @@ export const TEMPLATES = Object.freeze({ purchaseLicense: (version: string): string => `Purchase a license to continue use of DevExtreme (v${version}). Included in subscriptions: Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete. To purchase a license, visit https://js.devexpress.com/Buy/`, - installationInstructions: 'If you own a licensed/registered version or if you are using a 30-day trial version of DevExpress product libraries on a development machine, download your personal license key and verify it with the devextreme-license tools. Setup instructions: https://js.devexpress.com/Documentation/Guide/Common/Licensing', + installationInstructions: 'If you own a licensed/registered version or if you are using a 30-day trial version of DevExpress product libraries on a development machine, download your personal license key and verify it with the devextreme-license tool (https://devexpress.com/DX1001).', // eslint-disable-next-line spellcheck/spell-checker - lcxUsedInsteadOfLcp: 'The .NET license key (LCX) is used instead of the DevExpress license key (LCP).', + lcxUsedInsteadOfLcp: 'A DevExpress license key has been specified instead of a key generated using devextreme-license.', - oldDevExtremeKey: 'The invalid/old DevExtreme key is used instead of the DevExpress license key.', + oldDevExtremeKey: 'A DevExtreme key (v25.2 or earlier) has been detected in the GlobalConfig. Generate a key with devextreme-license instead.', licenseId: (id: string): string => `License ID: ${id}`, }); From 3bc12926c7d3d3f99d7c208410f5912c177358ed Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 7 Apr 2026 05:16:33 +0400 Subject: [PATCH 38/48] cleanup and final fixes for the warnings. Added DX1003 handler --- .../lcp_key_validation/lcp_key_validator.ts | 17 ++- .../lcp_key_validation/license_info.ts | 8 ++ .../lcp_key_validation/product_info.ts | 9 +- .../core/license/lcp_key_validation/utils.ts | 10 ++ .../core/license/license_validation.test.ts | 23 ++-- .../core/license/license_validation.ts | 8 +- .../core/license/license_warnings.ts | 18 ++- .../js/__internal/core/license/types.ts | 6 +- .../devextreme/license/devextreme-license.js | 53 ++++++-- packages/devextreme/license/dx-lcx-2-lcp.js | 35 +++++- packages/devextreme/license/messages.js | 113 +++++++++++------- 11 files changed, 214 insertions(+), 86 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts index 8d5f223f95f3..575cfecc10e9 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts @@ -6,6 +6,7 @@ import { PRODUCT_KIND_ERROR, type Token, TokenKind, + TRIAL_EXPIRED_ERROR, VERIFICATION_ERROR, } from '../types'; import { @@ -13,9 +14,14 @@ import { RSA_PUBLIC_KEY_XML, SIGN_LENGTH, } from './const'; -import { findLatestDevExtremeVersion } from './license_info'; +import { findLatestDevExtremeVersion, getMaxExpiration } from './license_info'; import { createProductInfo, type ProductInfo } from './product_info'; -import { encodeString, shiftDecodeText, verifyHash } from './utils'; +import { + dotNetTicksToMs, + encodeString, + shiftDecodeText, + verifyHash, +} from './utils'; interface ParsedProducts { products: ProductInfo[]; @@ -41,9 +47,11 @@ function productsFromString(encodedString: string): ParsedProducts { const parts = tuple.split(','); const version = Number.parseInt(parts[0], 10); const productsValue = BigInt(parts[1]); + const expiration = parts.length > 3 ? dotNetTicksToMs(parts[3]) : Infinity; return createProductInfo( version, productsValue, + expiration, ); }); @@ -86,6 +94,11 @@ export function parseDevExpressProductKey(productsLicenseSource: string): Token return errorToken; } + const maxExpiration = getMaxExpiration({ products }); + if (maxExpiration !== Infinity && maxExpiration < Date.now()) { + return TRIAL_EXPIRED_ERROR; + } + const maxVersionAllowed = findLatestDevExtremeVersion({ products }); if (!maxVersionAllowed) { diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts index 66e40a5624e1..a526a3c61983 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/license_info.ts @@ -9,6 +9,14 @@ export function isLicenseValid(info: LicenseInfo): boolean { return Array.isArray(info.products) && info.products.length > 0; } +export function getMaxExpiration(info: LicenseInfo): number { + const expirationDates = info.products + .map((p) => p.expiration) + .filter((e) => e > 0 && e !== Infinity); + if (expirationDates.length === 0) return Infinity; + return Math.max(...expirationDates); +} + export function findLatestDevExtremeVersion(info: LicenseInfo): number | undefined { if (!isLicenseValid(info)) { return undefined; diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts index 0d051610cdb0..b7b5c8764b5a 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/product_info.ts @@ -2,10 +2,15 @@ export interface ProductInfo { readonly version: number; readonly products: bigint; + readonly expiration: number; } -export function createProductInfo(version: number, products: bigint): ProductInfo { - return { version, products: BigInt(products) }; +export function createProductInfo( + version: number, + products: bigint, + expiration = Infinity, +): ProductInfo { + return { version, products: BigInt(products), expiration }; } export function isProduct(info: ProductInfo, ...productIds: bigint[]): boolean { diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/utils.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/utils.ts index a851f87fd9e2..7299aa87ba29 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/utils.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/utils.ts @@ -51,6 +51,16 @@ export const shiftText = (text: string, map: string): string => { export const shiftDecodeText = (text: string): string => shiftText(text, DECODE_MAP); +const DOT_NET_TICKS_EPOCH_OFFSET = 621355968000000000n; +const DOT_NET_TICKS_PER_MS = 10000n; +const DOT_NET_MAX_VALUE_TICKS = 3155378975999999999n; + +export function dotNetTicksToMs(ticksStr: string): number { + const ticks = BigInt(ticksStr); + if (ticks >= DOT_NET_MAX_VALUE_TICKS) return Infinity; + return Number((ticks - DOT_NET_TICKS_EPOCH_OFFSET) / DOT_NET_TICKS_PER_MS); +} + export const verifyHash = (xmlKey: string, data: string, signature: string): boolean => { const { modulus, exponent } = parseRsaXml(xmlKey); diff --git a/packages/devextreme/js/__internal/core/license/license_validation.test.ts b/packages/devextreme/js/__internal/core/license/license_validation.test.ts index de13ecb07eff..74f27888c14a 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.test.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.test.ts @@ -316,7 +316,7 @@ describe('license check', () => { ])('Warning should be logged with no-key message if license is empty', ({ token, version }) => { validateLicense(token as string, version); expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('No valid DevExpress license key was found')); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('devextreme-license generated key has not been specified')); }); test.each([ @@ -338,10 +338,9 @@ describe('license check', () => { { token: TOKEN_23_1, version: '12.3.4' }, { token: TOKEN_23_2, version: '23.1.5' }, { token: TOKEN_23_2, version: '23.2.6' }, - ])('Old format license should trigger old-key warning', ({ token, version }) => { + ])('Old format license within version range should not trigger warnings', ({ token, version }) => { validateLicense(token, version); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('invalid/old DevExtreme key')); + expect(consoleWarnSpy).not.toHaveBeenCalled(); }); test.each([ @@ -349,9 +348,9 @@ describe('license check', () => { { token: TOKEN_23_1, version: '12.3.4' }, { token: TOKEN_23_2, version: '23.1.5' }, { token: TOKEN_23_2, version: '23.2.6' }, - ])('Trial panel should be displayed for old format license keys', ({ token, version }) => { + ])('Trial panel should not be displayed for valid old format license keys', ({ token, version }) => { validateLicense(token, version); - expect(trialPanelSpy).toHaveBeenCalledTimes(1); + expect(trialPanelSpy).not.toHaveBeenCalled(); }); test('Trial panel "Buy Now" link must use the jQuery link if no config has been set', () => { @@ -370,7 +369,7 @@ describe('license check', () => { validateLicense('', '1.0'); validateLicense('', '1.0'); - expect(errors.log).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); }); test('Base z-index should match the corresponding setting in DevExtreme', () => { @@ -387,10 +386,10 @@ describe('license check', () => { test.each([ { token: TOKEN_23_1, version: '23.2.3' }, { token: TOKEN_23_2, version: '42.4.5' }, - ])('Old format license should trigger old-key warning when outdated', ({ token, version }) => { + ])('Old format license should trigger version-mismatch warning when outdated', ({ token, version }) => { validateLicense(token, version); expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('invalid/old DevExtreme key')); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Incompatible DevExpress license key version')); }); test.each([ @@ -408,9 +407,9 @@ describe('license check', () => { { token: TOKEN_23_1, version: '23.2.3-alpha' }, { token: TOKEN_23_2, version: '24.1.0' }, { token: TOKEN_23_2, version: '24.1.abc' }, - ])('Trial panel should be displayed in previews for old format license keys', ({ token, version }) => { + ])('Trial panel should not be displayed in previews for valid old format license keys', ({ token, version }) => { validateLicense(token, version); - expect(trialPanelSpy).toHaveBeenCalledTimes(1); + expect(trialPanelSpy).not.toHaveBeenCalled(); }); test.each([ @@ -508,7 +507,7 @@ describe('DevExpress license check', () => { const token = 'LCXv1therestofthekey'; validateLicense(token, '25.1.3'); expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('.NET license key (LCX)')); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('DevExpress license key has been specified instead of a key generated using devextreme-license')); expect(trialPanelSpy).toHaveBeenCalled(); }); diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index bbf7c87c815a..062edfa001e1 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -165,13 +165,15 @@ function getLicenseCheckParams({ return { preview, error: 'W0021', warningType: 'lcx-used' }; } - const isProductKey = isProductOnlyLicense(licenseKey); const license = parseLicenseKey(licenseKey); if (license.kind === TokenKind.corrupted) { if (license.error === 'product-kind') { return { preview, error: 'W0021', warningType: 'no-devextreme-license' }; } + if (license.error === 'trial-expired') { + return { preview, error: 'W0020', warningType: 'trial-expired' }; + } return { preview, error: 'W0021', warningType: 'invalid-key' }; } @@ -183,10 +185,6 @@ function getLicenseCheckParams({ return { preview, error: 'W0021', warningType: 'invalid-key' }; } - if (!isProductKey) { - return { preview, error: 'W0021', warningType: 'old-devextreme-key' }; - } - if (major * 10 + minor > license.payload.maxVersionAllowed) { return { preview, diff --git a/packages/devextreme/js/__internal/core/license/license_warnings.ts b/packages/devextreme/js/__internal/core/license/license_warnings.ts index a16ee3dc9d48..a9516d4d0e58 100644 --- a/packages/devextreme/js/__internal/core/license/license_warnings.ts +++ b/packages/devextreme/js/__internal/core/license/license_warnings.ts @@ -25,9 +25,9 @@ export const TEMPLATES = Object.freeze({ keyWasFound: (type: string, path?: string): string => { switch (type) { case 'envVariable': - return 'The DevExpress license key was retrieved from the "DevExpress_License" environment variable.'; + return 'The DevExpress license key was retrieved from the \'DevExpress_License\' environment variable.'; case 'envPath': - return 'The DevExpress license key was retrieved from the "DevExpress_LicensePath" environment variable.'; + return 'The DevExpress license key was retrieved from the \'DevExpress_LicensePath\' environment variable.'; case 'file': return `The DevExpress license key was retrieved from file: "${path}".`; default: @@ -38,13 +38,15 @@ export const TEMPLATES = Object.freeze({ keyVerificationFailed: (type?: string, keyVersion?: string, requiredVersion?: string): string => { switch (type) { case 'incompatibleVersion': - return `Incompatible DevExpress license key version (${keyVersion}). Download and register an updated DevExpress license key (${requiredVersion}+). Clear npm/IDE/NuGet cache and rebuild your project.`; + return `Incompatible DevExpress license key version (v${keyVersion}). Download and register an updated DevExpress license key (v${requiredVersion}+). Clear npm/IDE/NuGet cache and rebuild your project (https://devexpress.com/DX1002).`; + case 'trialExpired': + return 'Your DevExpress trial period has expired. Purchase a license to continue using DevExpress product libraries.'; default: return 'License key verification has failed.'; } }, - purchaseLicense: (version: string): string => `Purchase a license to continue use of DevExtreme (v${version}). Included in subscriptions: Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete. To purchase a license, visit https://js.devexpress.com/Buy/`, + purchaseLicense: 'Please register an existing license (https://devexpress.com/DX1000) or purchase a new license (https://devexpress.com/Buy/) to continue use of the following DevExpress product libraries: DevExtreme - Included in Subscriptions: Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete.', installationInstructions: 'If you own a licensed/registered version or if you are using a 30-day trial version of DevExpress product libraries on a development machine, download your personal license key and verify it with the devextreme-license tool (https://devexpress.com/DX1001).', @@ -63,7 +65,7 @@ export function logLicenseWarning( ): void { const T = TEMPLATES; - const purchaseLine = `${T.warningPrefix('W0019')} ${T.purchaseLicense(version)}`; + const purchaseLine = `${T.warningPrefix('W0019')} ${T.purchaseLicense}`; const installLine = `${T.warningPrefix('W0021')} ${T.installationInstructions}`; const lines: string[] = [purchaseLine]; @@ -92,6 +94,12 @@ export function logLicenseWarning( break; } + case 'trial-expired': { + const expiredLine = `${T.warningPrefix('W0020')} ${T.keyVerificationFailed('trialExpired')}`; + lines.push(expiredLine); + break; + } + case 'no-devextreme-license': // Only the purchase line, no additional details break; diff --git a/packages/devextreme/js/__internal/core/license/types.ts b/packages/devextreme/js/__internal/core/license/types.ts index 104340d3584f..79fc50e752af 100644 --- a/packages/devextreme/js/__internal/core/license/types.ts +++ b/packages/devextreme/js/__internal/core/license/types.ts @@ -12,7 +12,7 @@ export enum TokenKind { export interface ErrorToken { readonly kind: TokenKind.corrupted; - readonly error: 'general' | 'verification' | 'decoding' | 'deserialization' | 'payload' | 'version' | 'product-kind'; + readonly error: 'general' | 'verification' | 'decoding' | 'deserialization' | 'payload' | 'version' | 'product-kind' | 'trial-expired'; } export interface VerifiedToken { @@ -36,13 +36,15 @@ export const DESERIALIZATION_ERROR: ErrorToken = { kind: TokenKind.corrupted, er export const PAYLOAD_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'payload' }; export const VERSION_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'version' }; export const PRODUCT_KIND_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'product-kind' }; +export const TRIAL_EXPIRED_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'trial-expired' }; export type LicenseWarningType = 'no-key' | 'invalid-key' | 'lcx-used' | 'old-devextreme-key' | 'version-mismatch' - | 'no-devextreme-license'; + | 'no-devextreme-license' + | 'trial-expired'; export interface LicenseCheckParams { preview: boolean; diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index bfd936f3f75a..b727dd84dbfb 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -12,6 +12,14 @@ const EXPORT_NAME = 'licenseKey'; const TRIAL_VALUE = 'TRIAL'; const CLI_PREFIX = '[devextreme-license]'; +function logStderr(...lines) { + process.stderr.write(lines.join('\n') + '\n\n'); +} + +function prefixed(msg) { + return `${CLI_PREFIX} ${msg}`; +} + function fail(msg) { process.stderr.write(msg.endsWith('\n') ? msg : msg + '\n'); process.exit(0); @@ -57,7 +65,7 @@ function parseArgs(argv) { else if(a === '--out') { const next = args[i + 1]; if(!next || next.startsWith('-')) { - process.stderr.write('[devextreme-license] Warning: --out requires a path argument but none was provided. Ignoring --out.\n'); + logStderr(prefixed('Warning: --out requires a path argument but none was provided. Ignoring --out.')); } else { out.outPath = args[++i]; } @@ -65,7 +73,7 @@ function parseArgs(argv) { else if(a.startsWith('--out=')) { const val = a.slice('--out='.length); if(!val) { - process.stderr.write('[devextreme-license] Warning: --out requires a path argument but none was provided. Ignoring --out.\n'); + logStderr(prefixed('Warning: --out requires a path argument but none was provided. Ignoring --out.')); } else { out.outPath = val; } @@ -163,24 +171,43 @@ function main() { lcp = tryConvertLCXtoLCP(lcx) || TRIAL_VALUE; const { warning, licenseId: id } = getLCPInfo(lcp); licenseId = id; + if(warning) { - const parts = []; + const lines = []; + + lines.push( + prefixed(`${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense}`), + ); + if(licenseId) { - parts.push(`${CLI_PREFIX} ${TEMPLATES.licenseId(licenseId)}`); + lines.push(TEMPLATES.licenseId(licenseId)); } - parts.push(`${CLI_PREFIX} ${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense(currentVersion)}`); - parts.push(TEMPLATES.keyWasFound(source.type, source.path)); + + lines.push( + TEMPLATES.keyWasFound(source.type, source.path), + ); + if(warning.type !== 'trial') { - parts.push(TEMPLATES.keyVerificationFailed(warning.type, warning.keyVersion, warning.currentVersion)); - parts.push(`${CLI_PREFIX} ${TEMPLATES.warningPrefix(TEMPLATES.warningCodeByType(warning.type))} ${TEMPLATES.installationInstructions}`); + const code = TEMPLATES.warningCodeByType(warning.type); + + lines.push( + TEMPLATES.keyVerificationFailed(warning.type, warning.keyVersion, warning.currentVersion), + ); + + if(warning.type === 'trialExpired') { + lines.push(prefixed(`${TEMPLATES.warningPrefix(code)} ${TEMPLATES.purchaseLicense}`)); + } else { + lines.push(prefixed(`${TEMPLATES.warningPrefix(code)} ${TEMPLATES.installationInstructions}`)); + } } - process.stderr.write(parts.join('\n') + '\n\n'); + + logStderr(...lines); } } else { - process.stderr.write( - `${CLI_PREFIX} ${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense(currentVersion)}\n` + - `${TEMPLATES.keyNotFound}\n` + - `${CLI_PREFIX} ${TEMPLATES.warningPrefix(1001)} ${TEMPLATES.installationInstructions}\n\n` + logStderr( + prefixed(`${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense}`), + TEMPLATES.keyNotFound, + prefixed(`${TEMPLATES.warningPrefix(1001)} ${TEMPLATES.installationInstructions}`), ); } diff --git a/packages/devextreme/license/dx-lcx-2-lcp.js b/packages/devextreme/license/dx-lcx-2-lcp.js index 1e863481e20f..4e790fca7a25 100644 --- a/packages/devextreme/license/dx-lcx-2-lcp.js +++ b/packages/devextreme/license/dx-lcx-2-lcp.js @@ -122,6 +122,16 @@ function tryConvertLCXtoLCP(licenseString) { const DEVEXTREME_HTMLJS_BIT = 1n << 54n; // ProductKind.DevExtremeHtmlJs from types.ts +const DOTNET_TICKS_EPOCH_OFFSET = 621355968000000000n; +const DOTNET_TICKS_PER_MS = 10000n; +const DOTNET_MAX_VALUE_TICKS = 3155378975999999999n; + +function dotnetTicksToMs(ticksStr) { + const ticks = BigInt(ticksStr); + if(ticks >= DOTNET_MAX_VALUE_TICKS) return Infinity; + return Number((ticks - DOTNET_TICKS_EPOCH_OFFSET) / DOTNET_TICKS_PER_MS); +} + const TokenKind = Object.freeze({ corrupted: 'corrupted', verified: 'verified', @@ -131,6 +141,7 @@ const TokenKind = Object.freeze({ const GENERAL_ERROR = { kind: TokenKind.corrupted, error: 'general' }; const DESERIALIZATION_ERROR = { kind: TokenKind.corrupted, error: 'deserialization' }; const PRODUCT_KIND_ERROR = { kind: TokenKind.corrupted, error: 'product-kind' }; +const TRIAL_EXPIRED_ERROR = { kind: TokenKind.corrupted, error: 'trial-expired' }; function readDevExtremeVersion() { try { @@ -163,7 +174,8 @@ function productsFromString(encodedString) { const parts = tuple.split(','); const version = Number.parseInt(parts[0], 10); const products = BigInt(parts[1]); - return { version, products }; + const expiration = parts.length > 3 ? dotnetTicksToMs(parts[3]) : Infinity; + return { version, products, expiration }; }); return { products, licenseId }; } catch{ @@ -171,6 +183,14 @@ function productsFromString(encodedString) { } } +function getMaxExpiration(products) { + const expirations = products + .map(p => p.expiration) + .filter(e => e > 0 && e !== Infinity); + if(expirations.length === 0) return Infinity; + return Math.max(...expirations); +} + function findLatestDevExtremeVersion(products) { if(!Array.isArray(products) || products.length === 0) return undefined; const sorted = [...products].sort((a, b) => b.version - a.version); @@ -195,12 +215,17 @@ function parseLCP(lcpString) { const decodedPayload = mapString(productsPayload, DECODE_MAP); const { products, errorToken, licenseId } = productsFromString(decodedPayload); if(errorToken) { - return errorToken; + return { ...errorToken, licenseId }; + } + + const maxExpiration = getMaxExpiration(products); + if(maxExpiration !== Infinity && maxExpiration < Date.now()) { + return { ...TRIAL_EXPIRED_ERROR, licenseId }; } const maxVersionAllowed = findLatestDevExtremeVersion(products); if(!maxVersionAllowed) { - return PRODUCT_KIND_ERROR; + return { ...PRODUCT_KIND_ERROR, licenseId }; } return { @@ -223,6 +248,7 @@ function getLCPInfo(lcpString) { let currentVersion = ''; if(token.kind === TokenKind.corrupted) { + licenseId = token.licenseId || null; switch(token.error) { case 'general': warning = { type: 'general' }; @@ -233,6 +259,9 @@ function getLCPInfo(lcpString) { case 'product-kind': warning = { type: 'trial' }; break; + case 'trial-expired': + warning = { type: 'trialExpired' }; + break; } } else { // token.kind === TokenKind.verified — check version compatibility diff --git a/packages/devextreme/license/messages.js b/packages/devextreme/license/messages.js index 6ce04451fba6..1399da776242 100644 --- a/packages/devextreme/license/messages.js +++ b/packages/devextreme/license/messages.js @@ -1,59 +1,88 @@ 'use strict'; const MESSAGES = Object.freeze({ - keyNotFound: - 'For evaluation purposes only. Redistribution prohibited. ' + - 'If you own a licensed/registered version or if you are using a 30-day trial version of DevExpress product libraries on a development machine, ' + - 'download your personal license key (devexpress.com/DX1001) and place DevExpress_License.txt in the following folder: ' + - '"%AppData%/DevExpress" (Windows) or "$HOME/Library/Application Support/DevExpress" (MacOS) or "$HOME/.config/DevExpress" (Linux). ' + - 'Alternatively, download and run the DevExpress Unified Component Installer to automatically activate your license.', - - trial: - 'For evaluation purposes only. Redistribution prohibited. ' + - 'Please purchase a license to continue use of the following DevExpress product libraries: ' + + keyNotFound: [ + 'For evaluation purposes only. Redistribution prohibited.', + 'If you own a licensed/registered version or if you are using a 30-day trial version', + 'of DevExpress product libraries on a development machine,', + 'download your personal license key (devexpress.com/DX1001)', + 'and place DevExpress_License.txt in the following folder:', + '"%AppData%/DevExpress" (Windows)', + 'or "$HOME/Library/Application Support/DevExpress" (MacOS)', + 'or "$HOME/.config/DevExpress" (Linux).', + 'Alternatively, download and run the DevExpress Unified Component Installer', + 'to automatically activate your license.', + ].join(' '), + + trial: [ + 'For evaluation purposes only. Redistribution prohibited.', + 'Please purchase a license to continue use of the following', + 'DevExpress product libraries:', 'Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete.', + ].join(' '), + + resolveFailed: 'Failed to resolve license key. Placeholder will remain.', +}); + +const KEY_SOURCES = Object.freeze({ + envVariable: 'License source: Environment Variable (DevExpress_License).', + envPath: 'License source: Environment Variable (DevExpress_LicensePath).', + file: (filePath) => `License source: File "${filePath}".`, + default: 'License source: default.', +}); - resolveFailed: - 'Failed to resolve license key. Placeholder will remain.', +const WARNING_CODES = Object.freeze({ + general: 1001, + incompatibleVersion: 1002, + trialExpired: 1003, }); const TEMPLATES = Object.freeze({ - warningPrefix: (number) => `Warning number: DX${number}. For evaluation purposes only. Redistribution prohibited.`, + warningPrefix: (number) => + `Warning number: DX${number}. For evaluation purposes only. Redistribution prohibited.`, + keyNotFound: 'A valid DevExpress license key was not found on this machine.', - keyWasFound: (type, path) => { - switch(type) { - case 'envVariable': - return 'The DevExpress license key was retrieved from the "DevExpress_License" environment variable.'; - case 'envPath': - return 'The DevExpress license key was retrieved from the "DevExpress_LicensePath" environment variable.'; - case 'file': - return `The DevExpress license key was retrieved from file: "${path}".`; - default: - return 'The DevExpress license key was retrieved.'; - } + + keyWasFound: (type, filePath) => { + if(type === 'file') return KEY_SOURCES.file(filePath); + return KEY_SOURCES[type] || KEY_SOURCES.default; }, + keyVerificationFailed: (type, keyVersion, requiredVersion) => { - switch(type) { - case 'incompatibleVersion': - return `Incompatible DevExpress license key version (${keyVersion}). Download and register an updated DevExpress license key (${requiredVersion}+). Clear npm/IDE/NuGet cache and rebuild your project.`; - default: - return 'License key verification has failed.'; + if(type === 'incompatibleVersion') { + return [ + `Incompatible DevExpress license key version (${keyVersion}).`, + `Download and register an updated DevExpress license key (${requiredVersion}+).`, + 'Clear npm/IDE/NuGet cache and rebuild your project (https://devexpress.com/DX1002).', + ].join(' '); } - }, - warningCodeByType: (type) => { - switch(type) { - case 'general': - return 1001; - case 'incompatibleVersion': - return 1002; - default: - return 1001; + if(type === 'trialExpired') { + return [ + 'Your DevExpress trial period has expired.', + 'Purchase a license to continue using DevExpress product libraries.', + ].join(' '); } + return 'License key verification has failed.'; }, - purchaseLicense: (version) => - `Purchase a license to continue use of DevExtreme (v${version}). Included in subscriptions: Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete. To purchase a license, visit https://js.devexpress.com/Buy/`, - installationInstructions: 'If you own a licensed/registered version or if you are using a 30-day trial version of DevExpress product libraries on a development machine, download your personal license key and verify it with the devextreme-license tool (https://devexpress.com/DX1001).', - oldDevExtremeKey: (version) => `A DevExtreme key (v25_1 or earlier) has been detected. Use DevExpress license key (v${version}+) instead.`, + + warningCodeByType: (type) => WARNING_CODES[type] || WARNING_CODES.general, + + purchaseLicense: [ + 'Please register an existing license (https://devexpress.com/DX1000) or purchase a new license', + '(https://devexpress.com/Buy/) to continue use of the following DevExpress product libraries:', + 'DevExtreme - Included in Subscriptions: Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete.' + ].join(' '), + + installationInstructions: [ + 'If you own a licensed/registered version or if you are using a 30-day trial version', + 'of DevExpress product libraries on a development machine,', + 'download your personal license key and verify it with the devextreme-license tool', + '(https://devexpress.com/DX1001).', + ].join(' '), + + oldDevExtremeKey: (version) => + `A DevExtreme key (v25_1 or earlier) has been detected. Use DevExpress license key (v${version}+) instead.`, + licenseId: (id) => `License ID: ${id}`, }); From d78da084dc07bb14a6f1aea4db35b938c025a5ea Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 7 Apr 2026 12:14:30 +0400 Subject: [PATCH 39/48] Fix expired trial detection and add a test --- .../lcp_key_validation.test.ts | 64 ++++++++++++++++++- .../lcp_key_validation/lcp_key_validator.ts | 12 ++-- packages/devextreme/license/dx-lcx-2-lcp.js | 10 +-- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts index c356ced5b2cd..ac40057213c1 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validation.test.ts @@ -1,4 +1,11 @@ -import { describe, expect, it } from '@jest/globals'; +/* eslint-disable */ + +import { + describe, + expect, + it, + jest, +} from '@jest/globals'; import { version as currentVersion } from '@js/core/version'; import { parseVersion } from '../../../utils/version'; @@ -7,6 +14,38 @@ import { parseDevExpressProductKey } from './lcp_key_validator'; import { findLatestDevExtremeVersion, isLicenseValid } from './license_info'; import { createProductInfo } from './product_info'; +const DOT_NET_TICKS_EPOCH_OFFSET = 621355968000000000n; +const DOT_NET_TICKS_PER_MS = 10000n; +const DEVEXTREME_HTML_JS_BIT = 1n << 54n; + +function msToDotNetTicks(ms: number): string { + return (BigInt(ms) * DOT_NET_TICKS_PER_MS + DOT_NET_TICKS_EPOCH_OFFSET).toString(); +} + +function createLcpSource(payload: string): string { + const signature = 'A'.repeat(136); + return `LCPv1${btoa(`${signature}${payload}`)}`; +} + +function loadParserWithBypassedSignatureCheck() { + jest.resetModules(); + jest.doMock('./utils', () => { + const actual = jest.requireActual('./utils') as Record; + return { + ...actual, + encodeString: (text: string) => text, + shiftDecodeText: (text: string) => text, + verifyHash: () => true, + }; + }); + + // eslint-disable-next-line + const { parseDevExpressProductKey } = require('./lcp_key_validator'); + // eslint-disable-next-line + const { TokenKind } = require('../types'); + return { parseDevExpressProductKey, TokenKind }; +} + function getTrialLicense() { const { major, minor } = parseVersion(currentVersion); const products = [ @@ -34,4 +73,27 @@ describe('LCP key validation', () => { expect(version).toBe(undefined); }); + + it('does not classify a valid DevExtreme product key as trial-expired when expiration metadata is in the past', () => { + const { parseDevExpressProductKey, TokenKind } = loadParserWithBypassedSignatureCheck(); + const expiredAt = msToDotNetTicks(Date.UTC(2020, 0, 1)); + + const payload = `meta;251,${DEVEXTREME_HTML_JS_BIT},0,${expiredAt};`; + const token = parseDevExpressProductKey(createLcpSource(payload)); + + expect(token.kind).toBe(TokenKind.verified); + }); + + it('returns trial-expired for expired trial keys without DevExtreme product access', () => { + const { parseDevExpressProductKey, TokenKind } = loadParserWithBypassedSignatureCheck(); + const expiredAt = msToDotNetTicks(Date.UTC(2020, 0, 1)); + + const payload = `meta;251,0,0,${expiredAt};`; + const token = parseDevExpressProductKey(createLcpSource(payload)); + + expect(token.kind).toBe(TokenKind.corrupted); + if (token.kind === TokenKind.corrupted) { + expect(token.error).toBe('trial-expired'); + } + }); }); diff --git a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts index 575cfecc10e9..4c936be7d9d7 100644 --- a/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts +++ b/packages/devextreme/js/__internal/core/license/lcp_key_validation/lcp_key_validator.ts @@ -94,13 +94,15 @@ export function parseDevExpressProductKey(productsLicenseSource: string): Token return errorToken; } - const maxExpiration = getMaxExpiration({ products }); - if (maxExpiration !== Infinity && maxExpiration < Date.now()) { - return TRIAL_EXPIRED_ERROR; - } - const maxVersionAllowed = findLatestDevExtremeVersion({ products }); + if (!maxVersionAllowed) { + const maxExpiration = getMaxExpiration({ products }); + if (maxExpiration !== Infinity && maxExpiration < Date.now()) { + return TRIAL_EXPIRED_ERROR; + } + } + if (!maxVersionAllowed) { return PRODUCT_KIND_ERROR; } diff --git a/packages/devextreme/license/dx-lcx-2-lcp.js b/packages/devextreme/license/dx-lcx-2-lcp.js index 4e790fca7a25..639042ef9799 100644 --- a/packages/devextreme/license/dx-lcx-2-lcp.js +++ b/packages/devextreme/license/dx-lcx-2-lcp.js @@ -218,12 +218,14 @@ function parseLCP(lcpString) { return { ...errorToken, licenseId }; } - const maxExpiration = getMaxExpiration(products); - if(maxExpiration !== Infinity && maxExpiration < Date.now()) { - return { ...TRIAL_EXPIRED_ERROR, licenseId }; + const maxVersionAllowed = findLatestDevExtremeVersion(products); + if(!maxVersionAllowed) { + const maxExpiration = getMaxExpiration(products); + if(maxExpiration !== Infinity && maxExpiration < Date.now()) { + return { ...TRIAL_EXPIRED_ERROR, licenseId }; + } } - const maxVersionAllowed = findLatestDevExtremeVersion(products); if(!maxVersionAllowed) { return { ...PRODUCT_KIND_ERROR, licenseId }; } From fe4db448ebaf5583f8ab2476b8c9aa2854e49552 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 7 Apr 2026 13:52:35 +0400 Subject: [PATCH 40/48] fix warning text --- packages/devextreme/license/messages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/license/messages.js b/packages/devextreme/license/messages.js index 1399da776242..cff0a8efaca2 100644 --- a/packages/devextreme/license/messages.js +++ b/packages/devextreme/license/messages.js @@ -39,7 +39,7 @@ const WARNING_CODES = Object.freeze({ const TEMPLATES = Object.freeze({ warningPrefix: (number) => - `Warning number: DX${number}. For evaluation purposes only. Redistribution prohibited.`, + `Warning DX${number}: For evaluation purposes only. Redistribution prohibited.`, keyNotFound: 'A valid DevExpress license key was not found on this machine.', From b299e3c0953d254c7921759e59fcd8d67564bdbf Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 7 Apr 2026 14:23:54 +0400 Subject: [PATCH 41/48] seperate warnings --- .../core/license/license_warnings.ts | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/license_warnings.ts b/packages/devextreme/js/__internal/core/license/license_warnings.ts index a9516d4d0e58..f4748fcb52d9 100644 --- a/packages/devextreme/js/__internal/core/license/license_warnings.ts +++ b/packages/devextreme/js/__internal/core/license/license_warnings.ts @@ -68,35 +68,40 @@ export function logLicenseWarning( const purchaseLine = `${T.warningPrefix('W0019')} ${T.purchaseLicense}`; const installLine = `${T.warningPrefix('W0021')} ${T.installationInstructions}`; - const lines: string[] = [purchaseLine]; + const warnings: string[][] = [[purchaseLine]]; switch (warningType) { case 'no-key': - lines.push(T.keyNotFound, installLine); + warnings[warnings.length - 1].push(T.keyNotFound); + warnings.push([installLine]); break; case 'invalid-key': - lines.push(T.keyVerificationFailed(), installLine); + warnings[warnings.length - 1].push(T.keyVerificationFailed()); + warnings.push([installLine]); break; case 'lcx-used': // eslint-disable-next-line spellcheck/spell-checker - lines.push(T.keyVerificationFailed(), T.lcxUsedInsteadOfLcp, installLine); + warnings[warnings.length - 1].push(T.keyVerificationFailed(), T.lcxUsedInsteadOfLcp); + warnings.push([installLine]); break; case 'old-devextreme-key': - lines.push(T.keyVerificationFailed(), T.oldDevExtremeKey, installLine); + warnings[warnings.length - 1].push(T.keyVerificationFailed(), T.oldDevExtremeKey); + warnings.push([installLine]); break; case 'version-mismatch': { const incompatibleLine = `${T.warningPrefix('W0020')} ${T.keyVerificationFailed('incompatibleVersion', versionInfo?.keyVersion, versionInfo?.requiredVersion)}`; - lines.push(T.keyVerificationFailed(), incompatibleLine); + warnings[warnings.length - 1].push(T.keyVerificationFailed()); + warnings.push([incompatibleLine]); break; } case 'trial-expired': { const expiredLine = `${T.warningPrefix('W0020')} ${T.keyVerificationFailed('trialExpired')}`; - lines.push(expiredLine); + warnings.push([expiredLine]); break; } @@ -107,5 +112,7 @@ export function logLicenseWarning( break; } - console.warn(lines.join('\n')); + warnings.forEach((group) => { + console.warn(group.join('\n')); + }); } From a79a7a8215295d8736b6e21ec144a72481c25645 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 7 Apr 2026 17:18:03 +0400 Subject: [PATCH 42/48] remove unnecessary comment --- packages/devextreme/license/dx-lcx-2-lcp.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/devextreme/license/dx-lcx-2-lcp.js b/packages/devextreme/license/dx-lcx-2-lcp.js index 639042ef9799..600e1ae90c2a 100644 --- a/packages/devextreme/license/dx-lcx-2-lcp.js +++ b/packages/devextreme/license/dx-lcx-2-lcp.js @@ -266,7 +266,6 @@ function getLCPInfo(lcpString) { break; } } else { - // token.kind === TokenKind.verified — check version compatibility licenseId = token.payload.licenseId || null; const devExtremeVersion = readDevExtremeVersion(); if(devExtremeVersion) { From a776da5f692808bb2ee2b83653f7f77b0cc2b1e3 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 7 Apr 2026 17:47:15 +0400 Subject: [PATCH 43/48] update tests after splitting warning --- .../core/license/license_validation.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/license_validation.test.ts b/packages/devextreme/js/__internal/core/license/license_validation.test.ts index 74f27888c14a..d17d190748b6 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.test.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.test.ts @@ -315,7 +315,7 @@ describe('license check', () => { { token: LICENSE_KEY_PLACEHOLDER, version: '1.0.3' }, ])('Warning should be logged with no-key message if license is empty', ({ token, version }) => { validateLicense(token as string, version); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledTimes(2); expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('devextreme-license generated key has not been specified')); }); @@ -369,7 +369,7 @@ describe('license check', () => { validateLicense('', '1.0'); validateLicense('', '1.0'); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledTimes(2); }); test('Base z-index should match the corresponding setting in DevExtreme', () => { @@ -388,7 +388,7 @@ describe('license check', () => { { token: TOKEN_23_2, version: '42.4.5' }, ])('Old format license should trigger version-mismatch warning when outdated', ({ token, version }) => { validateLicense(token, version); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledTimes(2); expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Incompatible DevExpress license key version')); }); @@ -424,7 +424,7 @@ describe('license check', () => { { token: '3.2.1', version: '1.2.3' }, ])('License verification warning should be logged if license is corrupted/invalid [%#]', ({ token, version }) => { validateLicense(token, version); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledTimes(2); expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('License key verification has failed')); }); @@ -506,7 +506,7 @@ describe('DevExpress license check', () => { test('DevExpress License Key copied from Download Manager (incorrect)', () => { const token = 'LCXv1therestofthekey'; validateLicense(token, '25.1.3'); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledTimes(2); expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('DevExpress license key has been specified instead of a key generated using devextreme-license')); expect(trialPanelSpy).toHaveBeenCalled(); }); @@ -514,7 +514,7 @@ describe('DevExpress license check', () => { test('DevExpress License Key generated from LCX key (incorrect)', () => { const token = 'LCPtherestofthekey'; validateLicense(token, '25.1.3'); - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledTimes(2); expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('License key verification has failed')); expect(trialPanelSpy).toHaveBeenCalled(); }); From 3eeb4fbcfb3f80b4199b0f7c7ed61c08c19506ee Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Thu, 9 Apr 2026 12:59:16 +0400 Subject: [PATCH 44/48] remove old key validation code and add warnings --- .../js/__internal/core/license/key.ts | 2 - .../core/license/license_validation.ts | 103 ++---------------- .../license/license_validation_internal.ts | 3 - .../devextreme/license/devextreme-license.js | 10 +- packages/devextreme/license/messages.js | 2 +- 5 files changed, 18 insertions(+), 102 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/key.ts b/packages/devextreme/js/__internal/core/license/key.ts index 8d801fb02cdf..e93a65ecdca7 100644 --- a/packages/devextreme/js/__internal/core/license/key.ts +++ b/packages/devextreme/js/__internal/core/license/key.ts @@ -13,5 +13,3 @@ export const PUBLIC_KEY: PublicKey = { 230, 44, 247, 200, 253, 170, 192, 246, 30, 12, 96, 205, 100, 249, 181, 93, 0, 231, ]), }; - -export const INTERNAL_USAGE_ID = 'V2QpQmJVXWy6Nexkq9Xk9o'; diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index 062edfa001e1..64798112b2b0 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -8,52 +8,24 @@ import { getPreviousMajorVersion, parseVersion, } from '../../utils/version'; -import { base64ToBytes } from './byte_utils'; import { - BUY_NOW_LINK, FORMAT, KEY_SPLITTER, LICENSE_KEY_PLACEHOLDER, + BUY_NOW_LINK, LICENSE_KEY_PLACEHOLDER, LICENSING_DOC_LINK, RTM_MIN_PATCH_VERSION, SUBSCRIPTION_NAMES, } from './const'; -import { INTERNAL_USAGE_ID, PUBLIC_KEY } from './key'; import { isProductOnlyLicense, parseDevExpressProductKey } from './lcp_key_validation/lcp_key_validator'; import { logLicenseWarning } from './license_warnings'; -import { pad } from './pkcs1'; -import { compareSignatures } from './rsa_bigint'; -import { sha1 } from './sha1'; import { showTrialPanel } from './trial_panel'; import type { - License, LicenseCheckParams, Token, } from './types'; import { - DECODING_ERROR, - DESERIALIZATION_ERROR, GENERAL_ERROR, - PAYLOAD_ERROR, TokenKind, - VERIFICATION_ERROR, - VERSION_ERROR, } from './types'; -interface Payload extends Partial { - readonly format?: number; - readonly internalUsageId?: string; -} - let validationPerformed = false; -// verifies RSASSA-PKCS1-v1.5 signature -function verifySignature({ text, signature: encodedSignature }: { - text: string; - signature: string; -}): boolean { - return compareSignatures({ - key: PUBLIC_KEY, - signature: base64ToBytes(encodedSignature), - actual: pad(sha1(text)), - }); -} - export function parseLicenseKey(encodedKey: string | undefined): Token { if (encodedKey === undefined) { return GENERAL_ERROR; @@ -63,57 +35,7 @@ export function parseLicenseKey(encodedKey: string | undefined): Token { return parseDevExpressProductKey(encodedKey); } - 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; - } - - 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; } function isPreview(patch: number): boolean { @@ -124,19 +46,6 @@ function hasLicensePrefix(licenseKey: string, prefix: string): boolean { return licenseKey.trim().startsWith(prefix); } -export function isUnsupportedKeyFormat(licenseKey: string | undefined): boolean { - if (!licenseKey) { - return false; - } - - if (hasLicensePrefix(licenseKey, 'LCXv1')) { - errors.log('W0000', 'config', 'licenseKey', 'LCXv1 is specified in the license key'); - return true; - } - - return false; -} - function displayTrialPanel(): void { const buyNowLink = config().buyNowLink ?? BUY_NOW_LINK; const licensingDocLink = config().licensingDocLink ?? LICENSING_DOC_LINK; @@ -165,6 +74,10 @@ function getLicenseCheckParams({ return { preview, error: 'W0021', warningType: 'lcx-used' }; } + if (hasLicensePrefix(licenseKey, 'ewog')) { + return { preview, error: 'W0021', warningType: 'old-devextreme-key' }; + } + const license = parseLicenseKey(licenseKey); if (license.kind === TokenKind.corrupted) { @@ -177,8 +90,8 @@ function getLicenseCheckParams({ return { preview, error: 'W0021', warningType: 'invalid-key' }; } - if (license.kind === TokenKind.internal) { - return { preview, internal: true, error: license.internalUsageId === INTERNAL_USAGE_ID ? undefined : 'W0020' }; + if (license.kind !== TokenKind.verified) { + return { preview, error: 'W0021', warningType: 'invalid-key' }; } if (!(major && minor)) { diff --git a/packages/devextreme/js/__internal/core/license/license_validation_internal.ts b/packages/devextreme/js/__internal/core/license/license_validation_internal.ts index a61f9cc003d5..df552c7a8878 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation_internal.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation_internal.ts @@ -4,9 +4,6 @@ import type { Token } from './types'; // @ts-expect-error - only for internal usage export function parseLicenseKey(encodedKey: string | undefined): Token {} -// @ts-expect-error - only for internal usage -export function isUnsupportedKeyFormat(licenseKey: string | undefined): boolean {} - export function validateLicense(licenseKey: string, version?: string): void {} // @ts-expect-error - only for internal usage diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index b727dd84dbfb..db29c6aa7aa8 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -167,7 +167,15 @@ function main() { let lcp = TRIAL_VALUE; let licenseId = null; - if(lcx) { + if(lcx && lcx.trimStart().startsWith('ewog')) { + logStderr( + prefixed(`${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense}`), + TEMPLATES.keyVerificationFailed(), + TEMPLATES.oldDevExtremeKey(currentVersion), + TEMPLATES.keyWasFound(source.type, source.path), + prefixed(`${TEMPLATES.warningPrefix(1001)} ${TEMPLATES.installationInstructions}`), + ); + } else if(lcx) { lcp = tryConvertLCXtoLCP(lcx) || TRIAL_VALUE; const { warning, licenseId: id } = getLCPInfo(lcp); licenseId = id; diff --git a/packages/devextreme/license/messages.js b/packages/devextreme/license/messages.js index cff0a8efaca2..9bf082cad13d 100644 --- a/packages/devextreme/license/messages.js +++ b/packages/devextreme/license/messages.js @@ -81,7 +81,7 @@ const TEMPLATES = Object.freeze({ ].join(' '), oldDevExtremeKey: (version) => - `A DevExtreme key (v25_1 or earlier) has been detected. Use DevExpress license key (v${version}+) instead.`, + `A DevExtreme key (v25_2 or earlier) has been detected. Use DevExpress license key (v${version}+) instead.`, licenseId: (id) => `License ID: ${id}`, }); From 5d33a2ba43f6230f6a580df8a9ff79da9dbf8b38 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Thu, 9 Apr 2026 13:10:00 +0400 Subject: [PATCH 45/48] package lock fix --- pnpm-lock.yaml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d5eabb092c9..4ce61c2938c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4860,9 +4860,6 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -23639,11 +23636,6 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -41897,7 +41889,7 @@ snapshots: unplugin@3.0.0: dependencies: '@jridgewell/remapping': 2.3.5 - picomatch: 4.0.3 + picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 unquote@1.1.1: {} From 03416e03a2732f232e61e3a2f61957cad596d4db Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Fri, 10 Apr 2026 12:20:01 +0400 Subject: [PATCH 46/48] update tests --- .../core/license/license_validation.test.ts | 97 ++++++++----------- 1 file changed, 40 insertions(+), 57 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/license_validation.test.ts b/packages/devextreme/js/__internal/core/license/license_validation.test.ts index d17d190748b6..40eeaff65b3b 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.test.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.test.ts @@ -36,44 +36,24 @@ jest.mock('./key', () => ({ describe('license token', () => { it.each([ - { - token: 'ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxCn0=.DiDceRbil4IzXl5av7pNkKieyqHHhRf+CM477zDu4N9fyrhkQsjRourYvgVfkbSm+EQplkXhlMBc3s8Vm9n+VtPaMbeWXis92cdW/6HiT+Dm54xw5vZ5POGunKRrNYUzd9zTbYcz0bYA/dc/mHFeUdXA0UlKcx1uMaXmtJrkK74=', - payload: { - customerId: 'b1140b46-fde1-41bd-a280-4db9f8e7d9bd', - maxVersionAllowed: 231, - }, - }, { - token: 'ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogIjYxMjFmMDIyLTFjMTItNDNjZC04YWE0LTkwNzJkNDU4YjYxNCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMyCn0=.RENyZ3Ga5rCB7/XNKYbk2Ffv1n9bUexYNhyOlqcAD02YVnPw6XyQcN+ZORScKDU9gOInJ4o7vPxkgh10KvMZNn+FuBK8UcUR7kchk7z0CHGuOcIn2jD5X2hG6SYJ0UCBG/JDG35AL09T7Uv/pGj4PolRsANxtuMpoqmvX2D2vkU=', - payload: { - customerId: '6121f022-1c12-43cd-8aa4-9072d458b614', - maxVersionAllowed: 232, - }, - }, { - token: 'ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogIjM3Yjg4ZjBmLWQ0MmMtNDJiZS05YjhkLTU1ZGMwYzUzYzAxZiIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjIxCn0=.NVsilC5uWlD5QGS6bocLMlsVVK0VpZXYwU2DstUiLRpEI79/onuR8dGWasCLBo4PORDHPkNA/Ej8XeCHzJ0EkXRRZ7E2LrP/xlEfHRXTruvW4IEbZt3LiwJBt6/isLz+wzXtYtjV7tpE07/Y0TFoy+mWpHoU11GVtwKh6weRxkg=', - payload: { - customerId: '37b88f0f-d42c-42be-9b8d-55dc0c53c01f', - maxVersionAllowed: 221, - }, - }, - ])('verifies and decodes payload [%#]', ({ token, payload: expected }) => { + 'ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxCn0=.DiDceRbil4IzXl5av7pNkKieyqHHhRf+CM477zDu4N9fyrhkQsjRourYvgVfkbSm+EQplkXhlMBc3s8Vm9n+VtPaMbeWXis92cdW/6HiT+Dm54xw5vZ5POGunKRrNYUzd9zTbYcz0bYA/dc/mHFeUdXA0UlKcx1uMaXmtJrkK74=', + 'ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogIjYxMjFmMDIyLTFjMTItNDNjZC04YWE0LTkwNzJkNDU4YjYxNCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMyCn0=.RENyZ3Ga5rCB7/XNKYbk2Ffv1n9bUexYNhyOlqcAD02YVnPw6XyQcN+ZORScKDU9gOInJ4o7vPxkgh10KvMZNn+FuBK8UcUR7kchk7z0CHGuOcIn2jD5X2hG6SYJ0UCBG/JDG35AL09T7Uv/pGj4PolRsANxtuMpoqmvX2D2vkU=', + 'ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogIjM3Yjg4ZjBmLWQ0MmMtNDJiZS05YjhkLTU1ZGMwYzUzYzAxZiIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjIxCn0=.NVsilC5uWlD5QGS6bocLMlsVVK0VpZXYwU2DstUiLRpEI79/onuR8dGWasCLBo4PORDHPkNA/Ej8XeCHzJ0EkXRRZ7E2LrP/xlEfHRXTruvW4IEbZt3LiwJBt6/isLz+wzXtYtjV7tpE07/Y0TFoy+mWpHoU11GVtwKh6weRxkg=', + ])('old format token is not parsed [%#]', (token) => { const license = parseLicenseKey(token); - expect(license.kind).toBe('verified'); - if (license.kind === 'verified') { - expect(license.payload).toEqual(expected); + expect(license.kind).toBe('corrupted'); + if (license.kind === 'corrupted') { + expect(license.error).toBe('general'); } }); - it('verifies and decodes payload with extra fields', () => { + it('old format token with extra fields is not parsed', () => { const license = parseLicenseKey('ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxLAogICJleHRyYUZpZWxkIjogIkE5OTk5OTkiCn0=.fqm8mVhQ9+x/R7E7MVwUP3nJaYL3KldhYffVXdDqPVyHIQi66Z2XZ2RdygH4J0jvUpjhZ6yzmGPV0J0WoPbKyhtnY4ELhove/IAwpn8WGfRw3wLSxfR+RWuaKcw2yvlUA1JqrQUrIrN23UwNQodbJ/hGm30s0h1bf8zCvQ/d31k='); - expect(license.kind).toBe('verified'); - if (license.kind === 'verified') { - expect(license.payload).toEqual({ - customerId: 'b1140b46-fde1-41bd-a280-4db9f8e7d9bd', - maxVersionAllowed: 231, - extraField: 'A999999', - }); + expect(license.kind).toBe('corrupted'); + if (license.kind === 'corrupted') { + expect(license.error).toBe('general'); } }); @@ -83,27 +63,27 @@ describe('license token', () => { expect(license.kind).toBe('corrupted'); if (license.kind === 'corrupted') { - expect(license.error).toBe('verification'); + expect(license.error).toBe('general'); } }); - it('fails if payload is invalid JSON', () => { + it('old format token with invalid JSON is not parsed', () => { const license = parseLicenseKey('YWJj.vjx6wAI9jVkHJAnKcsuYNZ5UvCq3UhypQ+0f+kZ37/Qc1uj4BM6//Kfi4SVsXGOaOTFYWgzesROnHCp3jZRqphJwal4yXHD1sGFi6FEdB4MgdgNZvsZSnxNWLs/7s07CzuHLTpJrAG7sTdHVkQWZNnSCKjzV7909c/Stl9+hkLo='); expect(license.kind).toBe('corrupted'); if (license.kind === 'corrupted') { - expect(license.error).toBe('deserialization'); + expect(license.error).toBe('general'); } }); - it('fails if payload is invalid Base64', () => { + it('old format token with invalid Base64 is not parsed', () => { const license = parseLicenseKey('ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogIjM3Yjg4ZjBmLWQ0MmMtNDJiZS05YjhkLTU1ZGMwYzUzYzAxZiIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjIxCn0-.EnP/RDKg0eSyaPU1eDUFll1lqOdYbhN3u73LhN1op8vjNwA0P1vKiT1DfQRmXudlleGWgDkLA2OmJYUER8j7I3LSFf3hLkBAoWoBErgveTb2zkbz8P1i9lE+XmzIXeYHyZBYUt0IPkNfajF9zzbSDDin1CvW7pnADi0vIeZ5ICQ='); expect(license.kind).toBe('corrupted'); if (license.kind === 'corrupted') { - expect(license.error).toBe('decoding'); + expect(license.error).toBe('general'); } }); @@ -111,23 +91,23 @@ describe('license token', () => { 'ewogICJmb3JtYXQiOiAxLAogICJtYXhWZXJzaW9uQWxsb3dlZCI6IDIzMQp9.WH30cajUFcKqw/fwt4jITM/5tzVwPpbdbezhhdBi5oeOvU06zKY0J4M8gQy8GQ++RPYVCAo2md6vI9D80FD2CC4w+hpQLJNJJgNUHYPrgG6CX1yAB3M+NKHsPP9S71bXAgwvignb5uPo0R5emQzr4RKDhWQMKtgqEcRe+yme2mU=', 'ewogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxCn0=.ok32DBaAgf3ijLmNQb+A0kUV2AiSivqvZJADdF607qqlAaduAVnotJtgdwm/Ib3MErfaGrDohCYoFMnKQevkRxFkA7tK3kOBnTZPUnZY0r3wyulMQmr4Qo+Sjf/fyXs4IYpGsC7/uJjgrCos8uzBegfmgfM93XSt6pKl9+c5xvc=', 'ewogICJmb3JtYXQiOiAxLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIKfQ==.resgTqmazrorRNw7mmtV31XQnmTSw0uLEArsmpzCjWMQJLocBfAjpFvKBf+SAG9q+1iOSFySj64Uv2xBVqHnyeNVBRbouOKOnAB8RpkKvN4sc5SDc8JAG5TkwPVSzK/VLBpQxpqbxlcrRfHwz9gXqQoPt4/ZVATn285iw3DW0CU=', - ])('fails if payload misses required fields [%#]', (token) => { + ])('old format token with missing fields is not parsed [%#]', (token) => { const license = parseLicenseKey(token); expect(license.kind).toBe('corrupted'); if (license.kind === 'corrupted') { - expect(license.error).toBe('payload'); + expect(license.error).toBe('general'); } }); - it('fails if payload has unsupported version', () => { + it('old format token with unsupported version is not parsed', () => { const license = parseLicenseKey('ewogICJmb3JtYXQiOiAyLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxCn0=.tTBymZMROsYyMiP6ldXFqGurbzqjhSQIu/pjyEUJA3v/57VgToomYl7FVzBj1asgHpadvysyTUiX3nFvPxbp166L3+LB3Jybw9ueMnwePu5vQOO0krqKLBqRq+TqHKn7k76uYRbkCIo5UajNfzetHhlkin3dJf3x2K/fcwbPW5A='); expect(license.kind).toBe('corrupted'); if (license.kind === 'corrupted') { - expect(license.error).toBe('version'); + expect(license.error).toBe('general'); } }); @@ -158,13 +138,14 @@ describe('version mismatch', () => { clearAssertedVersions(); }); - test('Perform license check if versions match', () => { + test('Old format token triggers old-devextreme-key warning even when versions match', () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); const token = 'ewogICJpbnRlcm5hbFVzYWdlSWQiOiAiUDdmNU5icU9WMDZYRFVpa3Q1bkRyQSIsCiAgImZvcm1hdCI6IDEKfQ==.ox52WAqudazQ0ZKdnJqvh/RmNNNX+IB9cmun97irvSeZK2JMf9sbBXC1YCrSZNIPBjQapyIV8Ctv9z2wzb3BkWy+R9CEh+ev7purq7Lk0ugpwDye6GaCzqlDg+58EHwPCNaasIuBiQC3ztvOItrGwWSu0aEFooiajk9uAWwzWeM='; assertDevExtremeVersion('DevExpress.Product.A', CORRECT_VERSION); assertDevExtremeVersion('DevExpress.Product.A', CORRECT_VERSION); assertDevExtremeVersion('DevExpress.Product.B', CORRECT_VERSION); validateLicense(token, CORRECT_VERSION); - expect(errors.log).toHaveBeenCalledWith('W0020'); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('A DevExtreme key (v25.2 or earlier)')); }); test('Perform version comparison if the license is okay', () => { @@ -338,9 +319,10 @@ describe('license check', () => { { token: TOKEN_23_1, version: '12.3.4' }, { token: TOKEN_23_2, version: '23.1.5' }, { token: TOKEN_23_2, version: '23.2.6' }, - ])('Old format license within version range should not trigger warnings', ({ token, version }) => { + ])('Old format license should trigger old-devextreme-key warning', ({ token, version }) => { validateLicense(token, version); - expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(consoleWarnSpy).toHaveBeenCalledTimes(2); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('A DevExtreme key (v25.2 or earlier)')); }); test.each([ @@ -348,9 +330,9 @@ describe('license check', () => { { token: TOKEN_23_1, version: '12.3.4' }, { token: TOKEN_23_2, version: '23.1.5' }, { token: TOKEN_23_2, version: '23.2.6' }, - ])('Trial panel should not be displayed for valid old format license keys', ({ token, version }) => { + ])('Trial panel should be displayed for old format license keys', ({ token, version }) => { validateLicense(token, version); - expect(trialPanelSpy).not.toHaveBeenCalled(); + expect(trialPanelSpy).toHaveBeenCalledTimes(1); }); test('Trial panel "Buy Now" link must use the jQuery link if no config has been set', () => { @@ -386,10 +368,10 @@ describe('license check', () => { test.each([ { token: TOKEN_23_1, version: '23.2.3' }, { token: TOKEN_23_2, version: '42.4.5' }, - ])('Old format license should trigger version-mismatch warning when outdated', ({ token, version }) => { + ])('Old format license should trigger old-devextreme-key warning even when outdated', ({ token, version }) => { validateLicense(token, version); expect(consoleWarnSpy).toHaveBeenCalledTimes(2); - expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Incompatible DevExpress license key version')); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('A DevExtreme key (v25.2 or earlier)')); }); test.each([ @@ -407,9 +389,9 @@ describe('license check', () => { { token: TOKEN_23_1, version: '23.2.3-alpha' }, { token: TOKEN_23_2, version: '24.1.0' }, { token: TOKEN_23_2, version: '24.1.abc' }, - ])('Trial panel should not be displayed in previews for valid old format license keys', ({ token, version }) => { + ])('Trial panel should be displayed for old format license keys even in previews', ({ token, version }) => { validateLicense(token, version); - expect(trialPanelSpy).not.toHaveBeenCalled(); + expect(trialPanelSpy).toHaveBeenCalledTimes(1); }); test.each([ @@ -458,6 +440,7 @@ describe('internal license check', () => { beforeEach(() => { jest.spyOn(errors, 'log').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); trialPanelSpy = jest.spyOn(trialPanel, 'renderTrialPanel'); setLicenseCheckSkipCondition(false); }); @@ -466,25 +449,25 @@ describe('internal license check', () => { jest.restoreAllMocks(); }); - test('valid internal usage token (correct)', () => { + test('old format internal usage token triggers old-devextreme-key warning', () => { const token = 'ewogICJpbnRlcm5hbFVzYWdlSWQiOiAiYVlDN0VIaWJwMHl4dFhUaWhKRVJrQSIsCiAgImZvcm1hdCI6IDEKfQ==.emWMjFDkBI2bvqc6R/hwh//2wE9YqS7yyTPSglqLBP7oPFMthW9tHNHsh1lG8MEuSKoi8TYOY+4R9GgvFi190f62iOy4iz8FenPXZodiv9hgDaovb2eIkwK4pilthOEAS9/JYhgTAentJ1f2+PlbjkTIqvYogk01GrRrd+WOtIA='; validateLicense(token, '1.2.3'); expect(errors.log).not.toHaveBeenCalled(); - expect(trialPanelSpy).not.toHaveBeenCalled(); + expect(trialPanelSpy).toHaveBeenCalledTimes(1); }); - test('valid internal usage token (correct, pre-release)', () => { + test('old format internal usage token triggers old-devextreme-key warning (pre-release)', () => { const token = 'ewogICJpbnRlcm5hbFVzYWdlSWQiOiAiYVlDN0VIaWJwMHl4dFhUaWhKRVJrQSIsCiAgImZvcm1hdCI6IDEKfQ==.emWMjFDkBI2bvqc6R/hwh//2wE9YqS7yyTPSglqLBP7oPFMthW9tHNHsh1lG8MEuSKoi8TYOY+4R9GgvFi190f62iOy4iz8FenPXZodiv9hgDaovb2eIkwK4pilthOEAS9/JYhgTAentJ1f2+PlbjkTIqvYogk01GrRrd+WOtIA='; validateLicense(token, '1.2.1'); expect(errors.log).not.toHaveBeenCalled(); - expect(trialPanelSpy).not.toHaveBeenCalled(); + expect(trialPanelSpy).toHaveBeenCalledTimes(1); }); - test('internal usage token (incorrect)', () => { + test('old format internal usage token (incorrect) triggers old-devextreme-key warning', () => { const token = 'ewogICJpbnRlcm5hbFVzYWdlSWQiOiAiUDdmNU5icU9WMDZYRFVpa3Q1bkRyQSIsCiAgImZvcm1hdCI6IDEKfQ==.ox52WAqudazQ0ZKdnJqvh/RmNNNX+IB9cmun97irvSeZK2JMf9sbBXC1YCrSZNIPBjQapyIV8Ctv9z2wzb3BkWy+R9CEh+ev7purq7Lk0ugpwDye6GaCzqlDg+58EHwPCNaasIuBiQC3ztvOItrGwWSu0aEFooiajk9uAWwzWeM='; validateLicense(token, '1.2.3'); - expect(errors.log).toHaveBeenCalledWith('W0020'); - expect(trialPanelSpy).not.toHaveBeenCalled(); + expect(errors.log).not.toHaveBeenCalled(); + expect(trialPanelSpy).toHaveBeenCalledTimes(1); }); }); From 36706df02e7c39b599ddc74a3f4671289b16076e Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Fri, 10 Apr 2026 12:40:36 +0400 Subject: [PATCH 47/48] some cleanup --- .../__internal/core/license/license_validation.test.ts | 1 - .../js/__internal/core/license/license_validation.ts | 10 +++------- .../devextreme/js/__internal/core/license/types.ts | 9 +-------- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/devextreme/js/__internal/core/license/license_validation.test.ts b/packages/devextreme/js/__internal/core/license/license_validation.test.ts index 40eeaff65b3b..cf78595fdfb9 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.test.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.test.ts @@ -31,7 +31,6 @@ jest.mock('./key', () => ({ 72, 62, 186, 243, 199, 73, ]), }, - INTERNAL_USAGE_ID: 'aYC7EHibp0yxtXTihJERkA', })); describe('license token', () => { diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index 64798112b2b0..7a8c9fab9336 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -121,20 +121,16 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer const version = parseVersion(versionStr); - const versionsCompatible = assertedVersionsCompatible(version); + assertedVersionsCompatible(version); const { - internal, error, warningType, maxVersionAllowed, + error, warningType, maxVersionAllowed, } = getLicenseCheckParams({ licenseKey, version, }); - if (!versionsCompatible && internal) { - return; - } - - if (error && !internal) { + if (error) { displayTrialPanel(); } diff --git a/packages/devextreme/js/__internal/core/license/types.ts b/packages/devextreme/js/__internal/core/license/types.ts index 79fc50e752af..40d5df223cab 100644 --- a/packages/devextreme/js/__internal/core/license/types.ts +++ b/packages/devextreme/js/__internal/core/license/types.ts @@ -7,7 +7,6 @@ export interface License { export enum TokenKind { corrupted = 'corrupted', verified = 'verified', - internal = 'internal', } export interface ErrorToken { @@ -20,12 +19,7 @@ export interface VerifiedToken { readonly payload: License; } -export interface InternalToken { - readonly kind: TokenKind.internal; - readonly internalUsageId: string; -} - -export type Token = ErrorToken | VerifiedToken | InternalToken; +export type Token = ErrorToken | VerifiedToken; type LicenseVerifyResult = 'W0019' | 'W0020' | 'W0021' | 'W0022' | 'W0023' | 'W0024'; @@ -48,7 +42,6 @@ export type LicenseWarningType = 'no-key' export interface LicenseCheckParams { preview: boolean; - internal?: true; error: LicenseVerifyResult | undefined; warningType?: LicenseWarningType; maxVersionAllowed?: number; From 04050862f9d4fa70e26f49bf80de562788d2f8a1 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Fri, 10 Apr 2026 12:45:15 +0400 Subject: [PATCH 48/48] Update cwd param handling Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Arman Jivanyan --- .../devextreme/license/devextreme-license.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index db29c6aa7aa8..e2604c2a9b2c 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -81,8 +81,22 @@ function parseArgs(argv) { else if(a === '--non-modular') out.nonModular = true; else if(a === '--no-gitignore') out.gitignore = false; else if(a === '--force') out.force = true; - 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; + } + } else fail(`Unknown argument: ${a}\nRun devextreme-license --help`); }