codex-copilot-dx 0.6.7 → 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 +28 -13
- package/package.json +1 -1
- package/src/config.mjs +18 -0
- package/src/pm-studio-asar.mjs +100 -1
- package/src/pm-studio-setup.mjs +302 -25
package/README.md
CHANGED
|
@@ -116,9 +116,11 @@ active account or profile. GPT requests continue to use the bearer supplied by
|
|
|
116
116
|
the current PM profile; exact enabled Anthropic model IDs use the isolated
|
|
117
117
|
CCDX Claude profile. Neither credential is copied into the other application.
|
|
118
118
|
|
|
119
|
-
The current patch
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
The current patch recipes support only the exact, unmodified PM Studio 2.9.7
|
|
120
|
+
and 2.9.10 bundles on macOS. This is exact-version support, not a version range:
|
|
121
|
+
2.9.11 and other unknown builds remain unsupported until they receive their own
|
|
122
|
+
verified recipe. Authorize the isolated Claude profile first, quit PM Studio
|
|
123
|
+
and its updater, then run the one-time setup command:
|
|
122
124
|
|
|
123
125
|
```bash
|
|
124
126
|
ccdx auth login claude --reauth --github-login <personal-github-login>
|
|
@@ -145,22 +147,31 @@ does not attempt a second automatic write: it preserves the verified backup,
|
|
|
145
147
|
retains any diagnostic stage still present, and prints recovery paths. Repeating
|
|
146
148
|
setup for the exact installed recipe is idempotent.
|
|
147
149
|
|
|
150
|
+
The official 2.9.10 artifact can fail macOS's vendor-signature verifier even
|
|
151
|
+
though its signing metadata remains readable. CCDX does not generally ignore
|
|
152
|
+
that failure: the 2.9.10 exception is accepted only when the complete App tree,
|
|
153
|
+
including every file, mode, symlink, and non-volatile extended attribute,
|
|
154
|
+
matches the frozen official fingerprint. The staged and installed CCDX patch
|
|
155
|
+
must still pass strict ad-hoc signature verification and match the complete
|
|
156
|
+
patched-tree record in the backup manifest.
|
|
157
|
+
|
|
148
158
|
The patch is App-wide: every PM profile sends its Copilot API traffic through
|
|
149
159
|
the loopback-only `/pm-ccdx/*` relay. Run ordinary `ccdx` before opening PM
|
|
150
160
|
Studio; if port 2026 is unavailable, PM Copilot requests cannot connect. The
|
|
151
|
-
relay explicitly supports only the
|
|
152
|
-
|
|
161
|
+
relay explicitly supports only the Copilot paths discovered in the validated
|
|
162
|
+
2.9.7 and 2.9.10 bundles (`GET /models` and `POST /chat/completions`, `/responses`, and
|
|
153
163
|
`/embeddings`) and is not a general-purpose proxy.
|
|
154
164
|
|
|
155
165
|
Patching replaces the vendor signature with an ad-hoc signature. PM Studio's
|
|
156
166
|
updater is not disabled; an official update may overwrite the patch or may
|
|
157
167
|
require reinstalling the official App. Setup prints the verified backup and
|
|
158
168
|
manifest paths plus the version-matched restore procedure. Never restore a
|
|
159
|
-
|
|
169
|
+
backup over a different PM Studio version or build.
|
|
160
170
|
|
|
161
171
|
To restore, first quit PM Studio and its updater. Use the exact verified backup
|
|
162
172
|
path printed by setup as `BACKUP_APP`, then confirm that both the installed App
|
|
163
|
-
and backup still report version/build `2.9.7/2.9.7
|
|
173
|
+
and backup still report the same version/build printed by setup (`2.9.7/2.9.7`
|
|
174
|
+
or `2.9.10/2.9.10`):
|
|
164
175
|
|
|
165
176
|
```bash
|
|
166
177
|
BACKUP_APP="<verified-backup-path-from-ccdx-pms-setup>"
|
|
@@ -170,8 +181,8 @@ BACKUP_APP="<verified-backup-path-from-ccdx-pms-setup>"
|
|
|
170
181
|
/usr/bin/plutil -extract CFBundleVersion raw -o - "$BACKUP_APP/Contents/Info.plist"
|
|
171
182
|
```
|
|
172
183
|
|
|
173
|
-
Only when all four values match
|
|
174
|
-
complete vendor
|
|
184
|
+
Only when all four values match the one exact recipe, move the patched App
|
|
185
|
+
aside, restore the complete vendor bundle, and verify it before launch:
|
|
175
186
|
|
|
176
187
|
```bash
|
|
177
188
|
RECOVERY_DIR="$(/usr/bin/mktemp -d '/Applications/CCDX-PM-Studio-recovery.XXXXXX')"
|
|
@@ -179,12 +190,16 @@ PATCHED_APP="$RECOVERY_DIR/PM Studio.app"
|
|
|
179
190
|
mv "/Applications/PM Studio.app" "$PATCHED_APP"
|
|
180
191
|
/usr/bin/ditto --rsrc --extattr --acl "$BACKUP_APP" "/Applications/PM Studio.app"
|
|
181
192
|
/usr/bin/codesign --verify --deep --strict --verbose=2 "/Applications/PM Studio.app"
|
|
193
|
+
ccdx pms status
|
|
182
194
|
```
|
|
183
195
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
196
|
+
For 2.9.10, the vendor `codesign` command may report the documented upstream
|
|
197
|
+
signature failure; in that case the App-patch row from `ccdx pms status` must
|
|
198
|
+
still classify the restored bundle as exact supported clean content rather than
|
|
199
|
+
integrity drift. If the version check, copy, or both verification paths fail,
|
|
200
|
+
do not launch the restored App. Keep the moved patched bundle at `$PATCHED_APP`
|
|
201
|
+
and reinstall the official PM Studio instead of applying a version-mismatched
|
|
202
|
+
backup. CCDX never runs these restore commands or `sudo` automatically.
|
|
188
203
|
|
|
189
204
|
To change the model used by Codex Auto-review, run:
|
|
190
205
|
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -6,6 +6,20 @@ import { atomicWriteFileIfChangedSync } from "./atomic-file.mjs";
|
|
|
6
6
|
import { adapterBaseUrl } from "./running-adapter.mjs";
|
|
7
7
|
|
|
8
8
|
const CONFIG_PATH = path.join(os.homedir(), ".codex", "config.toml");
|
|
9
|
+
const MODEL_CONTEXT_WINDOW = 1_000_000;
|
|
10
|
+
const MODEL_AUTO_COMPACT_TOKEN_LIMIT = 900_000;
|
|
11
|
+
|
|
12
|
+
function setTopLevelTomlDefault(lines, key, value) {
|
|
13
|
+
let end = lines.findIndex((line) => /^\s*\[[^\]]+\]\s*$/.test(line));
|
|
14
|
+
if (end === -1) end = lines.length;
|
|
15
|
+
|
|
16
|
+
const keyRe = new RegExp(`^\\s*${key}\\s*=`);
|
|
17
|
+
if (lines.slice(0, end).some((line) => keyRe.test(line))) return false;
|
|
18
|
+
|
|
19
|
+
while (end > 0 && lines[end - 1].trim() === "") end--;
|
|
20
|
+
lines.splice(end, 0, `${key} = ${value}`);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
9
23
|
|
|
10
24
|
function setTomlKey(lines, sectionName, key, value) {
|
|
11
25
|
const sectionLine = `[${sectionName}]`;
|
|
@@ -52,6 +66,8 @@ export function computeUpdatedCodexConfig(content, adapterPort = 2026, adapterHo
|
|
|
52
66
|
changed = true;
|
|
53
67
|
}
|
|
54
68
|
|
|
69
|
+
changed = setTopLevelTomlDefault(lines, "model_context_window", MODEL_CONTEXT_WINDOW) || changed;
|
|
70
|
+
changed = setTopLevelTomlDefault(lines, "model_auto_compact_token_limit", MODEL_AUTO_COMPACT_TOKEN_LIMIT) || changed;
|
|
55
71
|
changed = setTomlKey(lines, "shell_environment_policy.set", "ANTHROPIC_AUTH_TOKEN", "dummy") || changed;
|
|
56
72
|
changed = setTomlKey(lines, "shell_environment_policy.set", "ANTHROPIC_BASE_URL", anthropicBaseUrl) || changed;
|
|
57
73
|
changed = setTomlKey(lines, "shell_environment_policy.set", "OPENAI_BASE_URL", baseUrl) || changed;
|
|
@@ -64,6 +80,8 @@ function initialCodexConfig(adapterPort, adapterHost) {
|
|
|
64
80
|
const anthropicBaseUrl = adapterBaseUrl(adapterHost, adapterPort);
|
|
65
81
|
const baseUrl = `${anthropicBaseUrl}/v1`;
|
|
66
82
|
return `openai_base_url = "${baseUrl}"
|
|
83
|
+
model_context_window = ${MODEL_CONTEXT_WINDOW}
|
|
84
|
+
model_auto_compact_token_limit = ${MODEL_AUTO_COMPACT_TOKEN_LIMIT}
|
|
67
85
|
|
|
68
86
|
[shell_environment_policy]
|
|
69
87
|
inherit = "core"
|
package/src/pm-studio-asar.mjs
CHANGED
|
@@ -67,7 +67,106 @@ export const PM_STUDIO_2_9_7_RECIPE = Object.freeze({
|
|
|
67
67
|
]),
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
export const
|
|
70
|
+
export const PM_STUDIO_2_9_10_RECIPE = Object.freeze({
|
|
71
|
+
id: "pm-studio-2.9.10-build-2.9.10",
|
|
72
|
+
version: "2.9.10",
|
|
73
|
+
build: "2.9.10",
|
|
74
|
+
bundleIdentifier: "com.pm-studio.app",
|
|
75
|
+
sourceTeamIdentifier: "HL75GKK4W4",
|
|
76
|
+
sourceArtifact: Object.freeze({
|
|
77
|
+
releaseUrl: "https://github.com/gim-home/max-studio/releases/tag/v2.9.10",
|
|
78
|
+
asset: "PM-Studio-2.9.10-mac-arm64.zip",
|
|
79
|
+
sha256: "85654e6ed173ce2565b5ef3694137de2c5f92eba1b749316c9e5b63181ccc3b0",
|
|
80
|
+
}),
|
|
81
|
+
sourceBundleContent: Object.freeze({
|
|
82
|
+
scheme: "ccdx-bundle-content-v2",
|
|
83
|
+
sha256: "478ca7f1f0826b07b7706fd1f410dee01b6dbaae21c40c63ecbdc0942eab63d9",
|
|
84
|
+
entryCount: 1_521,
|
|
85
|
+
regularFileCount: 1_013,
|
|
86
|
+
regularBytes: 494_995_602,
|
|
87
|
+
symlinkCount: 14,
|
|
88
|
+
xattrCount: 735,
|
|
89
|
+
ignoredXattrs: Object.freeze([
|
|
90
|
+
"com.apple.macl",
|
|
91
|
+
"com.apple.provenance",
|
|
92
|
+
"com.apple.quarantine",
|
|
93
|
+
]),
|
|
94
|
+
}),
|
|
95
|
+
sourceCodeSignature: Object.freeze({
|
|
96
|
+
identifier: "com.pm-studio.app",
|
|
97
|
+
teamIdentifier: "HL75GKK4W4",
|
|
98
|
+
flags: "0x10000(runtime)",
|
|
99
|
+
runtimeVersion: "26.0.0",
|
|
100
|
+
cdHashFull: "86a4d64946123d21d20918041e88f58e8c497ce1a1f75a9f9d8cd592ce86e158",
|
|
101
|
+
notarizationTicket: "stapled",
|
|
102
|
+
}),
|
|
103
|
+
patchedSigningMetadata: Object.freeze({
|
|
104
|
+
identifier: "com.pm-studio.app",
|
|
105
|
+
flags: "0x10002(adhoc,runtime)",
|
|
106
|
+
runtime_version: "26.0.0",
|
|
107
|
+
entitlements_sha256: "9d4ccbda4fe0c81a70df3db93b3e61fe0500f67f14cdcbee4dea230e6512d05c",
|
|
108
|
+
}),
|
|
109
|
+
executable: "PM Studio",
|
|
110
|
+
sourceExecutableSha256: "a38f2054abb9770ba35d054d447be0de8e7c9d4fed6b37486bf15cf28de1e122",
|
|
111
|
+
electronFrameworkPath: "Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework",
|
|
112
|
+
sourceElectronFrameworkSha256: "a562a31f8f8d14440974466e5e611b19d93705dcf3ca2f57647578ffc002f5b4",
|
|
113
|
+
embeddedAsarIntegrity: "absent",
|
|
114
|
+
asarPath: "Contents/Resources/app.asar",
|
|
115
|
+
infoPlistPath: "Contents/Info.plist",
|
|
116
|
+
integrityKey: "ElectronAsarIntegrity.Resources/app.asar",
|
|
117
|
+
dataOffset: 3_833_292,
|
|
118
|
+
sourceAsarSha256: "d243860770e8b1d8044213924f9704d1fc52f900d6c33461eff6358962330b78",
|
|
119
|
+
sourceHeaderSha256: "5fc77d5b61e9647fba10fd2bd5752d0889c14da5a00b21d31bc48fdcccf74a69",
|
|
120
|
+
patchedAsarSha256: "49f72d999a6085102341c2d551577e164db6f22e4707d2112efa3fd280e7315e",
|
|
121
|
+
patchedHeaderSha256: "b264ece5b8e469cd3885a7a91f904f1902e1991f214ecbf228dfe91059ac7008",
|
|
122
|
+
sourceSentinel: PM_STUDIO_ORIGIN,
|
|
123
|
+
patchedSentinel: CCDX_PM_STUDIO_ORIGIN,
|
|
124
|
+
targets: Object.freeze([
|
|
125
|
+
Object.freeze({
|
|
126
|
+
path: "dist/main/main.js",
|
|
127
|
+
offset: 101_622_115,
|
|
128
|
+
size: 10_150_157,
|
|
129
|
+
sentinelOffset: 1_241_344,
|
|
130
|
+
absoluteSentinelOffset: 106_696_751,
|
|
131
|
+
sentinelCount: 1,
|
|
132
|
+
blockSize: 4_194_304,
|
|
133
|
+
sourceSha256: "03607bb1edc068396b99f02b9c9e0d8e10388fb658afc112e06e875c123dbc27",
|
|
134
|
+
patchedSha256: "e400300575e2d1c5ad089613f14e138612114547ff5060e94cb81e8de587a75a",
|
|
135
|
+
sourceBlocks: Object.freeze([
|
|
136
|
+
"3910a9bc5459f493956aafee7b3644e8d242027758f0dcdea97d7c10a324c6bf",
|
|
137
|
+
"49fa64c68425d35b1baff0d5745d856d377e014a7a7829322cd1b39aade9532e",
|
|
138
|
+
"bd7cc14a1d2e02abe66c0ec6fc4e97b263cfc1f67c4ae39e956c06d3bcc2fe67",
|
|
139
|
+
]),
|
|
140
|
+
patchedBlocks: Object.freeze([
|
|
141
|
+
"74022f5ad06c7879b37875efe0a090b23ccfbcf269afd26b10b77690413dca63",
|
|
142
|
+
"49fa64c68425d35b1baff0d5745d856d377e014a7a7829322cd1b39aade9532e",
|
|
143
|
+
"bd7cc14a1d2e02abe66c0ec6fc4e97b263cfc1f67c4ae39e956c06d3bcc2fe67",
|
|
144
|
+
]),
|
|
145
|
+
}),
|
|
146
|
+
Object.freeze({
|
|
147
|
+
path: "dist/renderer/js/main.5ad30c24.js",
|
|
148
|
+
offset: 137_266_992,
|
|
149
|
+
size: 2_520_547,
|
|
150
|
+
sentinelOffset: 528_919,
|
|
151
|
+
absoluteSentinelOffset: 141_629_203,
|
|
152
|
+
sentinelCount: 1,
|
|
153
|
+
blockSize: 4_194_304,
|
|
154
|
+
sourceSha256: "3d123f7a3eb91777f723fe2c4d412677e6722b6348373e4b4bfda41dda8b60ad",
|
|
155
|
+
patchedSha256: "dc4a9d550f4beb3a2cd7ad0fa6fc82286106762bdf789189d8f88abd0a90f619",
|
|
156
|
+
sourceBlocks: Object.freeze([
|
|
157
|
+
"3d123f7a3eb91777f723fe2c4d412677e6722b6348373e4b4bfda41dda8b60ad",
|
|
158
|
+
]),
|
|
159
|
+
patchedBlocks: Object.freeze([
|
|
160
|
+
"dc4a9d550f4beb3a2cd7ad0fa6fc82286106762bdf789189d8f88abd0a90f619",
|
|
161
|
+
]),
|
|
162
|
+
}),
|
|
163
|
+
]),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
export const PM_STUDIO_RECIPES = Object.freeze([
|
|
167
|
+
PM_STUDIO_2_9_7_RECIPE,
|
|
168
|
+
PM_STUDIO_2_9_10_RECIPE,
|
|
169
|
+
]);
|
|
71
170
|
|
|
72
171
|
export function sha256Hex(value) {
|
|
73
172
|
return createHash("sha256").update(value).digest("hex");
|
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
|
|