{"record":{"id":"f53643a73b3536d6","repo":"Budibase/budibase","slug":"invalid-offline-license","errorCode":null,"errorMessage":"Invalid offline license","messagePattern":"Invalid offline license","errorType":"exception","errorClass":"OfflineLicenseMismatchError","httpStatus":null,"severity":"error","filePath":"packages/pro/src/sdk/licensing/licenses/offline/offline.ts","lineNumber":85,"sourceCode":"\nexport function verifyExpiry(license: OfflineLicense) {\n  const now = Date.now()\n  const expireAt = new Date(license.expireAt).getTime()\n  if (!Number.isFinite(expireAt) || now > expireAt) {\n    throw new Error(`Offline license has expired. expireAt=${license.expireAt}`)\n  }\n}\n\nexport class OfflineLicenseMismatchError extends Error {}\n\nexport async function verifyInstallation(license: OfflineLicense) {\n  const identifier = await getIdentifier()\n  if (\n    license.identifier.installId !== identifier.installId ||\n    license.identifier.tenantId !== identifier.tenantId\n  ) {\n    // be intentionally vague\n    throw new OfflineLicenseMismatchError(\"Invalid offline license\")\n  }\n}\n\nexport function enrichLicense(license: OfflineLicense) {\n  const planType = license.plan.type\n  const hosting = Hosting.SELF // offline only applicable in self host\n  const _features = features.getOfflineFeatures(planType)\n  const _quotas = quotas.getQuotas(hosting, planType)\n\n  // apply the latest features and quotas to license\n  license.features = union(license.features, _features)\n  license.quotas = merge(license.quotas, _quotas)\n\n  return license\n}\n\nexport async function verifyOfflineLicenseToken(\n  token: string","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/pro/src/sdk/licensing/licenses/offline/offline.ts#L67-L103","documentation":"verifyInstallation compares the license's embedded identifier (installId + unique tenantId) against the current installation's identifier and throws OfflineLicenseMismatchError('Invalid offline license') when they differ. The message is intentionally vague for security reasons. It prevents an offline license issued for one installation/tenant from being used on another.","triggerScenarios":"verifyInstallation(license) called via verifyOfflineLicenseToken when the token was generated for a different installId or tenantId than the current environment — e.g. token exported from install A activated on install B.","commonSituations":"Copying an offline license token between dev/staging/prod environments; restoring a backup where the installId was regenerated; cloned VMs sharing one token; multi-tenant setups using a token issued for another tenant; re-issued install (installId changed) after a rebuild.","solutions":["Export the offline license token from the installation you intend to activate it on (matching installId and tenant).","Regenerate the identifier file from the target installation and request a new token from the account portal using that identifier.","If the installId legitimately changed (restore/migration), contact support to rebind the license to the new install.","Ensure the tenant context (tenantId) when activating matches the tenant the token was issued for."],"exampleFix":"// before\nawait activateOfflineLicenseToken(tokenFromOtherInstall)\n// after\nconst identifier = await getIdentifier()\nconsole.log(\"Request token for installId:\", identifier.installId) // then activate the matching token\nawait activateOfflineLicenseToken(tokenForThisInstall)","handlingStrategy":"validation","validationCode":"const localIdentifier = await getIdentifier()\nconst tokenLicense = /* decode token payload */\nif (tokenLicense.identifier.installId !== localIdentifier.installId ||\n    tokenLicense.identifier.tenantId !== localIdentifier.tenantId) {\n  throw new Error(\"Token was not issued for this installation\")\n}","typeGuard":"function matchesInstallation(license: OfflineLicense, identifier: OfflineIdentifier): boolean {\n  return license.identifier.installId === identifier.installId &&\n         license.identifier.tenantId === identifier.tenantId\n}","tryCatchPattern":"try {\n  await activateOfflineLicenseToken(token)\n} catch (e) {\n  if (e instanceof HTTPError && e.message.includes(\"does not match this installation\")) {\n    // re-export identifier and request a token for THIS install\n  } else throw e\n}","preventionTips":["Generate offline tokens per installation using that install's exported identifier.","Never copy offline tokens between environments or cloned VMs.","After restores/migrations, reissue tokens for the new installId.","Keep tenant context consistent during activation."],"tags":["licensing","offline-license","install-mismatch","tenant"],"backgroundTag":"offline-license-mismatch","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}