@moonbase.sh/licensing 3.1.0 → 3.2.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
@@ -59,19 +59,68 @@ console.log('License stored locally')
59
59
 
60
60
  ## Validate on startup
61
61
 
62
- ```ts
63
- const localLicense = await licensing.store.loadLocalLicense()
64
-
65
- if (localLicense) {
66
- // Validate token signature + device binding locally
67
- const locallyValidated = await licensing.validator.validateLicense(localLicense.token)
62
+ Only delete the stored license when Moonbase says the license is gone. Deleting it on any error makes
63
+ the user re-activate every time a check fails for a reason that has nothing to do with the license,
64
+ such as opening the app offline.
68
65
 
69
- // Re-validate against Moonbase (recommended for online activations)
70
- const refreshed = await licensing.client.validateLicense(locallyValidated)
71
- await licensing.store.storeLocalLicense(refreshed)
66
+ ```ts
67
+ import type { License } from '@moonbase.sh/licensing'
68
+ import { ActivationMethod, ErrorType, MoonbaseError } from '@moonbase.sh/licensing'
69
+
70
+ // Moonbase says the license can no longer be used on this device. Anything
71
+ // else, an ErrorType.ApiError or a network failure (which is not a
72
+ // MoonbaseError), only means Moonbase could not be reached right now.
73
+ const licenseGone = new Set<ErrorType | undefined>([
74
+ ErrorType.LicenseRevoked,
75
+ ErrorType.LicenseActivationRevoked,
76
+ ErrorType.LicenseExpired,
77
+ ErrorType.LicenseInvalid,
78
+ ErrorType.LicenseDeviceMismatch,
79
+ ErrorType.StoreClosed,
80
+ ])
81
+
82
+ async function licenseOnStartup(): Promise<License | null> {
83
+ const localLicense = await licensing.store.loadLocalLicense()
84
+ if (!localLicense)
85
+ return null
86
+
87
+ if (localLicense.activationMethod === ActivationMethod.Online) {
88
+ try {
89
+ // Ask Moonbase first: it is how a revocation reaches the app, and how a
90
+ // token whose expiry has passed is refreshed after a renewal
91
+ const refreshed = await licensing.client.validateLicense(localLicense)
92
+ await licensing.store.storeLocalLicense(refreshed)
93
+ return refreshed
94
+ }
95
+ catch (err) {
96
+ if (err instanceof MoonbaseError && licenseGone.has(err.type)) {
97
+ await licensing.store.deleteLocalLicense()
98
+ return null // show your activation flow
99
+ }
100
+ // Moonbase could not be reached, so check the license locally instead
101
+ }
102
+ }
103
+
104
+ try {
105
+ // Signature, device binding and expiry, which needs no network
106
+ return await licensing.validator.validateLicense(localLicense.token)
107
+ }
108
+ catch (err) {
109
+ // Keep the stored token: only Moonbase can say the license is gone, and the
110
+ // next launch that reaches it can still refresh a token that has expired
111
+ if (err instanceof MoonbaseError)
112
+ return null // show your activation flow
113
+ throw err
114
+ }
72
115
  }
73
116
  ```
74
117
 
118
+ A revoked license reports `ErrorType.LicenseRevoked`; a revoked device, whose license may still be
119
+ active, reports `ErrorType.LicenseActivationRevoked`. To limit how long an app keeps running without
120
+ reaching Moonbase, compare `license.validatedAt` with a limit of your choosing before returning the
121
+ locally validated license. For `ErrorType.LicenseDeviceMismatch`, see [Device fingerprint](#device-fingerprint)
122
+ first: after an SDK update, it can mean the same machine under an older fingerprint.
123
+
75
124
  ## Metadata, platform, and app version
76
125
 
77
126
  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.
package/dist/index.cjs CHANGED
@@ -68,6 +68,43 @@ var import_node_process3 = __toESM(require("process"), 1);
68
68
  // src/client.ts
69
69
  var import_cross_fetch = __toESM(require("cross-fetch"), 1);
70
70
 
71
+ // src/errors.ts
72
+ var ErrorType = /* @__PURE__ */ ((ErrorType2) => {
73
+ ErrorType2["None"] = "None";
74
+ ErrorType2["ApiError"] = "ApiError";
75
+ ErrorType2["NoEligibleLicense"] = "NoEligibleLicense";
76
+ ErrorType2["LicenseInvalid"] = "LicenseInvalid";
77
+ ErrorType2["LicenseRevoked"] = "LicenseRevoked";
78
+ ErrorType2["LicenseActivationRevoked"] = "LicenseActivationRevoked";
79
+ ErrorType2["LicenseExpired"] = "LicenseExpired";
80
+ ErrorType2["LicenseDeviceMismatch"] = "LicenseDeviceMismatch";
81
+ ErrorType2["DeviceIdentityUnavailable"] = "DeviceIdentityUnavailable";
82
+ ErrorType2["StoreClosed"] = "StoreClosed";
83
+ return ErrorType2;
84
+ })(ErrorType || {});
85
+ var MoonbaseError = class extends Error {
86
+ constructor(title, detail, type, inner) {
87
+ super();
88
+ this.title = title;
89
+ this.detail = detail;
90
+ this.type = type;
91
+ this.inner = inner;
92
+ this.name = "MoonbaseError";
93
+ this.message = detail != null ? detail : title;
94
+ }
95
+ };
96
+ var InsufficientDeviceIdentityError = class extends MoonbaseError {
97
+ constructor(platform, reason = "no identity parameter could be read") {
98
+ super(
99
+ "No device identity",
100
+ `Could not identify this device (platform: ${platform}): ${reason}`,
101
+ "DeviceIdentityUnavailable" /* DeviceIdentityUnavailable */
102
+ );
103
+ this.platform = platform;
104
+ this.name = "InsufficientDeviceIdentityError";
105
+ }
106
+ };
107
+
71
108
  // src/schemas.ts
72
109
  var import_zod = require("zod");
73
110
 
@@ -84,6 +121,11 @@ var activationRequestResponseSchema = import_zod.z.object({
84
121
  request: import_zod.z.string(),
85
122
  browser: import_zod.z.string()
86
123
  });
124
+ var problemDetailsSchema = import_zod.z.object({
125
+ title: import_zod.z.string().nullish(),
126
+ detail: import_zod.z.string().nullish(),
127
+ errorType: import_zod.z.string().nullish()
128
+ });
87
129
  var productSchema = import_zod.z.object({
88
130
  id: import_zod.z.string(),
89
131
  name: import_zod.z.string(),
@@ -138,7 +180,7 @@ var LicenseClient = class {
138
180
  body: JSON.stringify(content)
139
181
  });
140
182
  if (response.status >= 400) {
141
- throw new MoonbaseError("Request not successful", `The API responded with a ${response.status} ${response.statusText} response`, "ApiError" /* ApiError */);
183
+ throw await errorFromResponse(response);
142
184
  }
143
185
  try {
144
186
  return activationRequestResponseSchema.parse(await response.json());
@@ -203,12 +245,12 @@ var LicenseClient = class {
203
245
  body: license.token
204
246
  });
205
247
  if (response.status >= 400) {
206
- throw new MoonbaseError("Request not successful", `The API responded with a ${response.status} ${response.statusText} response`, "ApiError" /* ApiError */);
248
+ throw await errorFromResponse(response);
207
249
  }
208
250
  }
209
251
  async handleLicenseResponse(response) {
210
252
  if (response.status >= 400) {
211
- throw new MoonbaseError("Request not successful", `The API responded with a ${response.status} ${response.statusText} response`, "ApiError" /* ApiError */);
253
+ throw await errorFromResponse(response);
212
254
  }
213
255
  return await this.licenseValidator.validateLicense(await response.text());
214
256
  }
@@ -229,6 +271,36 @@ var LicenseClient = class {
229
271
  return `?${parts.join("&")}`;
230
272
  }
231
273
  };
274
+ var licenseErrorTypes = /* @__PURE__ */ new Set([
275
+ "NoEligibleLicense" /* NoEligibleLicense */,
276
+ "LicenseInvalid" /* LicenseInvalid */,
277
+ "LicenseRevoked" /* LicenseRevoked */,
278
+ "LicenseActivationRevoked" /* LicenseActivationRevoked */,
279
+ "LicenseExpired" /* LicenseExpired */,
280
+ "StoreClosed" /* StoreClosed */
281
+ ]);
282
+ async function errorFromResponse(response) {
283
+ const fallback = new MoonbaseError(
284
+ "Request not successful",
285
+ `The API responded with a ${response.status} ${response.statusText} response`,
286
+ "ApiError" /* ApiError */
287
+ );
288
+ let body;
289
+ try {
290
+ body = JSON.parse(await response.text());
291
+ } catch (e) {
292
+ return fallback;
293
+ }
294
+ const problem = problemDetailsSchema.safeParse(body);
295
+ if (!problem.success)
296
+ return fallback;
297
+ const { title, detail, errorType } = problem.data;
298
+ return new MoonbaseError(
299
+ title != null ? title : fallback.title,
300
+ detail != null ? detail : fallback.detail,
301
+ errorType && licenseErrorTypes.has(errorType) ? errorType : "ApiError" /* ApiError */
302
+ );
303
+ }
232
304
 
233
305
  // src/deviceIdResolver.ts
234
306
  var import_node_child_process2 = __toESM(require("child_process"), 1);
@@ -237,42 +309,6 @@ var import_node_os2 = __toESM(require("os"), 1);
237
309
  var import_node_process2 = __toESM(require("process"), 1);
238
310
  var import_systeminformation = __toESM(require("systeminformation"), 1);
239
311
 
240
- // src/errors.ts
241
- var ErrorType = /* @__PURE__ */ ((ErrorType2) => {
242
- ErrorType2["None"] = "None";
243
- ErrorType2["ApiError"] = "ApiError";
244
- ErrorType2["NoEligibleLicense"] = "NoEligibleLicense";
245
- ErrorType2["LicenseInvalid"] = "LicenseInvalid";
246
- ErrorType2["LicenseRevoked"] = "LicenseRevoked";
247
- ErrorType2["LicenseActivationRevoked"] = "LicenseActivationRevoked";
248
- ErrorType2["LicenseExpired"] = "LicenseExpired";
249
- ErrorType2["LicenseDeviceMismatch"] = "LicenseDeviceMismatch";
250
- ErrorType2["DeviceIdentityUnavailable"] = "DeviceIdentityUnavailable";
251
- return ErrorType2;
252
- })(ErrorType || {});
253
- var MoonbaseError = class extends Error {
254
- constructor(title, detail, type, inner) {
255
- super();
256
- this.title = title;
257
- this.detail = detail;
258
- this.type = type;
259
- this.inner = inner;
260
- this.name = "MoonbaseError";
261
- this.message = detail != null ? detail : title;
262
- }
263
- };
264
- var InsufficientDeviceIdentityError = class extends MoonbaseError {
265
- constructor(platform, reason = "no identity parameter could be read") {
266
- super(
267
- "No device identity",
268
- `Could not identify this device (platform: ${platform}): ${reason}`,
269
- "DeviceIdentityUnavailable" /* DeviceIdentityUnavailable */
270
- );
271
- this.platform = platform;
272
- this.name = "InsufficientDeviceIdentityError";
273
- }
274
- };
275
-
276
312
  // src/fingerprint.ts
277
313
  var import_node_buffer = require("buffer");
278
314
  var import_node_child_process = __toESM(require("child_process"), 1);
package/dist/index.d.cts CHANGED
@@ -597,18 +597,39 @@ declare class LicenseValidator implements ILicenseValidator {
597
597
  private parseLicenseToken;
598
598
  }
599
599
 
600
+ /**
601
+ * What went wrong, on a {@link MoonbaseError}. The license types say the license
602
+ * is no longer usable, so a stored copy can be deleted. `ApiError` only says no
603
+ * answer was had: keep the stored license and try again later.
604
+ */
600
605
  declare enum ErrorType {
601
606
  None = "None",
607
+ /**
608
+ * The request failed without saying anything about the license: an outage, a
609
+ * rate limit, a proxy error page. Transient, so keep any stored license. A
610
+ * network failure is not a `MoonbaseError` at all, and is transient too.
611
+ */
602
612
  ApiError = "ApiError",
613
+ /** The customer has no license for this product to activate. */
603
614
  NoEligibleLicense = "NoEligibleLicense",
615
+ /** The license token failed verification (signature, issuer or product), or the stored license could not be read. */
604
616
  LicenseInvalid = "LicenseInvalid",
617
+ /** The merchant revoked the license, for example after a refund. */
605
618
  LicenseRevoked = "LicenseRevoked",
619
+ /** This device's activation was revoked, freeing its seat. The license itself may still be active. */
606
620
  LicenseActivationRevoked = "LicenseActivationRevoked",
621
+ /** The license, or trial, has expired. */
607
622
  LicenseExpired = "LicenseExpired",
608
623
  /** The license is valid but bound to a different device, or to an older fingerprint version. */
609
624
  LicenseDeviceMismatch = "LicenseDeviceMismatch",
610
625
  /** No stable hardware identifier could be read, so no device id can be computed. */
611
- DeviceIdentityUnavailable = "DeviceIdentityUnavailable"
626
+ DeviceIdentityUnavailable = "DeviceIdentityUnavailable",
627
+ /**
628
+ * The store this product is sold through has closed its Moonbase account. Not transient:
629
+ * nothing about the license can be validated, activated or renewed online again. An offline
630
+ * license keeps validating, since that never asks the server.
631
+ */
632
+ StoreClosed = "StoreClosed"
612
633
  }
613
634
  declare class MoonbaseError extends Error {
614
635
  readonly title: string;
package/dist/index.d.ts CHANGED
@@ -597,18 +597,39 @@ declare class LicenseValidator implements ILicenseValidator {
597
597
  private parseLicenseToken;
598
598
  }
599
599
 
600
+ /**
601
+ * What went wrong, on a {@link MoonbaseError}. The license types say the license
602
+ * is no longer usable, so a stored copy can be deleted. `ApiError` only says no
603
+ * answer was had: keep the stored license and try again later.
604
+ */
600
605
  declare enum ErrorType {
601
606
  None = "None",
607
+ /**
608
+ * The request failed without saying anything about the license: an outage, a
609
+ * rate limit, a proxy error page. Transient, so keep any stored license. A
610
+ * network failure is not a `MoonbaseError` at all, and is transient too.
611
+ */
602
612
  ApiError = "ApiError",
613
+ /** The customer has no license for this product to activate. */
603
614
  NoEligibleLicense = "NoEligibleLicense",
615
+ /** The license token failed verification (signature, issuer or product), or the stored license could not be read. */
604
616
  LicenseInvalid = "LicenseInvalid",
617
+ /** The merchant revoked the license, for example after a refund. */
605
618
  LicenseRevoked = "LicenseRevoked",
619
+ /** This device's activation was revoked, freeing its seat. The license itself may still be active. */
606
620
  LicenseActivationRevoked = "LicenseActivationRevoked",
621
+ /** The license, or trial, has expired. */
607
622
  LicenseExpired = "LicenseExpired",
608
623
  /** The license is valid but bound to a different device, or to an older fingerprint version. */
609
624
  LicenseDeviceMismatch = "LicenseDeviceMismatch",
610
625
  /** No stable hardware identifier could be read, so no device id can be computed. */
611
- DeviceIdentityUnavailable = "DeviceIdentityUnavailable"
626
+ DeviceIdentityUnavailable = "DeviceIdentityUnavailable",
627
+ /**
628
+ * The store this product is sold through has closed its Moonbase account. Not transient:
629
+ * nothing about the license can be validated, activated or renewed online again. An offline
630
+ * license keeps validating, since that never asks the server.
631
+ */
632
+ StoreClosed = "StoreClosed"
612
633
  }
613
634
  declare class MoonbaseError extends Error {
614
635
  readonly title: string;
package/dist/index.js CHANGED
@@ -5,6 +5,43 @@ import process3 from "process";
5
5
  // src/client.ts
6
6
  import fetch from "cross-fetch";
7
7
 
8
+ // src/errors.ts
9
+ var ErrorType = /* @__PURE__ */ ((ErrorType2) => {
10
+ ErrorType2["None"] = "None";
11
+ ErrorType2["ApiError"] = "ApiError";
12
+ ErrorType2["NoEligibleLicense"] = "NoEligibleLicense";
13
+ ErrorType2["LicenseInvalid"] = "LicenseInvalid";
14
+ ErrorType2["LicenseRevoked"] = "LicenseRevoked";
15
+ ErrorType2["LicenseActivationRevoked"] = "LicenseActivationRevoked";
16
+ ErrorType2["LicenseExpired"] = "LicenseExpired";
17
+ ErrorType2["LicenseDeviceMismatch"] = "LicenseDeviceMismatch";
18
+ ErrorType2["DeviceIdentityUnavailable"] = "DeviceIdentityUnavailable";
19
+ ErrorType2["StoreClosed"] = "StoreClosed";
20
+ return ErrorType2;
21
+ })(ErrorType || {});
22
+ var MoonbaseError = class extends Error {
23
+ constructor(title, detail, type, inner) {
24
+ super();
25
+ this.title = title;
26
+ this.detail = detail;
27
+ this.type = type;
28
+ this.inner = inner;
29
+ this.name = "MoonbaseError";
30
+ this.message = detail != null ? detail : title;
31
+ }
32
+ };
33
+ var InsufficientDeviceIdentityError = class extends MoonbaseError {
34
+ constructor(platform, reason = "no identity parameter could be read") {
35
+ super(
36
+ "No device identity",
37
+ `Could not identify this device (platform: ${platform}): ${reason}`,
38
+ "DeviceIdentityUnavailable" /* DeviceIdentityUnavailable */
39
+ );
40
+ this.platform = platform;
41
+ this.name = "InsufficientDeviceIdentityError";
42
+ }
43
+ };
44
+
8
45
  // src/schemas.ts
9
46
  import { z } from "zod";
10
47
 
@@ -21,6 +58,11 @@ var activationRequestResponseSchema = z.object({
21
58
  request: z.string(),
22
59
  browser: z.string()
23
60
  });
61
+ var problemDetailsSchema = z.object({
62
+ title: z.string().nullish(),
63
+ detail: z.string().nullish(),
64
+ errorType: z.string().nullish()
65
+ });
24
66
  var productSchema = z.object({
25
67
  id: z.string(),
26
68
  name: z.string(),
@@ -75,7 +117,7 @@ var LicenseClient = class {
75
117
  body: JSON.stringify(content)
76
118
  });
77
119
  if (response.status >= 400) {
78
- throw new MoonbaseError("Request not successful", `The API responded with a ${response.status} ${response.statusText} response`, "ApiError" /* ApiError */);
120
+ throw await errorFromResponse(response);
79
121
  }
80
122
  try {
81
123
  return activationRequestResponseSchema.parse(await response.json());
@@ -140,12 +182,12 @@ var LicenseClient = class {
140
182
  body: license.token
141
183
  });
142
184
  if (response.status >= 400) {
143
- throw new MoonbaseError("Request not successful", `The API responded with a ${response.status} ${response.statusText} response`, "ApiError" /* ApiError */);
185
+ throw await errorFromResponse(response);
144
186
  }
145
187
  }
146
188
  async handleLicenseResponse(response) {
147
189
  if (response.status >= 400) {
148
- throw new MoonbaseError("Request not successful", `The API responded with a ${response.status} ${response.statusText} response`, "ApiError" /* ApiError */);
190
+ throw await errorFromResponse(response);
149
191
  }
150
192
  return await this.licenseValidator.validateLicense(await response.text());
151
193
  }
@@ -166,6 +208,36 @@ var LicenseClient = class {
166
208
  return `?${parts.join("&")}`;
167
209
  }
168
210
  };
211
+ var licenseErrorTypes = /* @__PURE__ */ new Set([
212
+ "NoEligibleLicense" /* NoEligibleLicense */,
213
+ "LicenseInvalid" /* LicenseInvalid */,
214
+ "LicenseRevoked" /* LicenseRevoked */,
215
+ "LicenseActivationRevoked" /* LicenseActivationRevoked */,
216
+ "LicenseExpired" /* LicenseExpired */,
217
+ "StoreClosed" /* StoreClosed */
218
+ ]);
219
+ async function errorFromResponse(response) {
220
+ const fallback = new MoonbaseError(
221
+ "Request not successful",
222
+ `The API responded with a ${response.status} ${response.statusText} response`,
223
+ "ApiError" /* ApiError */
224
+ );
225
+ let body;
226
+ try {
227
+ body = JSON.parse(await response.text());
228
+ } catch (e) {
229
+ return fallback;
230
+ }
231
+ const problem = problemDetailsSchema.safeParse(body);
232
+ if (!problem.success)
233
+ return fallback;
234
+ const { title, detail, errorType } = problem.data;
235
+ return new MoonbaseError(
236
+ title != null ? title : fallback.title,
237
+ detail != null ? detail : fallback.detail,
238
+ errorType && licenseErrorTypes.has(errorType) ? errorType : "ApiError" /* ApiError */
239
+ );
240
+ }
169
241
 
170
242
  // src/deviceIdResolver.ts
171
243
  import cp2 from "child_process";
@@ -174,42 +246,6 @@ import os2 from "os";
174
246
  import process2 from "process";
175
247
  import si from "systeminformation";
176
248
 
177
- // src/errors.ts
178
- var ErrorType = /* @__PURE__ */ ((ErrorType2) => {
179
- ErrorType2["None"] = "None";
180
- ErrorType2["ApiError"] = "ApiError";
181
- ErrorType2["NoEligibleLicense"] = "NoEligibleLicense";
182
- ErrorType2["LicenseInvalid"] = "LicenseInvalid";
183
- ErrorType2["LicenseRevoked"] = "LicenseRevoked";
184
- ErrorType2["LicenseActivationRevoked"] = "LicenseActivationRevoked";
185
- ErrorType2["LicenseExpired"] = "LicenseExpired";
186
- ErrorType2["LicenseDeviceMismatch"] = "LicenseDeviceMismatch";
187
- ErrorType2["DeviceIdentityUnavailable"] = "DeviceIdentityUnavailable";
188
- return ErrorType2;
189
- })(ErrorType || {});
190
- var MoonbaseError = class extends Error {
191
- constructor(title, detail, type, inner) {
192
- super();
193
- this.title = title;
194
- this.detail = detail;
195
- this.type = type;
196
- this.inner = inner;
197
- this.name = "MoonbaseError";
198
- this.message = detail != null ? detail : title;
199
- }
200
- };
201
- var InsufficientDeviceIdentityError = class extends MoonbaseError {
202
- constructor(platform, reason = "no identity parameter could be read") {
203
- super(
204
- "No device identity",
205
- `Could not identify this device (platform: ${platform}): ${reason}`,
206
- "DeviceIdentityUnavailable" /* DeviceIdentityUnavailable */
207
- );
208
- this.platform = platform;
209
- this.name = "InsufficientDeviceIdentityError";
210
- }
211
- };
212
-
213
249
  // src/fingerprint.ts
214
250
  import { Buffer } from "buffer";
215
251
  import cp from "child_process";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@moonbase.sh/licensing",
3
3
  "type": "module",
4
- "version": "3.1.0",
4
+ "version": "3.2.0",
5
5
  "description": "Package to add sotftware licensing using Moonbase.sh to your node.js apps",
6
6
  "author": "Tobias Lønnerød Madsen <m@dsen.tv>",
7
7
  "license": "MIT",
@@ -33,6 +33,8 @@
33
33
  "dev": "tsup src/index.ts --format esm,cjs --watch --dts",
34
34
  "version": "npm version",
35
35
  "type-check": "tsc --noEmit -p tsconfig.typecheck.json",
36
+ "lint": "eslint . --cache --cache-location node_modules/.cache/eslint/ --cache-strategy content",
37
+ "lint:fix": "eslint . --cache --cache-location node_modules/.cache/eslint/ --cache-strategy content --fix",
36
38
  "test": "vitest run",
37
39
  "test:watch": "vitest",
38
40
  "test:integration": "TEST_INTEGRATION=true vitest run"