@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 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
- await dependencies.install(cwd, version, npmToken);
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 writeFile(shellPath, createShellCiScript(metadata), { encoding: "utf8", mode: 0o755 });
76
- await writeFile(path.join(directory, "endoui-license-ci.ps1"), createPowerShellCiScript(metadata), "utf8");
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
  }
@@ -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
- 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
- }
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
- catch {
12
- throw new Error("Unable to contact npm to validate access to @endo-ui/core.");
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
- if (response.ok) {
15
- let metadata;
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
- metadata = await response.json();
98
+ reportedVersion = JSON.parse(output);
18
99
  }
19
100
  catch {
20
- throw new Error("npm returned invalid package metadata for @endo-ui/core.");
101
+ // Older npm versions can return the plain value even when --json is supplied.
21
102
  }
22
- if (!metadata.versions?.[version]) {
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
- if (response.status === 403 || response.status === 404) {
31
- throw new Error("The npm token does not have read access to @endo-ui/core.");
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
- const separator = content.length === 0 ? "" : content.endsWith("\n") ? "\n" : "\n\n";
11
- return { content: `${content}${separator}${NPMRC_REGION}\n`, changed: true };
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)}${NPMRC_REGION}${content.slice(end)}`,
41
+ content: `${content.slice(0, start)}${regionFor(content)}${content.slice(end)}`,
23
42
  changed: true
24
43
  };
25
44
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@endo-ui/license",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Developer and CI license activation for EndoUI.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "publishConfig": {