Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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;
const DEVEXTREME_ASPNET_BIT = 1n << 17n;

function msToDotNetTicks(ms: number): string {
return (BigInt(ms) * DOT_NET_TICKS_PER_MS + DOT_NET_TICKS_EPOCH_OFFSET).toString();
Expand Down Expand Up @@ -83,4 +84,28 @@ describe('LCP key validation', () => {

expect(token.kind).toBe(TokenKind.verified);
});

it('does not accept AspNet-only product bit without compatibility mode', () => {
const { parseDevExpressProductKey, TokenKind } = loadParserWithBypassedSignatureCheck();
const payload = `meta;251,${DEVEXTREME_ASPNET_BIT};`;

const token = parseDevExpressProductKey(createLcpSource(payload));

expect(token.kind).toBe(TokenKind.corrupted);
if (token.kind === TokenKind.corrupted) {
expect(token.error).toBe('product-kind');
}
});

it('accepts AspNet-only product bit in compatibility mode', () => {
const { parseDevExpressProductKey, TokenKind } = loadParserWithBypassedSignatureCheck();
const payload = `meta;251,${DEVEXTREME_ASPNET_BIT};`;

const token = parseDevExpressProductKey(createLcpSource(payload), true);

expect(token.kind).toBe(TokenKind.verified);
if (token.kind === TokenKind.verified) {
expect(token.payload.maxVersionAllowed).toBe(251);
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import {
RSA_PUBLIC_KEY_XML,
SIGN_LENGTH,
} from './const';
import { findLatestDevExtremeVersion } from './license_info';
import {
findLatestDevExtremeVersion,
findLatestDevExtremeVersionWithAspNetCompatibility,
} from './license_info';
import { createProductInfo, type ProductInfo } from './product_info';
import {
dotNetTicksToMs,
Expand Down Expand Up @@ -65,7 +68,10 @@ function productsFromString(encodedString: string): ParsedProducts {
}
}

export function parseDevExpressProductKey(productsLicenseSource: string): Token {
export function parseDevExpressProductKey(
productsLicenseSource: string,
allowAspNetProductCompatibility = false,
): Token {
if (!isProductOnlyLicense(productsLicenseSource)) {
return GENERAL_ERROR;
}
Expand Down Expand Up @@ -93,7 +99,9 @@ export function parseDevExpressProductKey(productsLicenseSource: string): Token
return errorToken;
}

const maxVersionAllowed = findLatestDevExtremeVersion({ products });
const maxVersionAllowed = allowAspNetProductCompatibility
? findLatestDevExtremeVersionWithAspNetCompatibility({ products })
: findLatestDevExtremeVersion({ products });

if (!maxVersionAllowed) {
return PRODUCT_KIND_ERROR;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,17 @@ export function findLatestDevExtremeVersion(info: LicenseInfo): number | undefin

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

export function findLatestDevExtremeVersionWithAspNetCompatibility(
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, ProductKind.DevExtremeAspNet))
?.version;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
} from '@jest/globals';
import config from '@js/core/config';
import errors from '@js/core/errors';
import { fullVersion } from '@js/core/version';

import { base } from '../../ui/overlay/z_index';
import {
Expand All @@ -11,12 +12,14 @@ import {
clearAssertedVersions,
} from '../../utils/version';
import { LICENSE_KEY_PLACEHOLDER } from './const';
import * as productKeyValidator from './lcp_key_validation/lcp_key_validator';
import {
parseLicenseKey,
setLicenseCheckSkipCondition,
validateLicense,
} from './license_validation';
import * as trialPanel from './trial_panel.client';
import { TokenKind } from './types';

jest.mock('./key', () => ({
PUBLIC_KEY: {
Expand Down Expand Up @@ -583,3 +586,51 @@ describe('assertedVersions integration', () => {
});
});
});

describe('AspNet compatibility gate', () => {
const LCP_LICENSE = 'LCPv1mock-license';

beforeEach(() => {
jest.spyOn(errors, 'log').mockImplementation(() => {});
jest.spyOn(console, 'warn').mockImplementation(() => {});
jest.spyOn(trialPanel, 'renderTrialPanel');
setLicenseCheckSkipCondition(false);
});

afterEach(() => {
jest.restoreAllMocks();
clearAssertedVersions();
});

it('passes AspNet compatibility=true when DevExtreme.AspNet assertion exists', () => {
const parseSpy = jest.spyOn(productKeyValidator, 'parseDevExpressProductKey').mockReturnValue({
kind: TokenKind.verified,
payload: {
customerId: 'test',
maxVersionAllowed: 999,
format: 1,
},
});

assertDevExtremeVersion('DevExtreme.AspNet.Core', fullVersion);
validateLicense(LCP_LICENSE, fullVersion);

expect(parseSpy).toHaveBeenCalledWith(LCP_LICENSE, true);
});

it('passes AspNet compatibility=false when there is no AspNet assertion', () => {
const parseSpy = jest.spyOn(productKeyValidator, 'parseDevExpressProductKey').mockReturnValue({
kind: TokenKind.verified,
payload: {
customerId: 'test',
maxVersionAllowed: 999,
format: 1,
},
});

assertDevExtremeVersion('DevExtreme.React', fullVersion);
validateLicense(LCP_LICENSE, fullVersion);

expect(parseSpy).toHaveBeenCalledWith(LCP_LICENSE, false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { fullVersion } from '@js/core/version';
import type { Version } from '../../utils/version';
import {
assertedVersionsCompatible,
getAssertedVersions,
parseVersion,
} from '../../utils/version';
import {
Expand All @@ -24,14 +25,23 @@ import {
} from './types';

let validationPerformed = false;
const ASPNET_ASSERTION_PREFIX = 'devextreme.aspnet';

export function parseLicenseKey(encodedKey: string | undefined): Token {
function hasAspNetVersionAssertion(): boolean {
return getAssertedVersions()
.some(({ packageName }) => packageName.toLowerCase().startsWith(ASPNET_ASSERTION_PREFIX));
}

export function parseLicenseKey(
encodedKey: string | undefined,
allowAspNetProductCompatibility = false,
): Token {
if (encodedKey === undefined) {
return GENERAL_ERROR;
}

if (isProductOnlyLicense(encodedKey)) {
return parseDevExpressProductKey(encodedKey);
return parseDevExpressProductKey(encodedKey, allowAspNetProductCompatibility);
}

return GENERAL_ERROR;
Expand Down Expand Up @@ -77,7 +87,8 @@ function getLicenseCheckParams({
return { preview, error: 'W0021', warningType: 'old-devextreme-key' };
}

const license = parseLicenseKey(licenseKey);
const allowAspNetProductCompatibility = hasAspNetVersionAssertion();
const license = parseLicenseKey(licenseKey, allowAspNetProductCompatibility);

if (license.kind === TokenKind.corrupted) {
if (license.error === 'product-kind') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { Token } from './types';

// @ts-expect-error - only for internal usage
export function parseLicenseKey(encodedKey: string | undefined): Token {}
export function parseLicenseKey(
encodedKey: string | undefined,
allowAspNetProductCompatibility?: boolean,
): Token {
return undefined as unknown as Token;
}

export function validateLicense(licenseKey: string, version?: string): void {}

// @ts-expect-error - only for internal usage
export function peekValidationPerformed(): boolean {}
export function peekValidationPerformed(): boolean {
return false;
}

export function setLicenseCheckSkipCondition(): void {}

Expand Down
Loading