feat(export): server-rendered artifacts behind one signed download URL #160

Merged
developer merged 7 commits from feature/export-server-render into development 2026-07-02 21:58:07 +00:00
Owner

Identical export on every platform (the owner's uniformity requirement): the client mints a signed relative URL (game.export_url — new op; date locale, IANA time zone and localized labels ride it), resolves it against its own origin and hands it to the platform-native download: Telegram downloadFile, VKWebAppDownloadFile, else a plain anchor (incl. the desktop VK iframe). Both artifacts use the route — the .gcg (clipboard mode survives only as the legacy pre-8.0-Telegram fallback) and the PNG.

Renderer sidecar (renderer/): node:22-slim + skia-canvas executing the SAME ui/src/lib/gameimage.ts the ui unit-tests, bundled at image build (esbuild); Liberation + Noto Color Emoji baked in; internal-only, healthchecked; container smoke render verified byte-identical to the approved samples. The browser no longer renders or delivers bytes.

Security: HMAC-SHA256 signed paths (BACKEND_EXPORT_SIGN_KEY, 10-min TTL), constant-time verify, uniform 404s, param length caps, per-IP public rate limit at the gateway, /dl/* added to the caddy @gateway matcher (the landing catch-all trap). Mint requires an authenticated caller + finished game. Nothing stored — artifacts re-derive from the immutable journal.

Deploy: renderer in compose + prod overlay + roll renderer before backend + prod push list; TEST_/PROD_EXPORT_SIGN_KEY secrets already set via API. CI: sidecar smoke in the ui job; renderer/ rides the ui changes-lane.

Tests: Go (sign/tamper/expiry/payload, transcode round-trip), renderer node smoke, ui vitest (codec round-trip incl. tz) + 8 e2e (download flows web/TG/legacy-TG, route-intercepted). Full suite green locally; compose (base+prod) validates.

After merge: on-device check on the contour — TG iOS/Android native download dialog, VK Android VKWebAppDownloadFile, VK desktop anchor, web download.

Identical export on every platform (the owner's uniformity requirement): the client mints a signed relative URL (`game.export_url` — new op; date locale, IANA time zone and localized labels ride it), resolves it against its own origin and hands it to the platform-native download: Telegram `downloadFile`, `VKWebAppDownloadFile`, else a plain anchor (incl. the desktop VK iframe). Both artifacts use the route — the `.gcg` (clipboard mode survives only as the legacy pre-8.0-Telegram fallback) and the PNG. **Renderer sidecar** (`renderer/`): node:22-slim + skia-canvas executing the SAME `ui/src/lib/gameimage.ts` the ui unit-tests, bundled at image build (esbuild); Liberation + Noto Color Emoji baked in; internal-only, healthchecked; container smoke render verified byte-identical to the approved samples. The browser no longer renders or delivers bytes. **Security:** HMAC-SHA256 signed paths (`BACKEND_EXPORT_SIGN_KEY`, 10-min TTL), constant-time verify, uniform 404s, param length caps, per-IP public rate limit at the gateway, `/dl/*` added to the caddy `@gateway` matcher (the landing catch-all trap). Mint requires an authenticated caller + finished game. Nothing stored — artifacts re-derive from the immutable journal. **Deploy:** renderer in compose + prod overlay + `roll renderer` before backend + prod push list; `TEST_/PROD_EXPORT_SIGN_KEY` secrets already set via API. CI: sidecar smoke in the ui job; `renderer/` rides the ui changes-lane. Tests: Go (sign/tamper/expiry/payload, transcode round-trip), renderer node smoke, ui vitest (codec round-trip incl. tz) + 8 e2e (download flows web/TG/legacy-TG, route-intercepted). Full suite green locally; compose (base+prod) validates. After merge: on-device check on the contour — TG iOS/Android native download dialog, VK Android `VKWebAppDownloadFile`, VK desktop anchor, web download.
developer added 1 commit 2026-07-02 16:44:27 +00:00
feat(export): server-rendered artifacts behind one signed download URL
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Successful in 1m4s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m27s
3471d40576
The finished-game export now works identically on every platform: the
client mints a signed relative URL (game.export_url) carrying its date
locale, IANA time zone and localized non-play labels, resolves it
against its own origin and hands it to the platform's native download —
Telegram downloadFile, VKWebAppDownloadFile, or a plain browser anchor
(also the desktop VK iframe). Both artifacts ride the route: the .gcg
text (no more clipboard mode, except the legacy pre-8.0 Telegram
fallback) and the PNG of the final position.

The PNG is rasterized by a new internal 'renderer' sidecar (node:22-slim
+ skia-canvas + baked Liberation/Noto Color Emoji fonts) executing the
SAME ui/src/lib/gameimage.ts the web project unit-tests, bundled at
image build time — one renderer, no drift; the browser no longer draws
or delivers bytes itself. The backend rebuilds the render payload from
the journal + engine.AlphabetTable, verifies the HMAC (10-minute TTL,
BACKEND_EXPORT_SIGN_KEY, constant-time, uniform 404s) on its public
group, and streams the artifact as a named attachment; the gateway
forwards /dl/* behind the per-IP public rate limiter (caddy @gateway
matcher extended — the landing catch-all trap).

Deploy: renderer service (compose + prod overlay + roll before backend
+ prod push list), EXPORT_SIGN_KEY env (TEST_/PROD_ secrets), CI runs
the sidecar smoke in the ui job. Docs: ARCHITECTURE export-delivery
section, FUNCTIONAL(+_ru), UI_DESIGN, TESTING, deploy/README.
developer added 1 commit 2026-07-02 17:02:06 +00:00
fix(export): explicit Content-Length on the download responses
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 1m2s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m54s
c3c27bba5e
Both hops (backend attachment + gateway /dl forward) wrote the body
through net/http's buffered writer, so anything over the write buffer
went out Transfer-Encoding: chunked. Android's system DownloadManager —
the executor behind VKWebAppDownloadFile — hangs indefinitely on a
download of unknown length (observed on-device: VK Android stuck in
'downloading'); Telegram's downloader tolerates chunked, which is why
only VK broke. The artifact is fully buffered anyway, so the length is
known — set it on both responses.
developer added 1 commit 2026-07-02 20:19:47 +00:00
feat(export): platform-native delivery — TG popup+downloadFile, VK viewer, mobile share sheet
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Failing after 1m7s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
c3c26bbbc0
Per the on-device review, the last hop is now the most native affordance
per platform, all fed by the same signed URL:

- Telegram: the native showPopup chooser returns (safe here — the whole
  TG chain is bridge calls, which need no user activation) and both
  formats go to the native downloadFile dialog on iOS and Android alike.
- VK: the PNG opens in VK's native photo viewer (VKWebAppShowImages) —
  on screen at once, saved/shared from the viewer's own controls; the
  GCG keeps VKWebAppDownloadFile; either falls back to a plain anchor
  download (the desktop iframe). The gateway now serves /dl/* via
  http.ServeContent (Content-Length + Range/206) — another swing at the
  VK Android DownloadManager hang; if it persists, the next step is the
  clipboard fallback for the VK-Android GCG.
- Mobile browsers: the OS share sheet with the fetched file (the proven
  fetch-then-share pattern) — nothing lands in Downloads first; desktop
  keeps the anchor download.

Legacy Telegram (< 8.0, no downloadFile) keeps the app modal + GCG
clipboard copy and hides the image option.
developer added 1 commit 2026-07-02 20:30:56 +00:00
fix(e2e): Buffer body for the /dl route fulfill
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Successful in 1m2s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m42s
a9376c20a2
route.fulfill silently never resolves a FETCH interception given a
Uint8Array body (a navigation download tolerates it), deadlocking the
new share-sheet test — the fetch promise hung, no share, no toast. A
node Buffer unblocks it. Also refreshes the spec header to the final
per-platform delivery matrix.
developer added 1 commit 2026-07-02 21:05:36 +00:00
fix(export): per-platform polish from the on-device review
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 1m4s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
8db18f2a2a
- TG iOS: the OS share sheet with the fetched file (WKWebView has
  navigator.share — the долгоживущий debug-overlay Share path proves
  it). The chooser stays the app modal on this one platform: a native
  popup callback cannot supply the user activation the sheet needs.
  TG Android/desktop keep the native popup + downloadFile (verified
  perfect, untouched).
- VK mobile: the PNG now rides VKWebAppDownloadFile like the GCG — the
  owner-verified path that lands in VK's native share flow; the
  ShowImages viewer is dropped (its preview offered save only, no
  share).
- VK desktop iframe: both formats are plain anchor downloads (it is an
  ordinary browser; the viewer/bridge path made no sense there).
developer added 1 commit 2026-07-02 21:32:54 +00:00
fix(export): VK Android viewer+clipboard; no share title on iOS
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Successful in 1m3s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m56s
77f222c209
Round-5 on-device findings:

- VK Android's DownloadFile hangs regardless of Content-Length and
  Range/206 (PNG stalls ~80%, GCG at 0%) — the download route is
  abandoned there: the PNG opens in VK's native image viewer
  (VKWebAppShowImages — its save works on-device) and the GCG returns
  to the clipboard copy (the pre-URL route, always solid). VK iOS keeps
  VKWebAppDownloadFile (verified perfect); the desktop iframe keeps
  plain anchor downloads.
- The iOS share sheet no longer carries a title: iOS pasted the
  'game-<hash>.png' filename as accompanying text next to the image.
owner approved these changes 2026-07-02 21:43:25 +00:00
Dismissed
developer added 1 commit 2026-07-02 21:47:15 +00:00
docs: settle the export docs on the final delivery matrix
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m3s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m52s
fca8fd038b
ARCHITECTURE: the time zone rides the URL too; 'only degraded platform'
(legacy TG) instead of 'only remaining branch' — the delivery is
per-platform by design now. CLAUDE.md: the renderer joins the build/test
commands and the Dockerfile list.
developer dismissed owner's review 2026-07-02 21:47:15 +00:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

owner approved these changes 2026-07-02 21:57:02 +00:00
developer merged commit d5fbaa3034 into development 2026-07-02 21:58:07 +00:00
developer deleted branch feature/export-server-render 2026-07-02 21:58:08 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: developer/scrabble-game#160