@moonbase.sh/licensing 2.0.1 → 3.1.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/README.md CHANGED
@@ -74,14 +74,14 @@ if (localLicense) {
74
74
 
75
75
  ## Metadata, platform, and app version
76
76
 
77
- The SDK can report analytics with every activation, trial request, and validation. These are configured once on the `MoonbaseLicensing` config and sent automatically there's no per-call parameter.
77
+ The SDK can report analytics with every activation, trial request, and validation. These are configured once on the `MoonbaseLicensing` config and sent automatically; there's no per-call parameter.
78
78
 
79
79
  - `appVersion` and `platform` are first-class fields and sent as plain query params. `metadata` is for arbitrary customer-defined keys, sent as `meta[key]=value`.
80
80
  - `platform` auto-detects from `process.platform` (`darwin` → `Mac`, `win32` → `Windows`, `linux` → `Linux`). Set it explicitly to override, or pass `null` to suppress.
81
- - All three are sent on `requestActivation`, `requestTrial`, and `validateLicense` never on `revokeLicense`.
81
+ - All three are sent on `requestActivation`, `requestTrial`, and `validateLicense`, never on `revokeLicense`.
82
82
  - `metadata` accepts string keys and string values only; empty values are dropped client-side.
83
83
  - Server-side, the metadata on the activation is **replaced on every call** (not merged) and surfaced in `LicenseActivatedEvent` / `LicenseValidatedEvent` webhooks.
84
- - The config object is re-read on every call mutate `metadata` between calls (e.g. to rotate a session id) and the next request picks it up.
84
+ - The config object is re-read on every call, so mutating `metadata` between calls (e.g. to rotate a session id) means the next request picks it up.
85
85
  - Keep payloads small; query-string length limits apply.
86
86
 
87
87
  ## Revoke a license activation
@@ -108,9 +108,146 @@ Read and validate raw license bytes (for example from a file):
108
108
  ```ts
109
109
  const license = await licensing.readRawLicense(rawLicenseBuffer)
110
110
  ```
111
+ ## Device fingerprint
111
112
 
112
- ## Custom stores and device resolvers
113
+ Every license is bound to the machine via a device id, stored in the token's `sig` claim and
114
+ re-checked on each local validation. The default `MoonbaseDeviceIdResolver` computes it from the
115
+ cross-SDK **[device fingerprint spec](./FINGERPRINT_SPEC.md)** (`moonbase:fingerprint:v2`): a
116
+ SHA-256 of stable native hardware identifiers, stamped with the spec version.
117
+
118
+ ```
119
+ mbd2_9f3c… // 'mbd' + version + source tag + '_' + 64 lowercase hex characters
120
+ mbd2n_9f3c… // 'n': the opt-in host-name fallback (see below)
121
+ mbd2s_9f3c… // 's': an app-scoped id, which only the mobile SDKs produce
122
+ ```
123
+
124
+ Sources are SMBIOS on Windows, `IOPlatformUUID` on macOS, and `machine-id` plus world-readable DMI
125
+ on Linux. The algorithm is language-neutral by design: any Moonbase SDK that implements the spec and
126
+ passes the shipped [`fingerprint-vectors.json`](./fingerprint-vectors.json) computes the same id on a
127
+ given machine, so a license activated by one validates in the others. Adoption is per-SDK, so check
128
+ the version of whichever SDK you are pairing with before relying on it.
129
+
130
+ This package **parses** all three forms — including a tag introduced by a newer SDK, which it
131
+ compares literally rather than rejecting — but its built-in readers only ever **produce** `mbd2_` or
132
+ `mbd2n_`. Scoped ids come from iOS and Android, where the OS exposes no identifier an unrelated app
133
+ can read; collecting them needs platform API no Node.js process can call, so that is the C++/.NET
134
+ SDKs' job. A scoped id is stable only within one app scope and must never be compared with one from
135
+ another, which is exactly what the `s` tag exists to announce.
136
+
137
+ > Both files ship inside the package. If the links above do not resolve where you are reading this,
138
+ > find them in `node_modules/@moonbase.sh/licensing/`.
139
+
140
+ The id survives a rename, a locale change, a firmware update, a vCPU resize, and running with or
141
+ without elevated privileges. The spec's **stability contract** is the definitive list. Read it
142
+ before shipping, along with the two Linux exceptions, which exist because every per-unit hardware
143
+ serial is root-only there and the id is tied to the OS installation rather than the hardware:
144
+
145
+ - A Linux **OS reinstall** requires re-activation.
146
+ - A Linux **VM cloned without clearing `/etc/machine-id`** keeps its device id, so a license copied
147
+ with the disk keeps validating. `machine-id(5)` requires reusable images to ship that file empty;
148
+ when they do, clones behave correctly. The SDK cannot detect a badly-prepared image, because the
149
+ value that would distinguish the instances is root-only.
150
+
151
+ Because the version is part of the id, a mismatch is diagnosable. `validateLicense` throws
152
+ `ErrorType.LicenseDeviceMismatch` either way, and the message says which case you are in:
153
+
154
+ ```ts
155
+ try {
156
+ await licensing.validator.validateLicense(token)
157
+ }
158
+ catch (err) {
159
+ if (err.type === ErrorType.LicenseDeviceMismatch)
160
+ console.error(err.message) // 'not for this device', plus any version difference
161
+ }
162
+ ```
163
+
164
+ ### When there is no hardware identity
165
+
166
+ The resolver throws `InsufficientDeviceIdentityError` rather than falling back to something weak, in
167
+ two cases:
168
+
169
+ - **Nothing readable.** A sandboxed process; a platform the spec defines no parameters for (BSD, and
170
+ anything unrecognised); or Android, whose parameter this package cannot reach from Node.
171
+ - **Only model-level values readable.** Vendor, product and board names are byte-identical across
172
+ every unit of a product line, so fingerprinting them would let those machines validate one
173
+ another's licenses. In practice: a Linux install with no `machine-id`, or a machine whose SMBIOS
174
+ carries an unset UUID *and* a blank or filler baseboard serial, the usual shape of a cloned VM
175
+ image.
176
+
177
+ Opt in explicitly if a weaker id beats none. Those ids are stamped `mbd2n_` so the server can tell
178
+ them apart:
179
+
180
+ ```ts
181
+ const deviceIdResolver = new MoonbaseDeviceIdResolver({ fallback: 'deviceName' })
182
+ ```
183
+
184
+ ### Diagnostics and parity checks
185
+
186
+ `describeDevice()` returns the id, spec version, platform and the *names* of the parameters that
187
+ contributed. It is safe to log or attach to a support ticket, and returns a fresh copy each call so
188
+ editing it cannot disturb the binding.
189
+
190
+ Parameter values are never exposed there, and neither are per-parameter hashes. They are hardware
191
+ serial numbers, and an unsalted per-value digest is no safer to publish than the value, since
192
+ low-entropy values such as host names or sequential serials fall to a dictionary. Which parameters
193
+ contributed is the useful diagnostic; what they read is not.
194
+
195
+ The device id itself is a one-way hash of all of them together, so it discloses no individual
196
+ serial. It is, however, **derived identically for every Moonbase-powered product**. The material
197
+ contains no product- or account-specific input, so the same machine yields the same device id
198
+ everywhere, and merchants receive that string through the integration API and webhooks. Treat it as
199
+ a stable cross-vendor machine identifier. That is more than `machine-id(5)` intends, which asks that
200
+ the Linux machine id only leave the host through an *application-specific keyed* derivation. If that
201
+ matters for your deployment, supply a custom `IDeviceIdResolver` that mixes in a key of your own.
202
+
203
+ The lower-level `buildFingerprintMaterial`, `fingerprintDigest`, `fingerprintDeviceId` and
204
+ `parseDeviceIdStamp` helpers are exported so you can verify cross-SDK parity against the vector
205
+ file.
206
+
207
+ ## Migrating from v2.x
208
+
209
+ Device ids computed by v2.x are not compatible with the spec, so **by default every device must
210
+ re-activate once** after you upgrade. That is not free: a new device id consumes a fresh activation
211
+ seat (the old one is not reclaimed) and resets any device-scoped trial. On a license with few seats,
212
+ a fleet-wide upgrade can exhaust them immediately.
213
+
214
+ Three options, in increasing order of effort:
215
+
216
+ **1. Let devices re-activate (default).** Simplest, and the id is correct from then on. Catch
217
+ `ErrorType.LicenseDeviceMismatch` and call `requestActivation()`. Best when seats are generous or the
218
+ install base is small.
219
+
220
+ **2. Accept the old id while binding the new one (recommended for existing fleets).**
221
+ `MigratingDeviceIdResolver` keeps recognising ids this device was previously bound to, without ever
222
+ issuing one:
113
223
 
114
- You can inject your own `ILicenseStore` and `IDeviceIdResolver` implementations.
224
+ ```ts
225
+ import { LegacyDeviceIdResolver, MigratingDeviceIdResolver, MoonbaseDeviceIdResolver } from '@moonbase.sh/licensing'
226
+
227
+ const licensing = new MoonbaseLicensing({
228
+ // …
229
+ deviceIdResolver: new MigratingDeviceIdResolver(
230
+ new MoonbaseDeviceIdResolver(), // always what a new activation binds
231
+ new LegacyDeviceIdResolver(), // additionally accepted during validation
232
+ ),
233
+ })
234
+ ```
235
+
236
+ Existing licenses keep validating untouched, while anything newly activated binds the current
237
+ fingerprint. The fleet migrates as devices naturally re-activate, with no flag day and no seat
238
+ churn. The legacy id is computed lazily, only when the fast comparison fails, and then memoized, so
239
+ apps on the happy path pay nothing. Drop the wrapper in a later release to finish the migration.
240
+
241
+ **3. Stay on the old id.** Pin `deviceIdResolver: new LegacyDeviceIdResolver()`. Nothing changes, but
242
+ you keep the old algorithm's defects (the machine name is part of the id, so renaming a computer
243
+ invalidates its license) and get no cross-SDK compatibility. Use this only as a short-term hold.
244
+
245
+ > Options 1 and 2 both recompute every accepted id from the machine's own hardware on each call.
246
+ > Nothing about a device binding is ever read from disk, so widening what a validator accepts does
247
+ > not widen what an attacker can assert.
248
+
249
+ ## Custom stores and device resolvers
115
250
 
116
- Note: the current configuration property name for custom device resolver is `deivceIdResolver`.
251
+ You can inject your own `ILicenseStore` and `IDeviceIdResolver` implementations via the
252
+ `licenseStore` and `deviceIdResolver` configuration options. A custom resolver's id is compared
253
+ literally, so it does not need to follow the `mbd2_` stamp format.