Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
b136f46
add modules to retrieve lcx key and convert lcx to lcp
ajivanyandev Feb 4, 2026
c886711
Add CLI command to write LCP to the given output file
ajivanyandev Feb 4, 2026
9d579ac
add LCP bundler-plugin
GoodDayForSurf Feb 4, 2026
448a14a
redefined plugin structure
ajivanyandev Feb 10, 2026
b299af2
fix file structure
ajivanyandev Feb 10, 2026
ea372db
remove unnecessary exports
ajivanyandev Feb 10, 2026
12b87ee
change file structure
ajivanyandev Feb 10, 2026
642664d
small fix
ajivanyandev Feb 10, 2026
281cbee
DX product key parsing for the client
VasilyStrelyaev Nov 18, 2025
c5f7195
validation logic fix
ajivanyandev Feb 11, 2026
8cb189e
add some errors, little change in key retrieval logic
ajivanyandev Feb 20, 2026
e7e15b7
change cli logic: make --out param optional
ajivanyandev Feb 20, 2026
0867245
small fix in warning
ajivanyandev Feb 20, 2026
aad224e
change trial oanel logic to match new warnings logic
ajivanyandev Feb 20, 2026
649342d
log source when running plugin
ajivanyandev Feb 20, 2026
5e2cdaf
Add validation on cli and plugin pipelines, minor fixes
ajivanyandev Mar 3, 2026
6215d5e
fix non modular behaviour
ajivanyandev Mar 11, 2026
842fd3f
cli wrapper to call from bin
ajivanyandev Mar 11, 2026
0aa8494
get rid of class based implementations for cleanup
ajivanyandev Mar 11, 2026
9b4fc95
add payload tests, fix key validation test logic
ajivanyandev Mar 11, 2026
701dab4
fix type error
ajivanyandev Mar 11, 2026
4d2d800
isolate warning logic into a helper function
ajivanyandev Mar 18, 2026
237859e
remove egow check for 25_2
ajivanyandev Mar 18, 2026
867043e
rename license command
ajivanyandev Mar 18, 2026
3d1f90e
Add license id to the generated license file and license related warn…
ajivanyandev Mar 26, 2026
bdec4ec
revert license id warnings in runtime
ajivanyandev Mar 30, 2026
86939cb
remove id from the generated file
ajivanyandev Mar 30, 2026
5c807fe
new warnings for devextreme-license (CLI tool)
ajivanyandev Mar 30, 2026
544ede9
cli fixes
ajivanyandev Apr 1, 2026
0a614d4
add browser warnings for license
ajivanyandev Apr 1, 2026
b1e31ec
fix typo
ajivanyandev Apr 1, 2026
25fe95b
fix small issues
ajivanyandev Apr 1, 2026
3391274
type fix
ajivanyandev Apr 1, 2026
1b0c8d0
minor fixes
ajivanyandev Apr 1, 2026
f6f5d27
browser warnings update
ajivanyandev Apr 6, 2026
d06ccd9
part of cli warning fixes
ajivanyandev Apr 6, 2026
d0ffcc0
browser warning messages update
ajivanyandev Apr 6, 2026
3bc1292
cleanup and final fixes for the warnings. Added DX1003 handler
ajivanyandev Apr 7, 2026
d78da08
Fix expired trial detection and add a test
ajivanyandev Apr 7, 2026
fe4db44
fix warning text
ajivanyandev Apr 7, 2026
b299e3c
seperate warnings
ajivanyandev Apr 7, 2026
a79a7a8
remove unnecessary comment
ajivanyandev Apr 7, 2026
a776da5
update tests after splitting warning
ajivanyandev Apr 7, 2026
3eeb4fb
remove old key validation code and add warnings
ajivanyandev Apr 9, 2026
5d33a2b
package lock fix
ajivanyandev Apr 9, 2026
b028da1
Merge branch '26_1' into license/26-1-full-pipeline
GoodDayForSurf Apr 9, 2026
133cf1e
Merge branch '26_1' into license/26-1-full-pipeline
GoodDayForSurf Apr 9, 2026
03416e0
update tests
ajivanyandev Apr 10, 2026
36706df
some cleanup
ajivanyandev Apr 10, 2026
0405086
Update cwd param handling
ajivanyandev Apr 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/devextreme/build/gulp/npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ const sources = (src, dist, distGlob) => (() => merge(
.pipe(eol('\n'))
.pipe(gulp.dest(`${dist}/bin`)),

gulp
.src(['license/**'])
.pipe(eol('\n'))
.pipe(gulp.dest(`${dist}/license`)),

gulp
.src('webpack.config.js')
.pipe(gulp.dest(`${dist}/bin`)),
Expand Down
4 changes: 4 additions & 0 deletions packages/devextreme/build/npm-bin/devextreme-license.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env node
'use strict';

require('../license/devextreme-license');
1 change: 1 addition & 0 deletions packages/devextreme/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default [
'js/viz/docs/*',
'node_modules/*',
'build/*',
'license/*',
'**/*.j.tsx',
'playground/*',
'themebuilder/data/metadata/*',
Expand Down
10 changes: 10 additions & 0 deletions packages/devextreme/js/__internal/core/license/byte_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 11 additions & 0 deletions packages/devextreme/js/__internal/core/license/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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 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`;
2 changes: 0 additions & 2 deletions packages/devextreme/js/__internal/core/license/key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -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<XGP=93zS%g:h(u-!14{|}~';
export const RSA_PUBLIC_KEY_XML = '<RSAKeyValue><Modulus>94ACmndawR6kB4PEJnXBBrz5Dn8ekEf5IvL7ro5ZvOyLVDiRwZXYR2uF8tFUSYjS5v7kOg74lfpZqfPXof7kcZwV3ENuy3tB7rqPBZaAqTMp5nBsZOc2H7MgDBXzrSdd4hzASQ==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>';
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/* eslint-disable */

import {
describe,
expect,
it,
jest,
} from '@jest/globals';
import { version as currentVersion } from '@js/core/version';

import { parseVersion } from '../../../utils/version';
import { TokenKind } from '../types';
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<string, unknown>;
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 = [
createProductInfo(parseInt(`${major}${minor}`, 10), 0n),
];
return { products };
}

describe('LCP key validation', () => {
it('serializer returns an invalid license for malformed input', () => {
const token = parseDevExpressProductKey('not-a-real-license');
expect(token.kind).toBe(TokenKind.corrupted);
});

(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);
});

it('trial fallback does not grant product access', () => {
const trialLicense = getTrialLicense();
expect(isLicenseValid(trialLicense)).toBe(true);

const version = findLatestDevExtremeVersion(trialLicense);

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');
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { FORMAT } from '../const';
import {
DESERIALIZATION_ERROR,
type ErrorToken,
GENERAL_ERROR,
PRODUCT_KIND_ERROR,
type Token,
TokenKind,
TRIAL_EXPIRED_ERROR,
VERIFICATION_ERROR,
} from '../types';
import {
LCP_SIGNATURE,
RSA_PUBLIC_KEY_XML,
SIGN_LENGTH,
} from './const';
import { findLatestDevExtremeVersion, getMaxExpiration } from './license_info';
import { createProductInfo, type ProductInfo } from './product_info';
import {
dotNetTicksToMs,
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]);
const expiration = parts.length > 3 ? dotNetTicksToMs(parts[3]) : Infinity;
return createProductInfo(
version,
productsValue,
expiration,
);
});

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

return {
kind: TokenKind.verified,
payload: {
customerId: '',
maxVersionAllowed,
format: FORMAT,
},
};
} catch (error) {
return GENERAL_ERROR;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { isProduct, type ProductInfo } from './product_info';
import { ProductKind } from './types';

export interface LicenseInfo {
readonly products: ProductInfo[];
}

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

const sorted = [...info.products].sort((a, b) => b.version - a.version);

return sorted.find((p) => isProduct(p, ProductKind.DevExtremeHtmlJs))?.version;
}
Loading
Loading