sliccy 6.146.2 → 6.147.1

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.
Files changed (131) hide show
  1. package/dist/node-server/fetch-proxy-gzip.d.ts +34 -0
  2. package/dist/node-server/fetch-proxy-gzip.js +123 -0
  3. package/dist/node-server/fetch-proxy-headers.js +7 -0
  4. package/dist/node-server/routes/fetch-proxy.js +21 -12
  5. package/dist/ui/.vite/manifest.json +170 -158
  6. package/dist/ui/assets/{SKILL-CkfptFag.js → SKILL-2Mi1pQ3F.js} +30 -4
  7. package/dist/ui/assets/SKILL-BmzmDv0e.js +1 -0
  8. package/dist/ui/assets/{account-store-BRUCr96D.js → account-store-B1iVHXUI.js} +1 -1
  9. package/dist/ui/assets/{account-store-BSBnFFPS.js → account-store-CWEAADeP.js} +1 -1
  10. package/dist/ui/assets/{adobe-D2ONtf9b.js → adobe-ONrE9NWb.js} +1 -1
  11. package/dist/ui/assets/{adobe-BePmoDqc.js → adobe-j0lX3kiM.js} +1 -1
  12. package/dist/ui/assets/{agent-bridge-DslQDtty.js → agent-bridge-KyX8EP3Z.js} +1 -1
  13. package/dist/ui/assets/{agent-panels-Ds3CUde9.js → agent-panels-CH_BrlMR.js} +1 -1
  14. package/dist/ui/assets/{apply-layout-CuJIEJ0g.js → apply-layout-BM1O4DWE.js} +1 -1
  15. package/dist/ui/assets/{apps-xYbVYVNJ.js → apps-x95UwLLV.js} +1 -1
  16. package/dist/ui/assets/{azure-openai-DLXgdMoO.js → azure-openai-BSpFid3b.js} +1 -1
  17. package/dist/ui/assets/{azure-openai-C9RMKMwt.js → azure-openai-Bk_7bp5-.js} +1 -1
  18. package/dist/ui/assets/{budget-usage-source-DRsm-qIe.js → budget-usage-source-P-1c__Ag.js} +1 -1
  19. package/dist/ui/assets/{cdp-Bgplxqwh.js → cdp-C2bgdwiN.js} +1 -1
  20. package/dist/ui/assets/{cerebras-Bc5srro5.js → cerebras-0C_EDA3g.js} +1 -1
  21. package/dist/ui/assets/{cerebras-Bw_QvTrp.js → cerebras-BwhQpaNk.js} +1 -1
  22. package/dist/ui/assets/{composer-speech-B9r4JYxu.js → composer-speech-9tQyDjVE.js} +1 -1
  23. package/dist/ui/assets/{connect-surface-VgwGEglg.js → connect-surface-Jc9hJ1qB.js} +1 -1
  24. package/dist/ui/assets/{directed-approval--0iRHwbB.js → directed-approval-DNDKlU_3.js} +1 -1
  25. package/dist/ui/assets/es-BR4l6CGS.js +1 -0
  26. package/dist/ui/assets/feature-flags-COdcJUhl.js +1 -0
  27. package/dist/ui/assets/frozen-archive-format-B4efpvB9.js +1 -0
  28. package/dist/ui/assets/frozen-archive-writer-cEYcj5un.js +8 -0
  29. package/dist/ui/assets/{fs-CWmEOkSn.js → fs-DL9hfAka.js} +1 -1
  30. package/dist/ui/assets/{fs-sNrIPaO_.js → fs-QdZLjZ9G.js} +2 -2
  31. package/dist/ui/assets/{github-BL7xlelB.js → github-BO3ntG4f.js} +2 -2
  32. package/dist/ui/assets/{github-DNtfKMGK.js → github-Bz0_AFKA.js} +1 -1
  33. package/dist/ui/assets/{github-copilot-C62fDFcu.js → github-copilot-DGZmiwmz.js} +1 -1
  34. package/dist/ui/assets/{github-copilot-CtPhNQnK.js → github-copilot-Un-Y_WFH.js} +1 -1
  35. package/dist/ui/assets/{github-install-DOMbiICh.js → github-install-BCoi8VPT.js} +1 -1
  36. package/dist/ui/assets/{github-zip-BjQhOm6P.js → github-zip-BeOCyf5X.js} +1 -1
  37. package/dist/ui/assets/{hear-C4mXu7ZW.js → hear-CGE2BC1s.js} +1 -1
  38. package/dist/ui/assets/{help-DHpBUyiO.js → help-CSZPurcQ.js} +1 -1
  39. package/dist/ui/assets/{js-realm-worker-EoBKAZHX.js → js-realm-worker-DDM4WJwj.js} +1 -1
  40. package/dist/ui/assets/{kernel-worker-DacBBg8w.js → kernel-worker-Tz_0rnJe.js} +644 -644
  41. package/dist/ui/assets/live-session-snapshot-BnA6Mq0G.js +1 -0
  42. package/dist/ui/assets/{local-llm-BLnqY8h6.js → local-llm-CxgSeEYX.js} +1 -1
  43. package/dist/ui/assets/{local-llm-Cr6CTkbj.js → local-llm-DEkl-I_B.js} +1 -1
  44. package/dist/ui/assets/{main-w_xCet4j.js → main-DSiIT2Zw.js} +3 -3
  45. package/dist/ui/assets/{main-cherry-DLuhoDS1.js → main-cherry-BlU4TnQR.js} +1 -1
  46. package/dist/ui/assets/memory-v2-flag-C1duOEOb.js +1 -0
  47. package/dist/ui/assets/{mount-7bYA8pGl.js → mount-Deyjm5x3.js} +3 -3
  48. package/dist/ui/assets/{mount-recovery-DgGwhnxS.js → mount-recovery-soJELy8U.js} +1 -1
  49. package/dist/ui/assets/{mount-uOkkZNjk.js → mount-w5_9Ng9R.js} +1 -1
  50. package/dist/ui/assets/{new-session-DBtk1rCL.js → new-session-B8XzIc8Q.js} +2 -2
  51. package/dist/ui/assets/{oauth-bootstrap-DYxjJLsL.js → oauth-bootstrap-DfSZxwQE.js} +2 -2
  52. package/dist/ui/assets/{onboarding-orchestrator-B6T_Ny1B.js → onboarding-orchestrator-o3VViOFw.js} +1 -1
  53. package/dist/ui/assets/{openai-codex-BlCGePv3.js → openai-codex-C5RekyZV.js} +1 -1
  54. package/dist/ui/assets/{openai-codex-DkzYl_Av.js → openai-codex-CiayWsof.js} +1 -1
  55. package/dist/ui/assets/{openrouter-Cj2pVk0X.js → openrouter-B83RCbFs.js} +1 -1
  56. package/dist/ui/assets/{openrouter-DfxV_kXn.js → openrouter-DMQy-KKM.js} +1 -1
  57. package/dist/ui/assets/{openrouter-free-BjPH_kdE.js → openrouter-free-C0JGq62M.js} +1 -1
  58. package/dist/ui/assets/{openrouter-free-DldbK6sa.js → openrouter-free-SS7fpd0i.js} +1 -1
  59. package/dist/ui/assets/{openrouter-oauth-DgDO8d-n.js → openrouter-oauth-D5vNIEH9.js} +1 -1
  60. package/dist/ui/assets/{openrouter-oauth-CH1i_4Ro.js → openrouter-oauth-DUGD2y3m.js} +1 -1
  61. package/dist/ui/assets/{panel-rpc-handlers-Ae7e5szM.js → panel-rpc-handlers-CXwQrHsy.js} +2 -2
  62. package/dist/ui/assets/{panelize-shell-9kCQ9OZk.js → panelize-shell-DZDs3zYf.js} +1 -1
  63. package/dist/ui/assets/{provider-BtR7FzYX.js → provider-BI8YYBre.js} +2 -2
  64. package/dist/ui/assets/{provider-Cb_dptea.js → provider-CXxMpJkz.js} +1 -1
  65. package/dist/ui/assets/provider-settings-DGIFYqZZ.js +1 -0
  66. package/dist/ui/assets/provider-store-access-BPqDMr97.js +1 -0
  67. package/dist/ui/assets/provider-store-access-DwXuPmyu.js +1 -0
  68. package/dist/ui/assets/{providers-Bj5Jgeyh.js → providers-D6Ef5U-a.js} +1 -1
  69. package/dist/ui/assets/{providers-CCYUf1lu.js → providers-SXCyVs8S.js} +2 -2
  70. package/dist/ui/assets/{quick-llm-B8LK4tjq.js → quick-llm-m2zlZjFT.js} +1 -1
  71. package/dist/ui/assets/{recovery-screen-D-eWqkZz.js → recovery-screen-jVLbowtH.js} +1 -1
  72. package/dist/ui/assets/{run-Clc4oMMw.js → run-BSIpZdkj.js} +2 -2
  73. package/dist/ui/assets/{run-BSItVSs5.js → run-DKJHr_5p.js} +1 -1
  74. package/dist/ui/assets/{run-QTBA9VNJ.js → run-Da0sp26F.js} +1 -1
  75. package/dist/ui/assets/session-command-memory-Bh_U73Lu.js +21 -0
  76. package/dist/ui/assets/session-jsonl-BxhPJRfn.js +3 -0
  77. package/dist/ui/assets/session-jsonl-DcEZap-X.js +11 -0
  78. package/dist/ui/assets/session-search-index-CYtd-I68.js +3 -0
  79. package/dist/ui/assets/{setup-feature-flags-remote-C0aFgOXn.js → setup-feature-flags-remote-C-dQV79P.js} +1 -1
  80. package/dist/ui/assets/sidecar-repair-9ZhXBSsu.js +1 -0
  81. package/dist/ui/assets/{silent-renew-backoff-BXFOEcR4.js → silent-renew-backoff-Bt2GjeOj.js} +1 -1
  82. package/dist/ui/assets/{skills-DH0igvmk.js → skills-DjZ7Eln1.js} +1 -1
  83. package/dist/ui/assets/slicc-version-B3akX0fg.js +1 -0
  84. package/dist/ui/assets/{snapshot-B23VxS3Z.js → snapshot-CHRkEn1u.js} +1 -1
  85. package/dist/ui/assets/{snapshot-features-EdqS3LYW.js → snapshot-features-CChvVb35.js} +1 -1
  86. package/dist/ui/assets/{soundscape-DbhVL1J6.js → soundscape-JzbhDh5W.js} +1 -1
  87. package/dist/ui/assets/{speak-Nt2Xatm_.js → speak-jhy2qJPd.js} +1 -1
  88. package/dist/ui/assets/{sprinkle-manager-Gcr6PESp.js → sprinkle-manager-CvYiBryj.js} +1 -1
  89. package/dist/ui/assets/{sprinkle-renderer-BgznbYny.js → sprinkle-renderer-COXUa0Hf.js} +1 -1
  90. package/dist/ui/assets/{state-BiRW-NX3.js → state-5QPaUSSz.js} +1 -1
  91. package/dist/ui/assets/{store-Sz2R7uqe.js → store-CCVnZCWJ.js} +1 -1
  92. package/dist/ui/assets/{store-BgpATPUF.js → store-ve5mq2br.js} +1 -1
  93. package/dist/ui/assets/{sudo-DcFRMDHW.js → sudo-BZ-HbvCF.js} +1 -1
  94. package/dist/ui/assets/{sync-dialog-CG7oN3ii.js → sync-dialog-D6fstzeQ.js} +1 -1
  95. package/dist/ui/assets/{transformers-env-CAqo4XNv.js → transformers-env-BVx9lRjS.js} +1 -1
  96. package/dist/ui/assets/{tray-sidecar-DH_L8_VL.js → tray-sidecar-6nc9OtBv.js} +1 -1
  97. package/dist/ui/assets/{upgrade-detection-CIrFLX7C.js → upgrade-detection-7OZzvJpD.js} +1 -1
  98. package/dist/ui/assets/{voice-reply-S9gknH7c.js → voice-reply-CutW51kw.js} +1 -1
  99. package/dist/ui/assets/{wc-attach-Dh-J7Xnm.js → wc-attach-C4gIYF_M.js} +2 -2
  100. package/dist/ui/assets/{wc-extension-DGzZxlv0.js → wc-extension-BUXOk9jr.js} +2 -2
  101. package/dist/ui/assets/{wc-follower-CBYRavvs.js → wc-follower-BrUt1SmG.js} +3 -3
  102. package/dist/ui/assets/{wc-follower-oauth-Dbdj3gVn.js → wc-follower-oauth-D5Q66d0Z.js} +1 -1
  103. package/dist/ui/assets/wc-live-ART73oe9.js +399 -0
  104. package/dist/ui/assets/{wc-nav-69LMhXN-.js → wc-nav-DYC4BF7x.js} +2 -2
  105. package/dist/ui/assets/{wc-onboarding-DeLL7QQz.js → wc-onboarding-DAb8qvn2.js} +2 -2
  106. package/dist/ui/assets/{wc-placeholder-B4z6NazB.js → wc-placeholder-CT4PfhBi.js} +3 -3
  107. package/dist/ui/assets/{wc-secret-request-CPlXgRlo.js → wc-secret-request-BMOHo8At.js} +1 -1
  108. package/dist/ui/assets/{wc-settings-CFSgQw3j.js → wc-settings-D7Z5KR5L.js} +3 -3
  109. package/dist/ui/assets/{wc-shell-CZCKkLJP.js → wc-shell-D9hwrlW6.js} +3 -3
  110. package/dist/ui/assets/{wc-shortcut-config-DO8_9Cf2.js → wc-shortcut-config-DuV5CR3I.js} +1 -1
  111. package/dist/ui/assets/{wc-shortcuts-_REWQgA9.js → wc-shortcuts-D0rBXwwC.js} +1 -1
  112. package/dist/ui/assets/{wc-sprinkles-Cmtrm7gu.js → wc-sprinkles-BeJqRgYQ.js} +2 -2
  113. package/dist/ui/assets/{wc-transcript-export-C7BaP8YL.js → wc-transcript-export-CiC7iZEN.js} +2 -2
  114. package/dist/ui/assets/{wc-tray-Juzpc0lo.js → wc-tray-BVsyQHIH.js} +4 -4
  115. package/dist/ui/assets/welcome-detection-oXuOQLgu.js +1 -0
  116. package/dist/ui/assets/{whisper-session-B1zyLwlp.js → whisper-session-DoglB0LV.js} +2 -2
  117. package/dist/ui/assets/{xai-grok-BkZGmqHN.js → xai-grok-Bud3uBoC.js} +1 -1
  118. package/dist/ui/assets/{xai-grok-CYoaZvHZ.js → xai-grok-DMvTy83_.js} +1 -1
  119. package/dist/ui/index.html +3 -3
  120. package/dist/ui/packages/webapp/index.html +3 -3
  121. package/package.json +2 -2
  122. package/dist/ui/assets/SKILL-u5bVJ6oJ.js +0 -1
  123. package/dist/ui/assets/frozen-archive-format-CP0rleV0.js +0 -1
  124. package/dist/ui/assets/live-session-snapshot-Dw8Q7uwS.js +0 -8
  125. package/dist/ui/assets/provider-settings-wIUvBGiz.js +0 -1
  126. package/dist/ui/assets/provider-store-access-Dt__oRzP.js +0 -1
  127. package/dist/ui/assets/provider-store-access-pBCuTMk1.js +0 -1
  128. package/dist/ui/assets/sidecar-repair-CODJ1Gjx.js +0 -1
  129. package/dist/ui/assets/slicc-version-Ddu_nmhS.js +0 -1
  130. package/dist/ui/assets/wc-live-DmfNvJns.js +0 -402
  131. package/dist/ui/assets/welcome-detection-B8VXP3l5.js +0 -1
@@ -0,0 +1,34 @@
1
+ import { Transform } from 'node:stream';
2
+ /** gzip member magic (`1f 8b`). */
3
+ export declare const GZIP_MAGIC_0 = 31;
4
+ export declare const GZIP_MAGIC_1 = 139;
5
+ /**
6
+ * True when the upstream `content-encoding` does not claim a compressed
7
+ * coding. Empty and `identity` both mean "the bytes are the representation"
8
+ * — which is a lie when an edge returns cached gzip without the header
9
+ * (#3037, AEM/Fastly).
10
+ */
11
+ export declare function contentEncodingLooksUncompressed(value: string | null | undefined): boolean;
12
+ export declare function bufferStartsWithGzipMagic(buf: Uint8Array): boolean;
13
+ export interface MaybeGunzipOptions {
14
+ /**
15
+ * Fired once the first two bytes have been seen (or on empty EOF), before
16
+ * any decoded bytes are pushed. `inflating` is true when gzip magic was
17
+ * present and the rest of the body will go through zlib.
18
+ */
19
+ onDecided?: (inflating: boolean) => void;
20
+ }
21
+ /**
22
+ * Peek-and-maybe-gunzip transform for `/api/fetch-proxy`.
23
+ *
24
+ * If the body starts with gzip magic, pipe through `zlib.createGunzip`.
25
+ * Otherwise pass bytes through. The route only inserts this transform for
26
+ * text content types so a real `application/gzip` / `.tar.gz` download is
27
+ * not silently inflated.
28
+ *
29
+ * Declared `gzip`/`br` is usually already inflated by undici; sniffing
30
+ * magic still catches the AEM/Fastly shape (gzip bytes, encoding
31
+ * absent/`identity`). The inner Gunzip is paused when `push` returns
32
+ * false so a highly compressible chunk cannot unbounded-buffer here.
33
+ */
34
+ export declare function createMaybeGunzipStream(options?: MaybeGunzipOptions): Transform;
@@ -0,0 +1,123 @@
1
+ import { Transform } from 'node:stream';
2
+ import { createGunzip } from 'node:zlib';
3
+ /** gzip member magic (`1f 8b`). */
4
+ export const GZIP_MAGIC_0 = 0x1f;
5
+ export const GZIP_MAGIC_1 = 0x8b;
6
+ /**
7
+ * True when the upstream `content-encoding` does not claim a compressed
8
+ * coding. Empty and `identity` both mean "the bytes are the representation"
9
+ * — which is a lie when an edge returns cached gzip without the header
10
+ * (#3037, AEM/Fastly).
11
+ */
12
+ export function contentEncodingLooksUncompressed(value) {
13
+ const encoding = (value ?? '').trim().toLowerCase();
14
+ return encoding === '' || encoding === 'identity';
15
+ }
16
+ export function bufferStartsWithGzipMagic(buf) {
17
+ return buf.length >= 2 && buf[0] === GZIP_MAGIC_0 && buf[1] === GZIP_MAGIC_1;
18
+ }
19
+ /**
20
+ * Peek-and-maybe-gunzip transform for `/api/fetch-proxy`.
21
+ *
22
+ * If the body starts with gzip magic, pipe through `zlib.createGunzip`.
23
+ * Otherwise pass bytes through. The route only inserts this transform for
24
+ * text content types so a real `application/gzip` / `.tar.gz` download is
25
+ * not silently inflated.
26
+ *
27
+ * Declared `gzip`/`br` is usually already inflated by undici; sniffing
28
+ * magic still catches the AEM/Fastly shape (gzip bytes, encoding
29
+ * absent/`identity`). The inner Gunzip is paused when `push` returns
30
+ * false so a highly compressible chunk cannot unbounded-buffer here.
31
+ */
32
+ export function createMaybeGunzipStream(options = {}) {
33
+ return new MaybeGunzipTransform(options.onDecided);
34
+ }
35
+ class MaybeGunzipTransform extends Transform {
36
+ #head = Buffer.alloc(0);
37
+ #decided = false;
38
+ #gunzip = null;
39
+ #onDecided;
40
+ constructor(onDecided) {
41
+ super();
42
+ this.#onDecided = onDecided;
43
+ // Destination drained — let zlib produce more. Attached once so a
44
+ // paused Gunzip cannot sit idle after `push` returned false.
45
+ this.on('resume', () => {
46
+ this.#gunzip?.resume();
47
+ });
48
+ }
49
+ _transform(chunk, _enc, cb) {
50
+ if (this.#gunzip) {
51
+ this.#gunzip.write(chunk, cb);
52
+ return;
53
+ }
54
+ if (this.#decided) {
55
+ cb(null, chunk);
56
+ return;
57
+ }
58
+ this.#head = Buffer.concat([this.#head, chunk]);
59
+ if (this.#head.length < 2) {
60
+ cb();
61
+ return;
62
+ }
63
+ this.#finishDecide(cb);
64
+ }
65
+ _flush(cb) {
66
+ if (!this.#decided) {
67
+ if (this.#head.length === 0) {
68
+ this.#onDecided?.(false);
69
+ cb();
70
+ return;
71
+ }
72
+ this.#finishDecide((err) => {
73
+ if (err) {
74
+ cb(err);
75
+ return;
76
+ }
77
+ this.#endGunzipOrFinish(cb);
78
+ });
79
+ return;
80
+ }
81
+ this.#endGunzipOrFinish(cb);
82
+ }
83
+ #finishDecide(cb) {
84
+ this.#decided = true;
85
+ const head = this.#head;
86
+ this.#head = Buffer.alloc(0);
87
+ if (!bufferStartsWithGzipMagic(head)) {
88
+ this.#onDecided?.(false);
89
+ cb(null, head);
90
+ return;
91
+ }
92
+ this.#onDecided?.(true);
93
+ const gunzip = createGunzip();
94
+ this.#gunzip = gunzip;
95
+ gunzip.on('data', (decoded) => {
96
+ if (!this.push(decoded)) {
97
+ gunzip.pause();
98
+ }
99
+ });
100
+ gunzip.on('error', (err) => {
101
+ this.destroy(err);
102
+ });
103
+ gunzip.write(head, cb);
104
+ }
105
+ #endGunzipOrFinish(cb) {
106
+ const gunzip = this.#gunzip;
107
+ if (!gunzip) {
108
+ cb();
109
+ return;
110
+ }
111
+ const onEnd = () => {
112
+ gunzip.off('error', onError);
113
+ cb();
114
+ };
115
+ const onError = (err) => {
116
+ gunzip.off('end', onEnd);
117
+ cb(err);
118
+ };
119
+ gunzip.once('end', onEnd);
120
+ gunzip.once('error', onError);
121
+ gunzip.end();
122
+ }
123
+ }
@@ -35,6 +35,9 @@ export const FETCH_PROXY_SKIP_HEADERS = new Set([
35
35
  // @slicc/shared-ts secrets-pipeline.ts) — consumed by the route handler
36
36
  // to compute and attach a real signature header; never forwarded as-is.
37
37
  'x-slicc-hmac-sign',
38
+ // Let undici negotiate gzip/br. Forcing `identity` made AEM/Fastly
39
+ // return cached gzip bytes with no `content-encoding` (#3037).
40
+ 'accept-encoding',
38
41
  ]);
39
42
  /**
40
43
  * Response-side header names the proxy must NOT copy from the upstream
@@ -53,6 +56,10 @@ export const FETCH_PROXY_SKIP_HEADERS = new Set([
53
56
  */
54
57
  export const FETCH_PROXY_SKIP_RESPONSE_HEADERS = new Set([
55
58
  'transfer-encoding',
59
+ // Stripped because this hop delivers decoded bytes: undici inflates a
60
+ // declared coding, and `streamUpstreamBody` gunzips a body that still
61
+ // starts with gzip magic when the coding is absent (#3037). A synthetic
62
+ // SW `Response` does not inflate `content-encoding`.
56
63
  'content-encoding',
57
64
  'content-length',
58
65
  'www-authenticate',
@@ -1,6 +1,7 @@
1
- import { Readable, Transform } from 'node:stream';
1
+ import { PassThrough, Readable, Transform } from 'node:stream';
2
2
  import { StringDecoder } from 'node:string_decoder';
3
3
  import { HMAC_SIGN_HEADER, isFormContentType, isLoopbackOrigin, isTextContentType, isTextRequestContentType, unmaskFormBody, } from '../_shared/index.js';
4
+ import { createMaybeGunzipStream } from '../fetch-proxy-gzip.js';
4
5
  import { buildFetchProxyExposeHeaders, FETCH_PROXY_CONTENT_LENGTH_HEADER, FETCH_PROXY_SKIP_HEADERS, FETCH_PROXY_SKIP_RESPONSE_HEADERS, FETCH_PROXY_SKIP_RESPONSE_PREFIXES, } from '../fetch-proxy-headers.js';
5
6
  import { AgentActivityTracker, registerAgentActivityRoute } from './agent-activity.js';
6
7
  /** Pick the first value of a possibly-multi-valued request header. */
@@ -24,7 +25,8 @@ async function collectRawBody(req) {
24
25
  /**
25
26
  * Build the forwarded header set: copy non-hop-by-hop headers, then restore
26
27
  * the forbidden-header transports (Cookie/Origin/Referer/Proxy-*) the browser
27
- * could not send via fetch(), and force an identity encoding.
28
+ * could not send via fetch(). `accept-encoding` is skipped so undici
29
+ * negotiates gzip/br and decompresses transparently (#3037).
28
30
  */
29
31
  function buildForwardHeaders(req, targetUrl) {
30
32
  const headers = {};
@@ -72,10 +74,6 @@ function buildForwardHeaders(req, targetUrl) {
72
74
  delete headers[key];
73
75
  }
74
76
  }
75
- // Always request uncompressed responses — the proxy doesn't decompress and
76
- // the browser→proxy hop is localhost, so compression has no benefit and
77
- // would arrive as garbage once Content-Encoding is stripped below.
78
- headers['accept-encoding'] = 'identity';
79
77
  return headers;
80
78
  }
81
79
  /**
@@ -246,25 +244,36 @@ function createScrubStream(secretProxy, isText) {
246
244
  function streamUpstreamBody(res, upstream, secretProxy, detachClientClose) {
247
245
  const isText = isTextContentType(upstream.headers.get('content-type') ?? '');
248
246
  const upstreamStream = Readable.fromWeb(upstream.body);
247
+ // Text only: a real `application/gzip` / `.tar.gz` body is a gzip member
248
+ // whose bytes ARE the representation. Sniffing it would silently inflate
249
+ // a download. JS/CSS/HTML (#3037) are all text.
250
+ const decoded = isText
251
+ ? createMaybeGunzipStream({
252
+ onDecided: (inflating) => {
253
+ if (inflating)
254
+ res.removeHeader(FETCH_PROXY_CONTENT_LENGTH_HEADER);
255
+ },
256
+ })
257
+ : new PassThrough();
249
258
  const scrubChunk = createScrubStream(secretProxy, isText);
250
- upstreamStream.on('error', (err) => {
259
+ const onStreamError = (err) => {
251
260
  detachClientClose();
252
261
  if (!res.headersSent) {
253
262
  res.setHeader('X-Proxy-Error', '1');
254
- res
255
- .status(502)
256
- .json({ error: `Proxy stream failed: ${err instanceof Error ? err.message : err}` });
263
+ res.status(502).json({ error: `Proxy stream failed: ${err.message}` });
257
264
  }
258
265
  else {
259
266
  res.destroy(err);
260
267
  }
261
- });
268
+ };
269
+ upstreamStream.on('error', onStreamError);
270
+ decoded.on('error', onStreamError);
262
271
  // Belt-and-braces cleanup: 'finish' fires once the response is fully flushed;
263
272
  // 'close' fires regardless of how the response ended. Either way the abort
264
273
  // listener should be gone.
265
274
  res.on('finish', detachClientClose);
266
275
  res.on('close', detachClientClose);
267
- upstreamStream.pipe(scrubChunk).pipe(res);
276
+ upstreamStream.pipe(decoded).pipe(scrubChunk).pipe(res);
268
277
  }
269
278
  /**
270
279
  * Fetch proxy — forwards cross-origin requests from the browser to bypass