@endo-ui/license 1.0.1 → 1.0.2
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 +25 -21
- package/bin/activation.js +77 -0
- package/bin/angular-config.js +47 -0
- package/bin/api.js +7 -7
- package/bin/builders/verify-authorization.js +3 -3
- package/bin/ci-scripts.js +79 -0
- package/bin/cli.js +57 -58
- package/bin/core-install.js +44 -0
- package/bin/lease.js +2 -0
- package/bin/npm-access.js +34 -0
- package/bin/npm-token.js +37 -0
- package/bin/npmrc.js +40 -0
- package/bin/package-version.js +34 -6
- package/bin/schematics/ng-add/index.js +7 -18
- package/bin/storage.js +23 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,40 +1,44 @@
|
|
|
1
1
|
# @endo-ui/license
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Public Node-only activation and Angular 22 build enforcement for EndoUI. Nothing in this
|
|
4
4
|
package is imported into the browser application or its production runtime.
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## Local activation
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Install the public activation package in the customer project first:
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
|
|
11
|
+
npm install --save-dev @endo-ui/license
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
official builders.
|
|
17
|
-
|
|
18
|
-
## Developer activation
|
|
14
|
+
Then provide the npm read token through an approved local environment/secret mechanism and run
|
|
15
|
+
the project-local CLI:
|
|
19
16
|
|
|
20
17
|
```bash
|
|
18
|
+
export NPM_TOKEN="<customer npm read token>"
|
|
21
19
|
npx endoui-license activate \
|
|
20
|
+
--license-id "<license-id>" \
|
|
22
21
|
--developer "jane@customer.com"
|
|
23
22
|
```
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
If `NPM_TOKEN` is absent, an interactive terminal securely prompts for it. `--npm-token` is also
|
|
25
|
+
supported for controlled automation. Command-line secrets can be exposed through shell history or
|
|
26
|
+
process inspection, so `NPM_TOKEN` is preferred. The token is validated but never persisted or
|
|
27
|
+
printed. Activation
|
|
28
|
+
validates the named developer Seat, creates the signed local lease, manages the project `.npmrc`,
|
|
29
|
+
configures Angular's licensed builders, generates CI bootstrap scripts, and finally installs the
|
|
30
|
+
private `@endo-ui/core` package.
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
Local activation fails before validation or project mutation when `@endo-ui/license` is not
|
|
33
|
+
already installed in the project. CI uses the generated isolated exact-version bootstrap instead.
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
## CI activation
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
Commit the generated `scripts/endoui-license-ci.sh` and `scripts/endoui-license-ci.ps1` files.
|
|
38
|
+
Store `NPM_TOKEN` and `ENDOUI_LICENSE_ID` in the CI provider's protected secret storage, then run
|
|
39
|
+
the platform-appropriate generated script immediately after checkout and before dependency
|
|
40
|
+
installation.
|
|
38
41
|
|
|
39
|
-
CI authorization does not consume a human Seat. The
|
|
40
|
-
|
|
42
|
+
CI authorization does not consume a human Seat. The scripts bootstrap the exact license-tool
|
|
43
|
+
version that generated them, validate npm and ECAP access, and cache a signed authorization for
|
|
44
|
+
the bounded build period. They contain no credentials.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { activateCi, activateDeveloper } from "./api.js";
|
|
2
|
+
import { configureAngularWorkspace } from "./angular-config.js";
|
|
3
|
+
import { generateCiScripts } from "./ci-scripts.js";
|
|
4
|
+
import { installCore } from "./core-install.js";
|
|
5
|
+
import { verifyLease } from "./lease.js";
|
|
6
|
+
import { validateNpmAccess } from "./npm-access.js";
|
|
7
|
+
import { ensureEndoUiNpmrc } from "./npmrc.js";
|
|
8
|
+
import { resolveNpmToken } from "./npm-token.js";
|
|
9
|
+
import { resolveLicensePackageVersion } from "./package-version.js";
|
|
10
|
+
import { writeLease } from "./storage.js";
|
|
11
|
+
const defaults = {
|
|
12
|
+
version: resolveLicensePackageVersion,
|
|
13
|
+
resolveToken: explicitToken => resolveNpmToken({
|
|
14
|
+
explicitToken,
|
|
15
|
+
environmentToken: process.env["NPM_TOKEN"]
|
|
16
|
+
}),
|
|
17
|
+
validateNpm: validateNpmAccess,
|
|
18
|
+
developerLease: activateDeveloper,
|
|
19
|
+
ciLease: activateCi,
|
|
20
|
+
verify: verifyLease,
|
|
21
|
+
writeLease,
|
|
22
|
+
npmrc: ensureEndoUiNpmrc,
|
|
23
|
+
angular: configureAngularWorkspace,
|
|
24
|
+
scripts: generateCiScripts,
|
|
25
|
+
install: installCore
|
|
26
|
+
};
|
|
27
|
+
export function resolveCiInputs(environment) {
|
|
28
|
+
const npmToken = environment["NPM_TOKEN"]?.trim();
|
|
29
|
+
if (!npmToken)
|
|
30
|
+
throw new Error("CI activation requires NPM_TOKEN.");
|
|
31
|
+
const licenseId = environment["ENDOUI_LICENSE_ID"]?.trim();
|
|
32
|
+
if (!licenseId)
|
|
33
|
+
throw new Error("CI activation requires ENDOUI_LICENSE_ID.");
|
|
34
|
+
return { npmToken, licenseId };
|
|
35
|
+
}
|
|
36
|
+
function major(version) {
|
|
37
|
+
const value = Number(version.split(".", 1)[0]);
|
|
38
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
39
|
+
throw new Error(`EndoUI package version ${version} is not eligible for activation.`);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function assertLicense(lease, licenseId, kind) {
|
|
44
|
+
if (lease.licenseId !== licenseId || lease.kind !== kind) {
|
|
45
|
+
throw new Error("The signed EndoUI authorization does not match this activation request.");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function activateLocal(input, overrides = {}) {
|
|
49
|
+
const dependencies = { ...defaults, ...overrides };
|
|
50
|
+
const cwd = input.cwd ?? process.cwd();
|
|
51
|
+
const version = dependencies.version();
|
|
52
|
+
const npmToken = await dependencies.resolveToken(input.npmToken);
|
|
53
|
+
// All external authorization checks complete before the first project or lease mutation.
|
|
54
|
+
await dependencies.validateNpm(npmToken, version);
|
|
55
|
+
const signedLease = await dependencies.developerLease(input.licenseId, input.developer, version);
|
|
56
|
+
const lease = dependencies.verify(signedLease, version);
|
|
57
|
+
assertLicense(lease, input.licenseId, "developer");
|
|
58
|
+
if (lease.user !== input.developer) {
|
|
59
|
+
throw new Error("The signed EndoUI authorization does not match the named developer.");
|
|
60
|
+
}
|
|
61
|
+
await dependencies.npmrc(cwd);
|
|
62
|
+
await dependencies.angular(cwd);
|
|
63
|
+
await dependencies.scripts({ packageVersion: version, customer: lease.customer, licenseId: input.licenseId }, cwd);
|
|
64
|
+
await dependencies.install(cwd, version, npmToken);
|
|
65
|
+
await dependencies.writeLease(major(version), signedLease);
|
|
66
|
+
return { version, lease };
|
|
67
|
+
}
|
|
68
|
+
export async function activateForCi(input, overrides = {}) {
|
|
69
|
+
const dependencies = { ...defaults, ...overrides };
|
|
70
|
+
const version = dependencies.version();
|
|
71
|
+
await dependencies.validateNpm(input.npmToken, version);
|
|
72
|
+
const signedLease = await dependencies.ciLease(input.licenseId, version);
|
|
73
|
+
const lease = dependencies.verify(signedLease, version);
|
|
74
|
+
assertLicense(lease, input.licenseId, "ci");
|
|
75
|
+
await dependencies.writeLease(major(version), signedLease);
|
|
76
|
+
return { version, lease };
|
|
77
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function configureTarget(target, expected, replacement) {
|
|
4
|
+
if (!target)
|
|
5
|
+
return false;
|
|
6
|
+
if (target.builder === replacement)
|
|
7
|
+
return false;
|
|
8
|
+
if (target.builder !== expected && !target.builder?.startsWith("@endo-ui/license:")) {
|
|
9
|
+
throw new Error(`Cannot replace custom Angular builder ${target.builder ?? "(missing)"}.`);
|
|
10
|
+
}
|
|
11
|
+
target.builder = replacement;
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
export function configureAngularWorkspaceObject(workspace) {
|
|
15
|
+
let found = false;
|
|
16
|
+
let changed = false;
|
|
17
|
+
for (const project of Object.values(workspace.projects ?? {})) {
|
|
18
|
+
if (project.projectType !== "application")
|
|
19
|
+
continue;
|
|
20
|
+
const targets = project.architect ?? project.targets;
|
|
21
|
+
if (!targets)
|
|
22
|
+
continue;
|
|
23
|
+
if (targets["build"] || targets["serve"])
|
|
24
|
+
found = true;
|
|
25
|
+
changed = configureTarget(targets["build"], "@angular/build:application", "@endo-ui/license:application") || changed;
|
|
26
|
+
changed = configureTarget(targets["serve"], "@angular/build:dev-server", "@endo-ui/license:dev-server") || changed;
|
|
27
|
+
}
|
|
28
|
+
if (!found)
|
|
29
|
+
throw new Error("No Angular 22 application build or serve targets were found.");
|
|
30
|
+
return changed;
|
|
31
|
+
}
|
|
32
|
+
export async function configureAngularWorkspace(cwd = process.cwd()) {
|
|
33
|
+
const file = path.join(cwd, "angular.json");
|
|
34
|
+
let workspace;
|
|
35
|
+
try {
|
|
36
|
+
workspace = JSON.parse(await readFile(file, "utf8"));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error instanceof SyntaxError)
|
|
40
|
+
throw new Error("angular.json is not valid JSON.");
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
const changed = configureAngularWorkspaceObject(workspace);
|
|
44
|
+
if (changed)
|
|
45
|
+
await writeFile(file, `${JSON.stringify(workspace, null, 2)}\n`, "utf8");
|
|
46
|
+
return changed;
|
|
47
|
+
}
|
package/bin/api.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
const DEFAULT_SERVICE_URL = "https://license.ecapsoftware.com";
|
|
2
|
-
async function requestLease(pathname,
|
|
2
|
+
async function requestLease(pathname, body) {
|
|
3
3
|
const baseUrl = process.env['ENDOUI_LICENSE_URL'] ?? DEFAULT_SERVICE_URL;
|
|
4
4
|
const response = await fetch(new URL(pathname, baseUrl), {
|
|
5
5
|
method: "POST",
|
|
6
6
|
headers: {
|
|
7
7
|
Accept: "application/json",
|
|
8
|
-
"Content-Type": "application/json"
|
|
9
|
-
Authorization: `License ${licenseKey}`
|
|
8
|
+
"Content-Type": "application/json"
|
|
10
9
|
},
|
|
11
10
|
body: JSON.stringify(body)
|
|
12
11
|
});
|
|
@@ -23,12 +22,13 @@ async function requestLease(pathname, licenseKey, body) {
|
|
|
23
22
|
}
|
|
24
23
|
return value.lease;
|
|
25
24
|
}
|
|
26
|
-
export function activateDeveloper(
|
|
27
|
-
return requestLease("/v1/endoui/leases/developer",
|
|
25
|
+
export function activateDeveloper(licenseId, developerEmail, packageVersion) {
|
|
26
|
+
return requestLease("/v1/endoui/leases/developer", {
|
|
27
|
+
licenseId,
|
|
28
28
|
developerEmail,
|
|
29
29
|
packageVersion
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
|
-
export function activateCi(
|
|
33
|
-
return requestLease("/v1/endoui/leases/ci",
|
|
32
|
+
export function activateCi(licenseId, packageVersion) {
|
|
33
|
+
return requestLease("/v1/endoui/leases/ci", { licenseId, packageVersion });
|
|
34
34
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { resolveEndoUiVersion } from "../package-version.js";
|
|
2
2
|
import { verifyLease } from "../lease.js";
|
|
3
3
|
import { readLease } from "../storage.js";
|
|
4
|
-
export async function verifyAuthorization() {
|
|
5
|
-
const version = resolveEndoUiVersion();
|
|
4
|
+
export async function verifyAuthorization(overrides = {}) {
|
|
5
|
+
const version = (overrides.resolveVersion ?? resolveEndoUiVersion)();
|
|
6
6
|
const major = Number(version.split(".", 1)[0]);
|
|
7
|
-
verifyLease(await readLease(major), version);
|
|
7
|
+
(overrides.verify ?? verifyLease)(await (overrides.read ?? readLease)(major), version);
|
|
8
8
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function maskLicenseId(_licenseId) {
|
|
4
|
+
return "********";
|
|
5
|
+
}
|
|
6
|
+
function safeComment(value) {
|
|
7
|
+
return value.replace(/[\r\n]+/g, " ").trim();
|
|
8
|
+
}
|
|
9
|
+
function header(metadata) {
|
|
10
|
+
const generated = (metadata.generatedAt ?? new Date()).toISOString().slice(0, 10);
|
|
11
|
+
return [
|
|
12
|
+
`# Generated by @endo-ui/license@${metadata.packageVersion}`,
|
|
13
|
+
`# Customer: ${safeComment(metadata.customer)}`,
|
|
14
|
+
`# License ID: ${maskLicenseId(metadata.licenseId)}`,
|
|
15
|
+
`# Generated: ${generated}`,
|
|
16
|
+
"#",
|
|
17
|
+
"# Do not edit manually. Re-run EndoUI activation to regenerate."
|
|
18
|
+
].join("\n");
|
|
19
|
+
}
|
|
20
|
+
export function createShellCiScript(metadata) {
|
|
21
|
+
const version = metadata.packageVersion;
|
|
22
|
+
return `#!/usr/bin/env sh
|
|
23
|
+
set -eu
|
|
24
|
+
|
|
25
|
+
${header(metadata)}
|
|
26
|
+
|
|
27
|
+
if [ -z "\${NPM_TOKEN:-}" ]; then
|
|
28
|
+
echo "EndoUI CI activation requires NPM_TOKEN." >&2
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
if [ -z "\${ENDOUI_LICENSE_ID:-}" ]; then
|
|
32
|
+
echo "EndoUI CI activation requires ENDOUI_LICENSE_ID." >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
BOOTSTRAP_DIR="\${TMPDIR:-/tmp}/endoui-license-$$"
|
|
37
|
+
cleanup() { rm -rf "$BOOTSTRAP_DIR"; }
|
|
38
|
+
trap cleanup EXIT HUP INT TERM
|
|
39
|
+
mkdir -p "$BOOTSTRAP_DIR"
|
|
40
|
+
npm install --prefix "$BOOTSTRAP_DIR" --no-save "@endo-ui/license@${version}"
|
|
41
|
+
node "$BOOTSTRAP_DIR/node_modules/@endo-ui/license/bin/cli.js" ci-activate
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
export function createPowerShellCiScript(metadata) {
|
|
45
|
+
const version = metadata.packageVersion;
|
|
46
|
+
return `$ErrorActionPreference = "Stop"
|
|
47
|
+
|
|
48
|
+
${header(metadata)}
|
|
49
|
+
|
|
50
|
+
if ([string]::IsNullOrWhiteSpace($env:NPM_TOKEN)) {
|
|
51
|
+
throw "EndoUI CI activation requires NPM_TOKEN."
|
|
52
|
+
}
|
|
53
|
+
if ([string]::IsNullOrWhiteSpace($env:ENDOUI_LICENSE_ID)) {
|
|
54
|
+
throw "EndoUI CI activation requires ENDOUI_LICENSE_ID."
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
$bootstrapDirectory = Join-Path ([System.IO.Path]::GetTempPath()) ("endoui-license-" + [guid]::NewGuid())
|
|
58
|
+
try {
|
|
59
|
+
New-Item -ItemType Directory -Path $bootstrapDirectory | Out-Null
|
|
60
|
+
npm install --prefix $bootstrapDirectory --no-save "@endo-ui/license@${version}"
|
|
61
|
+
if ($LASTEXITCODE -ne 0) { throw "Unable to bootstrap @endo-ui/license." }
|
|
62
|
+
node (Join-Path $bootstrapDirectory "node_modules/@endo-ui/license/bin/cli.js") ci-activate
|
|
63
|
+
if ($LASTEXITCODE -ne 0) { throw "EndoUI CI activation failed." }
|
|
64
|
+
} finally {
|
|
65
|
+
if (Test-Path -LiteralPath $bootstrapDirectory) {
|
|
66
|
+
Remove-Item -LiteralPath $bootstrapDirectory -Recurse -Force
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
export async function generateCiScripts(metadata, cwd = process.cwd()) {
|
|
72
|
+
const directory = path.join(cwd, "scripts");
|
|
73
|
+
await mkdir(directory, { recursive: true });
|
|
74
|
+
const shellPath = path.join(directory, "endoui-license-ci.sh");
|
|
75
|
+
await writeFile(shellPath, createShellCiScript(metadata), { encoding: "utf8", mode: 0o755 });
|
|
76
|
+
await writeFile(path.join(directory, "endoui-license-ci.ps1"), createPowerShellCiScript(metadata), "utf8");
|
|
77
|
+
if (process.platform !== "win32")
|
|
78
|
+
await chmod(shellPath, 0o755);
|
|
79
|
+
}
|
package/bin/cli.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { createInterface } from "node:readline/promises";
|
|
4
|
-
import { Writable } from "node:stream";
|
|
2
|
+
import { activateForCi, activateLocal, resolveCiInputs } from "./activation.js";
|
|
5
3
|
import { verifyLease } from "./lease.js";
|
|
6
|
-
import { resolveEndoUiVersion } from "./package-version.js";
|
|
7
|
-
import { readLease
|
|
4
|
+
import { resolveEndoUiVersion, resolveInstalledLicenseVersion, resolveLicensePackageVersion } from "./package-version.js";
|
|
5
|
+
import { readLease } from "./storage.js";
|
|
8
6
|
function argument(name) {
|
|
9
7
|
const index = process.argv.indexOf(name);
|
|
10
|
-
|
|
8
|
+
if (index < 0)
|
|
9
|
+
return undefined;
|
|
10
|
+
const value = process.argv[index + 1];
|
|
11
|
+
if (!value || value.startsWith("--"))
|
|
12
|
+
throw new Error(`${name} requires a value.`);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function required(value, message) {
|
|
16
|
+
const normalized = value?.trim();
|
|
17
|
+
if (!normalized)
|
|
18
|
+
throw new Error(message);
|
|
19
|
+
return normalized;
|
|
11
20
|
}
|
|
12
21
|
function packageMajor(version) {
|
|
13
22
|
const major = Number(version.split(".", 1)[0]);
|
|
@@ -16,64 +25,54 @@ function packageMajor(version) {
|
|
|
16
25
|
}
|
|
17
26
|
return major;
|
|
18
27
|
}
|
|
19
|
-
async function licenseCredential() {
|
|
20
|
-
const configured = process.env['ENDOUI_LICENSE_KEY'];
|
|
21
|
-
if (configured)
|
|
22
|
-
return configured;
|
|
23
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
24
|
-
throw new Error("ENDOUI_LICENSE_KEY is required in non-interactive environments.");
|
|
25
|
-
}
|
|
26
|
-
let muted = false;
|
|
27
|
-
const output = new Writable({
|
|
28
|
-
write(chunk, encoding, callback) {
|
|
29
|
-
if (!muted)
|
|
30
|
-
process.stdout.write(chunk, encoding);
|
|
31
|
-
callback();
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
const prompt = createInterface({ input: process.stdin, output, terminal: true });
|
|
35
|
-
try {
|
|
36
|
-
const answer = prompt.question("Customer license key: ");
|
|
37
|
-
muted = true;
|
|
38
|
-
const value = (await answer).trim();
|
|
39
|
-
process.stdout.write("\n");
|
|
40
|
-
if (!value)
|
|
41
|
-
throw new Error("A customer license key is required.");
|
|
42
|
-
return value;
|
|
43
|
-
}
|
|
44
|
-
finally {
|
|
45
|
-
prompt.close();
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
28
|
async function activate() {
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
throw new Error(
|
|
29
|
+
const installedVersion = resolveInstalledLicenseVersion();
|
|
30
|
+
const runningVersion = resolveLicensePackageVersion();
|
|
31
|
+
if (installedVersion !== runningVersion) {
|
|
32
|
+
throw new Error(`The project has @endo-ui/license@${installedVersion}, but activation is running ` +
|
|
33
|
+
`@endo-ui/license@${runningVersion}. Run the project-local command with npx endoui-license.`);
|
|
53
34
|
}
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
await
|
|
60
|
-
|
|
35
|
+
const licenseId = required(argument("--license-id"), "Activation requires --license-id.");
|
|
36
|
+
const developer = required(argument("--developer") ?? process.env["ENDOUI_DEVELOPER_EMAIL"], "Activation requires --developer.").toLowerCase();
|
|
37
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(developer)) {
|
|
38
|
+
throw new Error("--developer must be a valid email address.");
|
|
39
|
+
}
|
|
40
|
+
await activateLocal({
|
|
41
|
+
licenseId,
|
|
42
|
+
developer,
|
|
43
|
+
npmToken: argument("--npm-token")
|
|
44
|
+
});
|
|
45
|
+
console.log(`
|
|
46
|
+
EndoUI activation successful.
|
|
47
|
+
|
|
48
|
+
✓ npm access verified
|
|
49
|
+
✓ EndoUI license verified
|
|
50
|
+
✓ Developer authorization created
|
|
51
|
+
✓ Angular licensed builders configured
|
|
52
|
+
✓ .npmrc configured
|
|
53
|
+
✓ CI activation scripts generated
|
|
54
|
+
✓ @endo-ui/core installed
|
|
55
|
+
|
|
56
|
+
Commit the generated CI scripts and .npmrc changes to your repository.
|
|
57
|
+
|
|
58
|
+
Keep NPM_TOKEN out of source control. Provide it through your approved
|
|
59
|
+
local secret mechanism and your CI secret store.
|
|
60
|
+
|
|
61
|
+
CI must provide:
|
|
62
|
+
NPM_TOKEN
|
|
63
|
+
ENDOUI_LICENSE_ID
|
|
64
|
+
|
|
65
|
+
Run the generated EndoUI CI activation script immediately after checkout
|
|
66
|
+
and before npm ci/npm install.`);
|
|
61
67
|
}
|
|
62
68
|
async function activateBuild() {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const version = resolveEndoUiVersion();
|
|
67
|
-
const major = packageMajor(version);
|
|
68
|
-
const lease = await activateCi(licenseKey, version);
|
|
69
|
-
verifyLease(lease, version);
|
|
70
|
-
await writeLease(major, lease);
|
|
71
|
-
console.log(`EndoUI ${version} CI authorization activated.`);
|
|
69
|
+
const { npmToken, licenseId } = resolveCiInputs(process.env);
|
|
70
|
+
const result = await activateForCi({ licenseId, npmToken });
|
|
71
|
+
console.log(`EndoUI ${result.version} CI authorization activated.`);
|
|
72
72
|
}
|
|
73
73
|
async function check() {
|
|
74
74
|
const version = resolveEndoUiVersion();
|
|
75
|
-
const
|
|
76
|
-
const lease = verifyLease(await readLease(major), version);
|
|
75
|
+
const lease = verifyLease(await readLease(packageMajor(version)), version);
|
|
77
76
|
const refreshNotice = Math.floor(Date.now() / 1000) >= lease.refreshAfter
|
|
78
77
|
? " Refresh activation when connectivity is available."
|
|
79
78
|
: "";
|
|
@@ -92,7 +91,7 @@ async function main() {
|
|
|
92
91
|
return;
|
|
93
92
|
default:
|
|
94
93
|
console.log("Usage: endoui-license <activate|ci-activate|verify> " +
|
|
95
|
-
"[--developer <email>]");
|
|
94
|
+
"[--license-id <id>] [--developer <email>] [--npm-token <token>]");
|
|
96
95
|
}
|
|
97
96
|
}
|
|
98
97
|
main().catch((error) => {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
const MAX_STDERR_LENGTH = 65_536;
|
|
3
|
+
function redact(value, npmToken) {
|
|
4
|
+
return npmToken ? value.split(npmToken).join("[REDACTED]") : value;
|
|
5
|
+
}
|
|
6
|
+
export function npmInstallFailure(specification, exitCode, stderr, npmToken, signal) {
|
|
7
|
+
const reason = redact(stderr.trim(), npmToken) ||
|
|
8
|
+
(signal ? `npm was terminated by signal ${signal}.` : "npm did not provide error output.");
|
|
9
|
+
return new Error(`Failed to install ${specification}.\n` +
|
|
10
|
+
`npm exit code: ${exitCode ?? "unknown"}\n` +
|
|
11
|
+
`Reason:\n${reason}`);
|
|
12
|
+
}
|
|
13
|
+
export async function installCore(cwd = process.cwd(), version, npmToken) {
|
|
14
|
+
const command = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
15
|
+
await new Promise((resolve, reject) => {
|
|
16
|
+
const specification = version ? `@endo-ui/core@${version}` : "@endo-ui/core";
|
|
17
|
+
let stderr = "";
|
|
18
|
+
const child = spawn(command, ["install", specification], {
|
|
19
|
+
cwd,
|
|
20
|
+
env: {
|
|
21
|
+
...process.env,
|
|
22
|
+
...(npmToken ? { NPM_TOKEN: npmToken } : {})
|
|
23
|
+
},
|
|
24
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
25
|
+
shell: false
|
|
26
|
+
});
|
|
27
|
+
child.stderr?.setEncoding("utf8");
|
|
28
|
+
child.stderr?.on("data", (chunk) => {
|
|
29
|
+
stderr = `${stderr}${chunk}`.slice(-MAX_STDERR_LENGTH);
|
|
30
|
+
});
|
|
31
|
+
child.once("error", error => reject(new Error(`Unable to start npm while installing ${specification}. ${error.message}`)));
|
|
32
|
+
child.once("exit", (code, signal) => {
|
|
33
|
+
const safeStderr = redact(stderr, npmToken);
|
|
34
|
+
if (code === 0) {
|
|
35
|
+
if (safeStderr)
|
|
36
|
+
process.stderr.write(safeStderr);
|
|
37
|
+
resolve();
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
reject(npmInstallFailure(specification, code, stderr, npmToken, signal));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
package/bin/lease.js
CHANGED
|
@@ -26,6 +26,8 @@ export function verifyLease(token, packageVersion, now = Date.now(), trust = {
|
|
|
26
26
|
const major = Number(packageVersion.split(".", 1)[0]);
|
|
27
27
|
if (lease.iss !== trust.issuer || lease.aud !== "@endo-ui/license" || lease.product !== "endoui" || lease.package !== "@endo-ui/core" || lease.majorVersion !== major || lease.entitlement !== `endoui-v${major}`)
|
|
28
28
|
throw new Error("The authorization does not cover this EndoUI package.");
|
|
29
|
+
if (typeof lease.licenseId !== "string" || !lease.licenseId || typeof lease.customer !== "string" || !lease.customer)
|
|
30
|
+
throw new Error("The authorization is missing required customer details.");
|
|
29
31
|
if (!Number.isSafeInteger(lease.nbf) || !Number.isSafeInteger(lease.exp))
|
|
30
32
|
throw new Error("The authorization has invalid timestamps.");
|
|
31
33
|
if (seconds < lease.nbf)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export async function validateNpmAccess(token, version, request = fetch) {
|
|
2
|
+
let response;
|
|
3
|
+
try {
|
|
4
|
+
response = await request("https://registry.npmjs.org/@endo-ui%2Fcore", {
|
|
5
|
+
headers: {
|
|
6
|
+
Accept: "application/vnd.npm.install-v1+json",
|
|
7
|
+
Authorization: `Bearer ${token}`
|
|
8
|
+
}
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new Error("Unable to contact npm to validate access to @endo-ui/core.");
|
|
13
|
+
}
|
|
14
|
+
if (response.ok) {
|
|
15
|
+
let metadata;
|
|
16
|
+
try {
|
|
17
|
+
metadata = await response.json();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error("npm returned invalid package metadata for @endo-ui/core.");
|
|
21
|
+
}
|
|
22
|
+
if (!metadata.versions?.[version]) {
|
|
23
|
+
throw new Error(`@endo-ui/core@${version} is not available to this npm token.`);
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (response.status === 401) {
|
|
28
|
+
throw new Error("The npm token is invalid, expired, or revoked.");
|
|
29
|
+
}
|
|
30
|
+
if (response.status === 403 || response.status === 404) {
|
|
31
|
+
throw new Error("The npm token does not have read access to @endo-ui/core.");
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`npm access validation failed with HTTP ${response.status}.`);
|
|
34
|
+
}
|
package/bin/npm-token.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { Writable } from "node:stream";
|
|
3
|
+
async function securePrompt() {
|
|
4
|
+
let muted = false;
|
|
5
|
+
const output = new Writable({
|
|
6
|
+
write(chunk, encoding, callback) {
|
|
7
|
+
if (!muted)
|
|
8
|
+
process.stdout.write(chunk, encoding);
|
|
9
|
+
callback();
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
const prompt = createInterface({ input: process.stdin, output, terminal: true });
|
|
13
|
+
try {
|
|
14
|
+
const answer = prompt.question("npm token: ");
|
|
15
|
+
muted = true;
|
|
16
|
+
const value = (await answer).trim();
|
|
17
|
+
process.stdout.write("\n");
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
prompt.close();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function resolveNpmToken(options = {}) {
|
|
25
|
+
const explicit = options.explicitToken?.trim();
|
|
26
|
+
if (explicit)
|
|
27
|
+
return explicit;
|
|
28
|
+
const environment = options.environmentToken?.trim();
|
|
29
|
+
if (environment)
|
|
30
|
+
return environment;
|
|
31
|
+
if (options.interactive ?? (process.stdin.isTTY && process.stdout.isTTY)) {
|
|
32
|
+
const prompted = (await (options.prompt ?? securePrompt)()).trim();
|
|
33
|
+
if (prompted)
|
|
34
|
+
return prompted;
|
|
35
|
+
}
|
|
36
|
+
throw new Error("An npm token is required. Set NPM_TOKEN or provide --npm-token in an approved automation environment.");
|
|
37
|
+
}
|
package/bin/npmrc.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const NPMRC_BEGIN = "# BEGIN ENDOUI MANAGED CONFIG";
|
|
4
|
+
export const NPMRC_END = "# END ENDOUI MANAGED CONFIG";
|
|
5
|
+
export const NPMRC_REGION = `${NPMRC_BEGIN}\n@endo-ui:registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=\${NPM_TOKEN}\n${NPMRC_END}`;
|
|
6
|
+
export function updateNpmrcContent(content) {
|
|
7
|
+
const begins = [...content.matchAll(new RegExp(NPMRC_BEGIN, "g"))];
|
|
8
|
+
const ends = [...content.matchAll(new RegExp(NPMRC_END, "g"))];
|
|
9
|
+
if (begins.length === 0 && ends.length === 0) {
|
|
10
|
+
const separator = content.length === 0 ? "" : content.endsWith("\n") ? "\n" : "\n\n";
|
|
11
|
+
return { content: `${content}${separator}${NPMRC_REGION}\n`, changed: true };
|
|
12
|
+
}
|
|
13
|
+
if (begins.length !== 1 || ends.length !== 1 || begins[0].index >= ends[0].index) {
|
|
14
|
+
throw new Error("The EndoUI managed .npmrc markers are malformed or ambiguous.");
|
|
15
|
+
}
|
|
16
|
+
const start = begins[0].index;
|
|
17
|
+
const end = ends[0].index + NPMRC_END.length;
|
|
18
|
+
const existing = content.slice(start, end);
|
|
19
|
+
if (existing === NPMRC_REGION)
|
|
20
|
+
return { content, changed: false };
|
|
21
|
+
return {
|
|
22
|
+
content: `${content.slice(0, start)}${NPMRC_REGION}${content.slice(end)}`,
|
|
23
|
+
changed: true
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export async function ensureEndoUiNpmrc(cwd = process.cwd()) {
|
|
27
|
+
const file = path.join(cwd, ".npmrc");
|
|
28
|
+
let original = "";
|
|
29
|
+
try {
|
|
30
|
+
original = await readFile(file, "utf8");
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
const updated = updateNpmrcContent(original);
|
|
37
|
+
if (updated.changed)
|
|
38
|
+
await writeFile(file, updated.content, "utf8");
|
|
39
|
+
return updated.changed;
|
|
40
|
+
}
|
package/bin/package-version.js
CHANGED
|
@@ -1,9 +1,37 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
function packageVersion(packageName, cwd) {
|
|
5
|
+
const require = createRequire(path.join(cwd, "package.json"));
|
|
6
|
+
let file;
|
|
7
|
+
try {
|
|
8
|
+
file = require.resolve(`${packageName}/package.json`);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new Error(`Unable to find ${packageName} from the current project.`);
|
|
12
|
+
}
|
|
13
|
+
const value = require(file);
|
|
14
|
+
if (value.name !== packageName || typeof value.version !== "string") {
|
|
15
|
+
throw new Error(`${packageName} has invalid package metadata.`);
|
|
16
|
+
}
|
|
17
|
+
return value.version;
|
|
18
|
+
}
|
|
19
|
+
export function resolveEndoUiVersion(cwd = process.cwd()) {
|
|
20
|
+
return packageVersion("@endo-ui/core", cwd);
|
|
21
|
+
}
|
|
22
|
+
export function resolveInstalledLicenseVersion(cwd = process.cwd()) {
|
|
23
|
+
try {
|
|
24
|
+
return packageVersion("@endo-ui/license", cwd);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new Error("Install @endo-ui/license in this project before activation: " +
|
|
28
|
+
"npm install --save-dev @endo-ui/license");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function resolveLicensePackageVersion() {
|
|
32
|
+
const value = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
33
|
+
if (value.name !== "@endo-ui/license" || typeof value.version !== "string") {
|
|
34
|
+
throw new Error("@endo-ui/license has invalid package metadata.");
|
|
35
|
+
}
|
|
36
|
+
return value.version;
|
|
5
37
|
}
|
|
6
|
-
catch {
|
|
7
|
-
throw new Error("Unable to find @endo-ui/core from the current project.");
|
|
8
|
-
} const value = require(file); if (value.name !== "@endo-ui/core" || typeof value.version !== "string")
|
|
9
|
-
throw new Error("@endo-ui/core has invalid package metadata."); return value.version; }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SchematicsException } from "@angular-devkit/schematics";
|
|
2
|
+
import { configureAngularWorkspaceObject } from "../../angular-config.js";
|
|
2
3
|
export default function ngAdd() {
|
|
3
4
|
return (tree) => {
|
|
4
5
|
const workspaceFile = tree.get("angular.json");
|
|
@@ -6,26 +7,14 @@ export default function ngAdd() {
|
|
|
6
7
|
throw new SchematicsException("This project does not contain angular.json.");
|
|
7
8
|
}
|
|
8
9
|
const workspace = JSON.parse(workspaceFile.content.toString("utf8"));
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
const targets = project.architect ?? project.targets;
|
|
14
|
-
if (!targets)
|
|
15
|
-
continue;
|
|
16
|
-
if (targets['build']?.builder === "@angular/build:application") {
|
|
17
|
-
targets['build'].builder = "@endo-ui/license:application";
|
|
18
|
-
configured = true;
|
|
19
|
-
}
|
|
20
|
-
if (targets['serve']?.builder === "@angular/build:dev-server") {
|
|
21
|
-
targets['serve'].builder = "@endo-ui/license:dev-server";
|
|
22
|
-
configured = true;
|
|
23
|
-
}
|
|
10
|
+
try {
|
|
11
|
+
const changed = configureAngularWorkspaceObject(workspace);
|
|
12
|
+
if (changed)
|
|
13
|
+
tree.overwrite("angular.json", `${JSON.stringify(workspace, null, 2)}\n`);
|
|
24
14
|
}
|
|
25
|
-
|
|
26
|
-
throw new SchematicsException(
|
|
15
|
+
catch (error) {
|
|
16
|
+
throw new SchematicsException(error instanceof Error ? error.message : "Unable to configure Angular.");
|
|
27
17
|
}
|
|
28
|
-
tree.overwrite("angular.json", `${JSON.stringify(workspace, null, 2)}\n`);
|
|
29
18
|
return tree;
|
|
30
19
|
};
|
|
31
20
|
}
|
package/bin/storage.js
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
return
|
|
4
|
+
export function leasePath(major, environment = process.env, homeDirectory = os.homedir()) {
|
|
5
|
+
const override = environment["ENDOUI_LICENSE_CACHE"]?.trim();
|
|
6
|
+
return override
|
|
7
|
+
? path.resolve(override)
|
|
8
|
+
: path.join(homeDirectory, ".endoui", `endoui-v${major}.jwt`);
|
|
9
|
+
}
|
|
10
|
+
export async function readLease(major) {
|
|
11
|
+
try {
|
|
12
|
+
return (await readFile(leasePath(major), "utf8")).trim();
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
16
|
+
throw new Error("EndoUI is not activated. Run `endoui-license activate`.");
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function writeLease(major, lease, environment = process.env, homeDirectory = os.homedir()) {
|
|
22
|
+
const file = leasePath(major, environment, homeDirectory);
|
|
23
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
24
|
+
await writeFile(file, `${lease}\n`, { encoding: "utf8", mode: 0o600 });
|
|
25
|
+
if (process.platform !== "win32")
|
|
26
|
+
await chmod(file, 0o600);
|
|
7
27
|
}
|
|
8
|
-
catch (error) {
|
|
9
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
10
|
-
throw new Error("EndoUI is not activated. Run `endoui-license activate`.");
|
|
11
|
-
throw error;
|
|
12
|
-
} }
|
|
13
|
-
export async function writeLease(major, lease) { const file = target(major); await mkdir(path.dirname(file), { recursive: true, mode: 0o700 }); await writeFile(file, `${lease}\n`, { encoding: "utf8", mode: 0o600 }); if (process.platform !== "win32")
|
|
14
|
-
await chmod(file, 0o600); }
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@endo-ui/license",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Developer and CI license activation for EndoUI.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"publishConfig": {
|
|
7
|
-
"access": "
|
|
7
|
+
"access": "public"
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"builders": "./builders.json",
|