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
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
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.
This commit is contained in:
+20
-1
@@ -67,6 +67,7 @@ interface TelegramWebApp {
|
||||
};
|
||||
showConfirm?: (message: string, cb?: (ok: boolean) => void) => void;
|
||||
showPopup?: (params: TelegramPopupParams, cb?: (buttonId: string) => void) => void;
|
||||
downloadFile?: (params: { url: string; file_name: string }, cb?: (accepted: boolean) => void) => void;
|
||||
}
|
||||
|
||||
function webApp(): TelegramWebApp | undefined {
|
||||
@@ -372,7 +373,8 @@ export function telegramShowConfirm(message: string): Promise<boolean> {
|
||||
/**
|
||||
* telegramShowPopup shows Telegram's native popup and resolves the pressed button id (the empty
|
||||
* string when dismissed without pressing a button). Resolves null outside Telegram or on a client
|
||||
* predating the popup, so callers can fall back to their own modal.
|
||||
* predating the popup, so callers can fall back to their own modal. NOTE: the callback runs with
|
||||
* no user activation — never lead from it into navigator.share or a clipboard write.
|
||||
*/
|
||||
export function telegramShowPopup(params: TelegramPopupParams): Promise<string | null> {
|
||||
const w = webApp();
|
||||
@@ -380,6 +382,23 @@ export function telegramShowPopup(params: TelegramPopupParams): Promise<string |
|
||||
return new Promise((resolve) => w.showPopup!(params, (id) => resolve(id ?? '')));
|
||||
}
|
||||
|
||||
/** telegramCanDownloadFile reports whether the native download dialog exists (Bot API 8.0). */
|
||||
export function telegramCanDownloadFile(): boolean {
|
||||
return !!webApp()?.downloadFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* telegramDownloadFile asks Telegram to download url as fileName through its native
|
||||
* dialog — the Mini App file delivery that works on every Telegram platform (a webview
|
||||
* <a download> does not). Returns false outside Telegram or on a client predating it.
|
||||
*/
|
||||
export function telegramDownloadFile(url: string, fileName: string): boolean {
|
||||
const w = webApp();
|
||||
if (!w?.downloadFile) return false;
|
||||
w.downloadFile({ url, file_name: fileName });
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Haptic is the set of feedbacks the app triggers. */
|
||||
export type Haptic = 'select' | 'success' | 'error' | 'warning' | 'light' | 'medium' | 'heavy';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user