12e616ceae
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Successful in 1m28s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m45s
The merchant accepts payments as a sole proprietor on НПД, which is outside 54-ФЗ: there is no online cash register, YooKassa does not serve receipts for that regime at all (checked with their support), and each operation is reported by the merchant to «Мой налог», which issues the чек. So no `receipt` is sent with a payment or a refund. This also removes a failure class rather than just code: a malformed receipt was an API error at payment creation, which broke the purchase outright. The fiscal code is kept dormant rather than deleted. All of it now sits behind one switch, `BACKEND_YOOKASSA_VAT_CODE`: unset — the default — builds and sends nothing; a 54-ФЗ rate code turns «Чеки от ЮKassa» back on unchanged. The return is foreseeable, which is why the switch exists: НПД carries an annual income ceiling, and losing the regime puts 54-ФЗ back in force, at which point this is a deploy-variable edit instead of writing the integration again. The D36 email anchor still gates a direct purchase. It had two justifications — a recovery anchor and the receipt address — and only the second is gone; without an email a paying customer who loses the account loses the chips with it. What was missing is that the rule was enforced but never communicated: the wallet showed the packs to a player signed in through VK or Telegram in a browser, and tapping Buy produced a bare "something went wrong". It now says "add an email in your profile" in the buy tab instead, linking to the profile; spending already-earned chips is untouched. The predicate is a pure function so it is covered by the node-env unit tests rather than needing a browser. Tests: the receipt-off default is pinned by an integration test asserting a purchase carries no receipt, and the dormant path by one that switches a VAT code on and checks the receipt reappears with the right fiscal attributes; plus unit coverage for the enable predicate and the wallet's email rule. A note for whoever runs the numbers next: the shared (svelte + i18n) chunk is now 40 bytes under its 31 KB gzip budget. Decision D51 revised.
169 lines
12 KiB
Bash
169 lines
12 KiB
Bash
# Environment for deploy/docker-compose.yml. The CI deploy job (ci.yaml) maps the
|
|
# Gitea TEST_-prefixed secrets/variables onto these unprefixed names; the prod
|
|
# deploy maps the PROD_-prefixed set the same way. Values that are identical on every
|
|
# contour (DICT_VERSION, SMTP_RELAY_HOST/PORT/TLS/USER/PASS, GRAFANA_SMTP_PORT,
|
|
# VITE_VK_APP_LINK/ID, the two VK secrets) live as ONE unprefixed Gitea entry, and the
|
|
# deploy derives TELEGRAM_MINIAPP_URL / GRAFANA_ROOT_URL / VITE_VK_ID_REDIRECT_URL from
|
|
# PUBLIC_BASE_URL. Copy to deploy/.env for a local run (set the derived ones directly).
|
|
#
|
|
# Full reference (required vs optional, defaults, secret-vs-variable): deploy/README.md.
|
|
|
|
# --- Postgres ---------------------------------------------------------------
|
|
POSTGRES_DB=scrabble
|
|
POSTGRES_USER=scrabble
|
|
POSTGRES_PASSWORD=change-me # required
|
|
|
|
# --- Point-in-time recovery (pgBackRest -> S3; PROD main host only) ----------
|
|
# Continuous WAL archiving for PITR. PROD-ONLY: the test contour never archives (these
|
|
# stay unset there and archive_mode stays off). The artifact ships DISARMED —
|
|
# PGBACKREST_ARCHIVE_MODE defaults off; arm it only after setting up the S3 repository +
|
|
# secrets and creating the stanza, then flip it on (deploy/README.md, point-in-time
|
|
# recovery). Gitea: PROD_PGBACKREST_* — endpoint/bucket/region/archive-mode are variables,
|
|
# the two S3 keys + the cipher passphrase are secrets.
|
|
PGBACKREST_ARCHIVE_MODE=off # on = archiving active (arm only after the stanza exists)
|
|
PGBACKREST_S3_ENDPOINT= # S3 HOST ONLY — no https://, no port, no bucket (e.g. s3.ru-1.storage.selcloud.ru); path-style addressing
|
|
PGBACKREST_S3_PORT= # optional; default 443 — set only if the provider uses a non-standard S3 port
|
|
PGBACKREST_S3_BUCKET= # bucket name; lowercase, no dots/underscores
|
|
PGBACKREST_S3_REGION= # e.g. ru-1
|
|
PGBACKREST_S3_KEY= # secret: S3 access key id
|
|
PGBACKREST_S3_KEY_SECRET= # secret: S3 secret access key
|
|
PGBACKREST_CIPHER_PASS= # secret: repo encryption passphrase — KEEP SAFE, stored apart from the S3 keys (losing it makes the archive unrecoverable)
|
|
|
|
# --- Dictionary -------------------------------------------------------------
|
|
# scrabble-dictionary release tag baked into the image as the SEED dictionary for a
|
|
# FRESH volume (image build-arg; also labels the resident seed version). After first
|
|
# boot the dawg-data volume preserves versions uploaded through the admin console and
|
|
# the active version lives in the DB. On a live volume a changed value is ignored (the
|
|
# recorded .seed_version marker wins — the seed-drift guard); change a running
|
|
# contour's dictionary through /_gm/dictionary (ARCHITECTURE.md §5). One shared Gitea
|
|
# variable (DICT_VERSION) seeds both contours + pins the CI test suite.
|
|
DICT_VERSION=v1.3.1
|
|
|
|
# --- Logging ----------------------------------------------------------------
|
|
LOG_LEVEL=info
|
|
|
|
# --- Finished-game export ----------------------------------------------------
|
|
# HMAC key signing the public export download URLs (/dl/*). Required; generate a
|
|
# real contour value with `openssl rand -base64 32` (Gitea TEST_/PROD_EXPORT_SIGN_KEY).
|
|
EXPORT_SIGN_KEY=dev-export-sign-key
|
|
|
|
# --- Transactional email (Selectel relay) -----------------------------------
|
|
# Confirm-code email via the shared Selectel relay (one relay for every contour;
|
|
# limit 100 msgs / 5 min). Empty SMTP_RELAY_HOST makes the backend log codes instead
|
|
# of sending (dev). Port 465 = implicit TLS, else STARTTLS; no client cert is needed.
|
|
# TLS is implicit on port 465 and STARTTLS otherwise, unless SMTP_RELAY_TLS forces it
|
|
# (ssl|starttls) — required for Selectel's non-standard ports (1127 = SSL, 1126 =
|
|
# STARTTLS). FROM must use the prod domain (Selectel only accepts the verified sender
|
|
# domain), so it is the same on every contour; a display name is fine
|
|
# (`"Игра Эрудит" <no-reply@erudit-game.ru>`). PUBLIC_BASE_URL is the canonical https
|
|
# origin for links in the email — the contour's own public URL. Gitea
|
|
# TEST_/PROD_SMTP_RELAY_* + TEST_/PROD_PUBLIC_BASE_URL.
|
|
SMTP_RELAY_HOST=
|
|
SMTP_RELAY_PORT=465
|
|
SMTP_RELAY_TLS= # empty = auto by port; ssl | starttls to force
|
|
SMTP_RELAY_USER= # secret
|
|
SMTP_RELAY_PASS= # secret
|
|
SMTP_RELAY_FROM=no-reply@erudit-game.ru
|
|
PUBLIC_BASE_URL= # required when SMTP_RELAY_HOST is set (e.g. https://erudit-game.ru)
|
|
|
|
# Operator alerts (email). The backend emails the admin on new feedback / word complaints
|
|
# (coalesced), and Grafana emails infra alerts. Distinct senders; recipients may be several
|
|
# comma-separated addresses. Grafana reuses SMTP_RELAY_HOST/USER/PASS but dials the STARTTLS
|
|
# port (it can't do the backend's implicit TLS), GRAFANA_SMTP_PORT. All empty = off.
|
|
SMTP_RELAY_ADMIN_FROM= # backend admin-alert From (e.g. alerts@erudit-game.ru)
|
|
ADMIN_EMAIL= # backend admin-alert recipient(s), comma-separated
|
|
SMTP_RELAY_SERVICE_FROM= # Grafana alert From; the deploy derives a bare address for Grafana (it rejects "Name" <addr>)
|
|
SERVICE_EMAIL= # Grafana alert recipient(s), comma-separated
|
|
GRAFANA_SMTP_PORT= # Grafana STARTTLS port on SMTP_RELAY_HOST (Selectel: 1126)
|
|
GF_SMTP_ENABLED=false # set true to enable Grafana alert emails
|
|
|
|
# --- Edge / caddy -----------------------------------------------------------
|
|
# Test: ":80" (the host caddy terminates TLS and forwards to scrabble:80 on the
|
|
# external `edge` network). Prod: a domain so caddy does its own ACME.
|
|
CADDY_SITE_ADDRESS=:80
|
|
GM_BASICAUTH_USER=gm
|
|
GM_BASICAUTH_HASH= # required; `caddy hash-password` bcrypt hash
|
|
|
|
# --- UI build args (baked into the gateway image) ---------------------------
|
|
VITE_TELEGRAM_BOT_ID=
|
|
VITE_TELEGRAM_LINK= # friend-invite Mini App link (full URL, e.g. https://telegram.me/<bot>/<app>)
|
|
VITE_TELEGRAM_GAME_CHANNEL_NAME= # landing "Play in Telegram" link, the bot's game channel
|
|
VITE_VK_APP_LINK= # landing "Play on VK" link (full URL, https://vk.com/app<id>)
|
|
VITE_VK_APP_ID= # VK ID "Web" app id (client_id) for VK web-login linking; also the gateway's GATEWAY_VK_ID_APP_ID
|
|
VITE_VK_ID_REDIRECT_URL= # VK ID trusted redirect URL (e.g. https://erudit-game.ru/app/); also the gateway's exchange redirect_uri. Deploy derives it as PUBLIC_BASE_URL + /app/
|
|
VITE_GATEWAY_URL=
|
|
|
|
# --- Grafana ----------------------------------------------------------------
|
|
GRAFANA_ROOT_URL=/_gm/grafana/ # deploy derives PUBLIC_BASE_URL + /_gm/grafana/; set the full https URL for a local run
|
|
GRAFANA_ADMIN_PASSWORD=admin
|
|
|
|
# --- Telegram validator + bot -----------------------------------------------
|
|
# The token is shared: the validator uses it as the HMAC secret, the bot for the
|
|
# Bot API. The bot-link wiring (validator/relay/mTLS addresses) is hard-wired in
|
|
# docker-compose.yml; the mTLS material is NOT here — run `deploy/gen-certs.sh`
|
|
# (writes deploy/certs/, gitignored) before `docker compose up`.
|
|
AWG_CONF= # required; AmneziaWG sidecar config (the bot's Telegram egress)
|
|
TELEGRAM_BOT_TOKEN= # required
|
|
TELEGRAM_GAME_CHANNEL_ID=
|
|
TELEGRAM_CHAT_ID= # moderated discussion chat (channel's linked group); empty disables gating
|
|
TELEGRAM_SUPPORT_CHAT_ID= # private forum supergroup for the support relay (topic per user); empty disables it
|
|
TELEGRAM_PROMO_BOT_TOKEN= # optional standalone promo bot token; empty disables it
|
|
TELEGRAM_BOT_USERNAME= # main bot @username without the @ (promo message); required when the promo token is set
|
|
TELEGRAM_BOT_LINK= # main bot Mini App link for the promo button (reuse VITE_TELEGRAM_LINK); required when the promo token is set
|
|
TELEGRAM_PROMO_START_PARAM= # promo button startapp payload — a variant-seed deep link (default verudit_ru-scrabble_en) adding English Scrabble for new users; empty forwards the user's /start payload
|
|
TELEGRAM_MINIAPP_URL= # required; deploy derives it as PUBLIC_BASE_URL + /telegram/
|
|
TELEGRAM_TEST_ENV=false
|
|
TELEGRAM_API_BASE_URL=
|
|
|
|
# --- Client-version gate (ARCHITECTURE.md §2) -------------------------------
|
|
# The hard minimum + the soft recommended client build. Empty ⇒ dormant. Plain (unprefixed) Gitea
|
|
# variables, shared across contours; in the test contour the stamped client version is a commit hash
|
|
# (unparseable ⇒ fail-open), so these only enforce on real semver prod builds. Recommended must be ≥ min.
|
|
GATEWAY_MIN_CLIENT_VERSION=
|
|
GATEWAY_RECOMMENDED_CLIENT_VERSION=
|
|
|
|
# --- VK Mini App ------------------------------------------------------------
|
|
# The VK app's "protected key" (client_secret): the gateway verifies the Mini App
|
|
# launch-parameter signature in-process under it (a pure offline HMAC, no VK API call).
|
|
# Empty disables the VK auth path (auth.vk). Set from the Gitea TEST_/PROD_ secret.
|
|
GATEWAY_VK_APP_SECRET=
|
|
# VK ID web login (browser VK-identity linking) — a SEPARATE VK "Web" app: the gateway runs
|
|
# the confidential OAuth 2.1 code exchange under this protected key. The app id + redirect URL
|
|
# come from VITE_VK_APP_ID / VITE_VK_ID_REDIRECT_URL above. All three empty disables link.vk.*.
|
|
GATEWAY_VK_ID_CLIENT_SECRET=
|
|
|
|
# --- Payments: YooKassa (direct RUB rail) ------------------------------------
|
|
# One merchant shop per channel (D42): the shop id (Настройки — Магазин, field shopId) and the
|
|
# secret key (Интеграция — Ключи API). Each shop credits the one direct wallet; a shop missing
|
|
# either credential drops that channel (order routing falls back to web), and no shop at all
|
|
# leaves the direct rail off. YOOKASSA_*_TEST=1 marks a TEST shop, which makes the intake refuse
|
|
# to credit a live payment against it (and a test payment against a live shop).
|
|
# Mapped in compose to BACKEND_YOOKASSA_*; Gitea TEST_/PROD_ secrets.
|
|
# Set each shop's notification URL in its cabinet (Интеграция — HTTP-уведомления) to
|
|
# ${PUBLIC_BASE_URL}/pay/yookassa/notify, events payment.succeeded + payment.canceled +
|
|
# refund.succeeded (the last one catches a refund issued in the cabinet, which bypasses our API).
|
|
YOOKASSA_WEB_SHOP_ID=
|
|
YOOKASSA_WEB_SECRET_KEY=
|
|
YOOKASSA_WEB_TEST=
|
|
# The android (RuStore) shop — add when that channel gets its own merchant account.
|
|
YOOKASSA_ANDROID_SHOP_ID=
|
|
YOOKASSA_ANDROID_SECRET_KEY=
|
|
YOOKASSA_ANDROID_TEST=
|
|
# Fiscal receipts (54-ФЗ) — OFF while empty, which is the intended state: the merchant runs on НПД,
|
|
# outside 54-ФЗ, so nothing is registered through the provider and each operation is reported to
|
|
# «Мой налог» instead. Set a VAT rate code (tag 1199: 1 = «Без НДС», 4 = 20%, 11 = 22%) to turn
|
|
# receipts back on — the switch a lost НПД regime would need. A Gitea VARIABLE, not a secret.
|
|
YOOKASSA_VAT_CODE=
|
|
#
|
|
# Robokassa (the retired direct rail) is deliberately absent here: no deployment sets its
|
|
# credentials, which is what keeps it dormant. The backend still reads BACKEND_ROBOKASSA_* and
|
|
# falls the direct rail back to it if they are ever set again — see
|
|
# backend/internal/robokassa/README.md for the full variable set and how to revive the rail.
|
|
|
|
# --- Gateway anti-abuse ------------------------------------------------------
|
|
# Planted honeytoken bearer value: any request presenting it is flagged — a 24h IP ban
|
|
# where the IP ban is on (prod), logs + a ban metric otherwise (test). Plant the value
|
|
# somewhere an attacker would find it; empty disables the trap. Gitea
|
|
# TEST_/PROD_GATEWAY_HONEYTOKEN (secret).
|
|
GATEWAY_HONEYTOKEN=
|