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 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
- The current patch recipes support only the exact, unmodified PM Studio 2.9.7
122
- and 2.9.10 bundles on macOS. This is exact-version support, not a version range:
123
- 2.9.11 and other unknown builds remain unsupported until they receive their own
124
- verified recipe. Authorize the isolated Claude profile first, quit PM Studio
125
- and its updater, then run the one-time setup command:
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 only local preflight, backup, staging, integrity,
142
- signing, and replacement work. It never starts Device Flow, the adapter,
143
- Codex, or PM Studio, and it does not modify PM profiles, cached model lists, or
144
- saved PM tokens. Unknown versions, hashes, signatures, partial patches, a
145
- running PM process, insufficient space, and every pre-replacement verification
146
- failure leave the installed App unchanged. If the final read-only verification
147
- after an atomic replacement detects an exceptional filesystem drift, setup
148
- does not attempt a second automatic write: it preserves the verified backup,
149
- retains any diagnostic stage still present, and prints recovery paths. Repeating
150
- setup for the exact installed recipe is idempotent.
151
-
152
- The official 2.9.10 artifact can fail macOS's vendor-signature verifier even
153
- though its signing metadata remains readable. CCDX does not generally ignore
154
- that failure: the 2.9.10 exception is accepted only when the complete App tree,
155
- including every file, mode, symlink, and non-volatile extended attribute,
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 replaces the vendor signature with an ad-hoc signature. PM Studio's
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 the official App. Setup prints the verified backup and
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 (`2.9.7/2.9.7`
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 the one exact recipe, move the patched App
196
- aside, restore the complete vendor bundle, and verify it before launch:
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
- For 2.9.10, the vendor `codesign` command may report the documented upstream
208
- signature failure; in that case the App-patch row from `ccdx pms status` must
209
- still classify the restored bundle as exact supported clean content rather than
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 the official PM Studio instead of applying a version-mismatched
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; unsupported versions are reported
284
- without modification, while integrity drift remains an error.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.6.11",
3
+ "version": "0.6.13",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
@@ -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 version-locked PM Studio loopback patch. setup modifies the installed app only after all preflight checks pass.`,
296
- "pms setup": `Usage:\n ${name} pms setup\n\nBacks up, patches, verifies, and ad-hoc signs an exactly supported PM Studio bundle. Unknown versions and drift are never modified.`,
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} is supported but not patched`,
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} has no exact patch recipe; no files will be changed` });
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
  }
@@ -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
- function splitOriginConfigModule() {
12
- const source = `47024(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)}}`;
13
- if (source.length > PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH) {
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(PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH - source.length)}}`;
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);