diff --git a/docs/data/advanced-components/overview.md b/docs/data/advanced-components/overview.md index 58c682a86cdcc..499f9ad711556 100644 --- a/docs/data/advanced-components/overview.md +++ b/docs/data/advanced-components/overview.md @@ -88,7 +88,7 @@ This key removes all watermarks and console warnings. import { LicenseInfo } from '@mui/x-license-pro'; LicenseInfo.setLicenseKey( - 'x0jTPl0USVkVZV0SsMjM1kDNyADM5cjM2ETPZJVSQhVRsIDN0YTM6IVREJ1T0b9586ef25c9853decfa7709eee27a1e', + '61628ce74db2c1b62783a6d438593bc5Tz1NVUktRG9jLEU9MTY4MzQ0NzgyMTI4NCxTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLEtWPTI=', ); ``` diff --git a/docs/data/date-pickers/migration-lab/migration-lab.md b/docs/data/date-pickers/migration-lab/migration-lab.md index 7e1fe1441cd79..7ebcf5390d8b5 100644 --- a/docs/data/date-pickers/migration-lab/migration-lab.md +++ b/docs/data/date-pickers/migration-lab/migration-lab.md @@ -55,7 +55,7 @@ You must set the license key before rendering the first component. import { LicenseInfo } from '@mui/x-license-pro'; LicenseInfo.setLicenseKey( - 'x0jTPl0USVkVZV0SsMjM1kDNyADM5cjM2ETPZJVSQhVRsIDN0YTM6IVREJ1T0b9586ef25c9853decfa7709eee27a1e', + '61628ce74db2c1b62783a6d438593bc5Tz1NVUktRG9jLEU9MTY4MzQ0NzgyMTI4NCxTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLEtWPTI=', ); ``` diff --git a/packages/storybook/.storybook/preview.tsx b/packages/storybook/.storybook/preview.tsx index 91a9709799e01..252a288c3e8f7 100644 --- a/packages/storybook/.storybook/preview.tsx +++ b/packages/storybook/.storybook/preview.tsx @@ -5,7 +5,7 @@ import { configureActions } from '@storybook/addon-actions'; // Remove the license warning from demonstration purposes LicenseInfo.setLicenseKey( - '0f94d8b65161817ca5d7f7af8ac2f042T1JERVI6TVVJLVN0b3J5Ym9vayxFWFBJUlk9MTY1NDg1ODc1MzU1MCxLRVlWRVJTSU9OPTE=', + '61628ce74db2c1b62783a6d438593bc5Tz1NVUktRG9jLEU9MTY4MzQ0NzgyMTI4NCxTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLEtWPTI=', ); configureActions({ diff --git a/packages/x-license-pro/src/cli/license-cli.ts b/packages/x-license-pro/src/cli/license-cli.ts index c3425bb333849..bc22eb33ddecc 100644 --- a/packages/x-license-pro/src/cli/license-cli.ts +++ b/packages/x-license-pro/src/cli/license-cli.ts @@ -1,7 +1,9 @@ /* eslint-disable no-console */ import * as yargs from 'yargs'; -import { generateLicense, LicenseScope } from '../generateLicense/generateLicense'; +import { generateLicense } from '../generateLicense/generateLicense'; import { base64Decode } from '../encoding/base64'; +import { LicenseScope } from '../utils/licenseScope'; +import { LicensingModel } from '../utils/licensingModel'; const oneDayInMs = 1000 * 60 * 60 * 24; @@ -64,8 +66,15 @@ export function licenseGenCli() { }) .option('scope', { default: 'pro', + alias: 's', describe: 'The license scope.', type: 'string', + }) + .option('licensingModel', { + default: 'subscription', + alias: 'l', + describe: 'The license sales model.', + type: 'string', }); }, handler: (argv: yargs.ArgumentsCamelCase) => { @@ -76,7 +85,8 @@ export function licenseGenCli() { const licenseDetails = { expiryDate: new Date(new Date().getTime() + parseInt(argv.expiry, 10) * oneDayInMs), orderNumber: argv.order, - scope: argv.scope as LicenseScope, + scope: argv.scope as LicenseScope | undefined, + licensingModel: argv.licensingModel as LicensingModel | undefined, }; console.log( diff --git a/packages/x-license-pro/src/generateLicense/generateLicense.test.ts b/packages/x-license-pro/src/generateLicense/generateLicense.test.ts index 0b6de8a3f1bd2..a6a81bc261e79 100644 --- a/packages/x-license-pro/src/generateLicense/generateLicense.test.ts +++ b/packages/x-license-pro/src/generateLicense/generateLicense.test.ts @@ -2,35 +2,81 @@ import { expect } from 'chai'; import { generateLicense } from './generateLicense'; describe('License: generateLicense', () => { - it('should generate DataGridPro License properly when "scope" is not provided', () => { + // TODO: Remove + it('should generate pro license properly when "scope" is not provided', () => { expect( - generateLicense({ expiryDate: new Date(1591723879062), orderNumber: 'MUI-123' }), + generateLicense({ + expiryDate: new Date(1591723879062), + orderNumber: 'MUI-123', + licensingModel: 'subscription', + }), ).to.equal( - '90b5a76151089447618ede1917227a1bT1JERVI6TVVJLTEyMyxFWFBJUlk9MTU5MTcyMzg3OTA2MixLRVlWRVJTSU9OPTIsU0NPUEU9cHJv', + 'b2b2ea9c6fd846e11770da3c795d6f63Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1zdWJzY3JpcHRpb24sS1Y9Mg==', ); }); - it('should generate DataGridPro License properly when `scope: "pro"`', () => { + it('should generate pro license properly when `scope: "pro"`', () => { expect( generateLicense({ expiryDate: new Date(1591723879062), orderNumber: 'MUI-123', scope: 'pro', + licensingModel: 'subscription', }), ).to.equal( - '90b5a76151089447618ede1917227a1bT1JERVI6TVVJLTEyMyxFWFBJUlk9MTU5MTcyMzg3OTA2MixLRVlWRVJTSU9OPTIsU0NPUEU9cHJv', + 'b2b2ea9c6fd846e11770da3c795d6f63Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1zdWJzY3JpcHRpb24sS1Y9Mg==', ); }); - it('should generate DataGridPremium License properly when `scope: "premium"`', () => { + it('should generate premium license when `scope: "premium"`', () => { expect( generateLicense({ expiryDate: new Date(1591723879062), orderNumber: 'MUI-123', scope: 'premium', + licensingModel: 'subscription', + }), + ).to.equal( + 'ac8d20b4ecd1f919157f3713f8ba1651Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLEtWPTI=', + ); + }); + + // TODO: Remove + it('should generate perpetual license when "licensingModel" is not provided', () => { + expect( + generateLicense({ + expiryDate: new Date(1591723879062), + orderNumber: 'MUI-123', + scope: 'pro', + }), + ).to.equal( + 'b16edd8e6bc83293a723779a259f520cTz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1wZXJwZXR1YWwsS1Y9Mg==', + ); + }); + + it('should generate subscription license when `licensingModel: "subscription"`', () => { + expect( + generateLicense({ + expiryDate: new Date(1591723879062), + orderNumber: 'MUI-123', + scope: 'pro', + licensingModel: 'subscription', + }), + ).to.equal( + 'b2b2ea9c6fd846e11770da3c795d6f63Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1zdWJzY3JpcHRpb24sS1Y9Mg==', + ); + }); + + it('should generate perpetual license when `licensingModel: "perpetual"`', () => { + expect( + generateLicense({ + expiryDate: new Date(1591723879062), + orderNumber: 'MUI-123', + scope: 'pro', + licensingModel: 'perpetual', }), ).to.equal( - '0d79eeaf5facce7184422f22eeeb369aT1JERVI6TVVJLTEyMyxFWFBJUlk9MTU5MTcyMzg3OTA2MixLRVlWRVJTSU9OPTIsU0NPUEU9cHJlbWl1bQ==', + 'b16edd8e6bc83293a723779a259f520cTz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1wZXJwZXR1YWwsS1Y9Mg==', ); }); }); diff --git a/packages/x-license-pro/src/generateLicense/generateLicense.ts b/packages/x-license-pro/src/generateLicense/generateLicense.ts index 94c77569b4355..b1c4b24334b2b 100644 --- a/packages/x-license-pro/src/generateLicense/generateLicense.ts +++ b/packages/x-license-pro/src/generateLicense/generateLicense.ts @@ -1,30 +1,35 @@ import { md5 } from '../encoding/md5'; import { base64Encode } from '../encoding/base64'; +import { LICENSE_SCOPES, LicenseScope } from '../utils/licenseScope'; +import { LICENSING_MODELS, LicensingModel } from '../utils/licensingModel'; const licenseVersion = '2'; -export type LicenseScope = 'pro' | 'premium'; - export interface LicenseDetails { orderNumber: string; expiryDate: Date; // TODO: to be made required once the store is updated scope?: LicenseScope; + // TODO: to be made required once the store is updated + licensingModel?: LicensingModel; } function getClearLicenseString(details: LicenseDetails) { - return `ORDER:${ - details.orderNumber - },EXPIRY=${details.expiryDate.getTime()},KEYVERSION=${licenseVersion},SCOPE=${details.scope}`; + if (details.scope && !LICENSE_SCOPES.includes(details.scope)) { + throw new Error('MUI: Invalid scope'); + } + + if (details.licensingModel && !LICENSING_MODELS.includes(details.licensingModel)) { + throw new Error('MUI: Invalid sales model'); + } + + return `O=${details.orderNumber},E=${details.expiryDate.getTime()},S=${ + details.scope ?? 'pro' + },LM=${details.licensingModel ?? 'perpetual'},KV=${licenseVersion}`; } export function generateLicense(details: LicenseDetails) { - let clearLicense; - if (details.scope) { - clearLicense = getClearLicenseString(details); - } else { - clearLicense = getClearLicenseString({ ...details, scope: 'pro' }); - } + const licenseStr = getClearLicenseString(details); - return `${md5(base64Encode(clearLicense))}${base64Encode(clearLicense)}`; + return `${md5(base64Encode(licenseStr))}${base64Encode(licenseStr)}`; } diff --git a/packages/x-license-pro/src/useLicenseVerifier/useLicenseVerifier.ts b/packages/x-license-pro/src/useLicenseVerifier/useLicenseVerifier.ts index 4d1661c22033a..85bec7b1add64 100644 --- a/packages/x-license-pro/src/useLicenseVerifier/useLicenseVerifier.ts +++ b/packages/x-license-pro/src/useLicenseVerifier/useLicenseVerifier.ts @@ -7,6 +7,7 @@ import { showNotFoundLicenseError, } from '../utils/licenseErrorMessageUtils'; import { LicenseStatus } from '../utils/licenseStatus'; +import { LicenseScope } from '../utils/licenseScope'; export type MuiCommercialPackageName = | 'x-data-grid-pro' @@ -27,7 +28,16 @@ export function useLicenseVerifier( return sharedLicenseStatuses[packageName]!.status; } - const licenseStatus = verifyLicense(releaseInfo, licenseKey); + const acceptedScopes: LicenseScope[] = packageName.includes('premium') + ? ['premium'] + : ['pro', 'premium']; + + const licenseStatus = verifyLicense({ + releaseInfo, + licenseKey, + acceptedScopes, + isProduction: process.env.NODE_ENV === 'production', + }); sharedLicenseStatuses[packageName] = { key: licenseStatus, status: licenseStatus }; diff --git a/packages/x-license-pro/src/utils/index.ts b/packages/x-license-pro/src/utils/index.ts index 8a54e1fc88fc7..20a11f9c8cd89 100644 --- a/packages/x-license-pro/src/utils/index.ts +++ b/packages/x-license-pro/src/utils/index.ts @@ -1,3 +1,5 @@ export * from './licenseErrorMessageUtils'; export * from './licenseInfo'; export * from './licenseStatus'; +export type { LicenseScope } from './licenseScope'; +export type { LicensingModel } from './licensingModel'; diff --git a/packages/x-license-pro/src/utils/licenseScope.ts b/packages/x-license-pro/src/utils/licenseScope.ts new file mode 100644 index 0000000000000..b9eb38f8cc623 --- /dev/null +++ b/packages/x-license-pro/src/utils/licenseScope.ts @@ -0,0 +1,3 @@ +export const LICENSE_SCOPES = ['pro', 'premium'] as const; + +export type LicenseScope = typeof LICENSE_SCOPES[number]; diff --git a/packages/x-license-pro/src/utils/licensingModel.ts b/packages/x-license-pro/src/utils/licensingModel.ts new file mode 100644 index 0000000000000..b0055fee107a0 --- /dev/null +++ b/packages/x-license-pro/src/utils/licensingModel.ts @@ -0,0 +1,14 @@ +export const LICENSING_MODELS = [ + /** + * A license is outdated if the current version of the software was released after the expiry date of the license. + * But the license can be used indefinitely with an older version of the software. + */ + 'perpetual', + /** + * On development, a license is outdated if the expiry date has been reached + * On production, a license is outdated if the current version of the software was released after the expiry date of the license (see "perpetual") + */ + 'subscription', +] as const; + +export type LicensingModel = typeof LICENSING_MODELS[number]; diff --git a/packages/x-license-pro/src/verifyLicense/verifyLicense.test.ts b/packages/x-license-pro/src/verifyLicense/verifyLicense.test.ts index a4809e8d54924..96945d52b8f29 100644 --- a/packages/x-license-pro/src/verifyLicense/verifyLicense.test.ts +++ b/packages/x-license-pro/src/verifyLicense/verifyLicense.test.ts @@ -4,40 +4,189 @@ import { generateReleaseInfo, verifyLicense } from './verifyLicense'; import { LicenseStatus } from '../utils/licenseStatus'; const oneDayInMS = 1000 * 60 * 60 * 24; -const oneYear = oneDayInMS * 365; -const RELEASE_INFO = generateReleaseInfo(); +const releaseDate = new Date(2018, 0, 0, 0, 0, 0, 0); +const RELEASE_INFO = generateReleaseInfo(releaseDate); describe('License: verifyLicense', () => { - const validLicense = generateLicense({ - expiryDate: new Date(new Date().getTime() + oneYear), - orderNumber: 'MUI-123', - }); + describe('key version: 1', () => { + const licenseKey = + '0f94d8b65161817ca5d7f7af8ac2f042T1JERVI6TVVJLVN0b3J5Ym9vayxFWFBJUlk9MTY1NDg1ODc1MzU1MCxLRVlWRVJTSU9OPTE='; - it('should log an error when ReleaseInfo is not valid', () => { - expect(() => verifyLicense('__RELEASE_INFO__', validLicense)).to.throw( - 'MUI: The release information is invalid. Not able to validate license.', - ); - }); + it('should log an error when ReleaseInfo is not valid', () => { + expect(() => + verifyLicense({ + releaseInfo: '__RELEASE_INFO__', + licenseKey, + acceptedScopes: ['pro', 'premium'], + isProduction: true, + }), + ).to.throw('MUI: The release information is invalid. Not able to validate license.'); + }); - it('should verify License properly', () => { - expect(verifyLicense(RELEASE_INFO, validLicense)).to.equal(LicenseStatus.Valid); + it('should verify License properly', () => { + expect( + verifyLicense({ + releaseInfo: RELEASE_INFO, + licenseKey, + acceptedScopes: ['pro', 'premium'], + isProduction: true, + }), + ).to.equal(LicenseStatus.Valid); + }); + + it('should check expired License properly', () => { + const expiredLicenseKey = generateLicense({ + expiryDate: new Date(releaseDate.getTime() - oneDayInMS), + orderNumber: 'MUI-123', + }); + + expect( + verifyLicense({ + releaseInfo: RELEASE_INFO, + licenseKey: expiredLicenseKey, + acceptedScopes: ['pro', 'premium'], + isProduction: true, + }), + ).to.equal(LicenseStatus.Expired); + }); + + it('should return Invalid for invalid license', () => { + expect( + verifyLicense({ + releaseInfo: RELEASE_INFO, + licenseKey: + 'b43ff5f9ac93f021855ff59ff0ba5220TkFNRTpNYC1VSSBTQVMsREVWRUxPUEVSX0NPVU5UPTEwLEVYUElSWT0xNTkxNzIzMDY3MDQyLFZFUlNJT049MS4yLjM', + acceptedScopes: ['pro', 'premium'], + isProduction: true, + }), + ).to.equal(LicenseStatus.Invalid); + }); }); - it('should check expired License properly', () => { - const expiredLicense = generateLicense({ - expiryDate: new Date(new Date().getTime() - oneDayInMS), + describe('key version: 2', () => { + const licenseKeyPro = generateLicense({ + expiryDate: new Date(releaseDate.getTime() + oneDayInMS), orderNumber: 'MUI-123', + scope: 'pro', + licensingModel: 'subscription', }); - expect(verifyLicense(RELEASE_INFO, expiredLicense)).to.equal(LicenseStatus.Expired); - }); + const licenseKeyPremium = generateLicense({ + expiryDate: new Date(releaseDate.getTime() + oneDayInMS), + orderNumber: 'MUI-123', + scope: 'premium', + licensingModel: 'subscription', + }); + + it('should log an error when ReleaseInfo is not valid', () => { + expect(() => + verifyLicense({ + releaseInfo: '__RELEASE_INFO__', + licenseKey: licenseKeyPro, + acceptedScopes: ['pro', 'premium'], + isProduction: true, + }), + ).to.throw('MUI: The release information is invalid. Not able to validate license.'); + }); + + describe('scope', () => { + it('should accept pro license for pro features', () => { + expect( + verifyLicense({ + releaseInfo: RELEASE_INFO, + licenseKey: licenseKeyPro, + acceptedScopes: ['pro', 'premium'], + isProduction: true, + }), + ).to.equal(LicenseStatus.Valid); + }); + + it('should accept premium license for premium features', () => { + expect( + verifyLicense({ + releaseInfo: RELEASE_INFO, + licenseKey: licenseKeyPremium, + acceptedScopes: ['premium'], + isProduction: true, + }), + ).to.equal(LicenseStatus.Valid); + }); + + it('should not accept pro license for premium feature', () => { + expect( + verifyLicense({ + releaseInfo: RELEASE_INFO, + licenseKey: licenseKeyPro, + acceptedScopes: ['premium'], + isProduction: true, + }), + ).to.equal(LicenseStatus.Invalid); + }); + }); + + describe('expiry date', () => { + it('should validate subscription license in prod if current date is after expiry date but release date is before expiry date', () => { + const expiredLicenseKey = generateLicense({ + expiryDate: new Date(releaseDate.getTime() + oneDayInMS), + orderNumber: 'MUI-123', + licensingModel: 'subscription', + }); + + expect( + verifyLicense({ + releaseInfo: RELEASE_INFO, + licenseKey: expiredLicenseKey, + acceptedScopes: ['pro', 'premium'], + isProduction: true, + }), + ).to.equal(LicenseStatus.Valid); + }); - it('should return Invalid for invalid license', () => { - expect( - verifyLicense( - RELEASE_INFO, - 'b43ff5f9ac93f021855ff59ff0ba5220TkFNRTpNYC1VSSBTQVMsREVWRUxPUEVSX0NPVU5UPTEwLEVYUElSWT0xNTkxNzIzMDY3MDQyLFZFUlNJT049MS4yLjM', - ), - ).to.equal(LicenseStatus.Invalid); + it('should not validate subscription license in dev if current date is after expiry date but release date is before expiry date', () => { + const expiredLicenseKey = generateLicense({ + expiryDate: new Date(releaseDate.getTime() + oneDayInMS), + orderNumber: 'MUI-123', + licensingModel: 'subscription', + }); + + expect( + verifyLicense({ + releaseInfo: RELEASE_INFO, + licenseKey: expiredLicenseKey, + acceptedScopes: ['pro', 'premium'], + isProduction: false, + }), + ).to.equal(LicenseStatus.Expired); + }); + + it('should validate perpetual license in dev if current date is after expiry date but release date is before expiry date', () => { + const expiredLicenseKey = generateLicense({ + expiryDate: new Date(releaseDate.getTime() + oneDayInMS), + orderNumber: 'MUI-123', + licensingModel: 'perpetual', + }); + + expect( + verifyLicense({ + releaseInfo: RELEASE_INFO, + licenseKey: expiredLicenseKey, + acceptedScopes: ['pro', 'premium'], + isProduction: false, + }), + ).to.equal(LicenseStatus.Valid); + }); + }); + + it('should return Invalid for invalid license', () => { + expect( + verifyLicense({ + releaseInfo: RELEASE_INFO, + licenseKey: + 'b43ff5f9ac93f021855ff59ff0ba5220TkFNRTpNYC1VSSBTQVMsREVWRUxPUEVSX0NPVU5UPTEwLEVYUElSWT0xNTkxNzIzMDY3MDQyLFZFUlNJT049MS4yLjM', + acceptedScopes: ['pro', 'premium'], + isProduction: true, + }), + ).to.equal(LicenseStatus.Invalid); + }); }); }); diff --git a/packages/x-license-pro/src/verifyLicense/verifyLicense.ts b/packages/x-license-pro/src/verifyLicense/verifyLicense.ts index 132657b844977..933a1d637ccef 100644 --- a/packages/x-license-pro/src/verifyLicense/verifyLicense.ts +++ b/packages/x-license-pro/src/verifyLicense/verifyLicense.ts @@ -1,52 +1,165 @@ import { base64Decode, base64Encode } from '../encoding/base64'; import { md5 } from '../encoding/md5'; import { LicenseStatus } from '../utils/licenseStatus'; +import { LicenseScope, LICENSE_SCOPES } from '../utils/licenseScope'; +import { LicensingModel, LICENSING_MODELS } from '../utils/licensingModel'; -export function generateReleaseInfo() { +const getDefaultReleaseDate = () => { const today = new Date(); today.setHours(0, 0, 0, 0); - return base64Encode(today.getTime().toString()); + return today; +}; + +export function generateReleaseInfo(releaseDate = getDefaultReleaseDate()) { + return base64Encode(releaseDate.getTime().toString()); } const expiryReg = /^.*EXPIRY=([0-9]+),.*$/; -export function verifyLicense(releaseInfo: string, encodedLicense: string | undefined) { +interface MuiLicense { + licensingModel: LicensingModel | null; + scope: LicenseScope | null; + expiryTimestamp: number | null; +} + +/** + * Format: ORDER:${orderNumber},EXPIRY=${expiryTimestamp},KEYVERSION=1 + */ +const decodeLicenseVersion1 = (license: string): MuiLicense => { + let expiryTimestamp: number | null; + try { + expiryTimestamp = parseInt(license.match(expiryReg)![1], 10); + if (!expiryTimestamp || Number.isNaN(expiryTimestamp)) { + expiryTimestamp = null; + } + } catch (err) { + expiryTimestamp = null; + } + + return { + scope: 'pro', + licensingModel: 'perpetual', + expiryTimestamp, + }; +}; + +/** + * Format: O=${orderNumber},E=${expiryTimestamp},S=${scope},LM=${licensingModel},KV=2`; + */ +const decodeLicenseVersion2 = (license: string): MuiLicense => { + const licenseInfo: MuiLicense = { + scope: null, + licensingModel: null, + expiryTimestamp: null, + }; + + license + .split(',') + .map((token) => token.split('=')) + .filter((el) => el.length === 2) + .forEach(([key, value]) => { + if (key === 'S') { + licenseInfo.scope = value as LicenseScope; + } + + if (key === 'LM') { + licenseInfo.licensingModel = value as LicensingModel; + } + + if (key === 'E') { + const expiryTimestamp = parseInt(value, 10); + if (expiryTimestamp && !Number.isNaN(expiryTimestamp)) { + licenseInfo.expiryTimestamp = expiryTimestamp; + } + } + }); + + return licenseInfo; +}; + +/** + * Decode the license based on its key version and return a version-agnostic `MuiLicense` object. + */ +const decodeLicense = (encodedLicense: string): MuiLicense | null => { + const license = base64Decode(encodedLicense); + + if (license.includes('KEYVERSION=1')) { + return decodeLicenseVersion1(license); + } + + if (license.includes('KV=2')) { + return decodeLicenseVersion2(license); + } + + return null; +}; + +export function verifyLicense({ + releaseInfo, + licenseKey, + acceptedScopes, + isProduction, +}: { + releaseInfo: string; + licenseKey: string | undefined; + acceptedScopes: LicenseScope[]; + isProduction: boolean; +}) { if (!releaseInfo) { throw new Error('MUI: The release information is missing. Not able to validate license.'); } - if (!encodedLicense) { + if (!licenseKey) { return LicenseStatus.NotFound; } - const hash = encodedLicense.substr(0, 32); - const encoded = encodedLicense.substr(32); + const hash = licenseKey.substr(0, 32); + const encoded = licenseKey.substr(32); if (hash !== md5(encoded)) { return LicenseStatus.Invalid; } - const clearLicense = base64Decode(encoded); - let expiryTimestamp = 0; - try { - expiryTimestamp = parseInt(clearLicense.match(expiryReg)![1], 10); - if (!expiryTimestamp || Number.isNaN(expiryTimestamp)) { - console.error('Error checking license. Expiry timestamp not found or invalid!'); - return LicenseStatus.Invalid; - } - } catch (err) { - console.error('Error extracting license expiry timestamp.', err); + const license = decodeLicense(encoded); + + if (license == null) { + console.error('Error checking license. Key version not found!'); + return LicenseStatus.Invalid; + } + + if (license.licensingModel == null || !LICENSING_MODELS.includes(license.licensingModel)) { + console.error('Error checking license. Sales model not found or invalid!'); + return LicenseStatus.Invalid; + } + + if (license.expiryTimestamp == null) { + console.error('Error checking license. Expiry timestamp not found or invalid!'); return LicenseStatus.Invalid; } - const pkgTimestamp = parseInt(base64Decode(releaseInfo), 10); - if (Number.isNaN(pkgTimestamp)) { - throw new Error('MUI: The release information is invalid. Not able to validate license.'); + if (license.licensingModel === 'perpetual' || isProduction) { + const pkgTimestamp = parseInt(base64Decode(releaseInfo), 10); + if (Number.isNaN(pkgTimestamp)) { + throw new Error('MUI: The release information is invalid. Not able to validate license.'); + } + + if (license.expiryTimestamp < pkgTimestamp) { + return LicenseStatus.Expired; + } + } else if (license.licensingModel === 'subscription') { + if (license.expiryTimestamp < new Date().getTime()) { + return LicenseStatus.Expired; + } + } + + if (license.scope == null || !LICENSE_SCOPES.includes(license.scope)) { + console.error('Error checking license. scope not found or invalid!'); + return LicenseStatus.Invalid; } - if (expiryTimestamp < pkgTimestamp) { - return LicenseStatus.Expired; + if (!acceptedScopes.includes(license.scope)) { + return LicenseStatus.Invalid; } return LicenseStatus.Valid; diff --git a/scripts/x-license-pro.exports.json b/scripts/x-license-pro.exports.json index 4f7199ed757de..d9e18b81b2a7f 100644 --- a/scripts/x-license-pro.exports.json +++ b/scripts/x-license-pro.exports.json @@ -5,6 +5,7 @@ { "name": "LicenseInfo", "kind": "Class" }, { "name": "LicenseScope", "kind": "TypeAlias" }, { "name": "LicenseStatus", "kind": "Enum" }, + { "name": "LicensingModel", "kind": "TypeAlias" }, { "name": "MuiCommercialPackageName", "kind": "TypeAlias" }, { "name": "showExpiredLicenseError", "kind": "Function" }, { "name": "showInvalidLicenseError", "kind": "Function" }, diff --git a/test/regressions/index.js b/test/regressions/index.js index 145f6ecf347c7..08180b3ab6cbe 100644 --- a/test/regressions/index.js +++ b/test/regressions/index.js @@ -11,7 +11,7 @@ addons.setChannel(mockChannel()); // Remove the license warning from demonstration purposes LicenseInfo.setLicenseKey( - '0f94d8b65161817ca5d7f7af8ac2f042T1JERVI6TVVJLVN0b3J5Ym9vayxFWFBJUlk9MTY1NDg1ODc1MzU1MCxLRVlWRVJTSU9OPTE=', + '61628ce74db2c1b62783a6d438593bc5Tz1NVUktRG9jLEU9MTY4MzQ0NzgyMTI4NCxTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLEtWPTI=', ); const blacklist = [ diff --git a/test/utils/licenseKey.ts b/test/utils/licenseKey.ts index e145d424857c1..0e1403dd1d994 100644 --- a/test/utils/licenseKey.ts +++ b/test/utils/licenseKey.ts @@ -1,5 +1,5 @@ import { LicenseInfo } from '@mui/x-data-grid-pro'; LicenseInfo.setLicenseKey( - '0f94d8b65161817ca5d7f7af8ac2f042T1JERVI6TVVJLVN0b3J5Ym9vayxFWFBJUlk9MTY1NDg1ODc1MzU1MCxLRVlWRVJTSU9OPTE=', + '61628ce74db2c1b62783a6d438593bc5Tz1NVUktRG9jLEU9MTY4MzQ0NzgyMTI4NCxTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLEtWPTI=', );