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:
Ivan Fontosh
2026-07-23 10:39:46 +08:00
parent c1c332364c
commit 32a5479086
11 changed files with 399 additions and 24 deletions
+109
View File
@@ -0,0 +1,109 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
hashMachineRawId,
machineWideIdPath,
parseMacIOPlatformUUID,
parseWindowsMachineGuid,
parseWmicUuid,
resolveMachineFingerprint,
} from './machineFingerprint';
void test('hashMachineRawId: стабилен и не зависит от регистра GUID', () => {
const a = hashMachineRawId('win32', 'ABCDEF00-1111-2222-3333-444455556666');
const b = hashMachineRawId('win32', 'abcdef00-1111-2222-3333-444455556666');
assert.equal(a, b);
assert.equal(a.length, 64);
assert.notEqual(a, hashMachineRawId('linux', 'abcdef00-1111-2222-3333-444455556666'));
});
void test('parseWindowsMachineGuid', () => {
const out = `
HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography
MachineGuid REG_SZ A1B2C3D4-E5F6-7890-ABCD-EF1234567890
`;
assert.equal(parseWindowsMachineGuid(out), 'A1B2C3D4-E5F6-7890-ABCD-EF1234567890');
assert.equal(parseWindowsMachineGuid('nope'), null);
});
void test('parseMacIOPlatformUUID', () => {
const out = `
+-o IOPlatformExpertDevice <class IOPlatformExpertDevice, id 0x1000001ea, registered, matched>
{
"IOPlatformUUID" = "A1B2C3D4-E5F6-7890-ABCD-EF1234567890"
}
`;
assert.equal(parseMacIOPlatformUUID(out), 'A1B2C3D4-E5F6-7890-ABCD-EF1234567890');
});
void test('parseWmicUuid', () => {
assert.equal(parseWmicUuid('UUID\nA1B2C3D4-E5F6-7890-ABCD-EF1234567890\n'), 'A1B2C3D4-E5F6-7890-ABCD-EF1234567890');
assert.equal(parseWmicUuid('UUID\n00000000-0000-0000-0000-000000000000\n'), null);
});
void test('resolveMachineFingerprint: override через DND_LICENSE_DEVICE_ID', () => {
const id = resolveMachineFingerprint({
platform: 'linux',
env: { DND_LICENSE_DEVICE_ID: 'override-device-id-12345' },
});
assert.equal(id, 'override-device-id-12345');
});
void test('resolveMachineFingerprint: Windows MachineGuid → одинаковый hash', () => {
const exec = () =>
`
HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography
MachineGuid REG_SZ A1B2C3D4-E5F6-7890-ABCD-EF1234567890
`;
const a = resolveMachineFingerprint({
platform: 'win32',
env: {},
execFileSync: exec as never,
});
const b = resolveMachineFingerprint({
platform: 'win32',
env: {},
execFileSync: exec as never,
});
assert.equal(a, b);
assert.equal(a, hashMachineRawId('win32', 'A1B2C3D4-E5F6-7890-ABCD-EF1234567890'));
});
void test('resolveMachineFingerprint: Linux /etc/machine-id', () => {
const id = resolveMachineFingerprint({
platform: 'linux',
env: {},
readFileSync: (p) => {
if (p === '/etc/machine-id') return '0123456789abcdef0123456789abcdef\n';
throw new Error('enoent');
},
});
assert.equal(id, hashMachineRawId('linux', '0123456789abcdef0123456789abcdef'));
});
void test('resolveMachineFingerprint: fallback в machine-wide путь', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'machine-fp-'));
const env = { PROGRAMDATA: tmp };
const p = machineWideIdPath('win32', env);
const id1 = resolveMachineFingerprint({
platform: 'win32',
env,
execFileSync: () => {
throw new Error('no reg');
},
});
const id2 = resolveMachineFingerprint({
platform: 'win32',
env,
execFileSync: () => {
throw new Error('no reg');
},
});
assert.equal(id1, id2);
assert.ok(fs.existsSync(p));
fs.rmSync(tmp, { recursive: true, force: true });
});