string | The path to the file/folder to open. |
+
+
+## getDeviceID() ⇒ Promise.<(string\|null)>
+gets the os device id. this usually won't change till os reinstall.
+
+**Kind**: global function
+**Returns**: Promise.<(string\|null)> - - Resolves with the os identifier or null
+**Throws**:
+
+- Error - If called from the browser
+
+
+
+## addDeviceLicenseSystemWide() ⇒ Promise.<boolean>
+Enables device license by creating a system-wide license file.
+On Windows, macOS, and Linux this will request elevation if needed.
+
+**Kind**: global function
+**Returns**: Promise.<boolean> - - Resolves true if system wide defile file added, else false.
+**Throws**:
+
+- Error - If called from the browser
+
+
+
+## removeDeviceLicenseSystemWide() ⇒ Promise.<boolean>
+Removes the system-wide device license file.
+On Windows, macOS, and Linux this will request elevation if needed.
+
+**Kind**: global function
+**Returns**: Promise.<boolean> - - Resolves true if system wide defile file removed, else false.
+**Throws**:
+
+- Error - If called from the browser
+
+
+
+## isLicensedDeviceSystemWide() ⇒ Promise.<boolean>
+Checks if the current machine is configured to check for system-wide device license for all users at app start.
+This validates that the system-wide license file exists, contains valid JSON, and has `licensedDevice: true`.
+
+**Kind**: global function
+**Returns**: Promise.<boolean> - - Resolves with `true` if the device is licensed, `false` otherwise.
diff --git a/src-node/package-lock.json b/src-node/package-lock.json
index 2a5b79a17a..8bbf6cbb76 100644
--- a/src-node/package-lock.json
+++ b/src-node/package-lock.json
@@ -9,6 +9,7 @@
"version": "4.1.2-0",
"license": "GNU-AGPL3.0",
"dependencies": {
+ "@expo/sudo-prompt": "^9.3.2",
"@phcode/fs": "^3.0.1",
"cross-spawn": "^7.0.6",
"lmdb": "^2.9.2",
@@ -22,6 +23,11 @@
"node": "20"
}
},
+ "node_modules/@expo/sudo-prompt": {
+ "version": "9.3.2",
+ "resolved": "https://registry.npmjs.org/@expo/sudo-prompt/-/sudo-prompt-9.3.2.tgz",
+ "integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw=="
+ },
"node_modules/@lmdb/lmdb-darwin-arm64": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-2.9.2.tgz",
diff --git a/src-node/package.json b/src-node/package.json
index 196d54e228..3aa819148f 100644
--- a/src-node/package.json
+++ b/src-node/package.json
@@ -26,6 +26,7 @@
"lmdb": "^2.9.2",
"mime-types": "^2.1.35",
"cross-spawn": "^7.0.6",
- "which": "^2.0.1"
+ "which": "^2.0.1",
+ "@expo/sudo-prompt": "^9.3.2"
}
-}
\ No newline at end of file
+}
diff --git a/src-node/utils.js b/src-node/utils.js
index 5e0ab484bb..7167d60356 100644
--- a/src-node/utils.js
+++ b/src-node/utils.js
@@ -4,9 +4,13 @@ const fs = require('fs');
const fsPromise = require('fs').promises;
const path = require('path');
const os = require('os');
+const sudo = require('@expo/sudo-prompt');
const {lintFile} = require("./ESLint/service");
let openModule, open; // dynamic import when needed
+const options = { name: 'Phoenix Code' };
+const licenseFileContent = JSON.stringify({});
+
async function _importOpen() {
if(open){
return open;
@@ -269,6 +273,176 @@ async function getEnvironmentVariable(varName) {
return process.env[varName];
}
+function getLicensePath() {
+ switch (os.platform()) {
+ case 'win32':
+ return 'C:\\Program Files\\Phoenix Code Control\\device-license';
+ case 'darwin':
+ return '/Library/Application Support/phoenix-code-control/device-license';
+ case 'linux':
+ return '/etc/phoenix-code-control/device-license';
+ default:
+ throw new Error(`Unsupported platform: ${os.platform()}`);
+ }
+}
+
+function sudoExec(command) {
+ return new Promise((resolve, reject) => {
+ sudo.exec(command, options, (error, stdout, stderr) => {
+ if (error) {
+ return reject(error);
+ }
+ resolve({ stdout, stderr });
+ });
+ });
+}
+
+function readFileUtf8(p) {
+ return new Promise((resolve, reject) => {
+ fs.readFile(p, 'utf8', (err, data) => (err ? reject(err) : resolve(data)));
+ });
+}
+
+/**
+ * Writes the license file in a world-readable location.
+ * Works on Windows, macOS, and Linux.
+ */
+async function addDeviceLicense() {
+ const targetPath = getLicensePath();
+ let command;
+ // we should not store any sensitive information in this file as this is world readable. we use the
+ // device id itself as license key for that machine. the device id is not associated with any cloud credits
+ // and all entitlements are local to device only for this threat model to work. So stolen device IDs doesn't
+ // have any meaning.
+
+ if (os.platform() === 'win32') {
+ // Windows: write file and explicitly grant Everyone read rights
+ const dir = 'C:\\Program Files\\Phoenix Code Control';
+ command =
+ `powershell -Command "` +
+ `New-Item -ItemType Directory -Force '${dir}' | Out-Null; ` +
+ `Set-Content -Path '${targetPath}' -Value '${licenseFileContent}' -Encoding UTF8; ` +
+ `icacls '${targetPath}' /inheritance:e /grant *S-1-1-0:RX | Out-Null"`;
+ } else {
+ // macOS / Linux: mkdir + write + chmod 0644 (world-readable, owner-writable)
+ const dir = path.dirname(targetPath);
+ command =
+ `/bin/mkdir -p "${dir}"` +
+ ` && printf '%s' '${licenseFileContent}' > "${targetPath}"` +
+ ` && /bin/chmod 0644 "${targetPath}"`;
+ }
+
+ await sudoExec(command);
+ return targetPath;
+}
+
+async function removeDeviceLicense() {
+ const targetPath = getLicensePath();
+ let command;
+
+ if (os.platform() === 'win32') {
+ command = `powershell -Command "if (Test-Path '${targetPath}') { Remove-Item -Path '${targetPath}' -Force }"`;
+ } else {
+ command = `/bin/rm -f "${targetPath}"`;
+ }
+
+ await sudoExec(command);
+ return targetPath;
+}
+
+async function isLicensedDevice() {
+ const targetPath = getLicensePath();
+ try {
+ const data = await readFileUtf8(targetPath);
+ JSON.parse(data.trim());
+ return true; // currently, the existence of the file itself is flag. in future, we may choose to add more.
+ } catch {
+ // file missing, unreadable, or invalid JSON
+ return false;
+ }
+}
+
+async function _getLinuxDeviceID() {
+ const data = await fsPromise.readFile("/etc/machine-id", "utf8");
+ const id = data.trim();
+ return id || null;
+ // throw on error to main.
+ // no fallback, /var/lib/dbus/machine-id may need sudo in some machines
+}
+
+/**
+ * Get the macOS device ID (IOPlatformUUID).
+ * @returns {Promise