@nissyai/desktop 0.2.40 → 0.2.52
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 +15 -3
- package/package.json +4 -4
- package/src/resolve.mjs +19 -11
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
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.2.52",
|
|
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.
|
|
24
|
-
"@nissyai/app-darwin-arm64": "0.2.
|
|
25
|
-
"@nissyai/app-darwin-x64": "0.2.
|
|
23
|
+
"@nissyai/app-win32-x64": "0.2.52",
|
|
24
|
+
"@nissyai/app-darwin-arm64": "0.2.52",
|
|
25
|
+
"@nissyai/app-darwin-x64": "0.2.52"
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|