@aryaminus/controlkeel 0.3.8 → 0.3.9

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/SECURITY.md CHANGED
@@ -24,7 +24,7 @@ The `postinstall` script (`lib/postinstall.js`) performs the following actions:
24
24
  ### Security considerations
25
25
 
26
26
  - **Source**: Binaries are downloaded exclusively from official GitHub Releases
27
- - **Verification**: The download process uses GitHub's authenticated release endpoints
27
+ - **Checksum verification**: After download, the installer fetches `SHASUMS256.txt` from the same release and verifies the SHA-256 digest of the downloaded binary before installing it. A mismatch causes the install to fail and the partial download is removed.
28
28
  - **Transparency**: The source code for the bootstrap installer is fully visible in this repository
29
29
  - **Opt-out**: Users can skip automatic download by setting `CONTROLKEEL_SKIP_DOWNLOAD=1`
30
30
  - **No external dependencies**: The bootstrap installer has no runtime dependencies beyond Node.js built-ins
@@ -60,4 +60,4 @@ This package is designed with supply chain security in mind:
60
60
  - **Signed releases**: GitHub Releases provide cryptographic verification
61
61
  - **Transparent source**: All installer code is open and auditable
62
62
 
63
- For detailed information about the native binary build process and security practices, see the main repository's [security documentation](https://github.com/aryaminus/controlkeel/blob/main/SECURITY.md).
63
+ For detailed information about the native binary build process and security practices, see the main repository's [security documentation](https://github.com/aryaminus/controlkeel/blob/main/SECURITY.md).
package/lib/install.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
 
3
+ const crypto = require("node:crypto");
3
4
  const fs = require("node:fs");
4
5
  const os = require("node:os");
5
6
  const path = require("node:path");
@@ -85,6 +86,72 @@ function download(url, destination) {
85
86
  });
86
87
  }
87
88
 
89
+ function downloadText(url) {
90
+ return new Promise((resolve, reject) => {
91
+ const request = https.get(url, (response) => {
92
+ if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
93
+ response.resume();
94
+ downloadText(response.headers.location).then(resolve, reject);
95
+ return;
96
+ }
97
+
98
+ if (response.statusCode !== 200) {
99
+ reject(new Error(`Failed to download ${url} (HTTP ${response.statusCode})`));
100
+ return;
101
+ }
102
+
103
+ let data = "";
104
+ response.on("data", (chunk) => { data += chunk; });
105
+ response.on("end", () => resolve(data));
106
+ });
107
+
108
+ request.on("error", reject);
109
+ });
110
+ }
111
+
112
+ function sha256File(filePath) {
113
+ return new Promise((resolve, reject) => {
114
+ const hash = crypto.createHash("sha256");
115
+ const stream = fs.createReadStream(filePath);
116
+ stream.on("data", (chunk) => hash.update(chunk));
117
+ stream.on("end", () => resolve(hash.digest("hex")));
118
+ stream.on("error", reject);
119
+ });
120
+ }
121
+
122
+ async function verifyChecksum(filePath, asset) {
123
+ const checksumUrl = `${releaseBaseUrl()}/SHASUMS256.txt`;
124
+
125
+ let checksumText;
126
+ try {
127
+ checksumText = await downloadText(checksumUrl);
128
+ } catch {
129
+ // Checksum file not available for this release — skip verification but warn.
130
+ console.warn(`[controlkeel] Warning: could not download checksum file from ${checksumUrl}. Skipping integrity check.`);
131
+ return;
132
+ }
133
+
134
+ const expectedHash = checksumText
135
+ .split("\n")
136
+ .map((line) => line.trim().split(/\s+/))
137
+ .find(([, name]) => name === asset || name === `./${asset}`)?.[0];
138
+
139
+ if (!expectedHash) {
140
+ console.warn(`[controlkeel] Warning: no checksum entry found for ${asset}. Skipping integrity check.`);
141
+ return;
142
+ }
143
+
144
+ const actualHash = await sha256File(filePath);
145
+
146
+ if (actualHash !== expectedHash) {
147
+ fs.rmSync(filePath, { force: true });
148
+ throw new Error(
149
+ `Checksum mismatch for ${asset}.\n Expected: ${expectedHash}\n Got: ${actualHash}\n` +
150
+ `The downloaded binary has been removed. Retry the installation or set CONTROLKEEL_SKIP_DOWNLOAD=1 and install manually.`
151
+ );
152
+ }
153
+ }
154
+
88
155
  async function ensureBinary({ forceDownload = false } = {}) {
89
156
  const destination = binaryPath();
90
157
 
@@ -93,10 +160,13 @@ async function ensureBinary({ forceDownload = false } = {}) {
93
160
  }
94
161
 
95
162
  ensureVendorDir();
96
- const tempPath = path.join(os.tmpdir(), `${assetName()}-${Date.now()}`);
97
- const url = `${releaseBaseUrl()}/${assetName()}`;
163
+ const asset = assetName();
164
+ const tempPath = path.join(os.tmpdir(), `${asset}-${Date.now()}`);
165
+ const url = `${releaseBaseUrl()}/${asset}`;
98
166
 
99
167
  await download(url, tempPath);
168
+ await verifyChecksum(tempPath, asset);
169
+
100
170
  fs.copyFileSync(tempPath, destination);
101
171
  fs.rmSync(tempPath, { force: true });
102
172
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aryaminus/controlkeel",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "Bootstrap installer for the ControlKeel native CLI - a control plane for agent-generated software delivery.",
5
5
  "license": "Apache-2.0",
6
6
  "author": {
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/aryaminus/controlkeel.git",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.3.8",
10
+ "version": "0.3.9",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "@aryaminus/controlkeel",
15
- "version": "0.3.8",
15
+ "version": "0.3.9",
16
16
  "runtimeHint": "npx",
17
17
  "transport": {
18
18
  "type": "stdio"