@carchalabs/seal-hwid 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +51 -2
- package/index.d.ts +72 -0
- package/index.js +26 -0
- package/package.json +37 -4
- package/target.js +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bugra Omer Aydemir
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,52 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @carchalabs/seal-hwid
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Node-API layer over the `seal_hwid` native core (`../hwid`), for Node.js and Electron. One `.node` file per platform; Node-API is ABI-stable, and napi-rs looks up the Node-API functions in the host process at load time, so the same file loads in Node and in Electron.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
const { collect, version } = require('@carchalabs/seal-hwid');
|
|
7
|
+
const { vm, components, notes } = collect('your-product-key');
|
|
8
|
+
// components: [{ kind: 'uuid' | 'board' | 'disk' | 'mac' | 'os' | 'cpu' | 'gpu', hash: '<64 hex>' }]
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Only kinds, hashes, the VM flag and diagnostic notes reach JavaScript, never raw serials. If no native module exists for the platform, `require` throws (`code: 'HWID_UNAVAILABLE'`) instead of falling back.
|
|
12
|
+
|
|
13
|
+
## Lease verification
|
|
14
|
+
|
|
15
|
+
The module also verifies Carcha Seal leases (`../lease`, the `seal_lease` crate) with the same rules, order and failure reasons as `verifyLease` in `@carchalabs/seal-core`. The Ed25519 public keys it trusts are compiled in, so a lease signed with another key cannot be made to pass by changing a file or a setting.
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const { verifyLease, leaseKeyIds } = require('@carchalabs/seal-hwid');
|
|
19
|
+
verifyLease(token, { product, fingerprintHash, now, graceSeconds }); // { ok, reason?, kid? }
|
|
20
|
+
leaseKeyIds(); // ids of the compiled-in keys (keyId in @carchalabs/seal-core)
|
|
21
|
+
decryptFeature(token, { product, fingerprintHash, now }, licenseKey, 'pro', data); // { ok, data?, reason? }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`devicePublicKey('licensekit:<product>')` returns this machine's X25519 public key (base64url), derived from its hardware; the SDK sends it so the server wraps feature keys for this machine only, and `decryptFeature` derives the matching secret itself (it never reaches JavaScript). A test build with `--features test-device-secret` (CI only) takes the secret as a sixth argument so the shared vectors can run through this layer.
|
|
25
|
+
|
|
26
|
+
`decryptFeature` verifies the lease first, then unwraps the feature's content key from the lease (claim `fk`) for this license key and device and decrypts data encrypted for the feature; the key never reaches JavaScript.
|
|
27
|
+
|
|
28
|
+
The published build trusts the hosted service's current and next keys (`../quickstart/public-keys`). A self-hosted server's operator builds with their own: `SEAL_LEASE_PUBLIC_KEYS=<base64 SPKI>,<base64 SPKI> npm run build` (the PEM bodies, comma-separated). The Electron SDK uses this verifier next to its JavaScript one when the compiled-in keys include every key the app was given; a different answer from the two is treated as a modified license.
|
|
29
|
+
|
|
30
|
+
Both verifiers are tested against the same vectors: `../lease/tests/vectors.json`, leases signed by @carchalabs/seal-core with a throwaway key and the result core gives for each (`node ../lease/tests/make-vectors.js` regenerates them). The Rust crate (`cargo test` in `../lease`), core's own tests and this module (a build that trusts the vectors' key, in CI) must all give those results.
|
|
31
|
+
|
|
32
|
+
## Packages
|
|
33
|
+
|
|
34
|
+
Published as napi-rs lays out prebuilt modules: the main package holds the loader, and each target is its own package listed as an exact-version optional dependency, with `os`/`cpu` (and `libc`) so npm installs only the one for the machine. Nothing is compiled or downloaded at install time.
|
|
35
|
+
|
|
36
|
+
| Target | Package | Built on |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| macOS arm64 | `@carchalabs/seal-hwid-darwin-arm64` | `macos-latest` |
|
|
39
|
+
| macOS x64 | `@carchalabs/seal-hwid-darwin-x64` | `macos-15-intel` (GitHub's Intel macOS runners end in fall 2027) |
|
|
40
|
+
| Windows x64 | `@carchalabs/seal-hwid-win32-x64-msvc` | `windows-latest` |
|
|
41
|
+
| Linux x64 (glibc) | `@carchalabs/seal-hwid-linux-x64-gnu` | `ubuntu-22.04` |
|
|
42
|
+
|
|
43
|
+
Other platforms (Linux musl or arm64, Windows arm64) have no module yet: `require` throws `HWID_UNAVAILABLE`. CI (`.github/workflows/hwid-node.yml`) builds each target on its own runner, runs the tests there, installs the main and platform package from tarballs the way users get them, and uploads the platform package.
|
|
44
|
+
|
|
45
|
+
In Electron the `.node` file must sit outside the app's asar archive (Electron loads native modules with `process.dlopen`); electron-builder unpacks native modules automatically.
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
npm run build # cargo build --release, then seal-hwid.<target>.node next to index.js (loaded first)
|
|
49
|
+
npm test # Node tests
|
|
50
|
+
node scripts/build.js --npm # also copies it into npm/<target>/ (CI)
|
|
51
|
+
npx electron test/electron-check.js # loads it in Electron (with electron installed)
|
|
52
|
+
```
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export type ComponentKind = 'uuid' | 'board' | 'disk' | 'mac' | 'os' | 'cpu' | 'gpu';
|
|
2
|
+
|
|
3
|
+
export interface Component {
|
|
4
|
+
kind: ComponentKind;
|
|
5
|
+
/** Lowercase hex HMAC-SHA256 of the component, keyed by the product. */
|
|
6
|
+
hash: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Collected {
|
|
10
|
+
/** Version of the result shape. */
|
|
11
|
+
format: number;
|
|
12
|
+
/** True when a hypervisor was detected (best effort). */
|
|
13
|
+
vm: boolean;
|
|
14
|
+
components: Component[];
|
|
15
|
+
/** Why kinds are missing: the failing call and its error code, never a value. */
|
|
16
|
+
notes: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Reads this machine's components and hashes each with the product key. */
|
|
20
|
+
export function collect(key: string | Buffer): Collected;
|
|
21
|
+
|
|
22
|
+
export interface LeaseCheck {
|
|
23
|
+
product: string;
|
|
24
|
+
fingerprintHash: string;
|
|
25
|
+
/** Seconds since the Unix epoch; default: now. */
|
|
26
|
+
now?: number;
|
|
27
|
+
graceSeconds?: number;
|
|
28
|
+
leewaySeconds?: number;
|
|
29
|
+
issuer?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface LeaseVerdict {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
/** The failure reason, as @carchalabs/seal-core names it; absent when ok. */
|
|
35
|
+
reason?: string;
|
|
36
|
+
/** Id of the key that signed the lease; present when ok. */
|
|
37
|
+
kid?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Verifies a lease with the public keys compiled into this build and the same rules,
|
|
42
|
+
* order and reasons as verifyLease in @carchalabs/seal-core.
|
|
43
|
+
*/
|
|
44
|
+
export function verifyLease(token: string, check: LeaseCheck): LeaseVerdict;
|
|
45
|
+
|
|
46
|
+
export interface FeatureDecrypt {
|
|
47
|
+
ok: boolean;
|
|
48
|
+
/** The plaintext; present when ok. */
|
|
49
|
+
data?: Buffer;
|
|
50
|
+
/** A lease failure reason, 'not_licensed' or 'decrypt_failed'; absent when ok. */
|
|
51
|
+
reason?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Verifies the lease (compiled-in keys), unwraps the feature's content key for this license key and
|
|
56
|
+
* device, and decrypts data encrypted for the feature (encryptForFeature in @carchalabs/seal-core). The
|
|
57
|
+
* content key never reaches JavaScript.
|
|
58
|
+
*/
|
|
59
|
+
export function decryptFeature(token: string, check: LeaseCheck, licenseKey: string, feature: string, data: Buffer): FeatureDecrypt;
|
|
60
|
+
|
|
61
|
+
/** Ids of the lease signing keys this build trusts (keyId in @carchalabs/seal-core). */
|
|
62
|
+
export function leaseKeyIds(): string[];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* This machine's X25519 device public key (base64url) for a product key, derived from its system UUID,
|
|
66
|
+
* motherboard serial, system disk serial and OS install id; null when it reports none of them. The
|
|
67
|
+
* secret half never leaves the module. It changes when one of those parts changes.
|
|
68
|
+
*/
|
|
69
|
+
export function devicePublicKey(key: string | Buffer): string | null;
|
|
70
|
+
|
|
71
|
+
/** Version of the native core, e.g. "0.1.0". */
|
|
72
|
+
export function version(): string;
|
package/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Loads the prebuilt addon for this platform: a local build next to this file (development),
|
|
3
|
+
// otherwise the platform package npm installed (@carchalabs/seal-hwid-<target>, one of
|
|
4
|
+
// the optionalDependencies). Fails loudly when there is none: the SDK then sends no components
|
|
5
|
+
// rather than a weaker fingerprint.
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { existsSync } = require('node:fs');
|
|
8
|
+
const { target } = require('./target');
|
|
9
|
+
|
|
10
|
+
function unavailable(detail) {
|
|
11
|
+
const err = new Error(`@carchalabs/seal-hwid: no native module for ${process.platform}-${process.arch}: ${detail}`);
|
|
12
|
+
err.code = 'HWID_UNAVAILABLE';
|
|
13
|
+
return err;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const name = target();
|
|
17
|
+
if (!name) throw unavailable('this platform has no prebuilt module');
|
|
18
|
+
const local = path.join(__dirname, `seal-hwid.${name}.node`);
|
|
19
|
+
let addon;
|
|
20
|
+
try {
|
|
21
|
+
addon = existsSync(local) ? require(local) : require(`@carchalabs/seal-hwid-${name}`);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
throw unavailable(e.message);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { collect: addon.collect, devicePublicKey: addon.devicePublicKey, verifyLease: addon.verifyLease, leaseKeyIds: addon.leaseKeyIds, decryptFeature: addon.decryptFeature, version: addon.version };
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carchalabs/seal-hwid",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Hardware component hashes for Carcha Seal (formerly LicenseKit) machine binding (Node.js and Electron).",
|
|
5
|
+
"keywords": ["hardware-id", "machine-id", "licensing", "electron", "carcha-seal"],
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"index.js",
|
|
10
|
+
"index.d.ts",
|
|
11
|
+
"target.js"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "node scripts/build.js",
|
|
15
|
+
"test": "node --test test/*.test.js"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22.13"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"optionalDependencies": {
|
|
22
|
+
"@carchalabs/seal-hwid-darwin-arm64": "0.3.0",
|
|
23
|
+
"@carchalabs/seal-hwid-darwin-x64": "0.3.0",
|
|
24
|
+
"@carchalabs/seal-hwid-win32-x64-msvc": "0.3.0",
|
|
25
|
+
"@carchalabs/seal-hwid-linux-x64-gnu": "0.3.0"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/carcha-labs/carcha-seal-sdk.git",
|
|
30
|
+
"directory": "packages/hwid-node"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/carcha-labs/carcha-seal-sdk/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://seal.carchalabs.com/docs/",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/target.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// The napi-rs target name for this machine (the platform package suffix), or null when
|
|
3
|
+
// no prebuilt module exists for it. Shared by index.js (loading) and scripts/build.js.
|
|
4
|
+
function isMusl() {
|
|
5
|
+
// glibc reports its version in the process report; musl does not.
|
|
6
|
+
const report = typeof process.report?.getReport === 'function' ? process.report.getReport() : null;
|
|
7
|
+
return !report?.header?.glibcVersionRuntime;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function target(platform = process.platform, arch = process.arch, musl = platform === 'linux' && isMusl()) {
|
|
11
|
+
if (platform === 'darwin' && (arch === 'arm64' || arch === 'x64')) return `darwin-${arch}`;
|
|
12
|
+
if (platform === 'win32' && arch === 'x64') return 'win32-x64-msvc';
|
|
13
|
+
if (platform === 'linux' && arch === 'x64' && !musl) return 'linux-x64-gnu';
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { target, TARGETS: ['darwin-arm64', 'darwin-x64', 'win32-x64-msvc', 'linux-x64-gnu'] };
|