@moonbase.sh/licensing 2.0.1 → 3.0.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/dist/index.d.ts CHANGED
@@ -1,10 +1,277 @@
1
+ import { Buffer } from 'node:buffer';
1
2
  import { z } from 'zod';
2
3
 
4
+ /**
5
+ * Reference implementation of the Moonbase device fingerprint spec (v2). The
6
+ * authoritative, language-neutral definition lives in FINGERPRINT_SPEC.md, and
7
+ * the machine-readable conformance suite in fingerprint-vectors.json. Any SDK
8
+ * that conforms computes the same id on a given machine. Keep all three in
9
+ * lockstep; when they disagree, the vectors decide.
10
+ *
11
+ * A device id is a stamped SHA-256 of a deterministic "material" string built
12
+ * from stable native hardware identifiers:
13
+ *
14
+ * mbd2_<lowercase-hex sha256 of the material>
15
+ *
16
+ * Material layout (lines joined with "\n", NO trailing newline):
17
+ *
18
+ * moonbase:fingerprint:v2
19
+ * platform=<tag>
20
+ * <name>=<value>
21
+ * ...
22
+ *
23
+ * Every value is canonicalized (NFC, non-printable-ASCII dropped, capped,
24
+ * space-trimmed); empty values are skipped; duplicate parameter names are
25
+ * rejected; and a material that does not identify the individual machine — no
26
+ * parameters at all, or only model-level ones — is an error rather than a digest,
27
+ * so a machine or a whole product line can never share one id.
28
+ */
29
+ declare const FINGERPRINT_PREFIX = "moonbase:fingerprint:v2";
30
+ /** Spec version. Always equal to the digit in {@link FINGERPRINT_PREFIX} and in the stamp. */
31
+ declare const FINGERPRINT_VERSION = 2;
32
+ /** Longest permitted canonical value, in characters. */
33
+ declare const MAX_VALUE_LENGTH = 128;
34
+ /**
35
+ * What the material was built from. `identity` is the real hardware fingerprint;
36
+ * `deviceName` is the opt-in, deliberately weaker host-name fallback, stamped
37
+ * distinctly so a server can tell the two apart.
38
+ */
39
+ type DeviceIdSource = 'identity' | 'deviceName';
40
+ type PlatformTag = 'mac' | 'windows' | 'android' | 'linux' | 'bsd' | 'unknown';
41
+ type FingerprintParam = readonly [name: string, value: string];
42
+ interface DeviceIdentity {
43
+ /** Ordered identity params. Empty values are dropped by the material builder. */
44
+ params: FingerprintParam[];
45
+ /** Human-readable device name. Never part of the default material. */
46
+ deviceName: string;
47
+ }
48
+ /** Reads raw device identity for the current platform. Injectable for testing. */
49
+ interface DeviceIdentityReader {
50
+ read: () => DeviceIdentity;
51
+ }
52
+ /** A device id stamp broken into its parts (see {@link parseDeviceIdStamp}). */
53
+ interface DeviceIdStamp {
54
+ /** Fingerprint spec version that produced the digest. */
55
+ version: number;
56
+ source: DeviceIdSource;
57
+ /** The 64-char lowercase-hex SHA-256. */
58
+ digest: string;
59
+ }
60
+ /**
61
+ * Canonicalize a raw value read from the system (see FINGERPRINT_SPEC.md,
62
+ * "Canonicalizing values"): NFC-normalize, drop every character outside
63
+ * printable ASCII, cap the length, then trim spaces from both ends.
64
+ *
65
+ * Dropping non-printables is what makes the algorithm implementable
66
+ * byte-identically in any language: it removes the newline-injection ambiguity
67
+ * from the material grammar, and it makes the decoding choice for raw firmware
68
+ * strings (latin1 vs. UTF-8 vs. raw bytes) immaterial, since every byte those
69
+ * decodings disagree about is discarded either way.
70
+ */
71
+ declare function canonicalizeValue(value: string): string;
72
+ /** Map a Node.js `process.platform` value onto a canonical platform tag. */
73
+ declare function platformTag(platform?: NodeJS.Platform): PlatformTag;
74
+ /**
75
+ * The identifying parameter names, as a detached frozen list.
76
+ *
77
+ * Deliberately not the `Set` this module checks against. `ReadonlySet` is erased
78
+ * at compile time, so exporting the live set would let any JavaScript consumer —
79
+ * or any dependency — call `.add('sysVendor')` and turn model-level values into
80
+ * accepted identity for the whole process, silently undoing the collision
81
+ * protection below.
82
+ */
83
+ declare const IDENTIFYING_PARAM_NAMES: readonly string[];
84
+ /**
85
+ * Canonicalize every value and drop the ones that end up empty, plus any
86
+ * identifying parameter holding an unprogrammed placeholder. Order is preserved.
87
+ * Exported so a caller can see which parameters actually contributed without
88
+ * rebuilding the material.
89
+ *
90
+ * @throws when two surviving parameters share a name — the material grammar has
91
+ * no way to express that, so it is a collection bug rather than a machine state.
92
+ */
93
+ declare function canonicalizeParams(params: ReadonlyArray<FingerprintParam>): FingerprintParam[];
94
+ /**
95
+ * Assemble the canonical fingerprint material string. Pure and deterministic —
96
+ * exported so consumers can verify cross-SDK parity against the shipped vectors.
97
+ *
98
+ * @throws {InsufficientDeviceIdentityError} when no parameter survives canonicalization, or when
99
+ * none of the survivors identifies the individual machine.
100
+ */
101
+ declare function buildFingerprintMaterial(platform: PlatformTag, params: ReadonlyArray<FingerprintParam>): string;
102
+ /** Hash fingerprint material into a bare digest: 64-char lowercase hex SHA-256. */
103
+ declare function fingerprintDigest(material: string): string;
104
+ /** Prefix a digest with its version and source, producing the wire-form device id. */
105
+ declare function stampDeviceId(digest: string, source?: DeviceIdSource): string;
106
+ /** Hash material and stamp it: the full device id as sent to Moonbase and stored in `sig`. */
107
+ declare function fingerprintDeviceId(material: string, source?: DeviceIdSource): string;
108
+ /**
109
+ * Split a stamped device id into its parts, or `null` if it is not a Moonbase
110
+ * stamp (a legacy id, or one from a custom resolver). Lets a validator tell
111
+ * "this license belongs to another machine" apart from "this license was bound
112
+ * by an older fingerprint version".
113
+ */
114
+ declare function parseDeviceIdStamp(deviceId: string): DeviceIdStamp | null;
115
+ /** Extract `IOPlatformUUID` from `ioreg` output: hyphens stripped, uppercased (spec: macOS `ioPlatformUuid`). */
116
+ declare function parseIoregPlatformUuid(ioregOutput: string): string;
117
+ /**
118
+ * Pick the first source holding a real machine id, or `''`.
119
+ *
120
+ * Each candidate is *validated*, not merely checked for being non-empty:
121
+ * `machine-id(5)` defines the file as exactly 32 lowercase hex digits, and
122
+ * legitimately holds the literal marker `uninitialized` in an initrd or a golden
123
+ * image awaiting first boot. Every machine deployed from such an image reads that
124
+ * same marker, so accepting it would hand them all one device id — and would also
125
+ * stop the fall-through to a D-Bus id that may be perfectly valid.
126
+ */
127
+ declare function selectMachineId(...sources: string[]): string;
128
+ /**
129
+ * Extract the ordered identity params from a raw SMBIOS structure table: the
130
+ * **first** type-1 (System) and **first** type-2 (Baseboard) structures only.
131
+ *
132
+ * Type 4 (Processor) is deliberately not collected — its values are model-class
133
+ * rather than per-machine, and the number of type-4 structures tracks the CPU
134
+ * socket / vCPU count, so collecting them would change the device id whenever a
135
+ * VM is resized.
136
+ */
137
+ declare function parseSmbiosParams(smbiosData: Buffer): FingerprintParam[];
138
+ /** The real, platform-dispatching identity reader used by {@link MoonbaseDeviceIdResolver}. */
139
+ declare function defaultDeviceIdentityReader(platform?: NodeJS.Platform): DeviceIdentityReader;
140
+
3
141
  interface IDeviceIdResolver {
142
+ resolveDeviceName: () => Promise<string>;
143
+ resolveDeviceId: () => Promise<string>;
144
+ }
145
+ /**
146
+ * A device id plus the provenance needed to reason about it — safe to log, show
147
+ * in an about box, or attach to a support ticket.
148
+ *
149
+ * Parameter *names* only, deliberately. Their values are hardware serial numbers,
150
+ * and a hash of one is no safer to publish: an unsalted digest is a stable global
151
+ * correlator for the machine, and low-entropy values like host names or
152
+ * sequential serials fall to a dictionary. `machine-id(5)` is explicit that the
153
+ * Linux machine id is confidential and must only ever be exposed through an
154
+ * application-specific *keyed* hash. Which parameters contributed is the useful
155
+ * diagnostic anyway; what they read is not.
156
+ */
157
+ interface DeviceIdDescription {
158
+ deviceId: string;
159
+ /** Fingerprint spec version that produced it. */
160
+ version: number;
161
+ platform: PlatformTag;
162
+ source: DeviceIdSource;
163
+ /** Names of the identity parameters that went into the material, in order. */
164
+ paramNames: string[];
165
+ }
166
+ /** A resolver that can explain how it arrived at its device id. */
167
+ interface IDescribableDeviceIdResolver extends IDeviceIdResolver {
168
+ describeDevice: () => Promise<DeviceIdDescription>;
169
+ }
170
+ /**
171
+ * A resolver that also recognises device ids this machine used to have.
172
+ *
173
+ * Deliberately separate from {@link IDeviceIdResolver.resolveDeviceId}, which
174
+ * stays single-valued: what a device binds on activation and what a validator
175
+ * accepts are different questions, and conflating them is what forces the
176
+ * all-or-nothing migration choice.
177
+ */
178
+ interface IMigratingDeviceIdResolver extends IDeviceIdResolver {
179
+ acceptsDeviceId: (deviceId: string) => Promise<boolean>;
180
+ }
181
+ interface MoonbaseDeviceIdResolverOptions {
182
+ /** Overrides the identity source. Primarily for testing. */
183
+ reader?: DeviceIdentityReader;
184
+ /** Overrides the detected platform. Primarily for testing. */
185
+ platform?: NodeJS.Platform;
186
+ /**
187
+ * What to do when no hardware identity is readable. `'none'` (the default)
188
+ * throws {@link InsufficientDeviceIdentityError}; `'deviceName'` falls back to
189
+ * hashing the host name, producing a deliberately weaker id stamped `mbd2n_`.
190
+ *
191
+ * The fallback is opt-in because a host name is user-renameable, frequently
192
+ * duplicated across imaged machines, and regenerated on every container start.
193
+ */
194
+ fallback?: 'none' | 'deviceName';
195
+ }
196
+ /**
197
+ * Default resolver. Implements the Moonbase device fingerprint spec (v2; see
198
+ * FINGERPRINT_SPEC.md), building the `moonbase:fingerprint:v2` material from
199
+ * native hardware identifiers (SMBIOS on Windows, `IOPlatformUUID` on macOS,
200
+ * `machine-id` + DMI on Linux) and stamping its SHA-256 as `mbd2_<hex>`. Every
201
+ * Moonbase SDK that implements the spec produces the same id on a given machine.
202
+ *
203
+ * The result is memoized: reading identity can mean spawning a subprocess, and
204
+ * both activation and every validation ask for it.
205
+ */
206
+ declare class MoonbaseDeviceIdResolver implements IDescribableDeviceIdResolver {
207
+ private readonly reader;
208
+ private readonly platform;
209
+ private readonly fallback;
210
+ private identity?;
211
+ private described?;
212
+ constructor(options?: MoonbaseDeviceIdResolverOptions);
213
+ resolveDeviceName(): Promise<string>;
214
+ resolveDeviceId(): Promise<string>;
215
+ /**
216
+ * A fresh copy each call. The device binding is what activation sends and what
217
+ * validation compares, so handing out the resolver's own object would let a
218
+ * consumer that edits a diagnostic — or logs it through something that
219
+ * normalizes in place — silently change the id every later call returns.
220
+ */
221
+ describeDevice(): Promise<DeviceIdDescription>;
222
+ private compute;
223
+ /**
224
+ * Read identity at most once. Both halves of an activation request ask for it —
225
+ * the name and then the id — and a read can mean spawning `ioreg` or
226
+ * PowerShell, so reading per call would double the cost of every request and
227
+ * let the name and the id come from two different reads of the machine.
228
+ */
229
+ private readIdentity;
230
+ private computeDescription;
231
+ private describe;
232
+ }
233
+ /**
234
+ * Binds the current fingerprint but keeps recognising ids this device was bound
235
+ * to before — the migration path off an older algorithm without a flag day.
236
+ *
237
+ * `resolveDeviceId` always returns the *current* resolver's id, so every new
238
+ * activation binds the current algorithm; the historical resolvers are consulted
239
+ * only when a validator is deciding whether to accept an already-issued license.
240
+ * A fleet therefore migrates as licenses are naturally re-activated, instead of
241
+ * every device re-activating at once — which would burn a second activation seat
242
+ * per device and reset device-scoped trials.
243
+ *
244
+ * ```ts
245
+ * new MigratingDeviceIdResolver(new MoonbaseDeviceIdResolver(), new LegacyDeviceIdResolver())
246
+ * ```
247
+ *
248
+ * Historical ids are computed lazily — only on a mismatch — and then memoized,
249
+ * so the cost is never paid on the happy path. A historical resolver that throws
250
+ * is simply skipped.
251
+ *
252
+ * Every accepted id is recomputed from the machine's own hardware on each call.
253
+ * Nothing is read from disk, so widening what a validator accepts does not widen
254
+ * what an attacker can assert.
255
+ */
256
+ declare class MigratingDeviceIdResolver implements IMigratingDeviceIdResolver {
257
+ private readonly current;
258
+ private readonly previous;
259
+ private previousIds?;
260
+ /** Forwarded so the current resolver stays describable through this wrapper. */
261
+ describeDevice?: () => Promise<DeviceIdDescription>;
262
+ constructor(current: IDeviceIdResolver, ...previous: IDeviceIdResolver[]);
4
263
  resolveDeviceName(): Promise<string>;
5
264
  resolveDeviceId(): Promise<string>;
265
+ acceptsDeviceId(deviceId: string): Promise<boolean>;
6
266
  }
7
- declare class DefaultDeviceIdResolver implements IDeviceIdResolver {
267
+ /**
268
+ * @deprecated The previous default resolver. Its device id is a base64 SHA-256 of
269
+ * normalized `systeminformation` fields and does **not** implement the
270
+ * `moonbase:fingerprint:v2` spec (FINGERPRINT_SPEC.md). Kept so deployments can keep
271
+ * validating licenses that were bound under the old id during migration; prefer
272
+ * {@link MoonbaseDeviceIdResolver}.
273
+ */
274
+ declare class LegacyDeviceIdResolver implements IDeviceIdResolver {
8
275
  resolveDeviceName(): Promise<string>;
9
276
  resolveDeviceId(): Promise<string>;
10
277
  }
@@ -175,7 +442,7 @@ interface ILicenseClient {
175
442
  *
176
443
  * @returns Details about the requested activation with url to open in the browser
177
444
  */
178
- requestActivation(): Promise<ActivationRequestResponse>;
445
+ requestActivation: () => Promise<ActivationRequestResponse>;
179
446
  /**
180
447
  * Takes in a activation request created through {@link requestActivation}
181
448
  * and checks if the request has been fulfilled yet.
@@ -185,7 +452,7 @@ interface ILicenseClient {
185
452
  * @param request The activation request to poll
186
453
  * @returns A license if the request has been fulfilled, else null
187
454
  */
188
- getRequestedActivation(request: ActivationRequestResponse): Promise<License | null>;
455
+ getRequestedActivation: (request: ActivationRequestResponse) => Promise<License | null>;
189
456
  /**
190
457
  * Requests a trial license for the product.
191
458
  * This can only be done once per product per device, and skips
@@ -194,7 +461,7 @@ interface ILicenseClient {
194
461
  *
195
462
  * @returns The allocated trial license, throws if the trial license cannot be allocated (e.g. user already had a trial) or if the request failed
196
463
  */
197
- requestTrial(): Promise<License>;
464
+ requestTrial: () => Promise<License>;
198
465
  /**
199
466
  * Checks if the given license is still valid, not revoked
200
467
  * and returns an updated license if still active.
@@ -205,7 +472,7 @@ interface ILicenseClient {
205
472
  * @param license The license to validate
206
473
  * @returns An updated license
207
474
  */
208
- validateLicense(license: License): Promise<License>;
475
+ validateLicense: (license: License) => Promise<License>;
209
476
  /**
210
477
  * Contrary to {@linkvalidateLicense }, this method takes
211
478
  * in raw bytes of the license, which can be useful if you're
@@ -214,14 +481,14 @@ interface ILicenseClient {
214
481
  * @param license The license to validate
215
482
  * @returns An updated license
216
483
  */
217
- validateRawLicense(rawLicense: Buffer): Promise<License>;
484
+ validateRawLicense: (rawLicense: Buffer) => Promise<License>;
218
485
  /**
219
486
  * Given a license, this method will try to revoke the license activation,
220
487
  * freeing up a seat for other devices to activate the license.
221
488
  * This will not work for offline-activated devices.
222
489
  * @param license The license to revoke
223
490
  */
224
- revokeLicense(license: License): Promise<void>;
491
+ revokeLicense: (license: License) => Promise<void>;
225
492
  }
226
493
  declare class LicenseClient implements ILicenseClient {
227
494
  private readonly configuration;
@@ -239,9 +506,9 @@ declare class LicenseClient implements ILicenseClient {
239
506
  }
240
507
 
241
508
  interface ILicenseStore {
242
- loadLocalLicense(): Promise<License | null>;
243
- storeLocalLicense(license: License): Promise<void>;
244
- deleteLocalLicense(): Promise<void>;
509
+ loadLocalLicense: () => Promise<License | null>;
510
+ storeLocalLicense: (license: License) => Promise<void>;
511
+ deleteLocalLicense: () => Promise<void>;
245
512
  }
246
513
  declare class InMemoryLicenseStore implements ILicenseStore {
247
514
  private license?;
@@ -262,13 +529,19 @@ declare class FileLicenseStore implements ILicenseStore {
262
529
  }
263
530
 
264
531
  interface ILicenseValidator {
265
- validateLicense(token: string): Promise<License>;
532
+ validateLicense: (token: string) => Promise<License>;
266
533
  }
267
534
  declare class LicenseValidator implements ILicenseValidator {
268
535
  private readonly configuration;
269
536
  private readonly deviceIdResolver;
270
537
  constructor(configuration: MoonbaseConfiguration, deviceIdResolver: IDeviceIdResolver);
271
538
  validateLicense(token: string): Promise<License>;
539
+ /**
540
+ * Give a {@link IMigratingDeviceIdResolver} the chance to vouch for a device id
541
+ * this machine used to have. Only consulted after the fast path fails, so
542
+ * apps that have not opted into a migration pay nothing.
543
+ */
544
+ private acceptsHistoricalDeviceId;
272
545
  private parseLicenseToken;
273
546
  }
274
547
 
@@ -279,7 +552,11 @@ declare enum ErrorType {
279
552
  LicenseInvalid = "LicenseInvalid",
280
553
  LicenseRevoked = "LicenseRevoked",
281
554
  LicenseActivationRevoked = "LicenseActivationRevoked",
282
- LicenseExpired = "LicenseExpired"
555
+ LicenseExpired = "LicenseExpired",
556
+ /** The license is valid but bound to a different device, or to an older fingerprint version. */
557
+ LicenseDeviceMismatch = "LicenseDeviceMismatch",
558
+ /** No stable hardware identifier could be read, so no device id can be computed. */
559
+ DeviceIdentityUnavailable = "DeviceIdentityUnavailable"
283
560
  }
284
561
  declare class MoonbaseError extends Error {
285
562
  readonly title: string;
@@ -288,6 +565,29 @@ declare class MoonbaseError extends Error {
288
565
  readonly inner?: Error | undefined;
289
566
  constructor(title: string, detail: string | undefined, type?: ErrorType | undefined, inner?: Error | undefined);
290
567
  }
568
+ /**
569
+ * Thrown when the device fingerprint has nothing machine-specific to hash —
570
+ * either no parameter could be read at all, or the only ones that could are
571
+ * model-level (vendor, product and board names, shared by every unit of a
572
+ * product line).
573
+ *
574
+ * The spec deliberately makes both an error rather than hashing what is there:
575
+ * either would hand a whole class of machines the *same* device id, and a license
576
+ * bound to it would validate on all of them.
577
+ *
578
+ * Reachable on platforms with no defined identity parameters (Android, BSD,
579
+ * anything unknown); when every source fails — a sandboxed process that cannot
580
+ * spawn `ioreg`, a container with no DMI, a blocked PowerShell; and on machines
581
+ * whose per-device identifiers are simply absent, such as a Linux install with no
582
+ * `machine-id` or a VM whose SMBIOS carries an unset UUID and a blank baseboard
583
+ * serial. Enable the host-name fallback
584
+ * (`new MoonbaseDeviceIdResolver({ fallback: 'deviceName' })`) to accept a
585
+ * deliberately weaker id on those machines.
586
+ */
587
+ declare class InsufficientDeviceIdentityError extends MoonbaseError {
588
+ readonly platform: string;
589
+ constructor(platform: string, reason?: string);
590
+ }
291
591
 
292
592
  interface MoonbaseConfiguration {
293
593
  endpoint: string;
@@ -318,7 +618,7 @@ interface MoonbaseConfiguration {
318
618
  */
319
619
  appVersion?: string;
320
620
  licenseStore?: ILicenseStore;
321
- deivceIdResolver?: IDeviceIdResolver;
621
+ deviceIdResolver?: IDeviceIdResolver;
322
622
  }
323
623
  declare class MoonbaseLicensing {
324
624
  private readonly configuration;
@@ -331,4 +631,4 @@ declare class MoonbaseLicensing {
331
631
  readRawLicense(license: Buffer): Promise<License>;
332
632
  }
333
633
 
334
- export { ActivationMethod, type ActivationRequestResponse, DefaultDeviceIdResolver, type DeviceToken, ErrorType, FileLicenseStore, type IDeviceIdResolver, type ILicenseClient, type ILicenseStore, type ILicenseValidator, InMemoryLicenseStore, type License, LicenseClient, LicenseValidator, type Metadata, type MoonbaseConfiguration, MoonbaseError, MoonbaseLicensing, type Platform, type Product, type User };
634
+ export { ActivationMethod, type ActivationRequestResponse, type DeviceIdDescription, type DeviceIdSource, type DeviceIdStamp, type DeviceIdentity, type DeviceIdentityReader, type DeviceToken, ErrorType, FINGERPRINT_PREFIX, FINGERPRINT_VERSION, FileLicenseStore, type FingerprintParam, IDENTIFYING_PARAM_NAMES, type IDescribableDeviceIdResolver, type IDeviceIdResolver, type ILicenseClient, type ILicenseStore, type ILicenseValidator, type IMigratingDeviceIdResolver, InMemoryLicenseStore, InsufficientDeviceIdentityError, LegacyDeviceIdResolver, type License, LicenseClient, LicenseValidator, MAX_VALUE_LENGTH, type Metadata, MigratingDeviceIdResolver, type MoonbaseConfiguration, MoonbaseDeviceIdResolver, type MoonbaseDeviceIdResolverOptions, MoonbaseError, MoonbaseLicensing, type Platform, type PlatformTag, type Product, type User, buildFingerprintMaterial, canonicalizeParams, canonicalizeValue, defaultDeviceIdentityReader, fingerprintDeviceId, fingerprintDigest, parseDeviceIdStamp, parseIoregPlatformUuid, parseSmbiosParams, platformTag, selectMachineId, stampDeviceId };