Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
95de894
add modules to retrieve lcx key and convert lcx to lcp
ajivanyandev Feb 4, 2026
70a52b3
Add CLI command to write LCP to the given output file
ajivanyandev Feb 4, 2026
bd444fc
add LCP bundler-plugin
GoodDayForSurf Feb 4, 2026
91f599c
redefined plugin structure
ajivanyandev Feb 10, 2026
b5a6a2e
fix file structure
ajivanyandev Feb 10, 2026
867f1f0
remove unnecessary exports
ajivanyandev Feb 10, 2026
c91be04
change file structure
ajivanyandev Feb 10, 2026
a37f738
small fix
ajivanyandev Feb 10, 2026
cd369ab
DX product key parsing for the client
VasilyStrelyaev Nov 18, 2025
4d10b67
validation logic fix
ajivanyandev Feb 11, 2026
44c9804
add some errors, little change in key retrieval logic
ajivanyandev Feb 20, 2026
602b57a
change cli logic: make --out param optional
ajivanyandev Feb 20, 2026
f4f8c46
small fix in warning
ajivanyandev Feb 20, 2026
1760e3b
change trial oanel logic to match new warnings logic
ajivanyandev Feb 20, 2026
545ccef
log source when running plugin
ajivanyandev Feb 20, 2026
8c7a3b4
Add validation on cli and plugin pipelines, minor fixes
ajivanyandev Mar 3, 2026
8551671
fix non modular behaviour
ajivanyandev Mar 11, 2026
fbb7f3f
cli wrapper to call from bin
ajivanyandev Mar 11, 2026
673514c
get rid of class based implementations for cleanup
ajivanyandev Mar 11, 2026
630b0d1
add payload tests, fix key validation test logic
ajivanyandev Mar 11, 2026
800aa4e
fix type error
ajivanyandev Mar 11, 2026
5707a13
isolate warning logic into a helper function
ajivanyandev Mar 18, 2026
020d65c
rename license command
ajivanyandev Mar 18, 2026
5e80d37
Merge branch '26_1' into feat/license-pipeline-26-1
ajivanyandev Mar 18, 2026
25c963a
Merge branch '26_1' into feat/license-pipeline-26-1
ajivanyandev Mar 18, 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 @@ -111,6 +111,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 @@ -33,6 +33,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
9 changes: 9 additions & 0 deletions packages/devextreme/js/__internal/core/license/const.ts
Original file line number Diff line number Diff line change
@@ -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`;
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,37 @@
import { describe, expect, it } 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';

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);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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 { findLatestDevExtremeVersion } from './license_info';
import { createProductInfo, type 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 createProductInfo(
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 maxVersionAllowed = findLatestDevExtremeVersion({ products });

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,20 @@
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 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