codex-copilot-dx 0.6.11 → 0.6.13
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 +37 -36
- package/package.json +1 -1
- package/src/cli-options.mjs +2 -2
- package/src/doctor.mjs +2 -2
- package/src/pm-studio-asar.mjs +274 -4
- package/src/pm-studio-setup-lock.mjs +336 -0
- package/src/pm-studio-setup.mjs +314 -61
- package/src/pm-studio-status.mjs +25 -10
package/README.md
CHANGED
|
@@ -118,11 +118,15 @@ discovery tries the compatible local CCDX merge first, and only exact enabled
|
|
|
118
118
|
Anthropic model IDs use the isolated CCDX Claude profile. Neither credential is
|
|
119
119
|
copied into the other application.
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
and
|
|
121
|
+
CCDX treats the local PM Studio App selected for setup as the source of truth.
|
|
122
|
+
Its version and build label backup and recovery records; neither they nor the
|
|
123
|
+
installation source or original signature are compatibility admission gates.
|
|
124
|
+
Setup derives a source-bound recipe whenever the local ASAR integrity metadata
|
|
125
|
+
and exactly one compatible Copilot configuration module can be verified. A
|
|
126
|
+
missing or ambiguous module, changed request transport, or unsupported
|
|
127
|
+
integrity policy is rejected without modifying the App. Authorize the isolated
|
|
128
|
+
Claude profile first, quit PM Studio and its updater, then run the one-time
|
|
129
|
+
setup command:
|
|
126
130
|
|
|
127
131
|
```bash
|
|
128
132
|
ccdx auth login claude --reauth --github-login <personal-github-login>
|
|
@@ -138,24 +142,21 @@ ccdx pms status
|
|
|
138
142
|
|
|
139
143
|
`ccdx pm-studio status` and `ccdx pm-studio setup` are equivalent long-form aliases.
|
|
140
144
|
|
|
141
|
-
`ccdx pms setup` performs
|
|
142
|
-
signing, and replacement work. It
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
setup
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
matches the frozen official fingerprint. The staged and installed CCDX patch
|
|
157
|
-
must still pass strict ad-hoc signature verification and match the complete
|
|
158
|
-
patched-tree record in the backup manifest.
|
|
145
|
+
`ccdx pms setup` performs local preflight, structural verification, backup,
|
|
146
|
+
staging, integrity, signing, and replacement work. It does not access a PM
|
|
147
|
+
Studio repository, release asset, PAT, or source-signing service. It never
|
|
148
|
+
starts Device Flow, the adapter, Codex, or PM Studio, and it does not modify PM
|
|
149
|
+
profiles, cached model lists, or saved PM tokens. Local fingerprints bind the
|
|
150
|
+
recipe, backup, staged App, and installed App without deciding whether the
|
|
151
|
+
original package is trusted. Structure, partial-patch, running-process, space,
|
|
152
|
+
and every other pre-replacement failure leave the installed App unchanged. If final
|
|
153
|
+
read-only verification after an atomic replacement detects exceptional
|
|
154
|
+
filesystem drift, setup does not attempt a second automatic write: it preserves
|
|
155
|
+
the verified backup, retains any diagnostic stage still present, and prints
|
|
156
|
+
recovery paths. Repeating setup reuses the version/source-bound verified backup
|
|
157
|
+
and is idempotent. The staged and installed CCDX patch must still pass strict
|
|
158
|
+
ad-hoc signature verification, use the App's bundle identifier, and match the
|
|
159
|
+
complete patched-tree record in the backup manifest.
|
|
159
160
|
|
|
160
161
|
The patch uses split origins, not an App-wide proxy. GPT and other non-Claude
|
|
161
162
|
inference requests remain on PM Studio's native official GitHub Copilot origin;
|
|
@@ -173,16 +174,15 @@ CCDX uses the PM bearer only to fetch or validate official model access; it is
|
|
|
173
174
|
never used as the credential for the isolated Claude upstream request. Keep
|
|
174
175
|
port 2026 local to the machine.
|
|
175
176
|
|
|
176
|
-
Patching
|
|
177
|
+
Patching applies an ad-hoc signature to the staged App. PM Studio's
|
|
177
178
|
updater is not disabled; an official update may overwrite the patch or may
|
|
178
|
-
require reinstalling
|
|
179
|
+
require reinstalling PM Studio. Setup prints the verified backup and
|
|
179
180
|
manifest paths plus the version-matched restore procedure. Never restore a
|
|
180
181
|
backup over a different PM Studio version or build.
|
|
181
182
|
|
|
182
183
|
To restore, first quit PM Studio and its updater. Use the exact verified backup
|
|
183
184
|
path printed by setup as `BACKUP_APP`, then confirm that both the installed App
|
|
184
|
-
and backup still report the same version/build printed by setup
|
|
185
|
-
or `2.9.10/2.9.10`):
|
|
185
|
+
and backup still report the same version/build printed by setup:
|
|
186
186
|
|
|
187
187
|
```bash
|
|
188
188
|
BACKUP_APP="<verified-backup-path-from-ccdx-pms-setup>"
|
|
@@ -192,8 +192,9 @@ BACKUP_APP="<verified-backup-path-from-ccdx-pms-setup>"
|
|
|
192
192
|
/usr/bin/plutil -extract CFBundleVersion raw -o - "$BACKUP_APP/Contents/Info.plist"
|
|
193
193
|
```
|
|
194
194
|
|
|
195
|
-
Only when all four values match
|
|
196
|
-
|
|
195
|
+
Only when all four values match each other and the version/build recorded in the
|
|
196
|
+
source-bound manifest, move the patched App aside, restore the complete local
|
|
197
|
+
bundle, and verify it before launch:
|
|
197
198
|
|
|
198
199
|
```bash
|
|
199
200
|
RECOVERY_DIR="$(/usr/bin/mktemp -d '/Applications/CCDX-PM-Studio-recovery.XXXXXX')"
|
|
@@ -204,12 +205,11 @@ mv "/Applications/PM Studio.app" "$PATCHED_APP"
|
|
|
204
205
|
ccdx pms status
|
|
205
206
|
```
|
|
206
207
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
integrity drift. If the version check, copy, or both verification paths fail,
|
|
208
|
+
The App-patch row from `ccdx pms status` must classify the restored bundle as
|
|
209
|
+
the recorded clean local content rather than integrity drift. If the version
|
|
210
|
+
check, copy, or either verification path fails,
|
|
211
211
|
do not launch the restored App. Keep the moved patched bundle at `$PATCHED_APP`
|
|
212
|
-
and reinstall
|
|
212
|
+
and reinstall PM Studio instead of applying a version-mismatched
|
|
213
213
|
backup. CCDX never runs these restore commands or `sudo` automatically.
|
|
214
214
|
|
|
215
215
|
To change the model used by Codex Auto-review, run:
|
|
@@ -280,8 +280,9 @@ The command exits with status `1` when it finds an invalid configuration and `0`
|
|
|
280
280
|
The doctor checks the GitHub token, Codex config, Claude Code settings, Claude
|
|
281
281
|
App gateway profile, adapter availability, and an installed PM Studio bundle.
|
|
282
282
|
It ends with passed/warning/error totals and deduplicated next-step commands.
|
|
283
|
-
An absent PM Studio installation is ignored;
|
|
284
|
-
without modification, while integrity
|
|
283
|
+
An absent PM Studio installation is ignored; versions whose compatibility
|
|
284
|
+
cannot be safely verified are reported without modification, while integrity
|
|
285
|
+
drift remains an error.
|
|
285
286
|
|
|
286
287
|
For a read-only live check of the saved GitHub token, Copilot entitlement, and models endpoint, run:
|
|
287
288
|
|
package/package.json
CHANGED
package/src/cli-options.mjs
CHANGED
|
@@ -292,8 +292,8 @@ function topicHelp(name, topic) {
|
|
|
292
292
|
doctor: `Usage:\n ${name} doctor [--online] [--compat] [--profile codex|claude|all]\n\nChecks local credentials, client configuration, PM Studio state, and the adapter. --online validates account entitlement; --compat sends minimal inference requests and consumes a small amount of Copilot usage.`,
|
|
293
293
|
status: `Usage:\n ${name} status\n\nShows bounded runtime, routing, performance, cache, queue, and PM relay metrics from a running adapter.`,
|
|
294
294
|
models: `Usage:\n ${name} models [--profile codex|claude] [--format table|plain]\n\nPerforms a fresh, read-only Copilot model-directory lookup for one saved profile. Interactive terminals use a table by default.`,
|
|
295
|
-
pms: `Usage:\n ${name} pms status [--format table|plain]\n ${name} pms setup\n ${name} pm-studio status [--format table|plain]\n ${name} pm-studio setup\n\nInspects or installs the
|
|
296
|
-
"pms setup": `Usage:\n ${name} pms setup\n\nBacks up, patches, verifies, and ad-hoc signs an
|
|
295
|
+
pms: `Usage:\n ${name} pms status [--format table|plain]\n ${name} pms setup\n ${name} pm-studio status [--format table|plain]\n ${name} pm-studio setup\n\nInspects or installs the fail-closed PM Studio split-origin patch. setup modifies the installed app only after exact or semantic compatibility and source checks pass.`,
|
|
296
|
+
"pms setup": `Usage:\n ${name} pms setup\n\nBacks up, patches, verifies, and ad-hoc signs an exact or structurally compatible PM Studio bundle. Unverifiable versions and drift are never modified.`,
|
|
297
297
|
"pms status": `Usage:\n ${name} pms status [--format table|plain]\n\nRead-only inspection of the installed PM Studio version, patch integrity, Claude profile, and relay availability. Interactive terminals use a table by default.`,
|
|
298
298
|
usage: `Usage:\n ${name} usage [--format table|plain]\n\nSummarizes local token usage metadata without reading prompt or completion content. Interactive terminals use a table by default.`,
|
|
299
299
|
"auto-review-model": `Usage:\n ${name} auto-review-model\n\nInteractively selects an enabled Responses model for Codex Auto-review.`,
|
package/src/doctor.mjs
CHANGED
|
@@ -599,11 +599,11 @@ export async function collectDoctorChecks({
|
|
|
599
599
|
} else if (pm.app.state === "clean") {
|
|
600
600
|
checks.push({
|
|
601
601
|
kind: "warn",
|
|
602
|
-
message: `PM Studio ${version}
|
|
602
|
+
message: `PM Studio ${version} has a compatible local patch structure but is not patched`,
|
|
603
603
|
fix: "ccdx pms setup",
|
|
604
604
|
});
|
|
605
605
|
} else if (pm.app.state === "unsupported") {
|
|
606
|
-
checks.push({ kind: "warn", message: `PM Studio ${version}
|
|
606
|
+
checks.push({ kind: "warn", message: `PM Studio ${version} does not expose one uniquely compatible patch structure; no files will be changed` });
|
|
607
607
|
} else if (pm.app.state === "drift" || pm.app.state === "error") {
|
|
608
608
|
checks.push({ kind: "err", message: `PM Studio integrity check failed: ${pm.app.issues.join("; ")}` });
|
|
609
609
|
}
|
package/src/pm-studio-asar.mjs
CHANGED
|
@@ -7,13 +7,43 @@ export const ELECTRON_ASAR_INTEGRITY_SENTINEL = "AGbevlPCksUGKNL8TSn7wGmJEuJsXb2
|
|
|
7
7
|
|
|
8
8
|
const PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH = 2_121;
|
|
9
9
|
const PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE_SHA256 = "3da6245d676015ab6cd26e4d6a0d2f54894a687c370aa419c5ad170533d086aa";
|
|
10
|
+
const PM_STUDIO_COPILOT_CONFIG_MODULE_ANCHOR = "47024(I,e,t){";
|
|
11
|
+
const PM_STUDIO_COPILOT_CONFIG_MODULE_SENTINEL = `API_ENDPOINT:"${PM_STUDIO_ORIGIN}"`;
|
|
12
|
+
const PM_STUDIO_COPILOT_CONFIG_MODULE_PATH = "dist/main/main.js";
|
|
13
|
+
const PM_STUDIO_COMPATIBILITY_ID = "exact-copilot-config-module-v1";
|
|
14
|
+
const PM_STUDIO_COMPATIBLE_TARGET_MAX_BYTES = 128 * 1024 * 1024;
|
|
15
|
+
const PM_STUDIO_COMPATIBLE_BLOCK_MAX_COUNT = 16_384;
|
|
16
|
+
const PM_STUDIO_COMPATIBLE_MODULE_PREFIX_MAX_BYTES = 512;
|
|
17
|
+
const PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE = '47024(I,e,t){"use strict";t.d(e,{qn:()=>l});const l={CLIENT_ID:"Iv1.b507a08c87ecfe98",CLIENT_SECRET:void 0,API_ENDPOINT:"https://api.githubcopilot.com",DEVICE_CODE_URL:"https://github.com/login/device/code",ACCESS_TOKEN_URL:"https://github.com/login/oauth/access_token",COPILOT_TOKEN_URL:"https://api.github.com/copilot_internal/v2/token",USER_AGENT:"GitHubCopilotChat/0.26.7",EDITOR_VERSION:"vscode/1.99.3",EDITOR_PLUGIN_VERSION:"copilot-chat/0.26.7",INTEGRATION_ID:"vscode-chat",STANDARD_HEADERS:{Accept:"application/json","Content-Type":"application/json","User-Agent":"GitHubCopilotChat/0.26.7","Editor-Version":"vscode/1.99.3","Editor-Plugin-Version":"copilot-chat/0.26.7","Copilot-Integration-Id":"vscode-chat","X-Request-Id":()=>`req_${Date.now()}_${Math.random().toString(36).substr(2,9)}`}};class c{constructor(){this.config={...l}}static getInstance(){return c.instance||(c.instance=new c),c.instance}getConfig(){return{...this.config}}updateConfig(I){this.config={...this.config,...I}}resetConfig(){this.config={...l}}validateConfig(){return function(){const I=[];return l.CLIENT_ID||I.push("GitHub Copilot Client ID is required"),l.API_ENDPOINT||I.push("GitHub Copilot API endpoint is required"),{valid:0===I.length,errors:I}}()}getStandardHeaders(I){return function(I){const e={Accept:l.STANDARD_HEADERS.Accept,"Content-Type":l.STANDARD_HEADERS["Content-Type"],"User-Agent":l.STANDARD_HEADERS["User-Agent"],"Editor-Version":l.STANDARD_HEADERS["Editor-Version"],"Editor-Plugin-Version":l.STANDARD_HEADERS["Editor-Plugin-Version"],"Copilot-Integration-Id":l.STANDARD_HEADERS["Copilot-Integration-Id"],"X-Request-Id":"function"==typeof l.STANDARD_HEADERS["X-Request-Id"]?l.STANDARD_HEADERS["X-Request-Id"]():l.STANDARD_HEADERS["X-Request-Id"]};return I&&(e.Authorization=`Bearer ${I}`),e}(I)}getCopilotTokenHeaders(I){return function(I){return{Authorization:`Bearer ${I}`,Accept:"application/json","User-Agent":l.USER_AGENT,"Editor-Version":l.EDITOR_VERSION,"Editor-Plugin-Version":l.EDITOR_PLUGIN_VERSION}}(I)}getDeviceCodeHeaders(){return{Accept:"application/json","User-Agent":l.USER_AGENT}}}c.getInstance()}';
|
|
18
|
+
|
|
19
|
+
function copilotConfigAlphaTemplate() {
|
|
20
|
+
// This is a reviewed byte template, not a generic JavaScript normalizer:
|
|
21
|
+
// only the five minifier bindings vary; every other byte remains exact.
|
|
22
|
+
const parts = [];
|
|
23
|
+
const moduleIdEnd = PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE.indexOf("(");
|
|
24
|
+
const pattern = /(?<![$\w])(?:I|e|t|l|c)(?![$\w])/g;
|
|
25
|
+
pattern.lastIndex = moduleIdEnd;
|
|
26
|
+
let offset = moduleIdEnd;
|
|
27
|
+
for (let match = pattern.exec(PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE); match; match = pattern.exec(PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE)) {
|
|
28
|
+
parts.push(PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE.slice(offset, match.index));
|
|
29
|
+
parts.push(Object.freeze({ identifier: match[0] }));
|
|
30
|
+
offset = match.index + match[0].length;
|
|
31
|
+
}
|
|
32
|
+
parts.push(PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE.slice(offset));
|
|
33
|
+
return Object.freeze(parts);
|
|
34
|
+
}
|
|
10
35
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
36
|
+
const PM_STUDIO_COPILOT_CONFIG_ALPHA_TEMPLATE = copilotConfigAlphaTemplate();
|
|
37
|
+
|
|
38
|
+
function splitOriginConfigModule(
|
|
39
|
+
moduleId = "47024",
|
|
40
|
+
windowLength = PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH,
|
|
41
|
+
) {
|
|
42
|
+
const source = `${moduleId}(I,e,t){"use strict";t.d(e,{qn:()=>l});const l={CLIENT_ID:"Iv1.b507a08c87ecfe98",CLIENT_SECRET:void 0,API_ENDPOINT:"${PM_STUDIO_ORIGIN}",DEVICE_CODE_URL:"https://github.com/login/device/code",ACCESS_TOKEN_URL:"https://github.com/login/oauth/access_token",COPILOT_TOKEN_URL:"https://api.github.com/copilot_internal/v2/token",USER_AGENT:"GitHubCopilotChat/0.26.7",EDITOR_VERSION:"vscode/1.99.3",EDITOR_PLUGIN_VERSION:"copilot-chat/0.26.7",INTEGRATION_ID:"vscode-chat",STANDARD_HEADERS:{Accept:"application/json","Content-Type":"application/json","User-Agent":"GitHubCopilotChat/0.26.7","Editor-Version":"vscode/1.99.3","Editor-Plugin-Version":"copilot-chat/0.26.7","Copilot-Integration-Id":"vscode-chat","X-Request-Id":()=>\`req_\${Date.now()}_\${Math.random().toString(36).substr(2,9)}\`}};const c=fetch.bind(globalThis),n="${CCDX_PM_STUDIO_ORIGIN}",a="${PM_STUDIO_SPLIT_ORIGIN_MARKER}",h="X-CCDX-PM-Relay",d=new Set,m=I=>I.headers.get(h)===a,q=I=>AbortSignal.any([I,AbortSignal.timeout(750)].filter(Boolean)),f=(I,e=400)=>Response.json({error:{code:I}},{status:e,headers:{[h]:a}});globalThis.fetch=async(I,e)=>{const t=I.url||I+"";if(t===\`\${l.API_ENDPOINT}/models\`){try{const t=await c(\`\${n}/models\`,e);if(t.ok&&m(t))try{let I=await t.clone().json(),e=I.data||I;if(Array.isArray(e)){d.clear();for(const I of e)/anthropic/i.test(I.vendor||I.owned_by)&&d.add(I.id);return t}}catch{}t.body?.cancel?.()?.catch?.(()=>{})}catch(I){if(e?.signal?.aborted)throw e.signal.reason||I}return c(I,e)}const s=t.startsWith(l.API_ENDPOINT)?t.slice(l.API_ENDPOINT.length):"";if("POST"===e?.method&&["/chat/completions","/responses","/embeddings"].includes(s)){let t="";try{t=(JSON.parse(e.body).model||"").trim()}catch{}if(d.has(t)||/^claude-/i.test(t)){if("/chat/completions"!==s)return f("model_not_supported");try{const I=await c(\`\${n}/models\`,{signal:q(e?.signal)}),t=401===I.status&&m(I);await I.text();if(!t)throw 0;const l=await c(\`\${n}\${s}\`,e);if(!m(l)){await l.body?.cancel?.();throw 0}return l}catch(I){if(e?.signal?.aborted)throw e.signal.reason||I;return f("relay_incompatible",503)}}}return c(I,e)}}`;
|
|
43
|
+
if (source.length > windowLength) {
|
|
14
44
|
throw new Error("PM Studio split-origin module exceeds its exact source window");
|
|
15
45
|
}
|
|
16
|
-
return `${source.slice(0, -1)}${" ".repeat(
|
|
46
|
+
return `${source.slice(0, -1)}${" ".repeat(windowLength - source.length)}}`;
|
|
17
47
|
}
|
|
18
48
|
|
|
19
49
|
export const PM_STUDIO_SPLIT_ORIGIN_CONFIG_MODULE = splitOriginConfigModule();
|
|
@@ -315,6 +345,93 @@ function occurrencePositions(haystack, needle) {
|
|
|
315
345
|
return positions;
|
|
316
346
|
}
|
|
317
347
|
|
|
348
|
+
function asciiDigit(byte) {
|
|
349
|
+
return byte >= 48 && byte <= 57;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function asciiIdentifierStart(byte) {
|
|
353
|
+
return byte === 36
|
|
354
|
+
|| byte === 95
|
|
355
|
+
|| (byte >= 65 && byte <= 90)
|
|
356
|
+
|| (byte >= 97 && byte <= 122);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function matchCopilotConfigAlphaTemplate(source, offset) {
|
|
360
|
+
let moduleIdEnd = offset;
|
|
361
|
+
while (moduleIdEnd < source.length && asciiDigit(source[moduleIdEnd])) moduleIdEnd += 1;
|
|
362
|
+
const moduleIdLength = moduleIdEnd - offset;
|
|
363
|
+
if (moduleIdLength === 0
|
|
364
|
+
|| moduleIdLength > 10
|
|
365
|
+
|| (moduleIdLength > 1 && source[offset] === 48)) return null;
|
|
366
|
+
const moduleId = source.subarray(offset, moduleIdEnd).toString("ascii");
|
|
367
|
+
if (!Number.isSafeInteger(Number(moduleId))) return null;
|
|
368
|
+
|
|
369
|
+
let cursor = moduleIdEnd;
|
|
370
|
+
const identifiers = new Map();
|
|
371
|
+
const reverseIdentifiers = new Map();
|
|
372
|
+
for (const part of PM_STUDIO_COPILOT_CONFIG_ALPHA_TEMPLATE) {
|
|
373
|
+
if (typeof part === "string") {
|
|
374
|
+
const literal = Buffer.from(part);
|
|
375
|
+
if (cursor + literal.length > source.length
|
|
376
|
+
|| !source.subarray(cursor, cursor + literal.length).equals(literal)) return null;
|
|
377
|
+
cursor += literal.length;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!asciiIdentifierStart(source[cursor])
|
|
382
|
+
|| asciiIdentifierStart(source[cursor + 1])
|
|
383
|
+
|| asciiDigit(source[cursor + 1])) return null;
|
|
384
|
+
const candidate = String.fromCharCode(source[cursor]);
|
|
385
|
+
// The audited module uses single-byte minifier names. Keeping that bound
|
|
386
|
+
// rejects capture-prone "semantic" guesses while allowing permutations.
|
|
387
|
+
const mapped = identifiers.get(part.identifier);
|
|
388
|
+
const reverse = reverseIdentifiers.get(candidate);
|
|
389
|
+
if ((mapped && mapped !== candidate) || (reverse && reverse !== part.identifier)) return null;
|
|
390
|
+
identifiers.set(part.identifier, candidate);
|
|
391
|
+
reverseIdentifiers.set(candidate, part.identifier);
|
|
392
|
+
cursor += 1;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (cursor < source.length && source[cursor] !== 44 && source[cursor] !== 125) return null;
|
|
396
|
+
return {
|
|
397
|
+
offset,
|
|
398
|
+
end: cursor,
|
|
399
|
+
length: cursor - offset,
|
|
400
|
+
moduleId,
|
|
401
|
+
anchor: `${moduleId}(`,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function locateCompatibleCopilotConfigModule(source) {
|
|
406
|
+
const sentinel = Buffer.from(PM_STUDIO_COPILOT_CONFIG_MODULE_SENTINEL);
|
|
407
|
+
const matches = new Map();
|
|
408
|
+
let sentinelCount = 0;
|
|
409
|
+
let searchOffset = 0;
|
|
410
|
+
while (searchOffset <= source.length - sentinel.length) {
|
|
411
|
+
const sentinelOffset = source.indexOf(sentinel, searchOffset);
|
|
412
|
+
if (sentinelOffset < 0) break;
|
|
413
|
+
sentinelCount += 1;
|
|
414
|
+
searchOffset = sentinelOffset + sentinel.length;
|
|
415
|
+
|
|
416
|
+
const lowerBound = Math.max(0, sentinelOffset - PM_STUDIO_COMPATIBLE_MODULE_PREFIX_MAX_BYTES);
|
|
417
|
+
for (let offset = lowerBound; offset <= sentinelOffset; offset += 1) {
|
|
418
|
+
if (!asciiDigit(source[offset]) || (offset > 0 && asciiDigit(source[offset - 1]))) continue;
|
|
419
|
+
if (offset > 0 && source[offset - 1] !== 44 && source[offset - 1] !== 123) continue;
|
|
420
|
+
const match = matchCopilotConfigAlphaTemplate(source, offset);
|
|
421
|
+
if (match && sentinelOffset >= match.offset && sentinelOffset < match.end) {
|
|
422
|
+
matches.set(match.offset, match);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (sentinelCount === 0) {
|
|
427
|
+
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} has no compatible Copilot configuration sentinel`);
|
|
428
|
+
}
|
|
429
|
+
if (matches.size !== 1) {
|
|
430
|
+
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} has ${matches.size} alpha-compatible Copilot configuration modules, expected exactly one`);
|
|
431
|
+
}
|
|
432
|
+
return matches.values().next().value;
|
|
433
|
+
}
|
|
434
|
+
|
|
318
435
|
export function blockSha256(buffer, blockSize) {
|
|
319
436
|
const size = integer(blockSize, "block size");
|
|
320
437
|
if (size === 0) throw new Error("Invalid ASAR block size: 0");
|
|
@@ -326,6 +443,159 @@ export function blockSha256(buffer, blockSize) {
|
|
|
326
443
|
return hashes;
|
|
327
444
|
}
|
|
328
445
|
|
|
446
|
+
function incompatibleAsar(message) {
|
|
447
|
+
const error = new Error(message);
|
|
448
|
+
error.code = "PM_STUDIO_ASAR_INCOMPATIBLE";
|
|
449
|
+
return error;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function requiredRecipeString(value, name) {
|
|
453
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
454
|
+
throw incompatibleAsar(`PM Studio compatible recipe has no ${name}`);
|
|
455
|
+
}
|
|
456
|
+
return value;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function createCompatiblePmStudioRecipe(value, options = {}) {
|
|
460
|
+
const parsed = parseAsarBuffer(value);
|
|
461
|
+
const entry = findAsarEntry(parsed.header, PM_STUDIO_COPILOT_CONFIG_MODULE_PATH);
|
|
462
|
+
if (!entry) throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} is missing`);
|
|
463
|
+
|
|
464
|
+
let targetOffset;
|
|
465
|
+
let targetSize;
|
|
466
|
+
let blockSize;
|
|
467
|
+
try {
|
|
468
|
+
targetOffset = entryOffset(entry.offset, `${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} offset`);
|
|
469
|
+
targetSize = entrySize(entry.size, `${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} size`);
|
|
470
|
+
blockSize = integer(entry.integrity?.blockSize,
|
|
471
|
+
`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} block size`);
|
|
472
|
+
} catch (error) {
|
|
473
|
+
throw incompatibleAsar(error.message);
|
|
474
|
+
}
|
|
475
|
+
const absoluteOffset = parsed.dataOffset + targetOffset;
|
|
476
|
+
if (entry.unpacked || entry.link || absoluteOffset + targetSize > parsed.buffer.length) {
|
|
477
|
+
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} is not one packed regular ASAR entry`);
|
|
478
|
+
}
|
|
479
|
+
if (blockSize === 0
|
|
480
|
+
|| entry.integrity?.algorithm !== "SHA256"
|
|
481
|
+
|| !sameArray(Object.keys(entry.integrity || {}).sort(), ["algorithm", "blockSize", "blocks", "hash"])) {
|
|
482
|
+
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} has unsupported integrity metadata`);
|
|
483
|
+
}
|
|
484
|
+
const expectedBlockCount = Math.ceil(targetSize / blockSize);
|
|
485
|
+
if (targetSize > PM_STUDIO_COMPATIBLE_TARGET_MAX_BYTES
|
|
486
|
+
|| blockSize < 16
|
|
487
|
+
|| expectedBlockCount > PM_STUDIO_COMPATIBLE_BLOCK_MAX_COUNT
|
|
488
|
+
|| entry.integrity.blocks.length !== expectedBlockCount) {
|
|
489
|
+
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} has unsafe integrity block geometry`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const sourceTarget = parsed.buffer.subarray(absoluteOffset, absoluteOffset + targetSize);
|
|
493
|
+
const sourceTargetSha256 = sha256Hex(sourceTarget);
|
|
494
|
+
const sourceBlocks = blockSha256(sourceTarget, blockSize);
|
|
495
|
+
if (entry.integrity.hash !== sourceTargetSha256
|
|
496
|
+
|| !sameArray(entry.integrity.blocks, sourceBlocks)) {
|
|
497
|
+
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} integrity metadata does not match its bytes`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const compatibleModule = locateCompatibleCopilotConfigModule(sourceTarget);
|
|
501
|
+
const editOffset = compatibleModule.offset;
|
|
502
|
+
const editEnd = compatibleModule.end;
|
|
503
|
+
const sourceEdit = sourceTarget.subarray(editOffset, editEnd);
|
|
504
|
+
const anchorPositions = occurrencePositions(sourceTarget, Buffer.from(compatibleModule.anchor));
|
|
505
|
+
if (anchorPositions.length !== 1 || anchorPositions[0] !== editOffset) {
|
|
506
|
+
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} compatible module header is not unique`);
|
|
507
|
+
}
|
|
508
|
+
let replacementSource;
|
|
509
|
+
try {
|
|
510
|
+
replacementSource = splitOriginConfigModule(compatibleModule.moduleId, compatibleModule.length);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
throw incompatibleAsar(error.message);
|
|
513
|
+
}
|
|
514
|
+
const replacement = Buffer.from(replacementSource);
|
|
515
|
+
if (replacement.length !== compatibleModule.length) {
|
|
516
|
+
throw incompatibleAsar("PM Studio split-origin replacement does not preserve the compatible module size");
|
|
517
|
+
}
|
|
518
|
+
const patchedTarget = Buffer.from(sourceTarget);
|
|
519
|
+
replacement.copy(patchedTarget, editOffset);
|
|
520
|
+
const patchedTargetSha256 = sha256Hex(patchedTarget);
|
|
521
|
+
const patchedBlocks = blockSha256(patchedTarget, blockSize);
|
|
522
|
+
const patchedAsar = Buffer.from(parsed.buffer);
|
|
523
|
+
replacement.copy(patchedAsar, absoluteOffset + editOffset);
|
|
524
|
+
const patchedParsed = parseAsarBuffer(patchedAsar);
|
|
525
|
+
const patchedEntry = findAsarEntry(patchedParsed.header, PM_STUDIO_COPILOT_CONFIG_MODULE_PATH);
|
|
526
|
+
patchedEntry.integrity = {
|
|
527
|
+
...patchedEntry.integrity,
|
|
528
|
+
algorithm: "SHA256",
|
|
529
|
+
hash: patchedTargetSha256,
|
|
530
|
+
blockSize,
|
|
531
|
+
blocks: [...patchedBlocks],
|
|
532
|
+
};
|
|
533
|
+
const patchedHeaderBytes = Buffer.from(JSON.stringify(patchedParsed.header));
|
|
534
|
+
if (patchedHeaderBytes.length !== patchedParsed.headerLength) {
|
|
535
|
+
throw incompatibleAsar(`Compatible ASAR header changed size (${patchedParsed.headerLength} -> ${patchedHeaderBytes.length})`);
|
|
536
|
+
}
|
|
537
|
+
patchedHeaderBytes.copy(patchedAsar, patchedParsed.headerStart);
|
|
538
|
+
|
|
539
|
+
const version = requiredRecipeString(String(options.version || ""), "version");
|
|
540
|
+
const build = requiredRecipeString(String(options.build || ""), "build");
|
|
541
|
+
const bundleIdentifier = requiredRecipeString(options.bundleIdentifier, "bundle identifier");
|
|
542
|
+
const sourceAsarSha256 = sha256Hex(parsed.buffer);
|
|
543
|
+
const identity = sha256Hex(`${PM_STUDIO_COMPATIBILITY_ID}\0${bundleIdentifier}\0${version}\0${build}\0${sourceAsarSha256}`).slice(0, 24);
|
|
544
|
+
const sourceBundleContent = options.sourceBundleContent
|
|
545
|
+
? Object.freeze({
|
|
546
|
+
...options.sourceBundleContent,
|
|
547
|
+
ignoredXattrs: Object.freeze([...(options.sourceBundleContent.ignoredXattrs || [])]),
|
|
548
|
+
})
|
|
549
|
+
: undefined;
|
|
550
|
+
const edit = Object.freeze({
|
|
551
|
+
offset: editOffset,
|
|
552
|
+
absoluteOffset: absoluteOffset + editOffset,
|
|
553
|
+
length: compatibleModule.length,
|
|
554
|
+
anchor: compatibleModule.anchor,
|
|
555
|
+
anchorCount: 1,
|
|
556
|
+
sourceSha256: sha256Hex(sourceEdit),
|
|
557
|
+
patchedSha256: sha256Hex(replacement),
|
|
558
|
+
replacement: replacementSource,
|
|
559
|
+
});
|
|
560
|
+
const target = Object.freeze({
|
|
561
|
+
path: PM_STUDIO_COPILOT_CONFIG_MODULE_PATH,
|
|
562
|
+
offset: targetOffset,
|
|
563
|
+
size: targetSize,
|
|
564
|
+
blockSize,
|
|
565
|
+
sourceSha256: sourceTargetSha256,
|
|
566
|
+
patchedSha256: patchedTargetSha256,
|
|
567
|
+
sourceBlocks: Object.freeze([...sourceBlocks]),
|
|
568
|
+
patchedBlocks: Object.freeze([...patchedBlocks]),
|
|
569
|
+
edit,
|
|
570
|
+
});
|
|
571
|
+
return Object.freeze({
|
|
572
|
+
id: `pm-studio-compatible-${identity}`,
|
|
573
|
+
compatibility: PM_STUDIO_COMPATIBILITY_ID,
|
|
574
|
+
version,
|
|
575
|
+
build,
|
|
576
|
+
bundleIdentifier,
|
|
577
|
+
sourceTeamIdentifier: requiredRecipeString(options.sourceTeamIdentifier, "source TeamIdentifier"),
|
|
578
|
+
executable: requiredRecipeString(options.executable, "executable"),
|
|
579
|
+
sourceExecutableSha256: requiredRecipeString(options.sourceExecutableSha256, "source executable SHA-256"),
|
|
580
|
+
electronFrameworkPath: requiredRecipeString(options.electronFrameworkPath, "Electron Framework path"),
|
|
581
|
+
sourceElectronFrameworkSha256: requiredRecipeString(
|
|
582
|
+
options.sourceElectronFrameworkSha256,
|
|
583
|
+
"source Electron Framework SHA-256",
|
|
584
|
+
),
|
|
585
|
+
embeddedAsarIntegrity: requiredRecipeString(options.embeddedAsarIntegrity, "embedded ASAR integrity state"),
|
|
586
|
+
asarPath: requiredRecipeString(options.asarPath, "ASAR path"),
|
|
587
|
+
infoPlistPath: requiredRecipeString(options.infoPlistPath, "Info.plist path"),
|
|
588
|
+
integrityKey: requiredRecipeString(options.integrityKey, "ASAR integrity key"),
|
|
589
|
+
dataOffset: parsed.dataOffset,
|
|
590
|
+
sourceAsarSha256,
|
|
591
|
+
sourceHeaderSha256: sha256Hex(parsed.headerBytes),
|
|
592
|
+
patchedAsarSha256: sha256Hex(patchedAsar),
|
|
593
|
+
patchedHeaderSha256: sha256Hex(patchedHeaderBytes),
|
|
594
|
+
...(sourceBundleContent ? { sourceBundleContent } : {}),
|
|
595
|
+
targets: Object.freeze([target]),
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
329
599
|
export function inspectElectronAsarIntegritySlots(value) {
|
|
330
600
|
const buffer = checkedBuffer(value);
|
|
331
601
|
const sentinel = Buffer.from(ELECTRON_ASAR_INTEGRITY_SENTINEL);
|