DeepLX/translate
Vincent Young 10f33401e7
fix(translate): align oneshot request bytes with the real extension
After capturing the exact bytes the Chrome extension's service-worker
fetch() emits (via an offline echo server pointed at deeplx in place of
oneshot-free.www.deepl.com) and diffing them against what we were
sending, several distinguishable signals remained. Close them all.

Headers
-------
- Origin: chrome-extension://cofdbpoegempjloogbagkncekinflcnj
  (was https://www.deepl.com — a request from www.deepl.com itself
   never lands on the oneshot endpoint, so that origin is unusual.
   The extension ID is the canonical sender.)
- Sec-Fetch-Site: cross-site
  (was same-site — wrong; chrome-extension -> www.deepl.com IS cross-site)
- Drop Referer entirely (extension SW fetch sends none)
- Drop Pragma / Cache-Control / Upgrade-Insecure-Requests / Sec-Fetch-User
  (req.ImpersonateChrome() sets these for top-level navigation; a
   fetch() never sends them — leaving them in is a strong nav-vs-XHR tell)
- Accept-Encoding: gzip, deflate, br
  (was just gzip, Go stdlib default — Chrome 120's fetch() sends all
   three; zstd only landed as a default in Chrome 123+ so leave it off)

Body
----
- Add usage_type: "Translate" and the full app_information object
  (os/os_version/app_version/app_build/instance_id) so the JSON the
  server sees is structurally identical to what background.js IN()
  assembles. Field order in oneshotRequest matches the extension's
  object-literal order so encoding/json produces byte-identical output.
- instance_id is a v4 UUID generated once at process start and reused,
  mirroring the extension's chrome.storage-pinned ID rather than
  rotating per-request (rotation would be a far stronger signal).
- All version strings (TLS handshake, User-Agent, sec-ch-ua,
  app_information.os_version) are pinned to Chrome 120 so they tell
  one consistent story.

Transport
---------
- SetBodyBytes instead of bytes.NewReader so Content-Length is set
  (an io.Reader body forces Transfer-Encoding: chunked, which a
   fetch() with JSON.stringify body never emits)
- Once we set Accept-Encoding manually, the Go stdlib disables its
  transparent decompression and req hands us raw compressed bytes.
  Handle gzip / deflate / br by hand from Content-Encoding.
- DisableAutoReadResponse so we own the body stream end-to-end.

The Chrome 120 TLS ClientHello, HTTP/2 SETTINGS frame, pseudo-header
order and sec-ch-ua claim continue to come from ImpersonateChrome()
unchanged.

Verified end-to-end:
- Outbound bytes (against a local echo server) diff-match the
  extension's observed profile on every header and on body JSON order.
- Live oneshot-free.www.deepl.com calls: 4 language pairs OK,
  /v2/translate official-API compat OK, 10x burst 10/10 200.
2026-05-22 11:53:14 +08:00
..
translate.go fix(translate): align oneshot request bytes with the real extension 2026-05-22 11:53:14 +08:00
types.go feat(translate): migrate to oneshot endpoint to bypass www2 anti-bot 2026-05-22 11:34:55 +08:00