6aeb529f13
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 53s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Failing after 2m6s
Move all Telegram egress off the main host. The single connector held the bot token, long-polled Telegram and answered the gateway/backend over the trusted internal network, so the whole component (including login validation) shared fate with its VPN sidecar. Split it into two binaries that share the token: - cmd/validator (home, no VPN): Mini App initData + Login Widget HMAC only, never calls the Bot API. The gateway dials it for Telegram auth, so game login is now independent of Telegram reachability. - cmd/bot (remote): Bot API long-poll + sendMessage, the only component reaching Telegram. It holds no inbound port — it dials the gateway over a new reverse mTLS bot-link (pkg/proto/botlink/v1) and executes the send commands the gateway pushes. The gateway funnels sends to the bot-link: out-of-app push is fire-and-forget (at-most-once, dropped if no bot is connected); the backend admin broadcasts reach a gateway-served relay that forwards them and awaits the bot's ack (SendToUser/SendToGameChannel contract preserved). mTLS (pkg/mtls) is the one inter-service link that leaves the trusted segment; validator<->gateway and the relay stay plaintext internal. The bot is Telegram-rate-limited. One bot now; the gateway bot registry, an owns_updates flag and per-command ids leave seams for N later. Webhook rejected (one URL per token, adds inbound + a static address). The unified test contour runs the split (the bot keeps its VPN sidecar and dials the gateway by its internal name; bot-link certs from deploy/gen-certs.sh, generated in CI). The prod wiring — the bot on a separate host (no VPN), the gateway bot-link port published, PROD_ certs with scheduled rotation, an SSH deploy of both hosts together — is the deferred final stage (PRERELEASE.md TX, Stage 18). Docs: ARCHITECTURE, PRERELEASE (phase TX), platform/telegram + gateway + backend + deploy READMEs, FUNCTIONAL(+ru), CLAUDE.md, .env.example.
100 lines
3.0 KiB
Go
100 lines
3.0 KiB
Go
// Command validator is the home-side Telegram validator. It serves the validation
|
|
// gRPC API (pkg/proto/telegram/v1 ValidateInitData/ValidateLoginWidget) the gateway
|
|
// calls during Telegram auth. It holds the bot token only as the HMAC secret and
|
|
// never reaches the Telegram Bot API, so it runs on the main host with no VPN and
|
|
// no Telegram egress, and game login stays independent of the remote bot. See
|
|
// platform/telegram/README.md.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"net"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
|
"go.uber.org/zap"
|
|
"google.golang.org/grpc"
|
|
|
|
telegramv1 "scrabble/pkg/proto/telegram/v1"
|
|
pkgtel "scrabble/pkg/telemetry"
|
|
"scrabble/platform/telegram/internal/config"
|
|
"scrabble/platform/telegram/internal/initdata"
|
|
"scrabble/platform/telegram/internal/loginwidget"
|
|
"scrabble/platform/telegram/internal/validator"
|
|
)
|
|
|
|
// telemetryShutdownTimeout bounds the OpenTelemetry flush during process exit.
|
|
const telemetryShutdownTimeout = 5 * time.Second
|
|
|
|
func main() {
|
|
cfg, err := config.LoadValidator()
|
|
if err != nil {
|
|
log.Fatalf("validator: load config: %v", err)
|
|
}
|
|
logger, err := newLogger(cfg.LogLevel)
|
|
if err != nil {
|
|
log.Fatalf("validator: build logger: %v", err)
|
|
}
|
|
defer func() { _ = logger.Sync() }()
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
if err := run(ctx, cfg, logger); err != nil {
|
|
logger.Fatal("validator: terminated", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
// run wires the validator gRPC server and serves it until the context is cancelled.
|
|
func run(ctx context.Context, cfg config.ValidatorConfig, logger *zap.Logger) error {
|
|
tel, err := pkgtel.New(ctx, cfg.Telemetry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), telemetryShutdownTimeout)
|
|
defer cancel()
|
|
if err := tel.Shutdown(shutdownCtx); err != nil {
|
|
logger.Warn("telemetry shutdown", zap.Error(err))
|
|
}
|
|
}()
|
|
if err := tel.StartRuntimeMetrics(); err != nil {
|
|
logger.Warn("telemetry: start runtime metrics", zap.Error(err))
|
|
}
|
|
|
|
srv := validator.NewServer(initdata.NewHMACValidator(cfg.Token), loginwidget.NewHMACValidator(cfg.Token))
|
|
grpcServer := grpc.NewServer(grpc.StatsHandler(otelgrpc.NewServerHandler()))
|
|
telegramv1.RegisterTelegramServer(grpcServer, srv)
|
|
|
|
lis, err := net.Listen("tcp", cfg.GRPCAddr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
grpcServer.GracefulStop()
|
|
}()
|
|
|
|
logger.Info("telegram validator starting", zap.String("grpc_addr", cfg.GRPCAddr))
|
|
if err := grpcServer.Serve(lis); err != nil && !errors.Is(err, grpc.ErrServerStopped) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// newLogger builds a production JSON logger at the given level.
|
|
func newLogger(level string) (*zap.Logger, error) {
|
|
var lvl zap.AtomicLevel
|
|
if err := lvl.UnmarshalText([]byte(level)); err != nil {
|
|
return nil, err
|
|
}
|
|
cfg := zap.NewProductionConfig()
|
|
cfg.Level = lvl
|
|
return cfg.Build()
|
|
}
|