@nissyai/desktop 0.2.40 → 0.2.51

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/bin/nissy.mjs CHANGED
@@ -68,7 +68,7 @@ function fail(message) {
68
68
  process.exit(1);
69
69
  }
70
70
 
71
- function validateArchive(archivePath, bundleName) {
71
+ function validateArchive(archivePath) {
72
72
  const listing = execFileSync("tar", ["-tzf", archivePath], {
73
73
  encoding: "utf8",
74
74
  stdio: ["ignore", "pipe", "inherit"],
@@ -77,7 +77,7 @@ function validateArchive(archivePath, bundleName) {
77
77
  .split(/\r?\n/u)
78
78
  .map((entry) => entry.trim())
79
79
  .filter((entry) => entry.length > 0);
80
- const unsafe = unsafeArchiveEntries(entries, bundleName);
80
+ const unsafe = unsafeArchiveEntries(entries);
81
81
  if (unsafe.length > 0) {
82
82
  throw new Error(
83
83
  `payload archive contains unsafe entries: ${unsafe.slice(0, 5).join(", ")}`,
@@ -224,7 +224,7 @@ if (existsSync(join(payloadRoot, "app"))) {
224
224
  try {
225
225
  rmSync(tmp, { recursive: true, force: true });
226
226
  mkdirSync(tmp, { recursive: true });
227
- validateArchive(archivePath, bundleName);
227
+ validateArchive(archivePath);
228
228
  execFileSync("tar", ["-xzf", archivePath, "-C", tmp], { stdio: "inherit" });
229
229
  const tmpApp = join(tmp, bundleName);
230
230
  if (!existsSync(tmpApp) || !isExtractedBundleInside(tmp, tmpApp)) {
@@ -256,6 +256,18 @@ if (existsSync(join(payloadRoot, "app"))) {
256
256
  if (!existsSync(exePath)) {
257
257
  fail(`failed to install ${installedApp}: ${err.message}`);
258
258
  }
259
+ // The previous install is still there, so we CAN carry on — and carrying
260
+ // on is right: a user with a working app should not be stopped from
261
+ // opening it because the new version would not unpack.
262
+ //
263
+ // But say so. Silence here is what made the validation bug above cost
264
+ // real users their updates rather than a bug report: the app relaunched
265
+ // the old version, found the same update waiting, and looped — with
266
+ // nothing written anywhere to say an install had been attempted at all.
267
+ console.error(
268
+ `[nissy] could not install ${version}: ${err.message}\n` +
269
+ `[nissy] continuing on the installed version at ${installedApp}`,
270
+ );
259
271
  }
260
272
  }
261
273
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nissyai/desktop",
3
- "version": "0.2.40",
3
+ "version": "0.2.51",
4
4
  "type": "module",
5
5
  "description": "Launcher for the Nissy desktop app. Resolves the host's app payload and spawns it. Run via `npx @nissyai/desktop`; a global install (`npm i -g @nissyai/desktop`) exposes the `nissy` command.",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -20,8 +20,8 @@
20
20
  "src/"
21
21
  ],
22
22
  "optionalDependencies": {
23
- "@nissyai/app-win32-x64": "0.2.40",
24
- "@nissyai/app-darwin-arm64": "0.2.40",
25
- "@nissyai/app-darwin-x64": "0.2.40"
23
+ "@nissyai/app-win32-x64": "0.2.51",
24
+ "@nissyai/app-darwin-arm64": "0.2.51",
25
+ "@nissyai/app-darwin-x64": "0.2.51"
26
26
  }
27
27
  }
package/src/resolve.mjs CHANGED
@@ -118,28 +118,36 @@ export function payloadCacheKey(pkg, version) {
118
118
  * this boundary: npm package replacement or a compromised publish could turn
119
119
  * a path traversal entry into a write outside the temporary install directory.
120
120
  *
121
+ * "Unsafe" means exactly one thing: the entry ESCAPES the directory we extract
122
+ * into — an absolute path, a drive letter, or a `..` segment. It does not mean
123
+ * "not part of the bundle". `pack-npm.mjs` archives the whole Forge output
124
+ * (`tar -C <forgeDir> .`), so every real payload carries `./LICENSE`,
125
+ * `./version` and `./LICENSES.chromium.html` alongside `./Nissy.app/`. Rejecting
126
+ * those made EVERY macOS update fail validation, and because the caller swallows
127
+ * the failure whenever the old app is still installed, the app silently relaunched
128
+ * the old version and found the same update again — for ever.
129
+ *
130
+ * Siblings are harmless by construction: they extract into a throwaway temp
131
+ * directory and only the `.app` is ever moved out of it. That the bundle is
132
+ * present, and is a real path inside the temp directory, is the caller's check
133
+ * (`isExtractedBundleInside`) — after extraction, where it can be verified
134
+ * rather than inferred from a name.
135
+ *
121
136
  * Tar listings commonly prefix names with `./`, and the bundle itself contains
122
- * legitimate framework symlinks, so this validates names only. The caller
123
- * separately verifies the extracted top-level bundle is a real path inside the
124
- * temporary directory before swapping it into ~/Applications.
137
+ * legitimate framework symlinks, so this validates names only.
125
138
  *
126
139
  * @param {readonly string[]} entries names from `tar -tzf`
127
- * @param {string} bundleName expected top-level `.app` directory name
128
140
  * @returns {string[]} original unsafe entry names
129
141
  */
130
- export function unsafeArchiveEntries(entries, bundleName) {
131
- const root = bundleName.replace(/\\/g, "/").replace(/^\.\/+|\/+$/g, "");
142
+ export function unsafeArchiveEntries(entries) {
132
143
  return entries.filter((entry) => {
133
144
  const normalized = entry.replace(/\\/g, "/").replace(/^\.\/+/, "");
134
145
  if (normalized === "") return false;
135
- if (
146
+ return (
136
147
  normalized.startsWith("/") ||
137
148
  /^[A-Za-z]:\//u.test(normalized) ||
138
149
  normalized.split("/").includes("..")
139
- ) {
140
- return true;
141
- }
142
- return normalized !== root && !normalized.startsWith(`${root}/`);
150
+ );
143
151
  });
144
152
  }
145
153