fix: prevent encryption key conflicts and add license backup
- Remove eager key generation from crypto-utils module load (was baking keys into Docker images that conflicted with mounted production keys) - Add license backup to config.json (survives credential store failures) - LicenseManager.load() falls back to config.json backup if credential store decryption fails (e.g. after container rebuild with new key) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -37,14 +37,16 @@ class LicenseManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load license state from storage on startup
|
||||
* Load license state from storage on startup.
|
||||
* Primary: encrypted credential store. Fallback: config.json backup.
|
||||
* If the credential store fails (e.g. encryption key changed after rebuild),
|
||||
* restores from the config.json backup automatically.
|
||||
*/
|
||||
async load() {
|
||||
try {
|
||||
const stored = await this.credentialManager.retrieve(LICENSE_CRED_KEY);
|
||||
if (stored) {
|
||||
this.activation = JSON.parse(stored);
|
||||
// Check if expired
|
||||
if (this.isExpired()) {
|
||||
this.log.info?.('license', 'License has expired', {
|
||||
code: this._maskCode(this.activation.code),
|
||||
@@ -57,13 +59,40 @@ class LicenseManager {
|
||||
daysRemaining: this.daysRemaining()
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.log.info?.('license', 'No active license');
|
||||
this._loaded = true;
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
this.log.error?.('license', 'Failed to load license state', { error: error.message });
|
||||
this.activation = null;
|
||||
this.log.warn?.('license', 'Failed to load from credential store, trying config backup', { error: error.message });
|
||||
}
|
||||
|
||||
// Fallback: restore from config.json backup
|
||||
try {
|
||||
const fsp = require('fs').promises;
|
||||
const data = await fsp.readFile(this.configFile, 'utf8');
|
||||
const config = JSON.parse(data);
|
||||
if (config.licenseBackup) {
|
||||
this.activation = config.licenseBackup;
|
||||
this.log.info?.('license', 'License restored from config backup', {
|
||||
code: this._maskCode(this.activation.code),
|
||||
lifetime: this.activation.lifetime
|
||||
});
|
||||
// Re-store in credential manager so future loads succeed
|
||||
try {
|
||||
await this.credentialManager.store(LICENSE_CRED_KEY, JSON.stringify(this.activation));
|
||||
this.log.info?.('license', 'License re-stored in credential manager');
|
||||
} catch (storeErr) {
|
||||
this.log.warn?.('license', 'Could not re-store license in credential manager', { error: storeErr.message });
|
||||
}
|
||||
this._loaded = true;
|
||||
return;
|
||||
}
|
||||
} catch (_) {
|
||||
// Config doesn't exist or no backup — continue
|
||||
}
|
||||
|
||||
this.log.info?.('license', 'No active license');
|
||||
this.activation = null;
|
||||
this._loaded = true;
|
||||
}
|
||||
|
||||
@@ -412,7 +441,9 @@ class LicenseManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update config.json with non-sensitive license info
|
||||
* Update config.json with license info and full activation backup.
|
||||
* The backup ensures the license survives encryption key changes
|
||||
* (e.g. container rebuilds that generate new keys).
|
||||
*/
|
||||
async _updateConfig() {
|
||||
try {
|
||||
@@ -433,8 +464,11 @@ class LicenseManager {
|
||||
daysRemaining: this.daysRemaining(),
|
||||
features: this.activation.features || Object.keys(PREMIUM_FEATURES)
|
||||
};
|
||||
// Full backup of activation data (config.json is volume-mounted and persists)
|
||||
config.licenseBackup = this.activation;
|
||||
} else {
|
||||
config.license = { active: false, tier: 'free' };
|
||||
delete config.licenseBackup;
|
||||
}
|
||||
|
||||
config.updatedAt = new Date().toISOString();
|
||||
|
||||
Reference in New Issue
Block a user