@onreza/nrz 0.36.1 → 0.36.3

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
@@ -33,14 +33,25 @@ nrz upgrade --channel beta
33
33
 
34
34
  **Linux/macOS:**
35
35
  ```bash
36
- curl -fsSL https://raw.githubusercontent.com/onreza/nrz-cli/main/install.sh | bash
36
+ installer="$(mktemp)"
37
+ curl -fsSL https://raw.githubusercontent.com/onreza/nrz-cli/main/install.sh -o "$installer"
38
+ less "$installer" # optional: review before running
39
+ bash "$installer"
40
+ rm -f "$installer"
37
41
  ```
38
42
 
39
43
  **Windows (PowerShell 7+):**
40
44
  ```powershell
41
- iwr -useb https://raw.githubusercontent.com/onreza/nrz-cli/main/install.ps1 | iex
45
+ $installer = (New-TemporaryFile).FullName
46
+ iwr -useb https://raw.githubusercontent.com/onreza/nrz-cli/main/install.ps1 -OutFile $installer
47
+ Get-Content $installer # optional: review before running
48
+ pwsh -NoProfile -File $installer
49
+ Remove-Item $installer
42
50
  ```
43
51
 
52
+ Both installers verify the downloaded archive against the SHA-256 manifest
53
+ published with the selected GitHub release.
54
+
44
55
  ## Documentation
45
56
 
46
57
  Full documentation and source code: [github.com/onreza/nrz-cli](https://github.com/onreza/nrz-cli)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onreza/nrz",
3
- "version": "0.36.1",
3
+ "version": "0.36.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "ONREZA platform CLI - dev, build, deploy",
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { platform, arch } from "node:process";
3
3
  import https from "node:https";
4
- import http from "node:http";
5
- import { chmodSync, mkdirSync, renameSync, existsSync } from "node:fs";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, renameSync, rmSync } from "node:fs";
6
6
  import { join, dirname } from "node:path";
7
7
  import { createGunzip } from "node:zlib";
8
8
  import { fileURLToPath } from "node:url";
@@ -13,19 +13,26 @@ const __dirname = dirname(__filename);
13
13
 
14
14
  const MANIFEST = {
15
15
  "assets": {
16
- "linux-x64": "https://github.com/ONREZA/nrz-cli/releases/download/v0.36.1/nrz-linux-x64.tar.gz",
17
- "darwin-x64": "https://github.com/ONREZA/nrz-cli/releases/download/v0.36.1/nrz-darwin-x64.tar.gz",
18
- "darwin-arm64": "https://github.com/ONREZA/nrz-cli/releases/download/v0.36.1/nrz-darwin-arm64.tar.gz",
19
- "win32-x64": "https://github.com/ONREZA/nrz-cli/releases/download/v0.36.1/nrz-win32-x64.tar.gz"
16
+ "linux-x64": "https://github.com/ONREZA/nrz-cli/releases/download/v0.36.3/nrz-linux-x64.tar.gz",
17
+ "darwin-x64": "https://github.com/ONREZA/nrz-cli/releases/download/v0.36.3/nrz-darwin-x64.tar.gz",
18
+ "darwin-arm64": "https://github.com/ONREZA/nrz-cli/releases/download/v0.36.3/nrz-darwin-arm64.tar.gz",
19
+ "win32-x64": "https://github.com/ONREZA/nrz-cli/releases/download/v0.36.3/nrz-win32-x64.tar.gz"
20
+ },
21
+ "checksums": {
22
+ "linux-x64": "8d0354827b77c7aed5af158c9c481340ae4bb7750f1f93306f6c6112f5856beb",
23
+ "darwin-x64": "54d8edfb035a0921790330257699ffd8b4b9097d9ebbdd1fc1afc3feee927eb0",
24
+ "darwin-arm64": "54b1ef47f7fe83d4f74f2f6e55d2adcd6d5f88c8c65f35289e7cb2de90faf3b2",
25
+ "win32-x64": "5d1abea64dad0df10b14b148d2e902212697bbf064180ad135e870999aeb38da"
20
26
  },
21
27
  "binName": "nrz",
22
- "version": "0.36.1"
28
+ "version": "0.36.3"
23
29
  };
24
30
 
25
31
  const platformKey = `${platform}-${arch}`;
26
32
  const downloadUrl = MANIFEST.assets[platformKey];
33
+ const expectedSha256 = MANIFEST.checksums[platformKey];
27
34
 
28
- if (!downloadUrl) {
35
+ if (!downloadUrl || !expectedSha256) {
29
36
  console.error(`No binary available for ${platformKey}`);
30
37
  console.error(`Supported platforms: ${Object.keys(MANIFEST.assets).join(", ")}`);
31
38
  process.exit(1);
@@ -33,36 +40,115 @@ if (!downloadUrl) {
33
40
 
34
41
  const binDir = join(__dirname, "..", "bin");
35
42
  mkdirSync(binDir, { recursive: true });
43
+ const stagingDir = mkdtempSync(join(binDir, ".nrz-install-"));
44
+ const sourceName = MANIFEST.sourceBinName || MANIFEST.binName;
45
+ const ext = platform === "win32" ? ".exe" : "";
46
+ const sourcePath = join(stagingDir, sourceName + ext);
47
+ const targetPath = join(binDir, MANIFEST.binName + ext);
48
+ const backupPath = join(binDir, `.${MANIFEST.binName}${ext}.old-${randomUUID()}`);
49
+
50
+ function replaceInstalledBinary(candidatePath, targetPath, backupPath) {
51
+ if (platform !== "win32" || !existsSync(targetPath)) {
52
+ renameSync(candidatePath, targetPath);
53
+ return;
54
+ }
55
+
56
+ const targetStat = lstatSync(targetPath);
57
+ if (!targetStat.isFile() && !targetStat.isSymbolicLink()) {
58
+ throw new Error(`Refusing to replace non-file binary path: ${targetPath}`);
59
+ }
60
+
61
+ renameSync(targetPath, backupPath);
62
+ try {
63
+ renameSync(candidatePath, targetPath);
64
+ } catch (installError) {
65
+ try {
66
+ renameSync(backupPath, targetPath);
67
+ } catch (restoreError) {
68
+ throw new Error(
69
+ `Failed to install replacement (${installError.message}) and restore previous binary (${restoreError.message}); previous binary remains at ${backupPath}`,
70
+ );
71
+ }
72
+ throw new Error(
73
+ `Failed to install replacement; previous binary was restored: ${installError.message}`,
74
+ );
75
+ }
76
+
77
+ try {
78
+ rmSync(backupPath, { force: true });
79
+ } catch (cleanupError) {
80
+ console.warn(
81
+ `Installed new binary but could not remove backup ${backupPath}: ${cleanupError.message}`,
82
+ );
83
+ }
84
+ }
36
85
 
37
- function download(url, redirectCount = 0) {
86
+ function download(url, expectedSha256, redirectCount = 0) {
38
87
  if (redirectCount > 5) {
39
88
  return Promise.reject(new Error("Too many redirects"));
40
89
  }
41
90
 
42
91
  return new Promise((resolve, reject) => {
43
- const protocol = url.startsWith("https") ? https : http;
92
+ const parsed = new URL(url);
93
+ if (parsed.protocol !== "https:") {
94
+ reject(new Error(`Refusing non-HTTPS download URL: ${url}`));
95
+ return;
96
+ }
44
97
 
45
- const request = protocol.get(url, (res) => {
98
+ const request = https.get(url, (res) => {
46
99
  if ([301, 302, 307, 308].includes(res.statusCode)) {
47
100
  const location = res.headers.location;
48
101
  if (!location) {
49
102
  reject(new Error("Redirect without location header"));
50
103
  return;
51
104
  }
52
- download(location, redirectCount + 1).then(resolve).catch(reject);
105
+ res.resume();
106
+ download(new URL(location, url).toString(), expectedSha256, redirectCount + 1)
107
+ .then(resolve)
108
+ .catch(reject);
53
109
  return;
54
110
  }
55
111
 
56
112
  if (res.statusCode !== 200) {
113
+ res.resume();
57
114
  reject(new Error(`Download failed: HTTP ${res.statusCode}`));
58
115
  return;
59
116
  }
117
+ const contentLength = Number(res.headers["content-length"]);
118
+ if (Number.isFinite(contentLength) && contentLength > 256 * 1024 * 1024) {
119
+ res.resume();
120
+ reject(new Error("Download exceeds 256 MiB"));
121
+ return;
122
+ }
60
123
 
61
- res
62
- .pipe(createGunzip())
63
- .pipe(tar.extract({ cwd: binDir, strip: 1 }))
64
- .on("finish", resolve)
65
- .on("error", reject);
124
+ const chunks = [];
125
+ let received = 0;
126
+ res.on("data", (chunk) => {
127
+ received += chunk.length;
128
+ if (received > 256 * 1024 * 1024) {
129
+ request.destroy(new Error("Download exceeds 256 MiB"));
130
+ return;
131
+ }
132
+ chunks.push(chunk);
133
+ });
134
+ res.on("end", () => {
135
+ const archive = Buffer.concat(chunks);
136
+ const actualSha256 = createHash("sha256").update(archive).digest("hex");
137
+ if (actualSha256 !== expectedSha256) {
138
+ reject(new Error(
139
+ `SHA-256 mismatch: expected ${expectedSha256}, received ${actualSha256}`,
140
+ ));
141
+ return;
142
+ }
143
+ const gunzip = createGunzip();
144
+ const extract = tar.extract({ cwd: stagingDir, strip: 1 });
145
+ gunzip.on("error", reject);
146
+ extract.on("error", reject);
147
+ extract.on("finish", resolve);
148
+ gunzip.pipe(extract);
149
+ gunzip.end(archive);
150
+ });
151
+ res.on("error", reject);
66
152
  });
67
153
 
68
154
  request.on("error", reject);
@@ -75,27 +161,32 @@ function download(url, redirectCount = 0) {
75
161
 
76
162
  console.log(`Installing ${MANIFEST.binName} for ${platformKey}...`);
77
163
 
78
- download(downloadUrl)
164
+ download(downloadUrl, expectedSha256)
79
165
  .then(() => {
80
- const sourceName = MANIFEST.sourceBinName || MANIFEST.binName;
81
- const ext = platform === "win32" ? ".exe" : "";
82
- const sourcePath = join(binDir, sourceName + ext);
83
- const targetPath = join(binDir, MANIFEST.binName + ext);
84
-
85
- if (sourceName !== MANIFEST.binName && existsSync(sourcePath)) {
86
- renameSync(sourcePath, targetPath);
166
+ if (!existsSync(sourcePath)) {
167
+ throw new Error(`Archive did not contain ${MANIFEST.binName}${ext}`);
87
168
  }
88
169
 
89
170
  if (platform !== "win32") {
90
171
  try {
91
- chmodSync(targetPath, 0o755);
172
+ chmodSync(sourcePath, 0o755);
92
173
  } catch {
93
174
  }
94
175
  }
176
+ replaceInstalledBinary(sourcePath, targetPath, backupPath);
95
177
  console.log(`Installed ${MANIFEST.binName} v${MANIFEST.version}`);
96
178
  })
97
179
  .catch((err) => {
98
180
  console.error(`Failed to install binary: ${err.message}`);
99
181
  console.error(`URL: ${downloadUrl}`);
100
- process.exit(1);
182
+ process.exitCode = 1;
183
+ })
184
+ .finally(() => {
185
+ try {
186
+ rmSync(stagingDir, { recursive: true, force: true });
187
+ } catch (cleanupError) {
188
+ console.warn(
189
+ `Could not remove staging directory ${stagingDir}: ${cleanupError.message}`,
190
+ );
191
+ }
101
192
  });