@endo-ui/license 1.0.2 → 1.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 +12 -0
- package/bin/api.js +27 -9
- package/bin/builders/verify-authorization.js +4 -1
- package/bin/refresh.js +72 -0
- package/bin/storage.js +45 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -32,6 +32,18 @@ private `@endo-ui/core` package.
|
|
|
32
32
|
Local activation fails before validation or project mutation when `@endo-ui/license` is not
|
|
33
33
|
already installed in the project. CI uses the generated isolated exact-version bootstrap instead.
|
|
34
34
|
|
|
35
|
+
## Automatic developer lease refresh
|
|
36
|
+
|
|
37
|
+
Developer leases are verified locally during `ng serve` and `ng build`. When five days or less
|
|
38
|
+
remain, the licensed builder attempts a five-second online refresh through ECAP. A successful
|
|
39
|
+
response is signature-verified and atomically replaces the existing lease.
|
|
40
|
+
|
|
41
|
+
Temporary network or ECAP service failures do not stop a build while the existing signed lease is
|
|
42
|
+
valid. Refresh retries use a six-hour backoff and report the remaining authorization period.
|
|
43
|
+
Definitive authorization failures, such as suspension, removed Seat assignment, or missing major
|
|
44
|
+
entitlement, stop the build immediately. CI leases are freshly issued by the generated bootstrap
|
|
45
|
+
script and are not automatically refreshed.
|
|
46
|
+
|
|
35
47
|
## CI activation
|
|
36
48
|
|
|
37
49
|
Commit the generated `scripts/endoui-license-ci.sh` and `scripts/endoui-license-ci.ps1` files.
|
package/bin/api.js
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
const DEFAULT_SERVICE_URL = "https://license.ecapsoftware.com";
|
|
2
|
+
export class EndoUiApiError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
constructor(message, status, options) {
|
|
5
|
+
super(message, options);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.name = "EndoUiApiError";
|
|
8
|
+
}
|
|
9
|
+
get isDefinitiveAuthorizationFailure() {
|
|
10
|
+
return this.status !== undefined && this.status >= 400 && this.status < 500;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
2
13
|
async function requestLease(pathname, body) {
|
|
3
14
|
const baseUrl = process.env['ENDOUI_LICENSE_URL'] ?? DEFAULT_SERVICE_URL;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
let response;
|
|
16
|
+
try {
|
|
17
|
+
response = await fetch(new URL(pathname, baseUrl), {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: {
|
|
20
|
+
Accept: "application/json",
|
|
21
|
+
"Content-Type": "application/json"
|
|
22
|
+
},
|
|
23
|
+
body: JSON.stringify(body),
|
|
24
|
+
signal: AbortSignal.timeout(5_000)
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new EndoUiApiError("EndoUI licensing service could not be reached.", undefined, { cause: error });
|
|
29
|
+
}
|
|
12
30
|
let value;
|
|
13
31
|
try {
|
|
14
32
|
value = await response.json();
|
|
@@ -18,7 +36,7 @@ async function requestLease(pathname, body) {
|
|
|
18
36
|
}
|
|
19
37
|
if (!response.ok || typeof value?.lease !== "string") {
|
|
20
38
|
const detail = typeof value?.error === "string" ? ` ${value.error}` : "";
|
|
21
|
-
throw new
|
|
39
|
+
throw new EndoUiApiError(`EndoUI activation failed.${detail}`, response.status);
|
|
22
40
|
}
|
|
23
41
|
return value.lease;
|
|
24
42
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { resolveEndoUiVersion } from "../package-version.js";
|
|
2
2
|
import { verifyLease } from "../lease.js";
|
|
3
3
|
import { readLease } from "../storage.js";
|
|
4
|
+
import { refreshDeveloperLeaseIfNeeded } from "../refresh.js";
|
|
4
5
|
export async function verifyAuthorization(overrides = {}) {
|
|
5
6
|
const version = (overrides.resolveVersion ?? resolveEndoUiVersion)();
|
|
6
7
|
const major = Number(version.split(".", 1)[0]);
|
|
7
|
-
|
|
8
|
+
const signedLease = await (overrides.read ?? readLease)(major);
|
|
9
|
+
const lease = (overrides.verify ?? verifyLease)(signedLease, version);
|
|
10
|
+
await (overrides.refresh ?? refreshDeveloperLeaseIfNeeded)(lease, version);
|
|
8
11
|
}
|
package/bin/refresh.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { activateDeveloper, EndoUiApiError } from "./api.js";
|
|
2
|
+
import { verifyLease } from "./lease.js";
|
|
3
|
+
import { clearRefreshState, readRefreshState, writeLease, writeRefreshState } from "./storage.js";
|
|
4
|
+
export const LEASE_REFRESH_WINDOW_SECONDS = 5 * 24 * 60 * 60;
|
|
5
|
+
export const LEASE_REFRESH_RETRY_SECONDS = 6 * 60 * 60;
|
|
6
|
+
const defaults = {
|
|
7
|
+
request: activateDeveloper,
|
|
8
|
+
verify: verifyLease,
|
|
9
|
+
writeLease,
|
|
10
|
+
readState: readRefreshState,
|
|
11
|
+
writeState: writeRefreshState,
|
|
12
|
+
clearState: clearRefreshState,
|
|
13
|
+
warn: message => console.warn(message),
|
|
14
|
+
info: message => console.info(message),
|
|
15
|
+
now: () => Math.floor(Date.now() / 1000)
|
|
16
|
+
};
|
|
17
|
+
function remainingMessage(expiration, now) {
|
|
18
|
+
const seconds = Math.max(0, expiration - now);
|
|
19
|
+
if (seconds < 24 * 60 * 60)
|
|
20
|
+
return "Existing authorization remains valid for less than 1 day.";
|
|
21
|
+
const days = Math.ceil(seconds / (24 * 60 * 60));
|
|
22
|
+
return `Existing authorization remains valid for ${days} days.`;
|
|
23
|
+
}
|
|
24
|
+
function warnTemporary(expiration, now, warn) {
|
|
25
|
+
warn("EndoUI license refresh could not be completed.\n" +
|
|
26
|
+
remainingMessage(expiration, now));
|
|
27
|
+
}
|
|
28
|
+
function assertRefreshMatches(current, refreshed) {
|
|
29
|
+
if (refreshed.kind !== "developer" ||
|
|
30
|
+
refreshed.licenseId !== current.licenseId ||
|
|
31
|
+
refreshed.user !== current.user ||
|
|
32
|
+
refreshed.majorVersion !== current.majorVersion ||
|
|
33
|
+
refreshed.exp <= current.exp) {
|
|
34
|
+
throw new Error("The refreshed EndoUI authorization does not match the existing developer lease.");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function refreshDeveloperLeaseIfNeeded(lease, version, overrides = {}) {
|
|
38
|
+
if (lease.kind !== "developer")
|
|
39
|
+
return lease;
|
|
40
|
+
if (!lease.user)
|
|
41
|
+
throw new Error("The developer authorization is missing its named Seat identity.");
|
|
42
|
+
const dependencies = { ...defaults, ...overrides };
|
|
43
|
+
const now = dependencies.now();
|
|
44
|
+
if (lease.exp - now > LEASE_REFRESH_WINDOW_SECONDS)
|
|
45
|
+
return lease;
|
|
46
|
+
const state = await dependencies.readState(lease.majorVersion);
|
|
47
|
+
if (state?.failed &&
|
|
48
|
+
now - state.lastAttempt >= 0 &&
|
|
49
|
+
now - state.lastAttempt < LEASE_REFRESH_RETRY_SECONDS) {
|
|
50
|
+
warnTemporary(lease.exp, now, dependencies.warn);
|
|
51
|
+
return lease;
|
|
52
|
+
}
|
|
53
|
+
let refreshedToken;
|
|
54
|
+
try {
|
|
55
|
+
refreshedToken = await dependencies.request(lease.licenseId, lease.user, version);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error instanceof EndoUiApiError && error.isDefinitiveAuthorizationFailure) {
|
|
59
|
+
throw new Error(`EndoUI authorization refresh was rejected. ${error.message}\nBuild stopped.`, { cause: error });
|
|
60
|
+
}
|
|
61
|
+
await dependencies.writeState(lease.majorVersion, { lastAttempt: now, failed: true }).catch(() => undefined);
|
|
62
|
+
warnTemporary(lease.exp, now, dependencies.warn);
|
|
63
|
+
return lease;
|
|
64
|
+
}
|
|
65
|
+
const refreshed = dependencies.verify(refreshedToken, version, now * 1000);
|
|
66
|
+
assertRefreshMatches(lease, refreshed);
|
|
67
|
+
await dependencies.writeLease(lease.majorVersion, refreshedToken);
|
|
68
|
+
await dependencies.clearState(lease.majorVersion).catch(() => undefined);
|
|
69
|
+
const validityDays = Math.ceil((refreshed.exp - now) / (24 * 60 * 60));
|
|
70
|
+
dependencies.info(`EndoUI authorization refreshed; valid for ${validityDays} days.`);
|
|
71
|
+
return refreshed;
|
|
72
|
+
}
|
package/bin/storage.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { chmod, mkdir, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
export function leasePath(major, environment = process.env, homeDirectory = os.homedir()) {
|
|
@@ -21,7 +22,49 @@ export async function readLease(major) {
|
|
|
21
22
|
export async function writeLease(major, lease, environment = process.env, homeDirectory = os.homedir()) {
|
|
22
23
|
const file = leasePath(major, environment, homeDirectory);
|
|
23
24
|
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
24
|
-
|
|
25
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
26
|
+
try {
|
|
27
|
+
await writeFile(temporary, `${lease}\n`, { encoding: "utf8", mode: 0o600 });
|
|
28
|
+
if (process.platform !== "win32")
|
|
29
|
+
await chmod(temporary, 0o600);
|
|
30
|
+
await rename(temporary, file);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function refreshStatePath(major, environment = process.env, homeDirectory = os.homedir()) {
|
|
38
|
+
return `${leasePath(major, environment, homeDirectory)}.refresh.json`;
|
|
39
|
+
}
|
|
40
|
+
export async function readRefreshState(major, environment = process.env, homeDirectory = os.homedir()) {
|
|
41
|
+
try {
|
|
42
|
+
const value = JSON.parse(await readFile(refreshStatePath(major, environment, homeDirectory), "utf8"));
|
|
43
|
+
return Number.isSafeInteger(value.lastAttempt) && typeof value.failed === "boolean"
|
|
44
|
+
? { lastAttempt: value.lastAttempt, failed: value.failed }
|
|
45
|
+
: undefined;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error instanceof SyntaxError)
|
|
49
|
+
return undefined;
|
|
50
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
51
|
+
return undefined;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function writeRefreshState(major, state, environment = process.env, homeDirectory = os.homedir()) {
|
|
56
|
+
const file = refreshStatePath(major, environment, homeDirectory);
|
|
57
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
58
|
+
await writeFile(file, `${JSON.stringify(state)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
25
59
|
if (process.platform !== "win32")
|
|
26
60
|
await chmod(file, 0o600);
|
|
27
61
|
}
|
|
62
|
+
export async function clearRefreshState(major, environment = process.env, homeDirectory = os.homedir()) {
|
|
63
|
+
try {
|
|
64
|
+
await unlink(refreshStatePath(major, environment, homeDirectory));
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@endo-ui/license",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Developer and CI license activation for EndoUI.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"publishConfig": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"builders": "./builders.json",
|
|
11
11
|
"schematics": "./collection.json",
|
|
12
12
|
"bin": {
|
|
13
|
-
"endoui-license": "
|
|
13
|
+
"endoui-license": "bin/cli.js"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"bin",
|