import { pick, isEqual } from 'lodash/fp'; import { createStrapiFetch } from '../utils/fetch.mjs'; import { readLicense, verifyLicense, LICENSE_REGISTRY_URI, LicenseCheckError, fetchLicense } from './license.mjs'; import { shiftCronExpression } from '../utils/cron.mjs'; const ONE_MINUTE = 1000 * 60; const ee = { enabled: false, licenseInfo: { isTrial: false } }; const disable = (message)=>{ // Prevent emitting ee.disable if it was already disabled const shouldEmitEvent = ee.enabled !== false; ee.logger?.warn(`${message} Switching to CE.`); // Only keep the license key and isTrial for potential re-enabling during a later check ee.licenseInfo = pick([ 'licenseKey', 'isTrial' ], ee.licenseInfo); ee.licenseInfo.isTrial = false; ee.enabled = false; if (shouldEmitEvent) { // Notify EE features that they should be disabled strapi.eventHub.emit('ee.disable'); } }; const enable = ()=>{ // Prevent emitting ee.enable if it was already enabled const shouldEmitEvent = ee.enabled !== true; ee.enabled = true; if (shouldEmitEvent) { // Notify EE features that they should be disabled strapi.eventHub.emit('ee.enable'); } }; let initialized = false; /** * Optimistically enable EE if the format of the license is valid, only run once. */ const init = (licenseDir, logger)=>{ if (initialized) { return; } initialized = true; ee.logger = logger; if (process.env.STRAPI_DISABLE_EE?.toLowerCase() === 'true') { return; } try { const license = process.env.STRAPI_LICENSE || readLicense(licenseDir); if (license) { ee.licenseInfo = verifyLicense(license); enable(); } } catch (error) { if (error instanceof Error) { disable(error.message); } else { disable('Invalid license.'); } } }; /** * Contact the license registry to update the license to its latest state. * * Store the result in database to avoid unecessary requests, and will fallback to that in case of a network failure. */ const onlineUpdate = async ({ strapi: strapi1 })=>{ const { get, commit, rollback } = await strapi1.db?.transaction(); const transaction = get(); try { const storedInfo = await strapi1.db?.queryBuilder('strapi::core-store').where({ key: 'ee_information' }).select('value').first().transacting(transaction).forUpdate().execute().then((result)=>result ? JSON.parse(result.value) : result); const shouldContactRegistry = (storedInfo?.lastCheckAt ?? 0) < Date.now() - ONE_MINUTE; const result = { lastCheckAt: Date.now() }; const fallback = (error)=>{ if (error instanceof LicenseCheckError && error.shouldFallback && storedInfo?.license) { ee.logger?.warn(`${error.message} The last stored one will be used as a potential fallback.`); return storedInfo.license; } result.error = error.message; disable(error.message); }; if (!ee?.licenseInfo?.licenseKey) { throw new Error('Missing license key.'); } const license = shouldContactRegistry ? await fetchLicense({ strapi: strapi1 }, ee.licenseInfo.licenseKey, strapi1.config.get('uuid')).catch(fallback) : storedInfo.license; if (license) { try { // Verify license and check if its info changed const newLicenseInfo = verifyLicense(license); const licenseInfoChanged = !isEqual(newLicenseInfo.features, ee.licenseInfo.features) || newLicenseInfo.seats !== ee.licenseInfo.seats || newLicenseInfo.type !== ee.licenseInfo.type; // Store the new license info ee.licenseInfo = newLicenseInfo; const wasEnabled = ee.enabled; validateInfo(); // Notify EE features if (licenseInfoChanged && wasEnabled) { strapi1.eventHub.emit('ee.update'); } } catch (error) { if (error instanceof Error) { disable(error.message); } else { disable('Invalid license.'); } } } else if (!shouldContactRegistry) { disable(storedInfo.error); } if (shouldContactRegistry) { result.license = license ?? null; const query = strapi1.db.queryBuilder('strapi::core-store').transacting(transaction); if (!storedInfo) { query.insert({ key: 'ee_information', value: JSON.stringify(result) }); } else { query.update({ value: JSON.stringify(result) }).where({ key: 'ee_information' }); } await query.execute(); } await commit(); } catch (error) { // Example of errors: SQLite does not support FOR UPDATE await rollback(); } }; const validateInfo = ()=>{ if (typeof ee.licenseInfo.expireAt === 'undefined') { throw new Error('Missing license key.'); } const expirationTime = new Date(ee.licenseInfo.expireAt).getTime(); if (expirationTime < new Date().getTime()) { return disable('License expired.'); } enable(); }; const checkLicense = async ({ strapi: strapi1 })=>{ const shouldStayOffline = ee.licenseInfo.type === 'gold' && // This env variable support is temporarily used to ease the migration between online vs offline process.env.STRAPI_DISABLE_LICENSE_PING?.toLowerCase() === 'true'; if (!shouldStayOffline) { await onlineUpdate({ strapi: strapi1 }); strapi1.cron.add({ onlineUpdate: { task: ()=>onlineUpdate({ strapi: strapi1 }), options: shiftCronExpression('0 0 */12 * * *') } }); } else { if (!ee.licenseInfo.expireAt) { return disable('Your license does not have offline support.'); } validateInfo(); } }; const getTrialEndDate = async ({ strapi: strapi1 })=>{ const silentFetch = createStrapiFetch(strapi1, { logs: false }); const res = await silentFetch(`${LICENSE_REGISTRY_URI}/api/licenses/${ee.licenseInfo.licenseKey}/trial-countdown`, { method: 'GET', headers: { 'Content-Type': 'application/json' } }).catch(()=>{ throw new LicenseCheckError('Could not proceed to retrieve the trial time left for your license.', true); }); const data = await res.json(); return data; }; const list = ()=>{ return ee.licenseInfo.features?.map((feature)=>typeof feature === 'object' ? feature : { name: feature }) || []; }; const get = (featureName)=>list().find((feature)=>feature.name === featureName); var index = Object.freeze({ init, checkLicense, getTrialEndDate, get isEE () { return ee.enabled; }, get seats () { return ee.licenseInfo.seats; }, get type () { return ee.licenseInfo.type; }, get isTrial () { return ee.licenseInfo.isTrial; }, features: Object.freeze({ list, get, isEnabled: (featureName)=>get(featureName) !== undefined }) }); export { index as default }; //# sourceMappingURL=index.mjs.map