diff --git a/server/private/license/license.ts b/server/private/license/license.ts index 81aae1439..649c3ab2e 100644 --- a/server/private/license/license.ts +++ b/server/private/license/license.ts @@ -104,11 +104,18 @@ LQIDAQAB } public async forceRecheck() { - this.statusCache.flushAll(); - this.licenseKeyCache.flushAll(); this.phoneHomeFailureCount = 0; - return await this.check(); + // Force a fresh check without discarding the last known good cache + // up front — check() only replaces the cache once it has a fresh + // result, so a failed recheck (e.g. a transient server error) won't + // leave listKeys()/status looking empty in the meantime. + this.doRecheck = true; + try { + return await this.check(); + } finally { + this.doRecheck = false; + } } public async isUnlocked(): Promise { @@ -181,10 +188,15 @@ LQIDAQAB } let foundHostKey = false; + // Keys that fully decrypted, to phone home with. A row that + // fails to decrypt (e.g. stored under a different server + // secret) is marked invalid below but excluded here, so it + // can't take down validation for every other key in the batch. + const keys: { licenseKey: string; instanceId: string }[] = []; // Validate stored license keys for (const key of allKeysRes) { try { - // Decrypt the license key and token + // Decrypt the license key, token, and instance ID const decryptedKey = decrypt( key.licenseKeyId, this.serverSecret @@ -193,6 +205,10 @@ LQIDAQAB key.token, this.serverSecret ); + const decryptedInstanceId = decrypt( + key.instanceId, + this.serverSecret + ); const payload = validateJWT( decryptedToken, @@ -214,6 +230,11 @@ LQIDAQAB if (payload.type === "host") { foundHostKey = true; } + + keys.push({ + licenseKey: decryptedKey, + instanceId: decryptedInstanceId + }); } catch (e) { logger.error( `Error validating license key: ${key.licenseKeyId}` @@ -233,37 +254,36 @@ LQIDAQAB status.isHostLicensed = false; } - const keys = allKeysRes.map((key) => ({ - licenseKey: decrypt(key.licenseKeyId, this.serverSecret), - instanceId: decrypt(key.instanceId, this.serverSecret) - })); - let apiResponse: ValidateLicenseAPIResponse | undefined; - try { - // Phone home to validate license keys - apiResponse = await this.phoneHome(keys, false); + if (keys.length > 0) { + try { + // Phone home to validate license keys + apiResponse = await this.phoneHome(keys, false); - if (!apiResponse?.success) { - throw new Error(apiResponse?.error); - } - // Reset failure count on success - this.phoneHomeFailureCount = 0; - } catch (e) { - this.phoneHomeFailureCount++; - if (this.phoneHomeFailureCount === 1) { - // First failure: fail silently - logger.error("Error communicating with license server:"); - logger.error(e); - logger.error( - `Allowing failure. Will retry one more time at next run interval.` - ); - // return last known good status - return this.statusCache.get( - this.statusKey - ) as LicenseStatus; - } else { - // Subsequent failures: fail abruptly - throw e; + if (!apiResponse?.success) { + throw new Error(apiResponse?.error); + } + // Reset failure count on success + this.phoneHomeFailureCount = 0; + } catch (e) { + this.phoneHomeFailureCount++; + if (this.phoneHomeFailureCount === 1) { + // First failure: fail silently + logger.error( + "Error communicating with license server:" + ); + logger.error(e); + logger.error( + `Allowing failure. Will retry one more time at next run interval.` + ); + // return last known good status + return this.statusCache.get( + this.statusKey + ) as LicenseStatus; + } else { + // Subsequent failures: fail abruptly + throw e; + } } }