codex-copilot-dx 0.6.6 → 0.6.8
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 +38 -14
- package/bin/cli.mjs +16 -3
- package/package.json +1 -1
- package/src/cli-auth.mjs +99 -3
- package/src/cli-models.mjs +60 -5
- package/src/cli-options.mjs +43 -16
- package/src/cli-table.mjs +114 -0
- package/src/config.mjs +18 -0
- package/src/pm-studio-asar.mjs +100 -1
- package/src/pm-studio-setup.mjs +302 -25
- package/src/pm-studio-status.mjs +81 -23
- package/src/usage.mjs +104 -9
package/src/pm-studio-setup.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import {
|
|
7
7
|
AUTH_PROFILE_CLAUDE,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
export const DEFAULT_PM_STUDIO_APP_PATH = "/Applications/PM Studio.app";
|
|
20
20
|
export const PM_STUDIO_CLAUDE_AUTH_COMMAND = "ccdx auth login claude --reauth --github-login <personal-login>";
|
|
21
21
|
const MIN_FREE_MARGIN_BYTES = 16 * 1024 * 1024;
|
|
22
|
+
const BUNDLE_CONTENT_SCHEME = "ccdx-bundle-content-v2";
|
|
22
23
|
|
|
23
24
|
function setupError(code, message, details) {
|
|
24
25
|
const error = new Error(message);
|
|
@@ -31,7 +32,7 @@ function defaultProcessRunner(command, args) {
|
|
|
31
32
|
return spawnSync(command, args, {
|
|
32
33
|
encoding: "utf8",
|
|
33
34
|
windowsHide: true,
|
|
34
|
-
maxBuffer:
|
|
35
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -53,6 +54,166 @@ function runChecked(processRunner, command, args, label) {
|
|
|
53
54
|
return result;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
function hashRegularFile(filePath, expected) {
|
|
58
|
+
const hash = createHash("sha256");
|
|
59
|
+
const file = fs.openSync(filePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
60
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
61
|
+
try {
|
|
62
|
+
const before = fs.fstatSync(file, { bigint: true });
|
|
63
|
+
if (before.dev !== BigInt(expected.dev)
|
|
64
|
+
|| before.ino !== BigInt(expected.ino)
|
|
65
|
+
|| before.size !== BigInt(expected.size)) {
|
|
66
|
+
throw setupError("PM_STUDIO_BUNDLE_CHANGED", `PM Studio bundle changed while reading ${filePath}`);
|
|
67
|
+
}
|
|
68
|
+
for (;;) {
|
|
69
|
+
const length = fs.readSync(file, buffer, 0, buffer.length, null);
|
|
70
|
+
if (length === 0) break;
|
|
71
|
+
hash.update(buffer.subarray(0, length));
|
|
72
|
+
}
|
|
73
|
+
const after = fs.fstatSync(file, { bigint: true });
|
|
74
|
+
if (before.dev !== after.dev
|
|
75
|
+
|| before.ino !== after.ino
|
|
76
|
+
|| before.size !== after.size
|
|
77
|
+
|| before.mtimeNs !== after.mtimeNs
|
|
78
|
+
|| before.ctimeNs !== after.ctimeNs) {
|
|
79
|
+
throw setupError("PM_STUDIO_BUNDLE_CHANGED", `PM Studio bundle changed while hashing ${filePath}`);
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
fs.closeSync(file);
|
|
83
|
+
}
|
|
84
|
+
return hash.digest("hex");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseXattrHex(output, root, ignoredXattrs) {
|
|
88
|
+
const records = [];
|
|
89
|
+
const ignored = new Set(ignoredXattrs);
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
let current = null;
|
|
92
|
+
const flush = () => {
|
|
93
|
+
if (!current) return;
|
|
94
|
+
if (!ignored.has(current.name)) {
|
|
95
|
+
const key = `${current.path}\0${current.name}`;
|
|
96
|
+
if (seen.has(key)) throw setupError("PM_STUDIO_XATTR_INVALID", `Duplicate extended attribute ${current.name}`);
|
|
97
|
+
seen.add(key);
|
|
98
|
+
const value = Buffer.from(current.hex.join(""), "hex");
|
|
99
|
+
records.push(["xattr", current.path, current.name, value.length, sha256Hex(value)]);
|
|
100
|
+
}
|
|
101
|
+
current = null;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
for (const line of String(output || "").split(/\r?\n/)) {
|
|
105
|
+
if (!line) continue;
|
|
106
|
+
const separator = line.lastIndexOf(": ");
|
|
107
|
+
if ((line === `${root}:` || line.startsWith(`${root}: `) || line.startsWith(`${root}${path.sep}`))
|
|
108
|
+
&& separator >= root.length && line.endsWith(":")) {
|
|
109
|
+
flush();
|
|
110
|
+
const target = line.slice(0, separator);
|
|
111
|
+
const relative = target === root ? "." : path.relative(root, target).split(path.sep).join("/");
|
|
112
|
+
if (!relative || relative === ".." || relative.startsWith("../") || path.isAbsolute(relative)) {
|
|
113
|
+
throw setupError("PM_STUDIO_XATTR_INVALID", `Extended attribute path escapes PM Studio: ${target}`);
|
|
114
|
+
}
|
|
115
|
+
current = { path: relative, name: line.slice(separator + 2, -1), hex: [] };
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (!current) throw setupError("PM_STUDIO_XATTR_INVALID", "Could not parse PM Studio extended attributes");
|
|
119
|
+
const data = line.includes(" |") ? line.slice(0, line.indexOf(" |")) : line;
|
|
120
|
+
const tokens = data.trim().split(/\s+/);
|
|
121
|
+
if (!/^[0-9a-f]{8}$/i.test(tokens[0]) || tokens.slice(1).some((token) => !/^[0-9a-f]{2}$/i.test(token))) {
|
|
122
|
+
throw setupError("PM_STUDIO_XATTR_INVALID", "Could not parse PM Studio extended attribute bytes");
|
|
123
|
+
}
|
|
124
|
+
current.hex.push(...tokens.slice(1));
|
|
125
|
+
}
|
|
126
|
+
flush();
|
|
127
|
+
return records.sort((left, right) => Buffer.compare(
|
|
128
|
+
Buffer.from(`${left[1]}\0${left[2]}`),
|
|
129
|
+
Buffer.from(`${right[1]}\0${right[2]}`),
|
|
130
|
+
));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function inspectBundleContent(appPath, {
|
|
134
|
+
xattrOutput = "",
|
|
135
|
+
ignoredXattrs = [],
|
|
136
|
+
} = {}) {
|
|
137
|
+
const root = path.resolve(appPath);
|
|
138
|
+
const rootStat = fs.lstatSync(root);
|
|
139
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
140
|
+
throw setupError("PM_STUDIO_APP_INVALID", `PM Studio path is not a regular app bundle: ${appPath}`);
|
|
141
|
+
}
|
|
142
|
+
const records = [];
|
|
143
|
+
const normalizedPaths = new Map();
|
|
144
|
+
let regularFileCount = 0;
|
|
145
|
+
let regularBytes = 0;
|
|
146
|
+
let symlinkCount = 0;
|
|
147
|
+
|
|
148
|
+
const rememberPath = (relative) => {
|
|
149
|
+
const normalized = relative.normalize("NFC");
|
|
150
|
+
const existing = normalizedPaths.get(normalized);
|
|
151
|
+
if (existing && existing !== relative) {
|
|
152
|
+
throw setupError("PM_STUDIO_BUNDLE_PATH_COLLISION", `PM Studio contains colliding paths: ${existing} and ${relative}`);
|
|
153
|
+
}
|
|
154
|
+
normalizedPaths.set(normalized, relative);
|
|
155
|
+
};
|
|
156
|
+
rememberPath(".");
|
|
157
|
+
records.push(["directory", ".", rootStat.mode & 0o7777]);
|
|
158
|
+
const visit = (directory) => {
|
|
159
|
+
const names = fs.readdirSync(directory).sort((left, right) =>
|
|
160
|
+
Buffer.compare(Buffer.from(left), Buffer.from(right)));
|
|
161
|
+
for (const name of names) {
|
|
162
|
+
const fullPath = path.join(directory, name);
|
|
163
|
+
const stat = fs.lstatSync(fullPath);
|
|
164
|
+
const relative = path.relative(root, fullPath).split(path.sep).join("/");
|
|
165
|
+
const mode = stat.mode & 0o7777;
|
|
166
|
+
rememberPath(relative);
|
|
167
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
168
|
+
records.push(["directory", relative, mode]);
|
|
169
|
+
visit(fullPath);
|
|
170
|
+
} else if (stat.isSymbolicLink()) {
|
|
171
|
+
const target = fs.readlinkSync(fullPath);
|
|
172
|
+
const resolved = path.resolve(path.dirname(fullPath), target);
|
|
173
|
+
if (path.isAbsolute(target)
|
|
174
|
+
|| (resolved !== root && !resolved.startsWith(`${root}${path.sep}`))
|
|
175
|
+
|| !fs.existsSync(resolved)) {
|
|
176
|
+
throw setupError("PM_STUDIO_BUNDLE_SYMLINK_INVALID", `PM Studio contains an unsafe symlink: ${relative}`);
|
|
177
|
+
}
|
|
178
|
+
symlinkCount += 1;
|
|
179
|
+
records.push(["symlink", relative, mode, target]);
|
|
180
|
+
} else if (stat.isFile()) {
|
|
181
|
+
if (stat.nlink !== 1) {
|
|
182
|
+
throw setupError("PM_STUDIO_BUNDLE_HARDLINK_INVALID", `PM Studio contains a hard-linked file: ${relative}`);
|
|
183
|
+
}
|
|
184
|
+
regularFileCount += 1;
|
|
185
|
+
regularBytes += stat.size;
|
|
186
|
+
records.push(["file", relative, mode, stat.size, hashRegularFile(fullPath, stat)]);
|
|
187
|
+
} else {
|
|
188
|
+
throw setupError("PM_STUDIO_BUNDLE_ENTRY_INVALID", `PM Studio contains an unsupported entry: ${relative}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
visit(root);
|
|
193
|
+
|
|
194
|
+
const xattrRecords = parseXattrHex(xattrOutput, root, ignoredXattrs);
|
|
195
|
+
const hash = createHash("sha256");
|
|
196
|
+
for (const record of [...records, ...xattrRecords]) hash.update(`${JSON.stringify(record)}\n`);
|
|
197
|
+
return {
|
|
198
|
+
scheme: BUNDLE_CONTENT_SCHEME,
|
|
199
|
+
sha256: hash.digest("hex"),
|
|
200
|
+
entryCount: records.length,
|
|
201
|
+
regularFileCount,
|
|
202
|
+
regularBytes,
|
|
203
|
+
symlinkCount,
|
|
204
|
+
xattrCount: xattrRecords.length,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function defaultInspectBundleContent({ appPath, recipe, processRunner }) {
|
|
209
|
+
const xattrs = runChecked(processRunner, "/usr/bin/xattr", ["-r", "-s", "-x", "-l", appPath],
|
|
210
|
+
"Reading PM Studio extended attributes");
|
|
211
|
+
return inspectBundleContent(appPath, {
|
|
212
|
+
xattrOutput: xattrs.stdout,
|
|
213
|
+
ignoredXattrs: recipe.sourceBundleContent?.ignoredXattrs || [],
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
56
217
|
function plistRead(processRunner, plistPath, key) {
|
|
57
218
|
return runChecked(processRunner, "/usr/bin/plutil", ["-extract", key, "raw", "-o", "-", plistPath],
|
|
58
219
|
`Reading ${path.basename(plistPath)}`).stdout.trim();
|
|
@@ -91,30 +252,39 @@ function defaultInspectCodeSign({ appPath, processRunner }) {
|
|
|
91
252
|
const verify = normalizedProcessResult(processRunner("/usr/bin/codesign", [
|
|
92
253
|
"--verify", "--deep", "--strict", "--verbose=2", appPath,
|
|
93
254
|
]));
|
|
94
|
-
if (verify.error || verify.status !== 0) return { valid: false, adHoc: false };
|
|
95
255
|
const display = normalizedProcessResult(processRunner("/usr/bin/codesign", [
|
|
96
256
|
"--display", "--verbose=4", appPath,
|
|
97
257
|
]));
|
|
98
|
-
if (display.error || display.status !== 0) return { valid: false, adHoc: false };
|
|
99
258
|
const details = `${display.stdout}\n${display.stderr}`;
|
|
100
259
|
const entitlementsResult = normalizedProcessResult(processRunner("/usr/bin/codesign", [
|
|
101
260
|
"--display", "--entitlements", "-", "--xml", appPath,
|
|
102
261
|
]));
|
|
103
|
-
if (entitlementsResult.error || entitlementsResult.status !== 0) return { valid: false, adHoc: false };
|
|
104
262
|
const entitlementOutput = `${entitlementsResult.stdout}\n${entitlementsResult.stderr}`;
|
|
105
263
|
const plistStart = entitlementOutput.indexOf("<?xml");
|
|
106
264
|
const plistEnd = entitlementOutput.lastIndexOf("</plist>");
|
|
107
|
-
|
|
108
|
-
const entitlements =
|
|
265
|
+
const hasEntitlements = plistStart >= 0 && plistEnd >= plistStart;
|
|
266
|
+
const entitlements = hasEntitlements
|
|
267
|
+
? entitlementOutput.slice(plistStart, plistEnd + "</plist>".length)
|
|
268
|
+
: "";
|
|
109
269
|
return {
|
|
110
|
-
valid:
|
|
270
|
+
valid: !verify.error && verify.status === 0
|
|
271
|
+
&& !display.error && display.status === 0
|
|
272
|
+
&& !entitlementsResult.error && entitlementsResult.status === 0
|
|
273
|
+
&& hasEntitlements,
|
|
274
|
+
verifyValid: !verify.error && verify.status === 0,
|
|
275
|
+
displayValid: !display.error && display.status === 0,
|
|
276
|
+
entitlementsState: hasEntitlements
|
|
277
|
+
? "xml"
|
|
278
|
+
: /invalid entitlements blob/i.test(entitlementOutput) ? "invalid" : "unavailable",
|
|
111
279
|
adHoc: /(?:^|\n)Signature=adhoc(?:\n|$)/i.test(details)
|
|
112
280
|
|| /(?:^|\n)TeamIdentifier=not set(?:\n|$)/i.test(details),
|
|
113
281
|
identifier: details.match(/(?:^|\n)Identifier=([^\n]+)/)?.[1]?.trim() || "",
|
|
114
282
|
teamIdentifier: details.match(/(?:^|\n)TeamIdentifier=([^\n]+)/)?.[1]?.trim() || "",
|
|
115
283
|
flags: details.match(/\bflags=([^\s]+)/)?.[1] || "",
|
|
116
284
|
runtimeVersion: details.match(/(?:^|\n)Runtime Version=([^\n]+)/)?.[1]?.trim() || "",
|
|
117
|
-
|
|
285
|
+
cdHashFull: details.match(/(?:^|\n)CandidateCDHashFull sha256=([0-9a-f]+)/i)?.[1]?.toLowerCase() || "",
|
|
286
|
+
notarizationTicket: details.match(/(?:^|\n)Notarization Ticket=([^\n]+)/)?.[1]?.trim() || "",
|
|
287
|
+
entitlementsSha256: hasEntitlements ? sha256Hex(entitlements) : "",
|
|
118
288
|
};
|
|
119
289
|
}
|
|
120
290
|
|
|
@@ -240,6 +410,7 @@ export function createPmStudioSetupOperations(overrides = {}) {
|
|
|
240
410
|
readAsarIntegrity: defaultReadAsarIntegrity,
|
|
241
411
|
writeAsarIntegrity: defaultWriteAsarIntegrity,
|
|
242
412
|
inspectCodeSign: defaultInspectCodeSign,
|
|
413
|
+
inspectBundleContent: defaultInspectBundleContent,
|
|
243
414
|
inspectExecutableIntegrity: defaultInspectExecutableIntegrity,
|
|
244
415
|
signApp: defaultSignApp,
|
|
245
416
|
listBlockingProcesses: defaultListBlockingProcesses,
|
|
@@ -271,12 +442,34 @@ function integrityMatches(actual, algorithm, hash) {
|
|
|
271
442
|
return actual?.algorithm === algorithm && actual?.hash === hash;
|
|
272
443
|
}
|
|
273
444
|
|
|
445
|
+
function bundleContentMatches(actual, expected) {
|
|
446
|
+
return actual?.scheme === expected?.scheme
|
|
447
|
+
&& actual?.sha256 === expected?.sha256
|
|
448
|
+
&& actual?.entryCount === expected?.entryCount
|
|
449
|
+
&& actual?.regularFileCount === expected?.regularFileCount
|
|
450
|
+
&& actual?.regularBytes === expected?.regularBytes
|
|
451
|
+
&& actual?.symlinkCount === expected?.symlinkCount
|
|
452
|
+
&& actual?.xattrCount === expected?.xattrCount;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function sourceCodeSignatureMatches(actual, expected) {
|
|
456
|
+
return actual?.adHoc === false
|
|
457
|
+
&& actual?.displayValid === true
|
|
458
|
+
&& actual?.identifier === expected?.identifier
|
|
459
|
+
&& actual?.teamIdentifier === expected?.teamIdentifier
|
|
460
|
+
&& actual?.flags === expected?.flags
|
|
461
|
+
&& actual?.runtimeVersion === expected?.runtimeVersion
|
|
462
|
+
&& actual?.cdHashFull === expected?.cdHashFull
|
|
463
|
+
&& actual?.notarizationTicket === expected?.notarizationTicket;
|
|
464
|
+
}
|
|
465
|
+
|
|
274
466
|
export function inspectPmStudioApp({
|
|
275
467
|
appPath,
|
|
276
468
|
recipe = PM_STUDIO_2_9_7_RECIPE,
|
|
277
469
|
operations = createPmStudioSetupOperations(),
|
|
278
470
|
} = {}) {
|
|
279
471
|
const paths = appPaths(appPath, recipe);
|
|
472
|
+
const issues = [];
|
|
280
473
|
const metadata = operations.readBundleMetadata({ ...paths, recipe, processRunner: operations.processRunner });
|
|
281
474
|
const asar = inspectAsarBuffer(fs.readFileSync(paths.asarPath), recipe);
|
|
282
475
|
const plistIntegrity = operations.readAsarIntegrity({ ...paths, recipe, processRunner: operations.processRunner });
|
|
@@ -284,7 +477,16 @@ export function inspectPmStudioApp({
|
|
|
284
477
|
...paths, recipe, processRunner: operations.processRunner,
|
|
285
478
|
});
|
|
286
479
|
const codeSign = operations.inspectCodeSign({ ...paths, recipe, processRunner: operations.processRunner });
|
|
287
|
-
|
|
480
|
+
let bundleContent = null;
|
|
481
|
+
if (recipe.sourceBundleContent) {
|
|
482
|
+
try {
|
|
483
|
+
bundleContent = operations.inspectBundleContent({
|
|
484
|
+
...paths, recipe, processRunner: operations.processRunner,
|
|
485
|
+
});
|
|
486
|
+
} catch (error) {
|
|
487
|
+
issues.push(`bundle content inspection failed: ${error.message}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
288
490
|
|
|
289
491
|
if (String(metadata.version) !== recipe.version) {
|
|
290
492
|
issues.push(`version is ${metadata.version}, expected ${recipe.version}`);
|
|
@@ -301,19 +503,33 @@ export function inspectPmStudioApp({
|
|
|
301
503
|
issues.push("Electron Framework has an unsupported embedded ASAR integrity slot");
|
|
302
504
|
}
|
|
303
505
|
|
|
506
|
+
const exactSourceBundle = !recipe.sourceBundleContent
|
|
507
|
+
|| bundleContentMatches(bundleContent, recipe.sourceBundleContent);
|
|
508
|
+
const strictVendorSignature = codeSign?.valid === true
|
|
509
|
+
&& codeSign?.adHoc === false
|
|
510
|
+
&& codeSign?.teamIdentifier === recipe.sourceTeamIdentifier;
|
|
511
|
+
const exactInvalidVendorSignature = recipe.sourceBundleContent
|
|
512
|
+
&& exactSourceBundle
|
|
513
|
+
&& codeSign?.valid === false
|
|
514
|
+
&& codeSign?.verifyValid === false
|
|
515
|
+
&& codeSign?.entitlementsState === "invalid"
|
|
516
|
+
&& sourceCodeSignatureMatches(codeSign, recipe.sourceCodeSignature);
|
|
517
|
+
const sourceSignatureAccepted = strictVendorSignature || exactInvalidVendorSignature;
|
|
518
|
+
const patchedSignatureAccepted = codeSign?.valid === true
|
|
519
|
+
&& codeSign?.adHoc === true
|
|
520
|
+
&& (!recipe.patchedSigningMetadata
|
|
521
|
+
|| JSON.stringify(signingMetadata(codeSign)) === JSON.stringify(recipe.patchedSigningMetadata));
|
|
304
522
|
const clean = issues.length === 0
|
|
305
523
|
&& asar.state === "clean"
|
|
306
524
|
&& integrityMatches(plistIntegrity, "SHA256", recipe.sourceHeaderSha256)
|
|
307
525
|
&& executableIntegrity.executableSha256 === recipe.sourceExecutableSha256
|
|
308
526
|
&& executableIntegrity.frameworkSha256 === recipe.sourceElectronFrameworkSha256
|
|
309
|
-
&&
|
|
310
|
-
&&
|
|
311
|
-
&& codeSign?.teamIdentifier === recipe.sourceTeamIdentifier;
|
|
527
|
+
&& exactSourceBundle
|
|
528
|
+
&& sourceSignatureAccepted;
|
|
312
529
|
const patched = issues.length === 0
|
|
313
530
|
&& asar.state === "patched"
|
|
314
531
|
&& integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)
|
|
315
|
-
&&
|
|
316
|
-
&& codeSign?.adHoc === true;
|
|
532
|
+
&& patchedSignatureAccepted;
|
|
317
533
|
|
|
318
534
|
if (!clean && !patched) {
|
|
319
535
|
if (asar.state === "drift") issues.push("app.asar is unknown drift");
|
|
@@ -324,7 +540,12 @@ export function inspectPmStudioApp({
|
|
|
324
540
|
&& !integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)) {
|
|
325
541
|
issues.push("patched ASAR does not match ElectronAsarIntegrity");
|
|
326
542
|
}
|
|
327
|
-
if (
|
|
543
|
+
if (asar.state === "clean" && recipe.sourceBundleContent && !exactSourceBundle) {
|
|
544
|
+
issues.push("bundle content does not match the exact clean recipe");
|
|
545
|
+
}
|
|
546
|
+
if (!codeSign?.valid && !(asar.state === "clean" && exactInvalidVendorSignature)) {
|
|
547
|
+
issues.push("codesign verification failed");
|
|
548
|
+
}
|
|
328
549
|
else if (asar.state === "clean" && codeSign.adHoc) issues.push("clean bundle has an unexpected ad-hoc signature");
|
|
329
550
|
else if (asar.state === "patched" && !codeSign.adHoc) issues.push("patched bundle is not ad-hoc signed");
|
|
330
551
|
else if (asar.state === "clean" && codeSign.teamIdentifier !== recipe.sourceTeamIdentifier) {
|
|
@@ -338,6 +559,11 @@ export function inspectPmStudioApp({
|
|
|
338
559
|
&& executableIntegrity.frameworkSha256 !== recipe.sourceElectronFrameworkSha256) {
|
|
339
560
|
issues.push("Electron Framework hash does not match the clean recipe");
|
|
340
561
|
}
|
|
562
|
+
if (asar.state === "patched" && codeSign?.valid && codeSign?.adHoc
|
|
563
|
+
&& recipe.patchedSigningMetadata
|
|
564
|
+
&& !patchedSignatureAccepted) {
|
|
565
|
+
issues.push("patched bundle signing metadata does not match the recipe");
|
|
566
|
+
}
|
|
341
567
|
}
|
|
342
568
|
|
|
343
569
|
return {
|
|
@@ -351,6 +577,10 @@ export function inspectPmStudioApp({
|
|
|
351
577
|
plistIntegrity,
|
|
352
578
|
executableIntegrity,
|
|
353
579
|
codeSign,
|
|
580
|
+
bundleContent,
|
|
581
|
+
sourceVerification: clean
|
|
582
|
+
? exactInvalidVendorSignature ? "exact-bundle-content" : "codesign"
|
|
583
|
+
: "",
|
|
354
584
|
paths,
|
|
355
585
|
issues,
|
|
356
586
|
};
|
|
@@ -393,7 +623,8 @@ function selectRecipe(metadata, recipes) {
|
|
|
393
623
|
}
|
|
394
624
|
|
|
395
625
|
function backupName(recipe) {
|
|
396
|
-
|
|
626
|
+
const sourceHash = recipe.sourceBundleContent?.sha256 || recipe.sourceAsarSha256;
|
|
627
|
+
return `${recipe.id}-${sourceHash.slice(0, 12)}`;
|
|
397
628
|
}
|
|
398
629
|
|
|
399
630
|
export function pmStudioBackupRoot(home = os.homedir()) {
|
|
@@ -411,7 +642,7 @@ export function pmStudioPatchManifestPath({
|
|
|
411
642
|
|
|
412
643
|
function backupManifest(recipe, createdAt) {
|
|
413
644
|
return {
|
|
414
|
-
schema_version: 1,
|
|
645
|
+
schema_version: recipe.sourceBundleContent ? 2 : 1,
|
|
415
646
|
kind: "ccdx-pm-studio-backup",
|
|
416
647
|
recipe_id: recipe.id,
|
|
417
648
|
created_at: createdAt.toISOString(),
|
|
@@ -430,12 +661,15 @@ function backupManifest(recipe, createdAt) {
|
|
|
430
661
|
electron_framework_sha256: recipe.sourceElectronFrameworkSha256,
|
|
431
662
|
embedded_asar_integrity: recipe.embeddedAsarIntegrity,
|
|
432
663
|
},
|
|
664
|
+
...(recipe.sourceBundleContent ? { bundle_content: recipe.sourceBundleContent } : {}),
|
|
665
|
+
...(recipe.sourceArtifact ? { artifact: recipe.sourceArtifact } : {}),
|
|
433
666
|
},
|
|
434
667
|
patched: {
|
|
435
668
|
asar_sha256: recipe.patchedAsarSha256,
|
|
436
669
|
asar_header_sha256: recipe.patchedHeaderSha256,
|
|
437
670
|
electron_asar_integrity: { algorithm: "SHA256", hash: recipe.patchedHeaderSha256 },
|
|
438
671
|
binaries: null,
|
|
672
|
+
...(recipe.sourceBundleContent ? { bundle_content: null } : {}),
|
|
439
673
|
},
|
|
440
674
|
backup_bundle: "PM Studio.app",
|
|
441
675
|
restore_rule: `Restore only while the installed app is PM Studio ${recipe.version} build ${recipe.build}.`,
|
|
@@ -451,7 +685,9 @@ function validateBackup({ backupDir, recipe, operations }) {
|
|
|
451
685
|
} catch {
|
|
452
686
|
throw setupError("PM_STUDIO_BACKUP_INVALID", `Existing PM Studio backup manifest is invalid: ${manifestPath}`);
|
|
453
687
|
}
|
|
454
|
-
|
|
688
|
+
const expectedSchema = recipe.sourceBundleContent ? 2 : 1;
|
|
689
|
+
if (manifest?.schema_version !== expectedSchema
|
|
690
|
+
|| manifest?.kind !== "ccdx-pm-studio-backup"
|
|
455
691
|
|| manifest?.recipe_id !== recipe.id
|
|
456
692
|
|| manifest?.app?.bundle_identifier !== recipe.bundleIdentifier
|
|
457
693
|
|| manifest?.app?.version !== recipe.version
|
|
@@ -462,6 +698,10 @@ function validateBackup({ backupDir, recipe, operations }) {
|
|
|
462
698
|
|| manifest?.source?.binaries?.main_executable_sha256 !== recipe.sourceExecutableSha256
|
|
463
699
|
|| manifest?.source?.binaries?.electron_framework_sha256 !== recipe.sourceElectronFrameworkSha256
|
|
464
700
|
|| manifest?.source?.binaries?.embedded_asar_integrity !== recipe.embeddedAsarIntegrity
|
|
701
|
+
|| (recipe.sourceBundleContent
|
|
702
|
+
&& JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent))
|
|
703
|
+
|| (recipe.sourceArtifact
|
|
704
|
+
&& JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact))
|
|
465
705
|
|| manifest?.patched?.asar_sha256 !== recipe.patchedAsarSha256
|
|
466
706
|
|| manifest?.patched?.asar_header_sha256 !== recipe.patchedHeaderSha256
|
|
467
707
|
|| !integrityMatches(manifest?.patched?.electron_asar_integrity, "SHA256", recipe.patchedHeaderSha256)) {
|
|
@@ -508,13 +748,14 @@ function ensureBackup({ appPath, backupRoot, recipe, operations }) {
|
|
|
508
748
|
}
|
|
509
749
|
}
|
|
510
750
|
|
|
511
|
-
function writePatchedBinaryRecord({ backup, inspection, operations }) {
|
|
751
|
+
function writePatchedBinaryRecord({ backup, inspection, operations, recipe }) {
|
|
512
752
|
const manifest = JSON.parse(fs.readFileSync(backup.manifestPath, "utf8"));
|
|
513
753
|
manifest.patched.binaries = {
|
|
514
754
|
main_executable_sha256: inspection.executableIntegrity.executableSha256,
|
|
515
755
|
electron_framework_sha256: inspection.executableIntegrity.frameworkSha256,
|
|
516
756
|
};
|
|
517
757
|
manifest.patched.signing_metadata = signingMetadata(inspection.codeSign);
|
|
758
|
+
if (recipe.sourceBundleContent) manifest.patched.bundle_content = inspection.bundleContent;
|
|
518
759
|
const temporaryPath = `${backup.manifestPath}.${operations.uuid()}.tmp`;
|
|
519
760
|
try {
|
|
520
761
|
fs.writeFileSync(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
@@ -532,12 +773,24 @@ export function assertPatchedBinaryRecord({ inspection, manifestPath, recipe })
|
|
|
532
773
|
} catch {
|
|
533
774
|
throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record is missing or invalid");
|
|
534
775
|
}
|
|
535
|
-
|
|
776
|
+
const expectedSchema = recipe?.sourceBundleContent ? 2 : 1;
|
|
777
|
+
if (recipe && (manifest?.schema_version !== expectedSchema
|
|
536
778
|
|| manifest?.kind !== "ccdx-pm-studio-backup"
|
|
537
779
|
|| manifest?.recipe_id !== recipe.id
|
|
538
780
|
|| manifest?.app?.bundle_identifier !== recipe.bundleIdentifier
|
|
539
781
|
|| manifest?.app?.version !== recipe.version
|
|
540
782
|
|| manifest?.app?.build !== recipe.build
|
|
783
|
+
|| (recipe.sourceBundleContent && (
|
|
784
|
+
manifest?.source?.asar_sha256 !== recipe.sourceAsarSha256
|
|
785
|
+
|| manifest?.source?.asar_header_sha256 !== recipe.sourceHeaderSha256
|
|
786
|
+
|| !integrityMatches(manifest?.source?.electron_asar_integrity, "SHA256", recipe.sourceHeaderSha256)
|
|
787
|
+
|| manifest?.source?.binaries?.main_executable_sha256 !== recipe.sourceExecutableSha256
|
|
788
|
+
|| manifest?.source?.binaries?.electron_framework_sha256 !== recipe.sourceElectronFrameworkSha256
|
|
789
|
+
|| manifest?.source?.binaries?.embedded_asar_integrity !== recipe.embeddedAsarIntegrity
|
|
790
|
+
|| JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent)
|
|
791
|
+
))
|
|
792
|
+
|| (recipe.sourceArtifact
|
|
793
|
+
&& JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact))
|
|
541
794
|
|| manifest?.patched?.asar_sha256 !== recipe.patchedAsarSha256
|
|
542
795
|
|| manifest?.patched?.asar_header_sha256 !== recipe.patchedHeaderSha256
|
|
543
796
|
|| !integrityMatches(manifest?.patched?.electron_asar_integrity, "SHA256", recipe.patchedHeaderSha256))) {
|
|
@@ -552,6 +805,10 @@ export function assertPatchedBinaryRecord({ inspection, manifestPath, recipe })
|
|
|
552
805
|
if (JSON.stringify(manifest?.patched?.signing_metadata) !== JSON.stringify(signingMetadata(inspection.codeSign))) {
|
|
553
806
|
throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio signing metadata differs from the installed patch record");
|
|
554
807
|
}
|
|
808
|
+
if (recipe?.sourceBundleContent
|
|
809
|
+
&& JSON.stringify(manifest?.patched?.bundle_content) !== JSON.stringify(inspection.bundleContent)) {
|
|
810
|
+
throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio bundle content differs from the installed patch record");
|
|
811
|
+
}
|
|
555
812
|
}
|
|
556
813
|
|
|
557
814
|
function signingMetadata(codeSign) {
|
|
@@ -570,7 +827,15 @@ function flagsWithoutAdHoc(value) {
|
|
|
570
827
|
return `0x${(BigInt(`0x${match[1]}`) & ~2n).toString(16)}`;
|
|
571
828
|
}
|
|
572
829
|
|
|
573
|
-
function assertSigningMetadataPreserved(source, staged) {
|
|
830
|
+
function assertSigningMetadataPreserved(source, staged, recipe) {
|
|
831
|
+
if (recipe.patchedSigningMetadata) {
|
|
832
|
+
if (JSON.stringify(signingMetadata(staged.codeSign))
|
|
833
|
+
!== JSON.stringify(recipe.patchedSigningMetadata)) {
|
|
834
|
+
throw setupError("PM_STUDIO_SIGNING_METADATA_CHANGED",
|
|
835
|
+
"Ad-hoc signing did not produce the exact PM Studio signing metadata required by the recipe");
|
|
836
|
+
}
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
574
839
|
const before = signingMetadata(source.codeSign);
|
|
575
840
|
const after = signingMetadata(staged.codeSign);
|
|
576
841
|
before.flags = flagsWithoutAdHoc(before.flags);
|
|
@@ -665,6 +930,10 @@ export async function runPmStudioSetup({
|
|
|
665
930
|
`PM Studio ${recipe.version} build ${recipe.build} does not match the clean or patched recipe; no files were changed`, source.issues);
|
|
666
931
|
}
|
|
667
932
|
|
|
933
|
+
if (source.sourceVerification === "exact-bundle-content") {
|
|
934
|
+
emit(`[WARN] macOS rejected the vendor signature; setup accepted only the exact PM Studio ${recipe.version} official bundle content fingerprint.`);
|
|
935
|
+
}
|
|
936
|
+
|
|
668
937
|
const backupExists = fs.existsSync(finalBackupDir);
|
|
669
938
|
assertSpace({ appPath, backupRoot, backupExists, operations });
|
|
670
939
|
|
|
@@ -674,6 +943,12 @@ export async function runPmStudioSetup({
|
|
|
674
943
|
let preserveStage = false;
|
|
675
944
|
try {
|
|
676
945
|
operations.copyBundle({ source: appPath, destination: stagePath, processRunner: operations.processRunner });
|
|
946
|
+
const stagedSource = inspectPmStudioApp({ appPath: stagePath, recipe, operations });
|
|
947
|
+
if (stagedSource.state !== "clean"
|
|
948
|
+
|| (recipe.sourceBundleContent
|
|
949
|
+
&& stagedSource.bundleContent?.sha256 !== source.bundleContent?.sha256)) {
|
|
950
|
+
throw setupError("PM_STUDIO_STAGE_INVALID", "PM Studio staging copy does not match the exact clean source bundle", stagedSource.issues);
|
|
951
|
+
}
|
|
677
952
|
const stageAsarPath = path.join(stagePath, recipe.asarPath);
|
|
678
953
|
const patched = patchAsarBuffer(fs.readFileSync(stageAsarPath), recipe);
|
|
679
954
|
if (!patched.changed) throw setupError("PM_STUDIO_STAGE_INVALID", "Staging copy was unexpectedly already patched");
|
|
@@ -691,14 +966,16 @@ export async function runPmStudioSetup({
|
|
|
691
966
|
if (staged.state !== "patched") {
|
|
692
967
|
throw setupError("PM_STUDIO_STAGE_VERIFY_FAILED", "Patched PM Studio staging bundle failed verification", staged.issues);
|
|
693
968
|
}
|
|
694
|
-
assertSigningMetadataPreserved(source, staged);
|
|
695
|
-
writePatchedBinaryRecord({ backup, inspection: staged, operations });
|
|
969
|
+
assertSigningMetadataPreserved(source, staged, recipe);
|
|
970
|
+
writePatchedBinaryRecord({ backup, inspection: staged, operations, recipe });
|
|
696
971
|
|
|
697
972
|
assertNoBlockingProcesses(operations, appPath);
|
|
698
973
|
const unchangedSource = inspectPmStudioApp({ appPath, recipe, operations });
|
|
699
974
|
if (unchangedSource.state !== "clean"
|
|
700
975
|
|| unchangedSource.asar.asarSha256 !== source.asar.asarSha256
|
|
701
|
-
|| unchangedSource.asar.headerSha256 !== source.asar.headerSha256
|
|
976
|
+
|| unchangedSource.asar.headerSha256 !== source.asar.headerSha256
|
|
977
|
+
|| (recipe.sourceBundleContent
|
|
978
|
+
&& unchangedSource.bundleContent?.sha256 !== source.bundleContent?.sha256)) {
|
|
702
979
|
throw setupError("PM_STUDIO_SOURCE_CHANGED", "PM Studio changed during setup; the verified staging bundle was not installed");
|
|
703
980
|
}
|
|
704
981
|
|