Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f11fd88
refactor: minor code shuffle for readability
abose Sep 30, 2025
1a91708
refactor: rename to EntitlementsManager convention instead of just En…
abose Sep 30, 2025
0739fb5
chore: manage-licenses.js
abose Sep 30, 2025
80c35b8
feat: manage license dialog and license registration initial
abose Sep 30, 2025
e27a823
chore: translate license dialog strings
abose Oct 3, 2025
f718dd5
chore: system wide license file write working
abose Oct 3, 2025
6e46e38
chore: reapply systemwide license feature
abose Oct 3, 2025
9e82873
chore: isLicensedDeviceSystemWide API
abose Oct 3, 2025
cf8dbed
chore: sending device license if configured, non logged in bugs present
abose Oct 3, 2025
a8a4962
chore: pro device license working in Linux
abose Oct 3, 2025
3967064
fix: desktop login tests failing deu to license flow changes
abose Oct 3, 2025
cec0371
fix: desktop device license tests without logging in
abose Oct 3, 2025
8e1078b
fix: desktop device license tests without logging in
abose Oct 3, 2025
e4f4c4e
chore: get device id moved to node
abose Oct 3, 2025
1212843
fix: logger for node in dev and device licensing working in mac
abose Oct 3, 2025
73707c5
fix: add Reinstall Credentials debug command and auto port credential…
abose Oct 3, 2025
d792739
feat: windows get device ID in node
abose Oct 3, 2025
1aeb409
fix: windows license file parse issues. now exitance of licence file …
abose Oct 3, 2025
1ca6ba3
fix: windows device license file parse fail
abose Oct 3, 2025
f1c255b
docs: update api docs
abose Oct 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/API-Reference/command/Commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,12 @@ Opens support resources
## HELP\_GET\_PRO
Opens Phoenix Pro page

**Kind**: global variable
<a name="HELP_MANAGE_LICENSES"></a>

## HELP\_MANAGE\_LICENSES
Manage Pro licenses

**Kind**: global variable
<a name="HELP_SUGGEST"></a>

Expand Down Expand Up @@ -914,6 +920,12 @@ Hides the sidebar
## SHOW\_SIDEBAR
Shows the sidebar

**Kind**: global variable
<a name="REINSTALL_CREDS"></a>

## REINSTALL\_CREDS
Reinstalls credentials in keychain

**Kind**: global variable
<a name="CMD_GIT_INIT"></a>

Expand Down
43 changes: 43 additions & 0 deletions docs/API-Reference/utils/NodeUtils.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,46 @@ Opens a file in the default application for its type on Windows, macOS, and Linu
| --- | --- | --- |
| fullPath | <code>string</code> | The path to the file/folder to open. |

<a name="getDeviceID"></a>

## getDeviceID() ⇒ <code>Promise.&lt;(string\|null)&gt;</code>
gets the os device id. this usually won't change till os reinstall.

**Kind**: global function
**Returns**: <code>Promise.&lt;(string\|null)&gt;</code> - - Resolves with the os identifier or null
**Throws**:

- <code>Error</code> - If called from the browser

<a name="addDeviceLicenseSystemWide"></a>

## addDeviceLicenseSystemWide() ⇒ <code>Promise.&lt;boolean&gt;</code>
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**: <code>Promise.&lt;boolean&gt;</code> - - Resolves true if system wide defile file added, else false.
**Throws**:

- <code>Error</code> - If called from the browser

<a name="removeDeviceLicenseSystemWide"></a>

## removeDeviceLicenseSystemWide() ⇒ <code>Promise.&lt;boolean&gt;</code>
Removes the system-wide device license file.
On Windows, macOS, and Linux this will request elevation if needed.

**Kind**: global function
**Returns**: <code>Promise.&lt;boolean&gt;</code> - - Resolves true if system wide defile file removed, else false.
**Throws**:

- <code>Error</code> - If called from the browser

<a name="isLicensedDeviceSystemWide"></a>

## isLicensedDeviceSystemWide() ⇒ <code>Promise.&lt;boolean&gt;</code>
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**: <code>Promise.&lt;boolean&gt;</code> - - Resolves with `true` if the device is licensed, `false` otherwise.
6 changes: 6 additions & 0 deletions src-node/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions src-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
178 changes: 178 additions & 0 deletions src-node/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string|null>}
*/
function _getMacDeviceID() {
// to read this in mac bash, do:
// #!/bin/bash
// device_id=$(ioreg -rd1 -c IOPlatformExpertDevice | awk -F\" '/IOPlatformUUID/ {print $4}' | tr -d '[:space:]')
// echo "$device_id"
return new Promise((resolve, reject) => {
exec(
'ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID',
{ encoding: 'utf8' },
(err, stdout) => {
if (err) {
console.error('Failed to get Mac device ID:', err.message);
return reject(err);
}

const match = stdout.match(/"IOPlatformUUID" = "([^"]+)"/);
if (match && match[1]) {
resolve(match[1]);
} else {
resolve(null);
}
}
);
});
}

/**
* Get the Windows device ID (MachineGuid).
* @returns {Promise<string|null>}
*
* In a Windows batch file, you can get this with:
* reg query HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography /v MachineGuid
*/
function _getWindowsDeviceID() {
return new Promise((resolve, reject) => {
exec(
'reg query HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid',
{ encoding: 'utf8' },
(err, stdout) => {
if (err) {
console.error('Failed to get Windows device ID:', err.message);
return reject(err);
}

// Example output:
// HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography
// MachineGuid REG_SZ 4c4c4544-0034-5a10-8051-cac04f305a31
const match = stdout.match(/MachineGuid\s+REG_[A-Z]+\s+([a-fA-F0-9-]+)/);
if (match && match[1]) {
resolve(match[1].trim());
} else {
resolve(null);
}
}
);
});
}

async function getDeviceID() {
if (process.platform === "linux") {
return _getLinuxDeviceID();
} else if (process.platform === "darwin") {
return _getMacDeviceID();
} else if (process.platform === "win32") {
return _getWindowsDeviceID();
}
throw new Error(`Unsupported platform: ${process.platform}`);
}

exports.getURLContent = getURLContent;
exports.setLocaleStrings = setLocaleStrings;
exports.getPhoenixBinaryVersion = getPhoenixBinaryVersion;
Expand All @@ -278,5 +452,9 @@ exports.getEnvironmentVariable = getEnvironmentVariable;
exports.ESLintFile = ESLintFile;
exports.openNativeTerminal = openNativeTerminal;
exports.openInDefaultApp = openInDefaultApp;
exports.addDeviceLicense = addDeviceLicense;
exports.removeDeviceLicense = removeDeviceLicense;
exports.isLicensedDevice = isLicensedDevice;
exports.getDeviceID = getDeviceID;
exports._loadNodeExtensionModule = _loadNodeExtensionModule;
exports._npmInstallInFolder = _npmInstallInFolder;
6 changes: 6 additions & 0 deletions src/command/Commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,9 @@ define(function (require, exports, module) {
/** Opens Phoenix Pro page */
exports.HELP_GET_PRO = "help.getPro"; // HelpCommandHandlers.js _handleLinkMenuItem()

/** Manage Pro licenses */
exports.HELP_MANAGE_LICENSES = "help.manageLicenses"; // HelpCommandHandlers.js _handleLinkMenuItem()

/** Opens feature suggestion page */
exports.HELP_SUGGEST = "help.suggest"; // HelpCommandHandlers.js _handleLinkMenuItem()

Expand Down Expand Up @@ -501,6 +504,9 @@ define(function (require, exports, module) {
/** Shows the sidebar */
exports.SHOW_SIDEBAR = "view.showSidebar"; // SidebarView.js show()

/** Reinstalls credentials in keychain */
exports.REINSTALL_CREDS = "debug.reinstallCreds"; // login-service.js handleReinstallCreds()

// commands
/** Initializes a new git repository */
exports.CMD_GIT_INIT = "git-init";
Expand Down
3 changes: 3 additions & 0 deletions src/command/DefaultMenus.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,9 @@ define(function (require, exports, module) {
menu.addMenuItem(Commands.HELP_SUPPORT);
menu.addMenuDivider();
menu.addMenuItem(Commands.HELP_GET_PRO);
if(Phoenix.isNativeApp) {
menu.addMenuItem(Commands.HELP_MANAGE_LICENSES);
}
menu.addMenuDivider();
if (brackets.config.suggest_feature_url) {
menu.addMenuItem(Commands.HELP_SUGGEST);
Expand Down
4 changes: 4 additions & 0 deletions src/extensions/default/DebugCommands/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,10 @@ define(function (require, exports, module) {
}
// this command is defined in core, but exposed only in Debug menu for now
debugMenu.addMenuItem(Commands.FILE_OPEN_KEYMAP, null);
// Reinstall credentials menu item (native apps only)
if(Phoenix.isNativeApp) {
debugMenu.addMenuItem(Commands.REINSTALL_CREDS, null);
}
const diagnosticsSubmenu = debugMenu.addSubMenu(Strings.CMD_DIAGNOSTIC_TOOLS, DIAGNOSTICS_SUBMENU);
diagnosticsSubmenu.addMenuItem(DEBUG_RUN_UNIT_TESTS);
CommandManager.register(Strings.CMD_BUILD_TESTS, DEBUG_BUILD_TESTS, TestBuilder.toggleTestBuilder);
Expand Down
4 changes: 3 additions & 1 deletion src/help/HelpCommandHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ define(function (require, exports, module) {
FileUtils = require("file/FileUtils"),
NativeApp = require("utils/NativeApp"),
Strings = require("strings"),
StringUtils = require("utils/StringUtils"),
StringUtils = require("utils/StringUtils"),
ManageLicenses = require("services/manage-licenses"),
AboutDialogTemplate = require("text!htmlContent/about-dialog.html"),
ContributorsTemplate = require("text!htmlContent/contributors-list.html"),
Mustache = require("thirdparty/mustache/mustache");
Expand Down Expand Up @@ -168,6 +169,7 @@ define(function (require, exports, module) {
CommandManager.register(Strings.CMD_GET_PRO, Commands.HELP_GET_PRO, _handleLinkMenuItem(brackets.config.purchase_url), {
htmlName: getProString
});
CommandManager.register(Strings.CMD_MANAGE_LICENSES, Commands.HELP_MANAGE_LICENSES, ManageLicenses.showManageLicensesDialog);
CommandManager.register(Strings.CMD_SUGGEST, Commands.HELP_SUGGEST, _handleLinkMenuItem(brackets.config.suggest_feature_url));
CommandManager.register(Strings.CMD_REPORT_ISSUE, Commands.HELP_REPORT_ISSUE, _handleLinkMenuItem(brackets.config.report_issue_url));
CommandManager.register(Strings.CMD_RELEASE_NOTES, Commands.HELP_RELEASE_NOTES, _handleLinkMenuItem(brackets.config.release_notes_url));
Expand Down
4 changes: 4 additions & 0 deletions src/loggerSetup.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@
Bugsnag.notify(message?
new CustomBugSnagError(message, error)
:error);
} else {
console.error(message, error, error.nodeStack);
}
},
/**
Expand All @@ -79,6 +81,8 @@
reportErrorMessage: function (message) {
if(isBugsnagEnabled) {
Bugsnag.notify(new CustomBugSnagError(message));
} else {
console.error(message);
}
},

Expand Down
Loading
Loading