@endo-ui/license 1.4.0 → 1.4.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 +39 -1
- package/bin/activation.js +5 -2
- package/bin/ci-scripts.js +25 -3
- package/bin/core-install.js +23 -0
- package/bin/npm-access.js +98 -22
- package/bin/npmrc.js +23 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,6 +32,42 @@ 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
|
+
### Enterprise HTTP/HTTPS proxies
|
|
36
|
+
|
|
37
|
+
EndoUI supports approved forward HTTP/HTTPS egress proxies for npm access. npm package-access
|
|
38
|
+
validation runs through the npm CLI in an isolated temporary project so npm can honor its normal
|
|
39
|
+
proxy, certificate-authority, and environment configuration. The temporary configuration still
|
|
40
|
+
pins the `@endo-ui` scope and `NPM_TOKEN` authentication to `https://registry.npmjs.org/`; the token
|
|
41
|
+
is never written into that temporary file.
|
|
42
|
+
|
|
43
|
+
Configure an approved proxy with normal npm settings, for example:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm config set --location=user proxy http://proxy.example:8080
|
|
47
|
+
npm config set --location=user https-proxy http://proxy.example:8080
|
|
48
|
+
# If the approved proxy uses an enterprise CA:
|
|
49
|
+
npm config set --location=user cafile /path/to/company-ca.pem
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or provide standard proxy environment variables. PowerShell example:
|
|
53
|
+
|
|
54
|
+
```powershell
|
|
55
|
+
$env:HTTP_PROXY = "http://proxy.example:8080"
|
|
56
|
+
$env:HTTPS_PROXY = "http://proxy.example:8080"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
A forward network proxy is different from an internal npm repository or package mirror such as
|
|
60
|
+
Artifactory, Nexus, Azure Artifacts, or Verdaccio. EndoUI does not redirect `NPM_TOKEN` to an
|
|
61
|
+
alternate registry and does not recommend storing the customer EndoUI npm credential in a
|
|
62
|
+
third-party or contractor-controlled repository manager. If organizational policy blocks
|
|
63
|
+
`registry.npmjs.org` entirely instead of allowing it through an approved egress proxy, contact
|
|
64
|
+
EndoUI Support rather than copying the customer npm credential into that repository.
|
|
65
|
+
|
|
66
|
+
If an approved proxy performs TLS interception, the organization operating that trusted proxy can
|
|
67
|
+
observe HTTPS request contents, including registry authorization headers. Use only proxy
|
|
68
|
+
infrastructure approved to handle the customer's EndoUI package credential. Configure the
|
|
69
|
+
organization's trusted CA instead of disabling npm or Node.js TLS verification.
|
|
70
|
+
|
|
35
71
|
## Upgrade
|
|
36
72
|
|
|
37
73
|
Once EndoUI is activated, use Angular's update workflow rather than uninstalling and reinstalling
|
|
@@ -71,7 +107,9 @@ script and are not automatically refreshed.
|
|
|
71
107
|
Commit the generated `scripts/endoui-license-ci.sh` and `scripts/endoui-license-ci.ps1` files.
|
|
72
108
|
Store `NPM_TOKEN` and `ENDOUI_LICENSE_ID` in the CI provider's protected secret storage, then run
|
|
73
109
|
the platform-appropriate generated script immediately after checkout and before dependency
|
|
74
|
-
installation.
|
|
110
|
+
installation. When CI requires an HTTP/HTTPS egress proxy, provide the same approved npm proxy
|
|
111
|
+
configuration or `HTTP_PROXY` / `HTTPS_PROXY` environment variables to both the EndoUI activation
|
|
112
|
+
step and the dependency-install step.
|
|
75
113
|
|
|
76
114
|
CI authorization does not consume a human Seat. The scripts bootstrap the exact license-tool
|
|
77
115
|
version that generated them, validate npm and ECAP access, and cache a signed authorization for
|
package/bin/activation.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { activateCi, activateDeveloper } from "./api.js";
|
|
2
2
|
import { configureAngularWorkspace } from "./angular-config.js";
|
|
3
3
|
import { generateCiScripts } from "./ci-scripts.js";
|
|
4
|
-
import { installCore } from "./core-install.js";
|
|
4
|
+
import { installCore, isCoreReady } from "./core-install.js";
|
|
5
5
|
import { verifyLease } from "./lease.js";
|
|
6
6
|
import { validateNpmAccess } from "./npm-access.js";
|
|
7
7
|
import { ensureEndoUiNpmrc } from "./npmrc.js";
|
|
@@ -22,6 +22,7 @@ const defaults = {
|
|
|
22
22
|
npmrc: ensureEndoUiNpmrc,
|
|
23
23
|
angular: configureAngularWorkspace,
|
|
24
24
|
scripts: generateCiScripts,
|
|
25
|
+
coreReady: isCoreReady,
|
|
25
26
|
install: installCore
|
|
26
27
|
};
|
|
27
28
|
export function resolveCiInputs(environment) {
|
|
@@ -58,10 +59,12 @@ export async function activateLocal(input, overrides = {}) {
|
|
|
58
59
|
if (lease.user !== input.developer) {
|
|
59
60
|
throw new Error("The signed EndoUI authorization does not match the named developer.");
|
|
60
61
|
}
|
|
62
|
+
const coreReady = await dependencies.coreReady(cwd, version);
|
|
61
63
|
await dependencies.npmrc(cwd);
|
|
62
64
|
await dependencies.angular(cwd);
|
|
63
65
|
await dependencies.scripts({ packageVersion: version, customer: lease.customer, licenseId: input.licenseId }, cwd);
|
|
64
|
-
|
|
66
|
+
if (!coreReady)
|
|
67
|
+
await dependencies.install(cwd, version, npmToken);
|
|
65
68
|
await dependencies.writeLease(major(version), signedLease);
|
|
66
69
|
return { version, lease };
|
|
67
70
|
}
|
package/bin/ci-scripts.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
export function maskLicenseId(_licenseId) {
|
|
4
4
|
return "********";
|
|
@@ -68,12 +68,34 @@ try {
|
|
|
68
68
|
}
|
|
69
69
|
`;
|
|
70
70
|
}
|
|
71
|
+
function comparableScript(content) {
|
|
72
|
+
return content
|
|
73
|
+
.replace(/\r\n/g, "\n")
|
|
74
|
+
.replace(/^# Generated: .*$/m, "# Generated: <date>");
|
|
75
|
+
}
|
|
76
|
+
async function writeScriptIfChanged(file, content, mode) {
|
|
77
|
+
try {
|
|
78
|
+
const existing = await readFile(file, "utf8");
|
|
79
|
+
if (comparableScript(existing) === comparableScript(content))
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
if (mode === undefined) {
|
|
87
|
+
await writeFile(file, content, "utf8");
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
await writeFile(file, content, { encoding: "utf8", mode });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
71
93
|
export async function generateCiScripts(metadata, cwd = process.cwd()) {
|
|
72
94
|
const directory = path.join(cwd, "scripts");
|
|
73
95
|
await mkdir(directory, { recursive: true });
|
|
74
96
|
const shellPath = path.join(directory, "endoui-license-ci.sh");
|
|
75
|
-
await
|
|
76
|
-
await
|
|
97
|
+
await writeScriptIfChanged(shellPath, createShellCiScript(metadata), 0o755);
|
|
98
|
+
await writeScriptIfChanged(path.join(directory, "endoui-license-ci.ps1"), createPowerShellCiScript(metadata));
|
|
77
99
|
if (process.platform !== "win32")
|
|
78
100
|
await chmod(shellPath, 0o755);
|
|
79
101
|
}
|
package/bin/core-install.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
const MAX_STDERR_LENGTH = 65_536;
|
|
3
5
|
function redact(value, npmToken) {
|
|
4
6
|
return npmToken ? value.split(npmToken).join("[REDACTED]") : value;
|
|
@@ -10,6 +12,26 @@ export function npmInstallFailure(specification, exitCode, stderr, npmToken, sig
|
|
|
10
12
|
`npm exit code: ${exitCode ?? "unknown"}\n` +
|
|
11
13
|
`Reason:\n${reason}`);
|
|
12
14
|
}
|
|
15
|
+
async function readJson(file) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
21
|
+
return undefined;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function isCoreReady(cwd, version) {
|
|
26
|
+
const manifest = await readJson(path.join(cwd, "package.json"));
|
|
27
|
+
const declared = Boolean(manifest?.dependencies?.["@endo-ui/core"] ??
|
|
28
|
+
manifest?.devDependencies?.["@endo-ui/core"] ??
|
|
29
|
+
manifest?.optionalDependencies?.["@endo-ui/core"]);
|
|
30
|
+
if (!declared)
|
|
31
|
+
return false;
|
|
32
|
+
const installed = await readJson(path.join(cwd, "node_modules", "@endo-ui", "core", "package.json"));
|
|
33
|
+
return installed?.["name"] === "@endo-ui/core" && installed?.["version"] === version;
|
|
34
|
+
}
|
|
13
35
|
export async function installCore(cwd = process.cwd(), version, npmToken) {
|
|
14
36
|
const isWindows = process.platform === "win32";
|
|
15
37
|
const command = isWindows
|
|
@@ -25,6 +47,7 @@ export async function installCore(cwd = process.cwd(), version, npmToken) {
|
|
|
25
47
|
cwd,
|
|
26
48
|
env: {
|
|
27
49
|
...process.env,
|
|
50
|
+
NPM_CONFIG_SAVE_EXACT: "true",
|
|
28
51
|
...(npmToken ? { NPM_TOKEN: npmToken } : {})
|
|
29
52
|
},
|
|
30
53
|
stdio: ["inherit", "inherit", "pipe"],
|
package/bin/npm-access.js
CHANGED
|
@@ -1,34 +1,110 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { NPMRC_REGION } from "./npmrc.js";
|
|
6
|
+
const MAX_OUTPUT_LENGTH = 65_536;
|
|
7
|
+
const NETWORK_ERROR_PATTERN = /\b(?:EAI_AGAIN|ECONNABORTED|ECONNREFUSED|ECONNRESET|ENETUNREACH|ENOTFOUND|ETIMEDOUT|ERR_SOCKET_TIMEOUT)\b/i;
|
|
8
|
+
const PROXY_AUTH_PATTERN = /\b407\b|proxy authentication required/i;
|
|
9
|
+
const TLS_ERROR_PATTERN = /\b(?:CERT_HAS_EXPIRED|DEPTH_ZERO_SELF_SIGNED_CERT|ERR_TLS_CERT_ALTNAME_INVALID|SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_VERIFY_LEAF_SIGNATURE)\b/i;
|
|
10
|
+
function appendOutput(current, chunk) {
|
|
11
|
+
return `${current}${chunk}`.slice(-MAX_OUTPUT_LENGTH);
|
|
12
|
+
}
|
|
13
|
+
function redact(value, token) {
|
|
14
|
+
return value.split(token).join("[REDACTED]");
|
|
15
|
+
}
|
|
16
|
+
async function runNpm(args, options) {
|
|
17
|
+
const isWindows = process.platform === "win32";
|
|
18
|
+
const command = isWindows ? process.env["ComSpec"] ?? "cmd.exe" : "npm";
|
|
19
|
+
const commandArgs = isWindows
|
|
20
|
+
? ["/d", "/s", "/c", "npm", ...args]
|
|
21
|
+
: [...args];
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const child = spawn(command, commandArgs, {
|
|
24
|
+
cwd: options.cwd,
|
|
25
|
+
env: options.env,
|
|
26
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
27
|
+
shell: false
|
|
28
|
+
});
|
|
29
|
+
let stdout = "";
|
|
30
|
+
let stderr = "";
|
|
31
|
+
child.stdout?.setEncoding("utf8");
|
|
32
|
+
child.stderr?.setEncoding("utf8");
|
|
33
|
+
child.stdout?.on("data", (chunk) => {
|
|
34
|
+
stdout = appendOutput(stdout, chunk);
|
|
35
|
+
});
|
|
36
|
+
child.stderr?.on("data", (chunk) => {
|
|
37
|
+
stderr = appendOutput(stderr, chunk);
|
|
9
38
|
});
|
|
39
|
+
child.once("error", reject);
|
|
40
|
+
child.once("exit", exitCode => resolve({ exitCode, stdout, stderr }));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function npmFailure(result, token) {
|
|
44
|
+
const output = redact(`${result.stdout}\n${result.stderr}`, token);
|
|
45
|
+
if (/\bE401\b|401\s+Unauthorized/i.test(output)) {
|
|
46
|
+
return new Error("The npm token is invalid, expired, or revoked.");
|
|
47
|
+
}
|
|
48
|
+
if (/\bE403\b|\bE404\b|403\s+Forbidden|404\s+Not Found/i.test(output)) {
|
|
49
|
+
return new Error("The npm token does not have read access to @endo-ui/core.");
|
|
50
|
+
}
|
|
51
|
+
if (PROXY_AUTH_PATTERN.test(output)) {
|
|
52
|
+
return new Error("The configured npm network proxy rejected authentication while EndoUI was validating " +
|
|
53
|
+
"access to @endo-ui/core. Verify the organization's approved npm proxy credentials.");
|
|
54
|
+
}
|
|
55
|
+
if (TLS_ERROR_PATTERN.test(output)) {
|
|
56
|
+
return new Error("npm TLS validation failed while EndoUI was checking @endo-ui/core. Configure the organization's " +
|
|
57
|
+
"approved certificate authority through npm's cafile setting or the approved Node.js trust " +
|
|
58
|
+
"configuration; do not disable TLS verification.");
|
|
10
59
|
}
|
|
11
|
-
|
|
12
|
-
|
|
60
|
+
if (NETWORK_ERROR_PATTERN.test(output)) {
|
|
61
|
+
return new Error("Unable to contact npm to validate access to @endo-ui/core. Configure an approved HTTP/HTTPS " +
|
|
62
|
+
"egress proxy through npm's proxy/https-proxy settings or HTTP_PROXY/HTTPS_PROXY. EndoUI " +
|
|
63
|
+
"keeps NPM_TOKEN scoped to registry.npmjs.org and does not use it with alternate package registries.");
|
|
13
64
|
}
|
|
14
|
-
|
|
15
|
-
|
|
65
|
+
return new Error(`npm access validation failed while checking @endo-ui/core (npm exit code ${result.exitCode ?? "unknown"}).`);
|
|
66
|
+
}
|
|
67
|
+
export async function validateNpmAccess(token, version, runner = runNpm) {
|
|
68
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "endoui-npm-access-"));
|
|
69
|
+
try {
|
|
70
|
+
await writeFile(path.join(directory, "package.json"), '{"private":true}\n', "utf8");
|
|
71
|
+
await writeFile(path.join(directory, ".npmrc"), `${NPMRC_REGION}\n`, {
|
|
72
|
+
encoding: "utf8",
|
|
73
|
+
mode: 0o600
|
|
74
|
+
});
|
|
75
|
+
let result;
|
|
76
|
+
try {
|
|
77
|
+
result = await runner(["view", `@endo-ui/core@${version}`, "version", "--json"], {
|
|
78
|
+
cwd: directory,
|
|
79
|
+
env: {
|
|
80
|
+
...process.env,
|
|
81
|
+
NPM_TOKEN: token,
|
|
82
|
+
NPM_CONFIG_CACHE: path.join(directory, "cache"),
|
|
83
|
+
NPM_CONFIG_OFFLINE: "false",
|
|
84
|
+
NPM_CONFIG_PREFER_OFFLINE: "false",
|
|
85
|
+
NPM_CONFIG_PREFER_ONLINE: "true",
|
|
86
|
+
NPM_CONFIG_UPDATE_NOTIFIER: "false"
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new Error("Unable to start npm while validating access to @endo-ui/core.");
|
|
92
|
+
}
|
|
93
|
+
if (result.exitCode !== 0)
|
|
94
|
+
throw npmFailure(result, token);
|
|
95
|
+
const output = result.stdout.trim();
|
|
96
|
+
let reportedVersion = output;
|
|
16
97
|
try {
|
|
17
|
-
|
|
98
|
+
reportedVersion = JSON.parse(output);
|
|
18
99
|
}
|
|
19
100
|
catch {
|
|
20
|
-
|
|
101
|
+
// Older npm versions can return the plain value even when --json is supplied.
|
|
21
102
|
}
|
|
22
|
-
if (
|
|
103
|
+
if (reportedVersion !== version) {
|
|
23
104
|
throw new Error(`@endo-ui/core@${version} is not available to this npm token.`);
|
|
24
105
|
}
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
if (response.status === 401) {
|
|
28
|
-
throw new Error("The npm token is invalid, expired, or revoked.");
|
|
29
106
|
}
|
|
30
|
-
|
|
31
|
-
|
|
107
|
+
finally {
|
|
108
|
+
await rm(directory, { recursive: true, force: true });
|
|
32
109
|
}
|
|
33
|
-
throw new Error(`npm access validation failed with HTTP ${response.status}.`);
|
|
34
110
|
}
|
package/bin/npmrc.js
CHANGED
|
@@ -3,12 +3,30 @@ import path from "node:path";
|
|
|
3
3
|
export const NPMRC_BEGIN = "# BEGIN ENDOUI MANAGED CONFIG";
|
|
4
4
|
export const NPMRC_END = "# END ENDOUI MANAGED CONFIG";
|
|
5
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
|
+
const NPMRC_REGISTRY_LINE = "@endo-ui:registry=https://registry.npmjs.org/";
|
|
7
|
+
const NPMRC_TOKEN_LINE = "//registry.npmjs.org/:_authToken=${NPM_TOKEN}";
|
|
8
|
+
function normalizedLines(content) {
|
|
9
|
+
return content.replace(/\r\n/g, "\n").split("\n").map(line => line.trim());
|
|
10
|
+
}
|
|
11
|
+
function hasEquivalentUnmanagedConfig(content) {
|
|
12
|
+
const lines = new Set(normalizedLines(content));
|
|
13
|
+
return lines.has(NPMRC_REGISTRY_LINE) && lines.has(NPMRC_TOKEN_LINE);
|
|
14
|
+
}
|
|
15
|
+
function newlineFor(content) {
|
|
16
|
+
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
17
|
+
}
|
|
18
|
+
function regionFor(content) {
|
|
19
|
+
return NPMRC_REGION.replace(/\n/g, newlineFor(content));
|
|
20
|
+
}
|
|
6
21
|
export function updateNpmrcContent(content) {
|
|
7
22
|
const begins = [...content.matchAll(new RegExp(NPMRC_BEGIN, "g"))];
|
|
8
23
|
const ends = [...content.matchAll(new RegExp(NPMRC_END, "g"))];
|
|
9
24
|
if (begins.length === 0 && ends.length === 0) {
|
|
10
|
-
|
|
11
|
-
|
|
25
|
+
if (hasEquivalentUnmanagedConfig(content))
|
|
26
|
+
return { content, changed: false };
|
|
27
|
+
const newline = newlineFor(content);
|
|
28
|
+
const separator = content.length === 0 ? "" : content.endsWith(newline) ? newline : `${newline}${newline}`;
|
|
29
|
+
return { content: `${content}${separator}${regionFor(content)}${newline}`, changed: true };
|
|
12
30
|
}
|
|
13
31
|
if (begins.length !== 1 || ends.length !== 1 || begins[0].index >= ends[0].index) {
|
|
14
32
|
throw new Error("The EndoUI managed .npmrc markers are malformed or ambiguous.");
|
|
@@ -16,10 +34,11 @@ export function updateNpmrcContent(content) {
|
|
|
16
34
|
const start = begins[0].index;
|
|
17
35
|
const end = ends[0].index + NPMRC_END.length;
|
|
18
36
|
const existing = content.slice(start, end);
|
|
19
|
-
if (existing === NPMRC_REGION)
|
|
37
|
+
if (existing.replace(/\r\n/g, "\n") === NPMRC_REGION) {
|
|
20
38
|
return { content, changed: false };
|
|
39
|
+
}
|
|
21
40
|
return {
|
|
22
|
-
content: `${content.slice(0, start)}${
|
|
41
|
+
content: `${content.slice(0, start)}${regionFor(content)}${content.slice(end)}`,
|
|
23
42
|
changed: true
|
|
24
43
|
};
|
|
25
44
|
}
|