diff --git a/package-lock.json b/package-lock.json index 8425fea3..fb66f12d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6998,6 +6998,10 @@ "resolved": "packages/get-os-info", "link": true }, + "node_modules/@mongodb-js/machine-id": { + "resolved": "packages/machine-id", + "link": true + }, "node_modules/@mongodb-js/mocha-config-devtools": { "resolved": "configs/mocha-config-devtools", "link": true @@ -10087,7 +10091,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "devOptional": true, "dependencies": { "file-uri-to-path": "1.0.0" } @@ -14339,8 +14342,7 @@ "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "devOptional": true + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, "node_modules/filelist": { "version": "1.0.4", @@ -21198,6 +21200,13 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/node-machine-id": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", + "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", + "dev": true, + "license": "MIT" + }, "node_modules/node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -28124,6 +28133,59 @@ "node": ">=14.17" } }, + "packages/machine-id": { + "name": "@mongodb-js/machine-id", + "version": "1.0.0", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^8.0.0" + }, + "bin": { + "machine-id": "dist/bin/machine-id.js" + }, + "devDependencies": { + "@mongodb-js/eslint-config-devtools": "0.9.11", + "@mongodb-js/mocha-config-devtools": "^1.0.5", + "@mongodb-js/prettier-config-devtools": "^1.0.2", + "@mongodb-js/tsconfig-devtools": "^1.0.3", + "@types/chai": "^4.2.21", + "@types/mocha": "^9.1.1", + "@types/node": "^17.0.35", + "@types/sinon-chai": "^3.2.5", + "chai": "^4.5.0", + "eslint": "^7.25.0", + "gen-esm-wrapper": "^1.1.1", + "mocha": "^8.4.0", + "node-machine-id": "^1.1.12", + "ts-node": "^10.9.2", + "typescript": "^5.0.4" + } + }, + "packages/machine-id/node_modules/node-addon-api": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", + "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "packages/machine-id/node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "packages/mongodb-cloud-info": { "version": "2.1.7", "license": "Apache-2.0", @@ -35338,6 +35400,41 @@ } } }, + "@mongodb-js/machine-id": { + "version": "file:packages/machine-id", + "requires": { + "@mongodb-js/eslint-config-devtools": "0.9.11", + "@mongodb-js/mocha-config-devtools": "^1.0.5", + "@mongodb-js/prettier-config-devtools": "^1.0.2", + "@mongodb-js/tsconfig-devtools": "^1.0.3", + "@types/chai": "^4.2.21", + "@types/mocha": "^9.1.1", + "@types/node": "^17.0.35", + "@types/sinon-chai": "^3.2.5", + "bindings": "^1.5.0", + "chai": "^4.5.0", + "eslint": "^7.25.0", + "gen-esm-wrapper": "^1.1.1", + "mocha": "^8.4.0", + "node-addon-api": "^8.0.0", + "node-machine-id": "^1.1.12", + "ts-node": "^10.9.2", + "typescript": "^5.0.4" + }, + "dependencies": { + "node-addon-api": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", + "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==" + }, + "typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true + } + } + }, "@mongodb-js/mocha-config-devtools": { "version": "file:configs/mocha-config-devtools", "requires": { @@ -38487,7 +38584,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "devOptional": true, "requires": { "file-uri-to-path": "1.0.0" } @@ -41626,8 +41722,7 @@ "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "devOptional": true + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, "filelist": { "version": "1.0.4", @@ -46944,6 +47039,12 @@ "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", "dev": true }, + "node-machine-id": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", + "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", + "dev": true + }, "node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", diff --git a/packages/machine-id/.eslintignore b/packages/machine-id/.eslintignore new file mode 100644 index 00000000..1521c8b7 --- /dev/null +++ b/packages/machine-id/.eslintignore @@ -0,0 +1 @@ +dist diff --git a/packages/machine-id/.eslintrc.js b/packages/machine-id/.eslintrc.js new file mode 100644 index 00000000..83296d73 --- /dev/null +++ b/packages/machine-id/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + root: true, + extends: ['@mongodb-js/eslint-config-devtools'], + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig-lint.json'], + }, +}; diff --git a/packages/machine-id/.gitignore b/packages/machine-id/.gitignore new file mode 100644 index 00000000..e3c4e1eb --- /dev/null +++ b/packages/machine-id/.gitignore @@ -0,0 +1,25 @@ +.DS_Store +.lock-wscript +.idea/ +.vscode/ +*.iml +.nvmrc +.nyc_output +*.swp +lerna-debug.log +lib-cov +npm-debug.log +.idea/ +coverage/ +dist/ +node_modules/ +.lock-wscript +.cache/ +expansions.yaml +tmp/expansions.yaml +.evergreen/mongodb +tmp/ +.esm-wrapper.mjs +package-lock.json +build/ +crash.log \ No newline at end of file diff --git a/packages/machine-id/.mocharc.js b/packages/machine-id/.mocharc.js new file mode 100644 index 00000000..64afeb1f --- /dev/null +++ b/packages/machine-id/.mocharc.js @@ -0,0 +1 @@ +module.exports = require('@mongodb-js/mocha-config-devtools'); diff --git a/packages/machine-id/.prettierignore b/packages/machine-id/.prettierignore new file mode 100644 index 00000000..009af543 --- /dev/null +++ b/packages/machine-id/.prettierignore @@ -0,0 +1,2 @@ +dist +coverage diff --git a/packages/machine-id/.prettierrc.json b/packages/machine-id/.prettierrc.json new file mode 100644 index 00000000..dfae21d0 --- /dev/null +++ b/packages/machine-id/.prettierrc.json @@ -0,0 +1 @@ +"@mongodb-js/prettier-config-devtools" diff --git a/packages/machine-id/LICENSE b/packages/machine-id/LICENSE new file mode 100644 index 00000000..7502810f --- /dev/null +++ b/packages/machine-id/LICENSE @@ -0,0 +1,192 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2024 MongoDB Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/packages/machine-id/README.md b/packages/machine-id/README.md new file mode 100644 index 00000000..5fea92c6 --- /dev/null +++ b/packages/machine-id/README.md @@ -0,0 +1,80 @@ +# @mongodb-js/machine-id + +> Native implementation for retrieving unique machine ID without admin privileges or child processes for desktop platforms. Faster and more reliable alternative to node-machine-id. + +## Installation + +``` +npm install @mongodb-js/machine-id +``` + +Or use it directly in the CLI + +``` +npx @mongodb-js/machine-id +``` + +## Usage + +### As a module + +```javascript +import { getMachineID } from '@mongodb-js/machine-id'; + +// Get the machine ID +const hashedId = getMachineID(); +console.log('SHA-256 Hashed Machine ID:', id); +const id = getMachineID({ raw: true }); +console.log('Original Machine ID:', id); +``` + +## Supported Platforms + +- **macOS**: Uses the `IOPlatformUUID` from the `IOKit` framework (Supported on macOS 12.0 and later). +- **Linux**: Uses the `/var/lib/dbus/machine-id` file to retrieve the machine ID. If this file does not exist, it falls back to `/etc/machine-id`. +- **Windows**: Uses the `MachineGuid` from the `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography` registry. + +## Comparison with `node-machine-id` + +This module provides similar functionality to [node-machine-id](https://www.npmjs.com/package/node-machine-id), but **using native access to system APIs without the need for child processes**, making it much faster and reliable. + +Here's a table of performance comparisons between the two libraries, based on the average runtime from 1000 iterations of the `getMachineId` and `machineIdSync` functions, from `scripts/benchmark.ts`: + +| Test | node-machine-id | @mongodb-js/machine-id | Improvement | +| ----------- | --------------- | ---------------------- | ----------- | +| **Mac** | +| Raw | 10.71ms | 0.0072ms | 1494x | +| Hashed | 12.42ms | 0.0176ms | 707x | +| **Linux** | +| Raw | 3.26ms | 0.0059ms | 557x | +| Hashed | 3.25ms | 0.0088ms | 368x | +| **Windows** | +| Raw | 45.36ms\* | 0.0122ms | 3704x | +| Hashed | 28.66ms\* | 0.0272ms | 1053x | + +\* - Windows tests may be inaccurate due to potential caching. + +### Migrating from `node-machine-id` + +If you were previously using `node-machine-id`, you can use the following mapping to get a result with the following hashing transformation. This is not guaranteed always to 1:1 match the output of `node-machine-id` for all cases. For example on Linux, it falls back to `/etc/machine-id` if `/var/lib/dbus/machine-id` is not available. + +```ts +import { createHash } from 'crypto'; +import { getMachineId } from '@mongodb-js/machine-id'; + +function machineIdSync(original: boolean): string | undefined { + const rawMachineId = getMachineId({ raw: true }).toLowerCase(); + + return original + ? rawMachineId + : createHash('sha256').update(rawMachineId).digest('hex'); +} +``` + +## Credits + +Influenced by the work from [denisbrodbeck/machineid](https://github.com/denisbrodbeck/machineid) and [automation-stack/node-machine-id](https://github.com/automation-stack/node-machine-id). + +## License + +Apache-2.0 diff --git a/packages/machine-id/binding.cc b/packages/machine-id/binding.cc new file mode 100644 index 00000000..efb39da3 --- /dev/null +++ b/packages/machine-id/binding.cc @@ -0,0 +1,184 @@ +#include +#include + +#ifdef __APPLE__ +#include +#include +#elif defined(__linux__) +#include +#include +#elif defined(_WIN32) +#include +#endif + +using namespace Napi; + +namespace +{ + +#ifdef __APPLE__ + // Get macOS machine ID using IOKit framework directly + std::string getMachineId() noexcept + { + std::string uuid; + io_registry_entry_t ioRegistryRoot = IORegistryEntryFromPath(kIOMainPortDefault, "IOService:/"); + + if (ioRegistryRoot == MACH_PORT_NULL) + { + return ""; + } + + CFStringRef uuidKey = CFSTR("IOPlatformUUID"); + CFTypeRef uuidProperty = IORegistryEntryCreateCFProperty(ioRegistryRoot, uuidKey, kCFAllocatorDefault, 0); + + if (uuidProperty) + { + char buffer[128]; + if (!CFStringGetCString((CFStringRef)uuidProperty, buffer, sizeof(buffer), kCFStringEncodingUTF8)) + { + CFRelease(uuidProperty); + IOObjectRelease(ioRegistryRoot); + return ""; + } + + uuid = buffer; + CFRelease(uuidProperty); + } + + IOObjectRelease(ioRegistryRoot); + return uuid; + } +#elif defined(__linux__) + // Linux machine ID paths + const char *DBUS_PATH = "/var/lib/dbus/machine-id"; + const char *DBUS_PATH_ETC = "/etc/machine-id"; + + // Trim whitespace and newlines from a string + std::string trim(const std::string &str) + { + if (str.empty()) + { + return str; + } + std::string result = str; + size_t from_right = result.find_last_not_of(" \n\r\t"); + if (from_right != std::string::npos) + { + result.erase(from_right + 1); + } + result.erase(0, result.find_first_not_of(" \n\r\t")); + return result; + } + + // Read file contents + std::string readFile(const char *path) + { + try + { + std::ifstream file(path); + std::string content; + + if (file.is_open()) + { + std::string line; + + if (!file.fail() && std::getline(file, line)) + { + content = line; + } + + file.close(); + } + return content; + } + catch (const std::exception &) + { + return ""; + } + } + + // Get Linux machine ID by reading from system files + std::string getMachineId() + { + std::string uuid = readFile(DBUS_PATH); + + // Try fallback path if the first path fails + if (uuid.empty()) + { + uuid = readFile(DBUS_PATH_ETC); + } + + return trim(uuid); + } +#elif defined(_WIN32) + // Get Windows machine ID from registry + std::string getMachineId() + { + std::string uuid; + HKEY hKey; + LONG result = RegOpenKeyExA( + HKEY_LOCAL_MACHINE, + "SOFTWARE\\Microsoft\\Cryptography", + 0, + KEY_QUERY_VALUE | KEY_WOW64_64KEY, + &hKey); + + if (result != ERROR_SUCCESS) + { + return ""; + } + + char value[128] = {0}; + DWORD valueSize = sizeof(value); + DWORD valueType; + + result = RegQueryValueExA( + hKey, + "MachineGuid", + NULL, + &valueType, + reinterpret_cast(value), + &valueSize); + + if (result == ERROR_SUCCESS && valueType == REG_SZ && valueSize > 0) + { + // Create string with explicit length based on returned valueSize + uuid = std::string(value, valueSize - (value[valueSize - 1] == '\0' ? 1 : 0)); + } + else + { + RegCloseKey(hKey); + return ""; + } + + RegCloseKey(hKey); + return uuid; + } +#endif + + // Function to get the machine ID + Value GetMachineId(const CallbackInfo &args) + { + Env env = args.Env(); + +#if defined(__APPLE__) || defined(__linux__) || defined(_WIN32) + std::string id = getMachineId(); + if (!id.empty()) + { + return String::New(env, id); + } +#endif + + // If we couldn't get a machine ID or platform not supported, return undefined. + return env.Undefined(); + } + +} + +static Object InitModule(Env env, Object exports) +{ + exports["getMachineId"] = Function::New(env, GetMachineId); + return exports; +} + +NODE_API_MODULE(machine_id, InitModule) diff --git a/packages/machine-id/binding.gyp b/packages/machine-id/binding.gyp new file mode 100644 index 00000000..8d530ceb --- /dev/null +++ b/packages/machine-id/binding.gyp @@ -0,0 +1,29 @@ +{ + 'targets': [{ + 'target_name': 'machine_id', + 'sources': [ 'binding.cc' ], + 'include_dirs': ["", + "gypfile": true, + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^8.0.0" + }, + "license": "Apache-2.0", + "exports": { + "require": "./dist/index.js", + "import": "./dist/.esm-wrapper.mjs" + }, + "homepage": "https://github.com/mongodb-js/devtools-shared", + "repository": { + "type": "git", + "url": "https://github.com/mongodb-js/devtools-shared.git" + }, + "bugs": "https://jira.mongodb.org/projects/COMPASS/issues", + "bin": { + "machine-id": "dist/bin/machine-id.js" + }, + "files": [ + "binding.cc", + "binding.gyp", + "dist", + "LICENSE" + ], + "devDependencies": { + "@mongodb-js/eslint-config-devtools": "0.9.11", + "@mongodb-js/mocha-config-devtools": "^1.0.5", + "@mongodb-js/prettier-config-devtools": "^1.0.2", + "@mongodb-js/tsconfig-devtools": "^1.0.3", + "@types/chai": "^4.2.21", + "@types/mocha": "^9.1.1", + "@types/sinon-chai": "^3.2.5", + "@types/node": "^17.0.35", + "eslint": "^7.25.0", + "gen-esm-wrapper": "^1.1.1", + "mocha": "^8.4.0", + "chai": "^4.5.0", + "node-machine-id": "^1.1.12", + "typescript": "^5.0.4", + "ts-node": "^10.9.2" + } +} diff --git a/packages/machine-id/scripts/benchmark.ts b/packages/machine-id/scripts/benchmark.ts new file mode 100644 index 00000000..2af90a63 --- /dev/null +++ b/packages/machine-id/scripts/benchmark.ts @@ -0,0 +1,103 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ +/** + * Performance comparison script for machine-id vs node-machine-id + * + * This script measures and compares the performance of @mongodb-js/machine-id + * against the node-machine-id package. + */ + +import { getMachineId } from '../dist/index.js'; +import { machineIdSync } from 'node-machine-id'; + +// Configuration +const ITERATIONS = 100; + +// Utility to format time +function formatTime(ms: number): string { + if (ms < 1) { + return `${(ms * 1000).toFixed(2)}µs`; + } + return `${ms.toFixed(2)}ms`; +} + +// Utility to format comparison +function formatComparison(time1: number, time2: number): string { + if (time1 < time2) { + return `${(time2 / time1).toFixed(2)}x faster`; + } else { + return `${(time1 / time2).toFixed(2)}x slower`; + } +} + +function runBenchmark() { + console.log('========================================'); + console.log('Machine ID Performance Benchmark'); + console.log('========================================'); + console.log(`Platform: ${process.platform}`); + console.log(`Node.js version: ${process.version}`); + console.log(`Test iterations: ${ITERATIONS}`); + console.log('----------------------------------------'); + + // Test raw mode (no hashing) + console.log('Raw:'); + + const startOursRaw = process.hrtime.bigint(); + for (let i = 0; i < ITERATIONS; i++) { + getMachineId({ raw: true }); + } + const endOursRaw = process.hrtime.bigint(); + const ourTimeRaw = Number(endOursRaw - startOursRaw) / 1_000_000; // ms + + // node-machine-id + const startOtherRaw = process.hrtime.bigint(); + for (let i = 0; i < ITERATIONS; i++) { + machineIdSync(true); + } + const endOtherRaw = process.hrtime.bigint(); + const otherTimeRaw = Number(endOtherRaw - startOtherRaw) / 1_000_000; // ms + + console.log( + `@mongodb-js/machine-id: ${formatTime(ourTimeRaw)} total, ${formatTime(ourTimeRaw / ITERATIONS)} per call`, + ); + console.log( + `node-machine-id: ${formatTime(otherTimeRaw)} total, ${formatTime(otherTimeRaw / ITERATIONS)} per call`, + ); + console.log( + `Comparison: @mongodb-js/machine-id is ${formatComparison(ourTimeRaw, otherTimeRaw)}`, + ); + + console.log('----------------------------------------'); + + // Test hashed mode + console.log('Hashed:'); + + // @mongodb-js/machine-id + const startOursHashed = process.hrtime.bigint(); + for (let i = 0; i < ITERATIONS; i++) { + getMachineId(); + } + const endOursHashed = process.hrtime.bigint(); + const ourTimeHashed = Number(endOursHashed - startOursHashed) / 1_000_000; // ms + + // node-machine-id + const startOtherHashed = process.hrtime.bigint(); + for (let i = 0; i < ITERATIONS; i++) { + machineIdSync(); + } + const endOtherHashed = process.hrtime.bigint(); + const otherTimeHashed = Number(endOtherHashed - startOtherHashed) / 1_000_000; // ms + + console.log( + `@mongodb-js/machine-id: ${formatTime(ourTimeHashed)} total, ${formatTime(ourTimeHashed / ITERATIONS)} per call`, + ); + console.log( + `node-machine-id: ${formatTime(otherTimeHashed)} total, ${formatTime(otherTimeHashed / ITERATIONS)} per call`, + ); + console.log( + `Comparison: @mongodb-js/machine-id is ${formatComparison(ourTimeHashed, otherTimeHashed)}`, + ); +} + +// Run the benchmark +runBenchmark(); diff --git a/packages/machine-id/src/bin/machine-id.ts b/packages/machine-id/src/bin/machine-id.ts new file mode 100644 index 00000000..580fde6e --- /dev/null +++ b/packages/machine-id/src/bin/machine-id.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node +import { getMachineId } from '..'; + +const id = getMachineId({ raw: process.argv.includes('--raw') }) || ''; + +// eslint-disable-next-line no-console +console.log(id); diff --git a/packages/machine-id/src/bindings.d.ts b/packages/machine-id/src/bindings.d.ts new file mode 100644 index 00000000..4b639513 --- /dev/null +++ b/packages/machine-id/src/bindings.d.ts @@ -0,0 +1,6 @@ +declare module 'bindings' { + function bindings(filename: 'machine_id'): { + getMachineId: () => string | undefined; + }; + export = bindings; +} diff --git a/packages/machine-id/src/index.spec.ts b/packages/machine-id/src/index.spec.ts new file mode 100644 index 00000000..27647e2f --- /dev/null +++ b/packages/machine-id/src/index.spec.ts @@ -0,0 +1,104 @@ +/* eslint-disable no-console */ +import { getMachineId } from '.'; +import { machineIdSync as otherMachineId } from 'node-machine-id'; +import chai, { expect } from 'chai'; +import { createHash } from 'crypto'; +import sinonChai from 'sinon-chai'; +import sinon from 'sinon'; +import bindings from 'bindings'; +import assert from 'assert'; + +chai.use(sinonChai); + +describe('machine-id', function () { + this.timeout(5_000); + + describe('without hashing', function () { + let id: string; + + beforeEach(function () { + const deviceId = getMachineId({ raw: true }); + assert(deviceId); + id = deviceId; + }); + + it('returns a valid UUID format machine ID', function () { + // UUID format: 8-4-4-4-12 hex digits + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + + expect(uuidRegex.test(id)); + }); + + it('is consistent with node-machine-id', function () { + const nodeId = otherMachineId(true); + + // node-machine-id returns a lowercase ID + expect(id.toLowerCase()).to.equal(nodeId); + }); + + it('can reproduce the hashed node-machine-id value', function () { + const nodeId = otherMachineId(); + + // Compare the lowercase workaround for consistency with node-machine-id + const hashedId = createHash('sha256') + .update(id.toLowerCase()) + .digest('hex'); + + expect(hashedId).to.equal(nodeId); + }); + }); + + describe('with hashing', function () { + let id: string; + + beforeEach(function () { + const deviceId = getMachineId(); + assert(deviceId); + id = deviceId; + }); + + it('returns a valid SHA256 hash format machine ID', function () { + // SHA256 hash format: 64 hex digits + const hashRegex = /^[0-9a-f]{64}$/i; + + expect(hashRegex.test(id)); + + const hashId = getMachineId({ raw: true }); + assert(hashId); + + expect(id).equals(createHash('sha256').update(hashId).digest('hex')); + }); + }); + + describe('edge cases', function () { + afterEach(function () { + sinon.restore(); + }); + + describe('returns undefined', function () { + it('if something goes wrong with the binding function', function () { + sinon + .stub(bindings('machine_id'), 'getMachineId') + .throws(new Error('Binding error')); + + expect(getMachineId({ raw: true })).to.be.undefined; + expect(getMachineId()).to.be.undefined; + }); + + it('if the binding function returns an empty string', function () { + sinon.stub(bindings('machine_id'), 'getMachineId').returns(''); + + expect(getMachineId({ raw: true })).to.be.undefined; + expect(getMachineId()).to.be.undefined; + }); + + it('if the binding function returns undefined', function () { + sinon.stub(bindings('machine_id'), 'getMachineId').returns(undefined); + + expect(getMachineId({ raw: true })).to.be.undefined; + expect(getMachineId()).to.be.undefined; + }); + }); + }); +}); diff --git a/packages/machine-id/src/index.ts b/packages/machine-id/src/index.ts new file mode 100644 index 00000000..85fb24a1 --- /dev/null +++ b/packages/machine-id/src/index.ts @@ -0,0 +1,34 @@ +import bindings from 'bindings'; +import { createHash } from 'crypto'; + +const binding = bindings('machine_id'); + +export type GetMachineIdOptions = { + /** If true, the machine ID will not be hashed with SHA256. */ + raw?: boolean; +}; + +function getMachineIdFromBinding(): string | undefined { + try { + return binding.getMachineId() || undefined; + } catch { + // If the binding fails, we can assume the machine ID is not available. + return undefined; + } +} + +/** + * Get the machine ID for the current system + * @returns The machine ID (UUID) or undefined if not available + */ +export function getMachineId({ raw = false }: GetMachineIdOptions = {}): + | string + | undefined { + const machineId = getMachineIdFromBinding(); + + if (!machineId || raw === true) { + return machineId; + } + + return createHash('sha256').update(machineId).digest('hex'); +} diff --git a/packages/machine-id/tsconfig-lint.json b/packages/machine-id/tsconfig-lint.json new file mode 100644 index 00000000..6bdef84f --- /dev/null +++ b/packages/machine-id/tsconfig-lint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/machine-id/tsconfig.json b/packages/machine-id/tsconfig.json new file mode 100644 index 00000000..a2af0408 --- /dev/null +++ b/packages/machine-id/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@mongodb-js/tsconfig-devtools/tsconfig.common.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["./src/**/*.spec.*", "scripts/benchmark.ts"] +}