ghc-proxy 0.4.1 → 0.5.0

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
@@ -96,14 +96,14 @@ See the [Claude Code settings docs](https://docs.anthropic.com/en/docs/claude-co
96
96
  ghc-proxy sits between your tools and the GitHub Copilot API:
97
97
 
98
98
  ```text
99
- ┌─────────────┐ ┌───────────┐ ┌──────────────────────┐
100
- │ Claude Code │──────│ ghc-proxy │──────│ api.githubcopilot.com│
101
- │ Cursor │ │ :4141 │ │
102
- │ Any client │ │ │ │
103
- └─────────────┘ └───────────┘ └──────────────────────┘
104
- OpenAI or Translates GitHub Copilot
105
- Anthropic between API
106
- format formats
99
+ ┌──────────────┐ ┌───────────┐ ┌───────────────────────┐
100
+ │ Claude Code │──────│ ghc-proxy │──────│ api.githubcopilot.com
101
+ │ Cursor │ │ :4141 │ │
102
+ │ Any client │ │ │ │
103
+ └──────────────┘ └───────────┘ └───────────────────────┘
104
+ OpenAI or Translates GitHub Copilot
105
+ Anthropic between API
106
+ format formats
107
107
  ```
108
108
 
109
109
  The proxy authenticates with GitHub using the [device code OAuth flow](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow) (the same flow VS Code uses), then exchanges the GitHub token for a short-lived Copilot token that auto-refreshes.
@@ -2376,12 +2376,20 @@ Primary entry point, Node.js specific entry point is index.js
2376
2376
  const reasonableDetectionSizeInBytes = 4100;
2377
2377
  const maximumMpegOffsetTolerance = reasonableDetectionSizeInBytes - 2;
2378
2378
  const maximumZipEntrySizeInBytes = 1024 * 1024;
2379
+ const maximumZipEntryCount = 1024;
2380
+ const maximumZipBufferedReadSizeInBytes = 2 ** 31 - 1;
2379
2381
  const maximumUntrustedSkipSizeInBytes = 16 * 1024 * 1024;
2382
+ const maximumZipTextEntrySizeInBytes = maximumZipEntrySizeInBytes;
2380
2383
  const maximumNestedGzipDetectionSizeInBytes = maximumUntrustedSkipSizeInBytes;
2384
+ const maximumNestedGzipProbeDepth = 1;
2381
2385
  const maximumId3HeaderSizeInBytes = maximumUntrustedSkipSizeInBytes;
2382
2386
  const maximumEbmlDocumentTypeSizeInBytes = 64;
2383
2387
  const maximumEbmlElementPayloadSizeInBytes = maximumUntrustedSkipSizeInBytes;
2384
2388
  const maximumEbmlElementCount = 256;
2389
+ const maximumPngChunkCount = 512;
2390
+ const maximumAsfHeaderObjectCount = 512;
2391
+ const maximumTiffTagCount = 512;
2392
+ const maximumDetectionReentryCount = 256;
2385
2393
  const maximumPngChunkSizeInBytes = maximumUntrustedSkipSizeInBytes;
2386
2394
  const maximumTiffIfdOffsetInBytes = maximumUntrustedSkipSizeInBytes;
2387
2395
  const recoverableZipErrorMessages = new Set([
@@ -2389,7 +2397,12 @@ const recoverableZipErrorMessages = new Set([
2389
2397
  "Encrypted ZIP",
2390
2398
  "Expected Central-File-Header signature"
2391
2399
  ]);
2392
- const recoverableZipErrorMessagePrefixes = ["Unsupported ZIP compression method:", "ZIP entry decompressed data exceeds "];
2400
+ const recoverableZipErrorMessagePrefixes = [
2401
+ "ZIP entry count exceeds ",
2402
+ "Unsupported ZIP compression method:",
2403
+ "ZIP entry compressed data exceeds ",
2404
+ "ZIP entry decompressed data exceeds "
2405
+ ];
2393
2406
  const recoverableZipErrorCodes = new Set([
2394
2407
  "Z_BUF_ERROR",
2395
2408
  "Z_DATA_ERROR",
@@ -2440,10 +2453,83 @@ async function decompressDeflateRawWithLimit(data, { maximumLength = maximumZipE
2440
2453
  }
2441
2454
  return uncompressedData;
2442
2455
  }
2456
+ const zipDataDescriptorSignature = 134695760;
2457
+ const zipDataDescriptorLengthInBytes = 16;
2458
+ const zipDataDescriptorOverlapLengthInBytes = zipDataDescriptorLengthInBytes - 1;
2459
+ function findZipDataDescriptorOffset(buffer, bytesConsumed) {
2460
+ if (buffer.length < zipDataDescriptorLengthInBytes) return -1;
2461
+ const lastPossibleDescriptorOffset = buffer.length - zipDataDescriptorLengthInBytes;
2462
+ for (let index = 0; index <= lastPossibleDescriptorOffset; index++) if (UINT32_LE.get(buffer, index) === zipDataDescriptorSignature && UINT32_LE.get(buffer, index + 8) === bytesConsumed + index) return index;
2463
+ return -1;
2464
+ }
2465
+ function mergeByteChunks(chunks, totalLength) {
2466
+ const merged = new Uint8Array(totalLength);
2467
+ let offset = 0;
2468
+ for (const chunk of chunks) {
2469
+ merged.set(chunk, offset);
2470
+ offset += chunk.length;
2471
+ }
2472
+ return merged;
2473
+ }
2474
+ async function readZipDataDescriptorEntryWithLimit(zipHandler, { shouldBuffer, maximumLength = maximumZipEntrySizeInBytes } = {}) {
2475
+ const { syncBuffer } = zipHandler;
2476
+ const { length: syncBufferLength } = syncBuffer;
2477
+ const chunks = [];
2478
+ let bytesConsumed = 0;
2479
+ for (;;) {
2480
+ const length = await zipHandler.tokenizer.peekBuffer(syncBuffer, { mayBeLess: true });
2481
+ const dataDescriptorOffset = findZipDataDescriptorOffset(syncBuffer.subarray(0, length), bytesConsumed);
2482
+ const retainedLength = dataDescriptorOffset >= 0 ? 0 : length === syncBufferLength ? Math.min(zipDataDescriptorOverlapLengthInBytes, length - 1) : 0;
2483
+ const chunkLength = dataDescriptorOffset >= 0 ? dataDescriptorOffset : length - retainedLength;
2484
+ if (chunkLength === 0) break;
2485
+ bytesConsumed += chunkLength;
2486
+ if (bytesConsumed > maximumLength) throw new Error(`ZIP entry compressed data exceeds ${maximumLength} bytes`);
2487
+ if (shouldBuffer) {
2488
+ const data = new Uint8Array(chunkLength);
2489
+ await zipHandler.tokenizer.readBuffer(data);
2490
+ chunks.push(data);
2491
+ } else await zipHandler.tokenizer.ignore(chunkLength);
2492
+ if (dataDescriptorOffset >= 0) break;
2493
+ }
2494
+ if (!shouldBuffer) return;
2495
+ return mergeByteChunks(chunks, bytesConsumed);
2496
+ }
2497
+ async function readZipEntryData(zipHandler, zipHeader, { shouldBuffer } = {}) {
2498
+ if (zipHeader.dataDescriptor && zipHeader.compressedSize === 0) return readZipDataDescriptorEntryWithLimit(zipHandler, { shouldBuffer });
2499
+ if (!shouldBuffer) {
2500
+ await zipHandler.tokenizer.ignore(zipHeader.compressedSize);
2501
+ return;
2502
+ }
2503
+ const maximumLength = getMaximumZipBufferedReadLength(zipHandler.tokenizer);
2504
+ if (!Number.isFinite(zipHeader.compressedSize) || zipHeader.compressedSize < 0 || zipHeader.compressedSize > maximumLength) throw new Error(`ZIP entry compressed data exceeds ${maximumLength} bytes`);
2505
+ const fileData = new Uint8Array(zipHeader.compressedSize);
2506
+ await zipHandler.tokenizer.readBuffer(fileData);
2507
+ return fileData;
2508
+ }
2443
2509
  ZipHandler.prototype.inflate = async function(zipHeader, fileData, callback) {
2444
2510
  if (zipHeader.compressedMethod === 0) return callback(fileData);
2445
2511
  if (zipHeader.compressedMethod !== 8) throw new Error(`Unsupported ZIP compression method: ${zipHeader.compressedMethod}`);
2446
- return callback(await decompressDeflateRawWithLimit(fileData, { maximumLength: hasUnknownFileSize(this.tokenizer) ? maximumZipEntrySizeInBytes : Number.MAX_SAFE_INTEGER }));
2512
+ return callback(await decompressDeflateRawWithLimit(fileData, { maximumLength: maximumZipEntrySizeInBytes }));
2513
+ };
2514
+ ZipHandler.prototype.unzip = async function(fileCallback) {
2515
+ let stop = false;
2516
+ let zipEntryCount = 0;
2517
+ do {
2518
+ const zipHeader = await this.readLocalFileHeader();
2519
+ if (!zipHeader) break;
2520
+ zipEntryCount++;
2521
+ if (zipEntryCount > maximumZipEntryCount) throw new Error(`ZIP entry count exceeds ${maximumZipEntryCount}`);
2522
+ const next = fileCallback(zipHeader);
2523
+ stop = Boolean(next.stop);
2524
+ await this.tokenizer.ignore(zipHeader.extraFieldLength);
2525
+ const fileData = await readZipEntryData(this, zipHeader, { shouldBuffer: Boolean(next.handler) });
2526
+ if (next.handler) await this.inflate(zipHeader, fileData, next.handler);
2527
+ if (zipHeader.dataDescriptor) {
2528
+ const dataDescriptor = new Uint8Array(zipDataDescriptorLengthInBytes);
2529
+ await this.tokenizer.readBuffer(dataDescriptor);
2530
+ if (UINT32_LE.get(dataDescriptor, 0) !== zipDataDescriptorSignature) throw new Error(`Expected data-descriptor-signature at position ${this.tokenizer.position - dataDescriptor.length}`);
2531
+ }
2532
+ } while (!stop);
2447
2533
  };
2448
2534
  function createByteLimitedReadableStream(stream, maximumBytes) {
2449
2535
  const reader = stream.getReader();
@@ -2622,12 +2708,17 @@ function hasUnknownFileSize(tokenizer) {
2622
2708
  function hasExceededUnknownSizeScanBudget(tokenizer, startOffset, maximumBytes) {
2623
2709
  return hasUnknownFileSize(tokenizer) && tokenizer.position - startOffset > maximumBytes;
2624
2710
  }
2711
+ function getMaximumZipBufferedReadLength(tokenizer) {
2712
+ const fileSize = tokenizer.fileInfo.size;
2713
+ const remainingBytes = Number.isFinite(fileSize) ? Math.max(0, fileSize - tokenizer.position) : Number.MAX_SAFE_INTEGER;
2714
+ return Math.min(remainingBytes, maximumZipBufferedReadSizeInBytes);
2715
+ }
2625
2716
  function isRecoverableZipError(error) {
2626
2717
  if (error instanceof EndOfStreamError) return true;
2627
2718
  if (error instanceof ParserHardLimitError) return true;
2628
2719
  if (!(error instanceof Error)) return false;
2629
2720
  if (recoverableZipErrorMessages.has(error.message)) return true;
2630
- if (error instanceof TypeError && recoverableZipErrorCodes.has(error.code)) return true;
2721
+ if (recoverableZipErrorCodes.has(error.code)) return true;
2631
2722
  for (const prefix of recoverableZipErrorMessagePrefixes) if (error.message.startsWith(prefix)) return true;
2632
2723
  return false;
2633
2724
  }
@@ -2703,8 +2794,13 @@ var FileTypeParser = class {
2703
2794
  }
2704
2795
  ];
2705
2796
  this.tokenizerOptions = { abortSignal: this.options.signal };
2797
+ this.gzipProbeDepth = 0;
2706
2798
  }
2707
- async fromTokenizer(tokenizer) {
2799
+ getTokenizerOptions() {
2800
+ return { ...this.tokenizerOptions };
2801
+ }
2802
+ async fromTokenizer(tokenizer, detectionReentryCount = 0) {
2803
+ this.detectionReentryCount = detectionReentryCount;
2708
2804
  const initialPosition = tokenizer.position;
2709
2805
  for (const detector of this.detectors) {
2710
2806
  let fileType;
@@ -2723,10 +2819,10 @@ var FileTypeParser = class {
2723
2819
  if (!(input instanceof Uint8Array || input instanceof ArrayBuffer)) throw new TypeError(`Expected the \`input\` argument to be of type \`Uint8Array\` or \`ArrayBuffer\`, got \`${typeof input}\``);
2724
2820
  const buffer = input instanceof Uint8Array ? input : new Uint8Array(input);
2725
2821
  if (!(buffer?.length > 1)) return;
2726
- return this.fromTokenizer(fromBuffer(buffer, this.tokenizerOptions));
2822
+ return this.fromTokenizer(fromBuffer(buffer, this.getTokenizerOptions()));
2727
2823
  }
2728
2824
  async fromBlob(blob) {
2729
- const tokenizer = fromBlob(blob, this.tokenizerOptions);
2825
+ const tokenizer = fromBlob(blob, this.getTokenizerOptions());
2730
2826
  try {
2731
2827
  return await this.fromTokenizer(tokenizer);
2732
2828
  } finally {
@@ -2734,7 +2830,7 @@ var FileTypeParser = class {
2734
2830
  }
2735
2831
  }
2736
2832
  async fromStream(stream) {
2737
- const tokenizer = fromWebStream(stream, this.tokenizerOptions);
2833
+ const tokenizer = fromWebStream(stream, this.getTokenizerOptions());
2738
2834
  try {
2739
2835
  return await this.fromTokenizer(tokenizer);
2740
2836
  } finally {
@@ -2832,6 +2928,8 @@ var FileTypeParser = class {
2832
2928
  187,
2833
2929
  191
2834
2930
  ])) {
2931
+ if (this.detectionReentryCount >= maximumDetectionReentryCount) return;
2932
+ this.detectionReentryCount++;
2835
2933
  await this.tokenizer.ignore(3);
2836
2934
  return this.detectConfident(tokenizer);
2837
2935
  }
@@ -2856,12 +2954,19 @@ var FileTypeParser = class {
2856
2954
  139,
2857
2955
  8
2858
2956
  ])) {
2957
+ if (this.gzipProbeDepth >= maximumNestedGzipProbeDepth) return {
2958
+ ext: "gz",
2959
+ mime: "application/gzip"
2960
+ };
2859
2961
  const limitedInflatedStream = createByteLimitedReadableStream(new GzipHandler(tokenizer).inflate(), maximumNestedGzipDetectionSizeInBytes);
2860
2962
  let compressedFileType;
2861
2963
  try {
2964
+ this.gzipProbeDepth++;
2862
2965
  compressedFileType = await this.fromStream(limitedInflatedStream);
2863
2966
  } catch (error) {
2864
2967
  if (error?.name === "AbortError") throw error;
2968
+ } finally {
2969
+ this.gzipProbeDepth--;
2865
2970
  }
2866
2971
  if (compressedFileType?.ext === "tar") return {
2867
2972
  ext: "tar.gz",
@@ -2904,7 +3009,9 @@ var FileTypeParser = class {
2904
3009
  if (error instanceof EndOfStreamError) return;
2905
3010
  throw error;
2906
3011
  }
2907
- return this.fromTokenizer(tokenizer);
3012
+ if (this.detectionReentryCount >= maximumDetectionReentryCount) return;
3013
+ this.detectionReentryCount++;
3014
+ return this.fromTokenizer(tokenizer, this.detectionReentryCount);
2908
3015
  }
2909
3016
  if (this.checkString("MP+")) return {
2910
3017
  ext: "mpc",
@@ -2988,7 +3095,7 @@ var FileTypeParser = class {
2988
3095
  };
2989
3096
  return { stop: true };
2990
3097
  case "mimetype":
2991
- if (!canReadZipEntryForDetection(zipHeader)) return {};
3098
+ if (!canReadZipEntryForDetection(zipHeader, maximumZipTextEntrySizeInBytes)) return {};
2992
3099
  return {
2993
3100
  async handler(fileData) {
2994
3101
  fileType = getFileTypeFromMimeType(new TextDecoder("utf-8").decode(fileData).trim());
@@ -2997,7 +3104,7 @@ var FileTypeParser = class {
2997
3104
  };
2998
3105
  case "[Content_Types].xml":
2999
3106
  openXmlState.hasContentTypesEntry = true;
3000
- if (!canReadZipEntryForDetection(zipHeader, hasUnknownFileSize(tokenizer) ? maximumZipEntrySizeInBytes : Number.MAX_SAFE_INTEGER)) {
3107
+ if (!canReadZipEntryForDetection(zipHeader, maximumZipTextEntrySizeInBytes)) {
3001
3108
  openXmlState.hasUnparseableContentTypes = true;
3002
3109
  return {};
3003
3110
  }
@@ -3242,6 +3349,7 @@ var FileTypeParser = class {
3242
3349
  while (children > 0) {
3243
3350
  ebmlElementCount++;
3244
3351
  if (ebmlElementCount > maximumEbmlElementCount) return;
3352
+ const previousPosition = tokenizer.position;
3245
3353
  const element = await readElement();
3246
3354
  if (element.id === 17026) {
3247
3355
  if (element.len > maximumEbmlDocumentTypeSizeInBytes) return;
@@ -3254,6 +3362,7 @@ var FileTypeParser = class {
3254
3362
  reason: "EBML payload"
3255
3363
  });
3256
3364
  --children;
3365
+ if (tokenizer.position <= previousPosition) return;
3257
3366
  }
3258
3367
  }
3259
3368
  switch (await readChildren((await readElement()).len)) {
@@ -3572,8 +3681,12 @@ var FileTypeParser = class {
3572
3681
  }
3573
3682
  const isUnknownPngStream = hasUnknownFileSize(tokenizer);
3574
3683
  const pngScanStart = tokenizer.position;
3684
+ let pngChunkCount = 0;
3575
3685
  do {
3686
+ pngChunkCount++;
3687
+ if (pngChunkCount > maximumPngChunkCount) break;
3576
3688
  if (hasExceededUnknownSizeScanBudget(tokenizer, pngScanStart, maximumPngChunkSizeInBytes)) break;
3689
+ const previousPosition = tokenizer.position;
3577
3690
  const chunk = await readChunkHeader();
3578
3691
  if (chunk.length < 0) return;
3579
3692
  switch (chunk.type) {
@@ -3591,6 +3704,7 @@ var FileTypeParser = class {
3591
3704
  throw error;
3592
3705
  }
3593
3706
  }
3707
+ if (tokenizer.position <= previousPosition) break;
3594
3708
  } while (tokenizer.position + 8 < tokenizer.fileInfo.size);
3595
3709
  return pngFileType;
3596
3710
  }
@@ -3836,7 +3950,10 @@ var FileTypeParser = class {
3836
3950
  });
3837
3951
  const isUnknownFileSize = hasUnknownFileSize(tokenizer);
3838
3952
  const asfHeaderScanStart = tokenizer.position;
3953
+ let asfHeaderObjectCount = 0;
3839
3954
  while (tokenizer.position + 24 < tokenizer.fileInfo.size) {
3955
+ asfHeaderObjectCount++;
3956
+ if (asfHeaderObjectCount > maximumAsfHeaderObjectCount) break;
3840
3957
  if (hasExceededUnknownSizeScanBudget(tokenizer, asfHeaderScanStart, maximumUntrustedSkipSizeInBytes)) break;
3841
3958
  const previousPosition = tokenizer.position;
3842
3959
  const header = await readHeader();
@@ -4419,6 +4536,7 @@ var FileTypeParser = class {
4419
4536
  }
4420
4537
  async readTiffIFD(bigEndian) {
4421
4538
  const numberOfTags = await this.tokenizer.readToken(bigEndian ? UINT16_BE : UINT16_LE);
4539
+ if (numberOfTags > maximumTiffTagCount) return;
4422
4540
  if (hasUnknownFileSize(this.tokenizer) && 2 + numberOfTags * 12 > maximumTiffIfdOffsetInBytes) return;
4423
4541
  for (let n = 0; n < numberOfTags; ++n) {
4424
4542
  const fileType = await this.readTiffTag(bigEndian);
@@ -4524,4 +4642,4 @@ const supportedMimeTypes = new Set(mimeTypes);
4524
4642
 
4525
4643
  //#endregion
4526
4644
  export { fileTypeFromBlob };
4527
- //# sourceMappingURL=file-type-BNd4XJK1.mjs.map
4645
+ //# sourceMappingURL=file-type-DlzWawJh.mjs.map