Skip to content

Commit 92517e1

Browse files
Merge branch '25_2' into fix/scheduler-form-animation-overlap
2 parents 150db33 + bc02705 commit 92517e1

28 files changed

Lines changed: 1419 additions & 40 deletions

packages/devextreme/build/gulp/npm.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ const sources = (src, dist, distGlob) => (() => merge(
111111
.pipe(eol('\n'))
112112
.pipe(gulp.dest(`${dist}/bin`)),
113113

114+
gulp
115+
.src(['license/**'])
116+
.pipe(eol('\n'))
117+
.pipe(gulp.dest(`${dist}/license`)),
118+
114119
gulp
115120
.src('webpack.config.js')
116121
.pipe(gulp.dest(`${dist}/bin`)),
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
4+
require('../license/devextreme-license');

packages/devextreme/eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export default [
5151
'js/viz/docs/*',
5252
'node_modules/*',
5353
'build/*',
54+
'license/*',
5455
'**/*.j.tsx',
5556
'playground/*',
5657
'themebuilder/data/metadata/*',

packages/devextreme/js/__internal/core/license/byte_utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ export function leftRotate(x: number, n: number): number {
4848
return ((x << n) | (x >>> (32 - n))) >>> 0;
4949
}
5050

51+
export function bigIntFromBytes(bytes: Uint8Array): bigint {
52+
const eight = BigInt(8);
53+
const zero = BigInt(0);
54+
55+
return bytes.reduce(
56+
(acc, cur) => (acc << eight) + BigInt(cur),
57+
zero,
58+
);
59+
}
60+
5161
export function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
5262
const result = new Uint8Array(a.length + b.length);
5363
result.set(a, 0);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const FORMAT = 1;
2+
export const RTM_MIN_PATCH_VERSION = 3;
3+
export const KEY_SPLITTER = '.';
4+
5+
export const BUY_NOW_LINK = 'https://go.devexpress.com/Licensing_Installer_Watermark_DevExtremeJQuery.aspx';
6+
export const LICENSING_DOC_LINK = 'https://go.devexpress.com/Licensing_Documentation_DevExtremeJQuery.aspx';
7+
8+
export const NBSP = '\u00A0';
9+
export const SUBSCRIPTION_NAMES = `Universal, DXperience, ASP.NET${NBSP}and${NBSP}Blazor, DevExtreme${NBSP}Complete`;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const LCP_SIGNATURE = 'LCPv1';
2+
export const SIGN_LENGTH = 68 * 2; // 136 characters
3+
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{|}~';
4+
export const RSA_PUBLIC_KEY_XML = '<RSAKeyValue><Modulus>94ACmndawR6kB4PEJnXBBrz5Dn8ekEf5IvL7ro5ZvOyLVDiRwZXYR2uF8tFUSYjS5v7kOg74lfpZqfPXof7kcZwV3ENuy3tB7rqPBZaAqTMp5nBsZOc2H7MgDBXzrSdd4hzASQ==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>';
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, expect, it } from '@jest/globals';
2+
import { version as currentVersion } from '@js/core/version';
3+
4+
import { parseVersion } from '../../../utils/version';
5+
import { TokenKind } from '../types';
6+
import { parseDevExpressProductKey } from './lcp_key_validator';
7+
import { findLatestDevExtremeVersion, isLicenseValid } from './license_info';
8+
import { createProductInfo } from './product_info';
9+
10+
function getTrialLicense() {
11+
const { major, minor } = parseVersion(currentVersion);
12+
const products = [
13+
createProductInfo(parseInt(`${major}${minor}`, 10), 0n),
14+
];
15+
return { products };
16+
}
17+
18+
describe('LCP key validation', () => {
19+
it('serializer returns an invalid license for malformed input', () => {
20+
const token = parseDevExpressProductKey('not-a-real-license');
21+
expect(token.kind).toBe(TokenKind.corrupted);
22+
});
23+
24+
(process.env.DX_PRODUCT_KEY ? it : it.skip)('developer product license fixtures parse into valid LicenseInfo instances', () => {
25+
const token = parseDevExpressProductKey(process.env.DX_PRODUCT_KEY as string);
26+
expect(token.kind).toBe(TokenKind.verified);
27+
});
28+
29+
it('trial fallback does not grant product access', () => {
30+
const trialLicense = getTrialLicense();
31+
expect(isLicenseValid(trialLicense)).toBe(true);
32+
33+
const version = findLatestDevExtremeVersion(trialLicense);
34+
35+
expect(version).toBe(undefined);
36+
});
37+
});
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { FORMAT } from '../const';
2+
import {
3+
DESERIALIZATION_ERROR,
4+
type ErrorToken,
5+
GENERAL_ERROR,
6+
PRODUCT_KIND_ERROR,
7+
type Token,
8+
TokenKind,
9+
VERIFICATION_ERROR,
10+
} from '../types';
11+
import {
12+
LCP_SIGNATURE,
13+
RSA_PUBLIC_KEY_XML,
14+
SIGN_LENGTH,
15+
} from './const';
16+
import { findLatestDevExtremeVersion } from './license_info';
17+
import { createProductInfo, type ProductInfo } from './product_info';
18+
import { encodeString, shiftDecodeText, verifyHash } from './utils';
19+
20+
interface ParsedProducts {
21+
products: ProductInfo[];
22+
errorToken?: ErrorToken;
23+
}
24+
25+
export function isProductOnlyLicense(license: string): boolean {
26+
return typeof license === 'string' && license.startsWith(LCP_SIGNATURE);
27+
}
28+
29+
function productsFromString(encodedString: string): ParsedProducts {
30+
if (!encodedString) {
31+
return {
32+
products: [],
33+
errorToken: GENERAL_ERROR,
34+
};
35+
}
36+
37+
try {
38+
const splitInfo = encodedString.split(';');
39+
const productTuples = splitInfo.slice(1).filter((entry) => entry.length > 0);
40+
const products = productTuples.map((tuple) => {
41+
const parts = tuple.split(',');
42+
const version = Number.parseInt(parts[0], 10);
43+
const productsValue = BigInt(parts[1]);
44+
return createProductInfo(
45+
version,
46+
productsValue,
47+
);
48+
});
49+
50+
return {
51+
products,
52+
};
53+
} catch (error) {
54+
return {
55+
products: [],
56+
errorToken: DESERIALIZATION_ERROR,
57+
};
58+
}
59+
}
60+
61+
export function parseDevExpressProductKey(productsLicenseSource: string): Token {
62+
if (!isProductOnlyLicense(productsLicenseSource)) {
63+
return GENERAL_ERROR;
64+
}
65+
66+
try {
67+
const productsLicense = atob(
68+
shiftDecodeText(productsLicenseSource.substring(LCP_SIGNATURE.length)),
69+
);
70+
71+
const signature = productsLicense.substring(0, SIGN_LENGTH);
72+
const productsPayload = productsLicense.substring(SIGN_LENGTH);
73+
74+
if (!verifyHash(RSA_PUBLIC_KEY_XML, productsPayload, signature)) {
75+
return VERIFICATION_ERROR;
76+
}
77+
78+
const {
79+
products,
80+
errorToken,
81+
} = productsFromString(
82+
encodeString(productsPayload, shiftDecodeText),
83+
);
84+
85+
if (errorToken) {
86+
return errorToken;
87+
}
88+
89+
const maxVersionAllowed = findLatestDevExtremeVersion({ products });
90+
91+
if (!maxVersionAllowed) {
92+
return PRODUCT_KIND_ERROR;
93+
}
94+
95+
return {
96+
kind: TokenKind.verified,
97+
payload: {
98+
customerId: '',
99+
maxVersionAllowed,
100+
format: FORMAT,
101+
},
102+
};
103+
} catch (error) {
104+
return GENERAL_ERROR;
105+
}
106+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { isProduct, type ProductInfo } from './product_info';
2+
import { ProductKind } from './types';
3+
4+
export interface LicenseInfo {
5+
readonly products: ProductInfo[];
6+
}
7+
8+
export function isLicenseValid(info: LicenseInfo): boolean {
9+
return Array.isArray(info.products) && info.products.length > 0;
10+
}
11+
12+
export function findLatestDevExtremeVersion(info: LicenseInfo): number | undefined {
13+
if (!isLicenseValid(info)) {
14+
return undefined;
15+
}
16+
17+
const sorted = [...info.products].sort((a, b) => b.version - a.version);
18+
19+
return sorted.find((p) => isProduct(p, ProductKind.DevExtremeHtmlJs))?.version;
20+
}

0 commit comments

Comments
 (0)