@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/FINGERPRINT_SPEC.md +647 -0
- package/README.md +143 -6
- package/dist/index.cjs +599 -66
- package/dist/index.d.cts +367 -14
- package/dist/index.d.ts +367 -14
- package/dist/index.js +581 -67
- package/fingerprint-vectors.json +948 -0
- package/package.json +8 -3
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|