fix(license): bind deviceId to physical machine, not OS user
Use OS machine identifiers (Windows MachineGuid, macOS IOPlatformUUID, Linux machine-id) hashed as deviceId so all accounts on one PC share one license slot. Keep legacy userData/device.id for migration and retire it on re-activation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,7 +10,7 @@ import type { LicensePayloadV1 } from '../../shared/license/payloadV1';
|
||||
import { isProductKey } from '../../shared/license/productKey';
|
||||
import { normalizeLicenseTokenInput } from '../../shared/license/tokenFormat';
|
||||
|
||||
import { getOrCreateDeviceId } from './deviceId';
|
||||
import { clearLegacyDeviceId, getOrCreateDeviceId, readLegacyDeviceId } from './deviceId';
|
||||
import { licenseEncryptedPath, licenseFallbackSealedPath, preferencesPath } from './paths';
|
||||
import { verifyLicenseToken } from './verifyLicenseToken';
|
||||
|
||||
@@ -66,12 +66,28 @@ function emitLicenseStatusChanged(): void {
|
||||
export class LicenseService {
|
||||
private readonly userData: string;
|
||||
private readonly deviceId: string;
|
||||
/** UUID из старого userData/device.id — только для совместимости до повторной активации. */
|
||||
private legacyDeviceId: string | null;
|
||||
private lastRemoteRevokeCheckMs = 0;
|
||||
private lastRemoteRevoked = false;
|
||||
|
||||
constructor(userData: string) {
|
||||
this.userData = userData;
|
||||
this.deviceId = getOrCreateDeviceId(userData);
|
||||
const legacy = readLegacyDeviceId(userData);
|
||||
this.legacyDeviceId = legacy && legacy !== this.deviceId ? legacy : null;
|
||||
}
|
||||
|
||||
private verifyOpts(nowSec: number): {
|
||||
nowSec: number;
|
||||
deviceId: string;
|
||||
alsoAcceptDeviceIds?: readonly string[];
|
||||
} {
|
||||
return {
|
||||
nowSec,
|
||||
deviceId: this.deviceId,
|
||||
...(this.legacyDeviceId ? { alsoAcceptDeviceIds: [this.legacyDeviceId] } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
private isSkipLicense(): boolean {
|
||||
@@ -190,10 +206,17 @@ export class LicenseService {
|
||||
private async activateWithProductKey(productKey: string): Promise<string> {
|
||||
const base = this.resolveLicenseActivateBaseUrl();
|
||||
const url = new URL('v1/activate', base);
|
||||
const body: { productKey: string; deviceId: string; retireDeviceId?: string } = {
|
||||
productKey: productKey.trim(),
|
||||
deviceId: this.deviceId,
|
||||
};
|
||||
if (this.legacyDeviceId) {
|
||||
body.retireDeviceId = this.legacyDeviceId;
|
||||
}
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({ productKey: productKey.trim(), deviceId: this.deviceId }),
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(20_000),
|
||||
});
|
||||
const text = await res.text();
|
||||
@@ -211,6 +234,10 @@ export class LicenseService {
|
||||
if (!token || typeof token !== 'string') {
|
||||
throw new Error('LICENSE_ACTIVATE_FAILED:token_missing');
|
||||
}
|
||||
if (this.legacyDeviceId) {
|
||||
clearLegacyDeviceId(this.userData);
|
||||
this.legacyDeviceId = null;
|
||||
}
|
||||
return normalizeLicenseTokenInput(token);
|
||||
}
|
||||
|
||||
@@ -278,7 +305,7 @@ export class LicenseService {
|
||||
};
|
||||
}
|
||||
|
||||
const v = verifyLicenseToken(token, { nowSec, deviceId: this.deviceId });
|
||||
const v = verifyLicenseToken(token, this.verifyOpts(nowSec));
|
||||
if (!v.ok) {
|
||||
return {
|
||||
active: false,
|
||||
@@ -328,10 +355,7 @@ export class LicenseService {
|
||||
if (!base.active || !base.summary) return base;
|
||||
const token = this.readSealedToken();
|
||||
if (!token?.trim()) return base;
|
||||
const v = verifyLicenseToken(token, {
|
||||
nowSec: Math.floor(Date.now() / 1000),
|
||||
deviceId: this.deviceId,
|
||||
});
|
||||
const v = verifyLicenseToken(token, this.verifyOpts(Math.floor(Date.now() / 1000)));
|
||||
if (!v.ok) return this.getStatusSync();
|
||||
void this.maybeRefreshRemoteRevocation(v.payload);
|
||||
return this.getStatusSync();
|
||||
@@ -346,7 +370,7 @@ export class LicenseService {
|
||||
trimmed = await this.activateWithProductKey(trimmed);
|
||||
}
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
const v = verifyLicenseToken(trimmed, { nowSec, deviceId: this.deviceId });
|
||||
const v = verifyLicenseToken(trimmed, this.verifyOpts(nowSec));
|
||||
if (!v.ok) {
|
||||
throw new Error(`LICENSE_INVALID:${v.reason}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user