@dinglebear/labby 2.5.0 → 2.6.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/lib/platform.js CHANGED
@@ -54,6 +54,10 @@ function installRoot() {
54
54
  return path.resolve(__dirname, "..", "vendor");
55
55
  }
56
56
 
57
+ function companionRoot() {
58
+ return path.join(installRoot(), "tailcat");
59
+ }
60
+
57
61
  function binaryPath(platform = process.platform, arch = process.arch) {
58
62
  const target = targetFor(platform, arch);
59
63
  return path.join(installRoot(), target.binary);
@@ -61,6 +65,7 @@ function binaryPath(platform = process.platform, arch = process.arch) {
61
65
 
62
66
  module.exports = {
63
67
  binaryPath,
68
+ companionRoot,
64
69
  downloadUrl,
65
70
  installRoot,
66
71
  packageVersion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dinglebear/labby",
3
- "version": "2.5.0",
3
+ "version": "2.6.2",
4
4
  "license": "AGPL-3.0-only",
5
5
  "description": "Rust MCP gateway with Code Mode, authentication, setup, logs, CLI, HTTP API, and operator web UI.",
6
6
  "homepage": "https://github.com/dinglebear-ai/labby#readme",
@@ -158,7 +158,8 @@ async function main() {
158
158
  const target = targetFor();
159
159
  const destination = binaryPath();
160
160
 
161
- if (fs.existsSync(destination)) {
161
+ recoverInstallation(installRoot());
162
+ if (installationValid(installRoot(), target, releaseVersion())) {
162
163
  log(`${path.basename(destination)} already installed for ${releaseVersion()}`);
163
164
  return;
164
165
  }
@@ -171,14 +172,90 @@ async function main() {
171
172
  log(`downloading ${url}`);
172
173
  await download(url, archive);
173
174
  await verifyChecksum(url, archive);
174
- extract(archive, installRoot(), target.archiveType);
175
- fs.chmodSync(destination, 0o755);
175
+ installArchive(archive, installRoot(), target, releaseVersion());
176
176
  log(`installed ${destination}`);
177
177
  } finally {
178
178
  fs.rmSync(tempDir, { recursive: true, force: true });
179
179
  }
180
180
  }
181
181
 
182
+ function requiresCompanions(version) {
183
+ const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(version);
184
+ if (!match) throw new Error("invalid release version");
185
+ // Published v2.5.0 archives contain only the binary. Later releases require the bundle.
186
+ const [, major, minor, patch] = match.map(Number);
187
+ return major > 2 || (major === 2 && (minor > 5 || (minor === 5 && patch > 0)));
188
+ }
189
+
190
+ function validateInstallation(root, target, version) {
191
+ const binary = path.join(root, target.binary);
192
+ if (!fs.lstatSync(binary).isFile()) throw new Error("release binary is not regular");
193
+ const companions = path.join(root, "tailcat");
194
+ if (!fs.existsSync(companions)) {
195
+ if (requiresCompanions(version)) throw new Error("release companions missing");
196
+ return;
197
+ }
198
+ if (!fs.lstatSync(companions).isDirectory()) throw new Error("invalid companion directory");
199
+ const manifestPath = path.join(companions, "manifest.json");
200
+ if (!fs.lstatSync(manifestPath).isFile() || fs.statSync(manifestPath).size > 2 * 1024 * 1024) throw new Error("invalid companion manifest");
201
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
202
+ if (manifest.schemaVersion !== 1 || manifest.protocol !== 1 || manifest.version !== version.replace(/^v/, "") || manifest.target !== target.asset.replace(/^lab-/, "").replace(/\.tar\.gz$/, "") || !Array.isArray(manifest.components) || manifest.components.length > 8192) throw new Error("companion release mismatch");
203
+ const expected = new Set();
204
+ let total = 0;
205
+ for (const entry of manifest.components) {
206
+ if (typeof entry.path !== "string" || !entry.path || entry.path.includes("\\") || entry.path.split("/").some(part => !part || part === "." || part === "..") || path.isAbsolute(entry.path) || expected.has(entry.path) || !/^[a-f0-9]{64}$/.test(entry.sha256)) throw new Error("invalid companion entry");
207
+ expected.add(entry.path);
208
+ let file = companions;
209
+ for (const part of entry.path.split("/")) {
210
+ file = path.join(file, part);
211
+ if (fs.lstatSync(file).isSymbolicLink()) throw new Error("symlink companion");
212
+ }
213
+ const stat = fs.statSync(file);
214
+ total += stat.size;
215
+ if (!stat.isFile() || stat.size > 128 * 1024 * 1024 || total > 512 * 1024 * 1024 || crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") !== entry.sha256) throw new Error("companion checksum mismatch");
216
+ }
217
+ let count = 0;
218
+ function inventory(directory, prefix = "", depth = 0) {
219
+ if (depth > 64) throw new Error("companion depth exceeded");
220
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
221
+ if (++count > 16384) throw new Error("companion inventory exceeded");
222
+ const relative = prefix + entry.name;
223
+ if (entry.isDirectory()) inventory(path.join(directory, entry.name), relative + "/", depth + 1);
224
+ else if (!entry.isFile() || (relative !== "manifest.json" && !expected.has(relative))) throw new Error("unlisted companion");
225
+ }
226
+ }
227
+ inventory(companions);
228
+ for (const required of ["native/tailcat-bridge", "browser/hook.mjs", "browser/transport.mjs", "browser/http.mjs", "browser/wasm.mjs", "browser/tailcat.wasm", "browser/tailcat.wasm.gz", "browser/wasm_exec.js", "browser/THIRD_PARTY_NOTICES.md", "adapter/server.mjs", "adapter/cleanup.mjs", "adapter/package.json", "adapter/package-lock.json"]) {
229
+ if (!expected.has(required)) throw new Error("incomplete companion inventory");
230
+ }
231
+ }
232
+
233
+ function installationValid(root, target, version) {
234
+ try { validateInstallation(root, target, version); return true; } catch { return false; }
235
+ }
236
+
237
+ function recoverInstallation(root) {
238
+ const backup = root + ".previous";
239
+ if (!fs.existsSync(root) && fs.existsSync(backup)) fs.renameSync(backup, root);
240
+ }
241
+
242
+ function installArchive(archive, root, target, version) {
243
+ fs.mkdirSync(path.dirname(root), { recursive: true });
244
+ const stage = fs.mkdtempSync(path.join(path.dirname(root), ".labby-install-"));
245
+ const backup = root + ".previous";
246
+ try {
247
+ extract(archive, stage, target.archiveType);
248
+ validateInstallation(stage, target, version);
249
+ fs.chmodSync(path.join(stage, target.binary), 0o755);
250
+ // Preserve the prior complete directory until the new pair has been published.
251
+ if (fs.existsSync(backup)) fs.rmSync(backup, { recursive: true, force: true });
252
+ if (fs.existsSync(root)) fs.renameSync(root, backup);
253
+ try { fs.renameSync(stage, root); }
254
+ catch (error) { recoverInstallation(root); throw error; }
255
+ fs.rmSync(backup, { recursive: true, force: true });
256
+ } finally { fs.rmSync(stage, { recursive: true, force: true }); }
257
+ }
258
+
182
259
  if (require.main === module) {
183
260
  main().catch((error) => {
184
261
  log(error.message);
@@ -187,6 +264,10 @@ if (require.main === module) {
187
264
  }
188
265
 
189
266
  module.exports = {
267
+ extract,
268
+ installArchive,
269
+ installationValid,
270
+ recoverInstallation,
190
271
  powershellExpandArchiveCommand,
191
272
  powershellLiteral,
192
273
  };