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 fe8eb8c867d2..49278b396f13 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 @@ -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(); @@ -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); + } + }); }); 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 126e6b38c740..82d9a767a566 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,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, @@ -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; } @@ -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; 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 a526a3c61983..87138c44ceae 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 @@ -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; +} 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 310c260a311f..28f5ba3de591 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.test.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.test.ts @@ -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 { @@ -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: { @@ -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); + }); +}); diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index 48d75c73d4a4..6d4060a82df8 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -5,6 +5,7 @@ import { fullVersion } from '@js/core/version'; import type { Version } from '../../utils/version'; import { assertedVersionsCompatible, + getAssertedVersions, parseVersion, } from '../../utils/version'; import { @@ -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; @@ -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') { 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..bab460725251 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation_internal.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation_internal.ts @@ -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 {}