@moonbase.sh/licensing 3.1.0 → 3.1.1

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,67 @@ 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
+ ])
80
+
81
+ async function licenseOnStartup(): Promise<License | null> {
82
+ const localLicense = await licensing.store.loadLocalLicense()
83
+ if (!localLicense)
84
+ return null
85
+
86
+ if (localLicense.activationMethod === ActivationMethod.Online) {
87
+ try {
88
+ // Ask Moonbase first: it is how a revocation reaches the app, and how a
89
+ // token whose expiry has passed is refreshed after a renewal
90
+ const refreshed = await licensing.client.validateLicense(localLicense)
91
+ await licensing.store.storeLocalLicense(refreshed)
92
+ return refreshed
93
+ }
94
+ catch (err) {
95
+ if (err instanceof MoonbaseError && licenseGone.has(err.type)) {
96
+ await licensing.store.deleteLocalLicense()
97
+ return null // show your activation flow
98
+ }
99
+ // Moonbase could not be reached, so check the license locally instead
100
+ }
101
+ }
102
+
103
+ try {
104
+ // Signature, device binding and expiry, which needs no network
105
+ return await licensing.validator.validateLicense(localLicense.token)
106
+ }
107
+ catch (err) {
108
+ // Keep the stored token: only Moonbase can say the license is gone, and the
109
+ // next launch that reaches it can still refresh a token that has expired
110
+ if (err instanceof MoonbaseError)
111
+ return null // show your activation flow
112
+ throw err
113
+ }
72
114
  }
73
115
  ```
74
116
 
117
+ A revoked license reports `ErrorType.LicenseRevoked`; a revoked device, whose license may still be
118
+ active, reports `ErrorType.LicenseActivationRevoked`. To limit how long an app keeps running without
119
+ reaching Moonbase, compare `license.validatedAt` with a limit of your choosing before returning the
120
+ locally validated license. For `ErrorType.LicenseDeviceMismatch`, see [Device fingerprint](#device-fingerprint)
121
+ first: after an SDK update, it can mean the same machine under an older fingerprint.
122
+
75
123
  ## Metadata, platform, and app version
76
124
 
77
125
  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,42 @@ 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
+ return ErrorType2;
83
+ })(ErrorType || {});
84
+ var MoonbaseError = class extends Error {
85
+ constructor(title, detail, type, inner) {
86
+ super();
87
+ this.title = title;
88
+ this.detail = detail;
89
+ this.type = type;
90
+ this.inner = inner;
91
+ this.name = "MoonbaseError";
92
+ this.message = detail != null ? detail : title;
93
+ }
94
+ };
95
+ var InsufficientDeviceIdentityError = class extends MoonbaseError {
96
+ constructor(platform, reason = "no identity parameter could be read") {
97
+ super(
98
+ "No device identity",
99
+ `Could not identify this device (platform: ${platform}): ${reason}`,
100
+ "DeviceIdentityUnavailable" /* DeviceIdentityUnavailable */
101
+ );
102
+ this.platform = platform;
103
+ this.name = "InsufficientDeviceIdentityError";
104
+ }
105
+ };
106
+
71
107
  // src/schemas.ts
72
108
  var import_zod = require("zod");
73
109
 
@@ -84,6 +120,11 @@ var activationRequestResponseSchema = import_zod.z.object({
84
120
  request: import_zod.z.string(),
85
121
  browser: import_zod.z.string()
86
122
  });
123
+ var problemDetailsSchema = import_zod.z.object({
124
+ title: import_zod.z.string().nullish(),
125
+ detail: import_zod.z.string().nullish(),
126
+ errorType: import_zod.z.string().nullish()
127
+ });
87
128
  var productSchema = import_zod.z.object({
88
129
  id: import_zod.z.string(),
89
130
  name: import_zod.z.string(),
@@ -138,7 +179,7 @@ var LicenseClient = class {
138
179
  body: JSON.stringify(content)
139
180
  });
140
181
  if (response.status >= 400) {
141
- throw new MoonbaseError("Request not successful", `The API responded with a ${response.status} ${response.statusText} response`, "ApiError" /* ApiError */);
182
+ throw await errorFromResponse(response);
142
183
  }
143
184
  try {
144
185
  return activationRequestResponseSchema.parse(await response.json());
@@ -203,12 +244,12 @@ var LicenseClient = class {
203
244
  body: license.token
204
245
  });
205
246
  if (response.status >= 400) {
206
- throw new MoonbaseError("Request not successful", `The API responded with a ${response.status} ${response.statusText} response`, "ApiError" /* ApiError */);
247
+ throw await errorFromResponse(response);
207
248
  }
208
249
  }
209
250
  async handleLicenseResponse(response) {
210
251
  if (response.status >= 400) {
211
- throw new MoonbaseError("Request not successful", `The API responded with a ${response.status} ${response.statusText} response`, "ApiError" /* ApiError */);
252
+ throw await errorFromResponse(response);
212
253
  }
213
254
  return await this.licenseValidator.validateLicense(await response.text());
214
255
  }
@@ -229,6 +270,35 @@ var LicenseClient = class {
229
270
  return `?${parts.join("&")}`;
230
271
  }
231
272
  };
273
+ var licenseErrorTypes = /* @__PURE__ */ new Set([
274
+ "NoEligibleLicense" /* NoEligibleLicense */,
275
+ "LicenseInvalid" /* LicenseInvalid */,
276
+ "LicenseRevoked" /* LicenseRevoked */,
277
+ "LicenseActivationRevoked" /* LicenseActivationRevoked */,
278
+ "LicenseExpired" /* LicenseExpired */
279
+ ]);
280
+ async function errorFromResponse(response) {
281
+ const fallback = new MoonbaseError(
282
+ "Request not successful",
283
+ `The API responded with a ${response.status} ${response.statusText} response`,
284
+ "ApiError" /* ApiError */
285
+ );
286
+ let body;
287
+ try {
288
+ body = JSON.parse(await response.text());
289
+ } catch (e) {
290
+ return fallback;
291
+ }
292
+ const problem = problemDetailsSchema.safeParse(body);
293
+ if (!problem.success)
294
+ return fallback;
295
+ const { title, detail, errorType } = problem.data;
296
+ return new MoonbaseError(
297
+ title != null ? title : fallback.title,
298
+ detail != null ? detail : fallback.detail,
299
+ errorType && licenseErrorTypes.has(errorType) ? errorType : "ApiError" /* ApiError */
300
+ );
301
+ }
232
302
 
233
303
  // src/deviceIdResolver.ts
234
304
  var import_node_child_process2 = __toESM(require("child_process"), 1);
@@ -237,42 +307,6 @@ var import_node_os2 = __toESM(require("os"), 1);
237
307
  var import_node_process2 = __toESM(require("process"), 1);
238
308
  var import_systeminformation = __toESM(require("systeminformation"), 1);
239
309
 
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
310
  // src/fingerprint.ts
277
311
  var import_node_buffer = require("buffer");
278
312
  var import_node_child_process = __toESM(require("child_process"), 1);
package/dist/index.d.cts CHANGED
@@ -597,13 +597,28 @@ 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",
package/dist/index.d.ts CHANGED
@@ -597,13 +597,28 @@ 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",
package/dist/index.js CHANGED
@@ -5,6 +5,42 @@ 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
+ return ErrorType2;
20
+ })(ErrorType || {});
21
+ var MoonbaseError = class extends Error {
22
+ constructor(title, detail, type, inner) {
23
+ super();
24
+ this.title = title;
25
+ this.detail = detail;
26
+ this.type = type;
27
+ this.inner = inner;
28
+ this.name = "MoonbaseError";
29
+ this.message = detail != null ? detail : title;
30
+ }
31
+ };
32
+ var InsufficientDeviceIdentityError = class extends MoonbaseError {
33
+ constructor(platform, reason = "no identity parameter could be read") {
34
+ super(
35
+ "No device identity",
36
+ `Could not identify this device (platform: ${platform}): ${reason}`,
37
+ "DeviceIdentityUnavailable" /* DeviceIdentityUnavailable */
38
+ );
39
+ this.platform = platform;
40
+ this.name = "InsufficientDeviceIdentityError";
41
+ }
42
+ };
43
+
8
44
  // src/schemas.ts
9
45
  import { z } from "zod";
10
46
 
@@ -21,6 +57,11 @@ var activationRequestResponseSchema = z.object({
21
57
  request: z.string(),
22
58
  browser: z.string()
23
59
  });
60
+ var problemDetailsSchema = z.object({
61
+ title: z.string().nullish(),
62
+ detail: z.string().nullish(),
63
+ errorType: z.string().nullish()
64
+ });
24
65
  var productSchema = z.object({
25
66
  id: z.string(),
26
67
  name: z.string(),
@@ -75,7 +116,7 @@ var LicenseClient = class {
75
116
  body: JSON.stringify(content)
76
117
  });
77
118
  if (response.status >= 400) {
78
- throw new MoonbaseError("Request not successful", `The API responded with a ${response.status} ${response.statusText} response`, "ApiError" /* ApiError */);
119
+ throw await errorFromResponse(response);
79
120
  }
80
121
  try {
81
122
  return activationRequestResponseSchema.parse(await response.json());
@@ -140,12 +181,12 @@ var LicenseClient = class {
140
181
  body: license.token
141
182
  });
142
183
  if (response.status >= 400) {
143
- throw new MoonbaseError("Request not successful", `The API responded with a ${response.status} ${response.statusText} response`, "ApiError" /* ApiError */);
184
+ throw await errorFromResponse(response);
144
185
  }
145
186
  }
146
187
  async handleLicenseResponse(response) {
147
188
  if (response.status >= 400) {
148
- throw new MoonbaseError("Request not successful", `The API responded with a ${response.status} ${response.statusText} response`, "ApiError" /* ApiError */);
189
+ throw await errorFromResponse(response);
149
190
  }
150
191
  return await this.licenseValidator.validateLicense(await response.text());
151
192
  }
@@ -166,6 +207,35 @@ var LicenseClient = class {
166
207
  return `?${parts.join("&")}`;
167
208
  }
168
209
  };
210
+ var licenseErrorTypes = /* @__PURE__ */ new Set([
211
+ "NoEligibleLicense" /* NoEligibleLicense */,
212
+ "LicenseInvalid" /* LicenseInvalid */,
213
+ "LicenseRevoked" /* LicenseRevoked */,
214
+ "LicenseActivationRevoked" /* LicenseActivationRevoked */,
215
+ "LicenseExpired" /* LicenseExpired */
216
+ ]);
217
+ async function errorFromResponse(response) {
218
+ const fallback = new MoonbaseError(
219
+ "Request not successful",
220
+ `The API responded with a ${response.status} ${response.statusText} response`,
221
+ "ApiError" /* ApiError */
222
+ );
223
+ let body;
224
+ try {
225
+ body = JSON.parse(await response.text());
226
+ } catch (e) {
227
+ return fallback;
228
+ }
229
+ const problem = problemDetailsSchema.safeParse(body);
230
+ if (!problem.success)
231
+ return fallback;
232
+ const { title, detail, errorType } = problem.data;
233
+ return new MoonbaseError(
234
+ title != null ? title : fallback.title,
235
+ detail != null ? detail : fallback.detail,
236
+ errorType && licenseErrorTypes.has(errorType) ? errorType : "ApiError" /* ApiError */
237
+ );
238
+ }
169
239
 
170
240
  // src/deviceIdResolver.ts
171
241
  import cp2 from "child_process";
@@ -174,42 +244,6 @@ import os2 from "os";
174
244
  import process2 from "process";
175
245
  import si from "systeminformation";
176
246
 
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
247
  // src/fingerprint.ts
214
248
  import { Buffer } from "buffer";
215
249
  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.1.1",
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"