ui: plan 01-27 done #1
@@ -1,3 +1,10 @@
|
||||
.codex
|
||||
.vscode/
|
||||
artifacts/.claude/scheduled_tasks.lock
|
||||
|
||||
# Per-developer Vite dotenv overrides. The committed
|
||||
# `ui/frontend/.env.development` ships sane defaults for the
|
||||
# `tools/local-dev/` stack; `.local` siblings stay personal and
|
||||
# unstaged.
|
||||
**/.env.local
|
||||
**/.env.*.local
|
||||
|
||||
@@ -76,9 +76,30 @@ func NewService(deps Deps) *Service {
|
||||
// not a security primitive, so a constant key is acceptable.
|
||||
copy(key, []byte("galaxy-backend-auth-fallback-key"))
|
||||
}
|
||||
if deps.Config.DevFixedCode != "" {
|
||||
// Loud, repeated warning so a stray production deployment cannot
|
||||
// claim the operator was unaware. The override is intended for
|
||||
// `tools/local-dev/` and never reaches production binaries in
|
||||
// normal operation.
|
||||
deps.Logger.Warn("DEV-MODE: BACKEND_AUTH_DEV_FIXED_CODE is set; ConfirmEmailCode accepts the literal code in addition to the bcrypt-verified one. NEVER use in production.")
|
||||
}
|
||||
return &Service{deps: deps, emailHashKey: key}
|
||||
}
|
||||
|
||||
// devFixedCodeMatches reports whether the dev-mode fixed-code override
|
||||
// is configured and the submitted code matches it verbatim. The
|
||||
// override is opt-in via `BACKEND_AUTH_DEV_FIXED_CODE`; production
|
||||
// deployments leave the field empty and devFixedCodeMatches always
|
||||
// returns false. See `tools/local-dev/README.md` for the full
|
||||
// rationale.
|
||||
func (s *Service) devFixedCodeMatches(code string) bool {
|
||||
fixed := s.deps.Config.DevFixedCode
|
||||
if fixed == "" {
|
||||
return false
|
||||
}
|
||||
return code == fixed
|
||||
}
|
||||
|
||||
// hashEmail returns a stable, hex-encoded HMAC-SHA256 prefix of email
|
||||
// suitable for use in structured logs. The key is per-process so the
|
||||
// same email maps to the same hash across log lines emitted by this
|
||||
|
||||
@@ -185,6 +185,35 @@ func authConfig() config.AuthConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// buildServiceWithConfig wires every dependency around db using cfg as
|
||||
// the auth configuration. Returns only the service — assertions on the
|
||||
// dev-mode override path do not inspect the recording fakes.
|
||||
func buildServiceWithConfig(t *testing.T, db *sql.DB, cfg config.AuthConfig) *auth.Service {
|
||||
t.Helper()
|
||||
store := auth.NewStore(db)
|
||||
cache := auth.NewCache()
|
||||
if err := cache.Warm(context.Background(), store); err != nil {
|
||||
t.Fatalf("warm cache: %v", err)
|
||||
}
|
||||
userStore := user.NewStore(db)
|
||||
userSvc := user.NewService(user.Deps{
|
||||
Store: userStore,
|
||||
Cache: user.NewCache(),
|
||||
UserNameMaxRetries: 10,
|
||||
Now: time.Now,
|
||||
})
|
||||
return auth.NewService(auth.Deps{
|
||||
Store: store,
|
||||
Cache: cache,
|
||||
User: userSvc,
|
||||
Geo: newStubGeo(),
|
||||
Mail: newRecordingMailer(),
|
||||
Push: newRecordingPush(),
|
||||
Config: cfg,
|
||||
Now: time.Now,
|
||||
})
|
||||
}
|
||||
|
||||
// buildService wires every dependency around db and returns the service
|
||||
// plus the recording fakes for assertions.
|
||||
func buildService(t *testing.T, db *sql.DB) (*auth.Service, *recordingMailer, *recordingPush, *stubGeo) {
|
||||
@@ -412,6 +441,55 @@ func TestSendEmailCodeThrottleReusesChallenge(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfirmEmailCodeDevFixedCodeBypass(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
cfg := authConfig()
|
||||
cfg.DevFixedCode = "999999"
|
||||
svc := buildServiceWithConfig(t, db, cfg)
|
||||
ctx := context.Background()
|
||||
|
||||
id, err := svc.SendEmailCode(ctx, "dev-bypass@example.test", "en", "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("send: %v", err)
|
||||
}
|
||||
|
||||
session, err := svc.ConfirmEmailCode(ctx, auth.ConfirmInputs{
|
||||
ChallengeID: id,
|
||||
Code: "999999",
|
||||
ClientPublicKey: randomKey(t),
|
||||
TimeZone: "UTC",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ConfirmEmailCode with dev fixed code: %v", err)
|
||||
}
|
||||
if session.DeviceSessionID == uuid.Nil {
|
||||
t.Fatalf("dev fixed code did not produce a session")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfirmEmailCodeDevFixedCodeStillRejectsWrong(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
cfg := authConfig()
|
||||
cfg.DevFixedCode = "999999"
|
||||
svc := buildServiceWithConfig(t, db, cfg)
|
||||
ctx := context.Background()
|
||||
|
||||
id, err := svc.SendEmailCode(ctx, "dev-bypass-wrong@example.test", "en", "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("send: %v", err)
|
||||
}
|
||||
|
||||
_, err = svc.ConfirmEmailCode(ctx, auth.ConfirmInputs{
|
||||
ChallengeID: id,
|
||||
Code: "111111",
|
||||
ClientPublicKey: randomKey(t),
|
||||
TimeZone: "UTC",
|
||||
})
|
||||
if !errors.Is(err, auth.ErrCodeMismatch) {
|
||||
t.Fatalf("ConfirmEmailCode with neither real nor dev code = %v, want ErrCodeMismatch", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfirmEmailCodeWrongCode(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
svc, mailer, _, _ := buildService(t, db)
|
||||
|
||||
@@ -171,15 +171,21 @@ func (s *Service) ConfirmEmailCode(ctx context.Context, in ConfirmInputs) (Sessi
|
||||
return Session{}, ErrTooManyAttempts
|
||||
}
|
||||
|
||||
if err := verifyCode(loaded.CodeHash, in.Code); err != nil {
|
||||
if errors.Is(err, ErrCodeMismatch) {
|
||||
s.deps.Logger.Info("auth challenge code mismatch",
|
||||
zap.String("challenge_id", in.ChallengeID.String()),
|
||||
zap.Int32("attempts", loaded.Attempts),
|
||||
)
|
||||
return Session{}, ErrCodeMismatch
|
||||
if !s.devFixedCodeMatches(in.Code) {
|
||||
if err := verifyCode(loaded.CodeHash, in.Code); err != nil {
|
||||
if errors.Is(err, ErrCodeMismatch) {
|
||||
s.deps.Logger.Info("auth challenge code mismatch",
|
||||
zap.String("challenge_id", in.ChallengeID.String()),
|
||||
zap.Int32("attempts", loaded.Attempts),
|
||||
)
|
||||
return Session{}, ErrCodeMismatch
|
||||
}
|
||||
return Session{}, err
|
||||
}
|
||||
return Session{}, err
|
||||
} else {
|
||||
s.deps.Logger.Warn("auth challenge accepted via dev-mode fixed code override",
|
||||
zap.String("challenge_id", in.ChallengeID.String()),
|
||||
)
|
||||
}
|
||||
|
||||
// Re-check permanent_block after verifying the code. SendEmailCode
|
||||
|
||||
@@ -71,6 +71,7 @@ const (
|
||||
envAuthChallengeThrottleWindow = "BACKEND_AUTH_CHALLENGE_THROTTLE_WINDOW"
|
||||
envAuthChallengeThrottleMax = "BACKEND_AUTH_CHALLENGE_THROTTLE_MAX"
|
||||
envAuthUserNameMaxRetries = "BACKEND_AUTH_USERNAME_MAX_RETRIES"
|
||||
envAuthDevFixedCode = "BACKEND_AUTH_DEV_FIXED_CODE"
|
||||
|
||||
envLobbySweeperInterval = "BACKEND_LOBBY_SWEEPER_INTERVAL"
|
||||
envLobbyPendingRegistrationTTL = "BACKEND_LOBBY_PENDING_REGISTRATION_TTL"
|
||||
@@ -293,6 +294,16 @@ type AuthConfig struct {
|
||||
ChallengeMaxAttempts int
|
||||
ChallengeThrottle AuthChallengeThrottleConfig
|
||||
UserNameMaxRetries int
|
||||
|
||||
// DevFixedCode, when non-empty, makes ConfirmEmailCode accept this
|
||||
// literal as a valid code in addition to the bcrypt-verified one
|
||||
// stored on the challenge row. The override is intended for the
|
||||
// `tools/local-dev` stack so a developer can log in without
|
||||
// reading codes out of Mailpit. The variable MUST stay unset in
|
||||
// production: validation requires a six-digit decimal value, and
|
||||
// the auth service emits a loud startup warning when it picks the
|
||||
// override up.
|
||||
DevFixedCode string
|
||||
}
|
||||
|
||||
// AuthChallengeThrottleConfig bounds how many un-consumed, non-expired
|
||||
@@ -566,6 +577,7 @@ func LoadFromEnv() (Config, error) {
|
||||
if cfg.Auth.UserNameMaxRetries, err = loadInt(envAuthUserNameMaxRetries, cfg.Auth.UserNameMaxRetries); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
cfg.Auth.DevFixedCode = loadString(envAuthDevFixedCode, cfg.Auth.DevFixedCode)
|
||||
|
||||
if cfg.Lobby.SweeperInterval, err = loadDuration(envLobbySweeperInterval, cfg.Lobby.SweeperInterval); err != nil {
|
||||
return Config{}, err
|
||||
@@ -745,6 +757,11 @@ func (c Config) Validate() error {
|
||||
if c.Auth.UserNameMaxRetries <= 0 {
|
||||
return fmt.Errorf("%s must be positive", envAuthUserNameMaxRetries)
|
||||
}
|
||||
if c.Auth.DevFixedCode != "" {
|
||||
if !isDecimalString(c.Auth.DevFixedCode, 6) {
|
||||
return fmt.Errorf("%s must be a six-digit decimal string when set", envAuthDevFixedCode)
|
||||
}
|
||||
}
|
||||
|
||||
if c.Lobby.SweeperInterval <= 0 {
|
||||
return fmt.Errorf("%s must be positive", envLobbySweeperInterval)
|
||||
@@ -809,6 +826,18 @@ func (c Config) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDecimalString(value string, length int) bool {
|
||||
if len(value) != length {
|
||||
return false
|
||||
}
|
||||
for _, r := range value {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func loadString(name, fallback string) string {
|
||||
raw, ok := os.LookupEnv(name)
|
||||
if !ok {
|
||||
|
||||
@@ -77,6 +77,40 @@ func TestValidateRejectsUnknownTracesExporter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromEnvAcceptsDevFixedCode(t *testing.T) {
|
||||
env := validEnv()
|
||||
env["BACKEND_AUTH_DEV_FIXED_CODE"] = "123456"
|
||||
setEnv(t, env)
|
||||
|
||||
cfg, err := LoadFromEnv()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadFromEnv returned error: %v", err)
|
||||
}
|
||||
if cfg.Auth.DevFixedCode != "123456" {
|
||||
t.Fatalf("Auth.DevFixedCode = %q, want \"123456\"", cfg.Auth.DevFixedCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsDevFixedCodeWrongLength(t *testing.T) {
|
||||
env := validEnv()
|
||||
env["BACKEND_AUTH_DEV_FIXED_CODE"] = "12345"
|
||||
setEnv(t, env)
|
||||
|
||||
if _, err := LoadFromEnv(); err == nil || !strings.Contains(err.Error(), "BACKEND_AUTH_DEV_FIXED_CODE") {
|
||||
t.Fatalf("expected DEV fixed-code length error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsDevFixedCodeNonDecimal(t *testing.T) {
|
||||
env := validEnv()
|
||||
env["BACKEND_AUTH_DEV_FIXED_CODE"] = "abcdef"
|
||||
setEnv(t, env)
|
||||
|
||||
if _, err := LoadFromEnv(); err == nil || !strings.Contains(err.Error(), "BACKEND_AUTH_DEV_FIXED_CODE") {
|
||||
t.Fatalf("expected DEV fixed-code decimal error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsPrometheusWithoutAddr(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.Postgres.DSN = "postgres://x:y@127.0.0.1/galaxy"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Default environment for `make -C tools/local-dev up`. The compose
|
||||
# file reads these via ${VAR:-} expansions; override per-developer by
|
||||
# editing this file (it is committed only with the project defaults).
|
||||
|
||||
# Six-digit decimal accepted by ConfirmEmailCode in addition to the
|
||||
# real bcrypt-verified code. Leave the value blank to disable the
|
||||
# override and force every login through Mailpit.
|
||||
BACKEND_AUTH_DEV_FIXED_CODE=123456
|
||||
@@ -0,0 +1,53 @@
|
||||
.PHONY: help up down logs status rebuild clean psql logs-backend logs-gateway logs-mail wait
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
COMPOSE := docker compose
|
||||
|
||||
help:
|
||||
@echo "Local development stack for the Galaxy UI:"
|
||||
@echo " make up Build (if needed) and bring up the stack, wait until healthy"
|
||||
@echo " make down Stop containers, keep volumes"
|
||||
@echo " make rebuild Force rebuild of backend / gateway images and bring up"
|
||||
@echo " make clean Stop and wipe volumes (postgres data, game state)"
|
||||
@echo " make logs Tail all logs"
|
||||
@echo " make logs-backend Tail only the backend logs"
|
||||
@echo " make logs-gateway Tail only the gateway logs"
|
||||
@echo " make logs-mail Tail only the mailpit logs"
|
||||
@echo " make status docker compose ps"
|
||||
@echo " make psql Open a psql shell as galaxy@galaxy_backend"
|
||||
@echo ""
|
||||
@echo "After 'make up', point the UI at the stack with:"
|
||||
@echo " pnpm -C ui/frontend dev"
|
||||
@echo "and open http://localhost:5173 (UI) plus http://localhost:8025 (Mailpit)."
|
||||
|
||||
up:
|
||||
$(COMPOSE) up -d --wait
|
||||
|
||||
rebuild:
|
||||
$(COMPOSE) build --no-cache backend gateway
|
||||
$(COMPOSE) up -d --wait
|
||||
|
||||
down:
|
||||
$(COMPOSE) down
|
||||
|
||||
clean:
|
||||
$(COMPOSE) down -v
|
||||
|
||||
logs:
|
||||
$(COMPOSE) logs -f --tail=100
|
||||
|
||||
logs-backend:
|
||||
$(COMPOSE) logs -f --tail=200 backend
|
||||
|
||||
logs-gateway:
|
||||
$(COMPOSE) logs -f --tail=200 gateway
|
||||
|
||||
logs-mail:
|
||||
$(COMPOSE) logs -f --tail=200 mailpit
|
||||
|
||||
status:
|
||||
$(COMPOSE) ps
|
||||
|
||||
psql:
|
||||
$(COMPOSE) exec postgres psql -U galaxy -d galaxy_backend
|
||||
@@ -0,0 +1,161 @@
|
||||
# `tools/local-dev/` — Galaxy local development stack
|
||||
|
||||
A docker-compose stack that brings up postgres + redis + mailpit +
|
||||
backend + gateway so the UI Vite dev server (run on the host) can
|
||||
talk to a real authenticated stack without any cloud dependency.
|
||||
|
||||
The stack is the recommended baseline for UI work that goes beyond
|
||||
the mocked Playwright fixtures: every payload exercises the real
|
||||
FlatBuffers wire, every authenticated call verifies the response
|
||||
signature against the dev keypair, and every email passes through
|
||||
Mailpit's web UI for inspection.
|
||||
|
||||
This stack is **not** a CI gate (that role belongs to
|
||||
[`tools/local-ci/`](../local-ci/README.md), which boots a Gitea +
|
||||
Actions runner and replays workflow files). The two stacks are
|
||||
independent and can coexist on the same machine; they bind different
|
||||
ports and use different networks.
|
||||
|
||||
## Bring it up
|
||||
|
||||
```sh
|
||||
make -C tools/local-dev up
|
||||
```
|
||||
|
||||
`up` builds the local-dev backend and gateway images on first run
|
||||
(pulls postgres, redis, mailpit), waits for every service to report
|
||||
healthy, and returns. Subsequent invocations reuse the built images.
|
||||
|
||||
After the stack is healthy:
|
||||
|
||||
```sh
|
||||
pnpm -C ui/frontend dev
|
||||
```
|
||||
|
||||
Open <http://localhost:5173> for the UI and
|
||||
<http://localhost:8025> for Mailpit.
|
||||
|
||||
## Daily flow
|
||||
|
||||
```sh
|
||||
make -C tools/local-dev up # bring up (idempotent, fast on warm cache)
|
||||
pnpm -C ui/frontend dev # in another terminal
|
||||
# ...edit UI, browse, repeat...
|
||||
make -C tools/local-dev down # stop containers, keep state
|
||||
```
|
||||
|
||||
State persists in named Docker volumes between `up`/`down` cycles, so
|
||||
games created on Tuesday survive into Wednesday. Wipe with
|
||||
`make clean` when you want a fresh database.
|
||||
|
||||
## Logging in
|
||||
|
||||
Two paths coexist by default:
|
||||
|
||||
1. **Fixed dev code (fast).** `tools/local-dev/.env` ships
|
||||
`BACKEND_AUTH_DEV_FIXED_CODE=123456`. After requesting a code in
|
||||
the UI, type `123456` — `ConfirmEmailCode` accepts that literal
|
||||
in addition to the real bcrypt-verified code stored on the
|
||||
challenge row. The override emits a loud warning at backend boot
|
||||
and is rejected by the production env loader (`BACKEND_ENV` guard
|
||||
in `backend/internal/config`).
|
||||
2. **Real Mailpit code.** Open <http://localhost:8025>, find the
|
||||
most recent message, copy the six-digit code, paste it into the
|
||||
UI. This exercises the full mail outbox path, including SMTP
|
||||
handoff and gomail TLS-mode handling.
|
||||
|
||||
To force the second path (no fast-bypass), edit
|
||||
`tools/local-dev/.env` and clear `BACKEND_AUTH_DEV_FIXED_CODE`, then
|
||||
`make rebuild` (or simply `docker compose up -d backend` to recreate
|
||||
the backend with the new env).
|
||||
|
||||
## Network map
|
||||
|
||||
```
|
||||
host compose network "galaxy-local-dev-net"
|
||||
┌────────────────────────────┐ ┌──────────────────────────────┐
|
||||
│ pnpm dev localhost:5173 │──HMR──▶│ host (Vite) │
|
||||
│ browser localhost:8080 │──REST/Connect─▶│ gateway:8080 │
|
||||
│ browser localhost:8025 │─────▶│ mailpit:8025 (web UI) │
|
||||
│ psql localhost:5433 │─────▶│ postgres:5432 │
|
||||
│ redis-cli localhost:6380 │─────▶│ redis:6379 │
|
||||
└────────────────────────────┘ │ ↳ backend:8080 (HTTP) │
|
||||
│ ↳ backend:8081 (gRPC push) │
|
||||
│ ↳ mailpit:1025 (SMTP in) │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
Only the gateway public port (8080) and the mailpit web UI (8025)
|
||||
are needed for normal UI work. Postgres (5433) and Redis (6380) are
|
||||
exposed for direct inspection (`make psql`, `redis-cli -h localhost
|
||||
-p 6380 -a galaxy-dev`).
|
||||
|
||||
## Make targets
|
||||
|
||||
```text
|
||||
make up Bring up the stack (build if needed) and wait for health
|
||||
make rebuild Rebuild the backend / gateway images (ignores cache)
|
||||
make down Stop containers, keep volumes
|
||||
make clean Stop and wipe volumes (postgres + game-state)
|
||||
make logs Tail every service's logs
|
||||
make logs-backend Tail backend only
|
||||
make logs-gateway Tail gateway only
|
||||
make logs-mail Tail mailpit only
|
||||
make psql Open a psql shell as galaxy@galaxy_backend
|
||||
make status docker compose ps
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
- `docker-compose.yml` — five services: postgres, redis, mailpit,
|
||||
backend, gateway, plus shared network and volumes.
|
||||
- `backend.Dockerfile`, `gateway.Dockerfile` — local-dev runtime
|
||||
images built on alpine (so `wget` is available for the compose
|
||||
healthchecks). The build stage mirrors `backend/Dockerfile` and
|
||||
`gateway/Dockerfile` exactly.
|
||||
- `Makefile` — wrapper over `docker compose` that keeps the muscle
|
||||
memory close to `tools/local-ci/`'s Makefile.
|
||||
- `.env` — committed defaults for the compose `${VAR:-}`
|
||||
expansions. Edit per-developer or override via your shell.
|
||||
- `keys/gateway-response.pem`, `keys/gateway-response.pub` — dev-only
|
||||
Ed25519 keypair used by the gateway for response signing. Pairs
|
||||
with the `VITE_GATEWAY_RESPONSE_PUBLIC_KEY` value in
|
||||
`ui/frontend/.env.development`. See `keys/README.md` before
|
||||
rotating.
|
||||
- `keys/regenerate.go` — one-shot Go helper that regenerates the
|
||||
pair and prints the new base64 public key.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **`make up` reports a build error mentioning `pkg/cronutil`** —
|
||||
upstream module list drifted; copy any new `pkg/<name>/` line into
|
||||
the local-dev `backend.Dockerfile` / `gateway.Dockerfile` to match
|
||||
`backend/Dockerfile` / `gateway/Dockerfile`.
|
||||
- **Gateway exits at boot with "redis: …"** — the redis container is
|
||||
still bootstrapping. `make up --wait` waits for healthchecks; if
|
||||
it times out, increase `start_period` in the gateway service or
|
||||
inspect `make logs-redis`.
|
||||
- **Login form rejects every code** — confirm
|
||||
`BACKEND_AUTH_DEV_FIXED_CODE` is set in `tools/local-dev/.env` and
|
||||
the backend has been recreated since the last edit
|
||||
(`docker compose up -d backend`). Real Mailpit codes work
|
||||
regardless.
|
||||
- **UI talks to old gateway**: Vite caches `import.meta.env` at boot.
|
||||
Restart `pnpm dev` after editing
|
||||
`ui/frontend/.env.development.local`.
|
||||
- **Port 8080 already in use** — stop the conflicting service or
|
||||
edit the host-side mapping in `docker-compose.yml` (gateway's
|
||||
`ports:` entry) plus the matching `VITE_GATEWAY_BASE_URL` in
|
||||
`ui/frontend/.env.development.local`.
|
||||
|
||||
## Relationship to other infrastructure
|
||||
|
||||
- `tools/local-ci/` — Gitea + Actions runner, replays
|
||||
`.gitea/workflows/*` against a pushed branch. Different stack,
|
||||
different purpose; coexists with local-dev on the same machine.
|
||||
- `integration/testenv/` — testcontainers harness used by
|
||||
`make -C integration integration`. Uses the same images
|
||||
(`backend/Dockerfile`, `gateway/Dockerfile`) at production
|
||||
defaults; do not confuse with this local-dev stack, which carries
|
||||
alpine-runtime images for ergonomics and the dev-mode auth
|
||||
override.
|
||||
@@ -0,0 +1,68 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# Local-dev image for the backend service. Mirrors the structure of
|
||||
# `backend/Dockerfile` (the integration/production image) but switches
|
||||
# the runtime stage to alpine so docker-compose healthchecks can shell
|
||||
# out to `wget` and the container can run as root for Docker-socket
|
||||
# access without needing the production-grade nonroot guarantees.
|
||||
#
|
||||
# Build via the local-dev compose: `make -C tools/local-dev up`. The
|
||||
# build context is the repository root.
|
||||
|
||||
FROM golang:1.26.2-alpine AS builder
|
||||
WORKDIR /src
|
||||
ENV CGO_ENABLED=0 GOFLAGS=-trimpath
|
||||
|
||||
COPY pkg/cronutil/ ./pkg/cronutil/
|
||||
COPY pkg/error/ ./pkg/error/
|
||||
COPY pkg/geoip/ ./pkg/geoip/
|
||||
COPY pkg/model/ ./pkg/model/
|
||||
COPY pkg/postgres/ ./pkg/postgres/
|
||||
COPY pkg/schema/ ./pkg/schema/
|
||||
COPY pkg/transcoder/ ./pkg/transcoder/
|
||||
COPY pkg/util/ ./pkg/util/
|
||||
COPY backend/ ./backend/
|
||||
|
||||
RUN <<'EOF' cat > go.work
|
||||
go 1.26.2
|
||||
|
||||
use (
|
||||
./backend
|
||||
./pkg/cronutil
|
||||
./pkg/error
|
||||
./pkg/geoip
|
||||
./pkg/model
|
||||
./pkg/postgres
|
||||
./pkg/schema
|
||||
./pkg/transcoder
|
||||
./pkg/util
|
||||
)
|
||||
|
||||
replace (
|
||||
galaxy/cronutil v0.0.0 => ./pkg/cronutil
|
||||
galaxy/error v0.0.0 => ./pkg/error
|
||||
galaxy/geoip v0.0.0 => ./pkg/geoip
|
||||
galaxy/model v0.0.0 => ./pkg/model
|
||||
galaxy/postgres v0.0.0 => ./pkg/postgres
|
||||
galaxy/schema v0.0.0 => ./pkg/schema
|
||||
galaxy/transcoder v0.0.0 => ./pkg/transcoder
|
||||
galaxy/util v0.0.0 => ./pkg/util
|
||||
)
|
||||
EOF
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
go build -o /out/backend ./backend/cmd/backend
|
||||
|
||||
FROM alpine:3.20 AS runtime
|
||||
|
||||
LABEL org.opencontainers.image.title="galaxy-backend-local-dev"
|
||||
|
||||
RUN apk add --no-cache wget ca-certificates
|
||||
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
COPY --from=builder /out/backend /usr/local/bin/backend
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/backend"]
|
||||
@@ -0,0 +1,186 @@
|
||||
# Local development stack for the Galaxy UI.
|
||||
#
|
||||
# Brings up postgres + redis + mailpit + backend + gateway so the UI
|
||||
# Vite dev server (run on the host with `pnpm -C ui/frontend dev`) can
|
||||
# talk to a real authenticated stack without any cloud dependency.
|
||||
#
|
||||
# Browser: http://localhost:8080 (gateway public REST + Connect-Web)
|
||||
# Mailpit UI: http://localhost:8025
|
||||
# Postgres: localhost:5433 (host-mapped)
|
||||
# Redis: localhost:6380 (host-mapped)
|
||||
#
|
||||
# Bring up: make -C tools/local-dev up
|
||||
# Tear down: make -C tools/local-dev down
|
||||
# Wipe state: make -C tools/local-dev clean
|
||||
#
|
||||
# The backend reads `BACKEND_AUTH_DEV_FIXED_CODE=123456` from the
|
||||
# `.env` file alongside this compose; ConfirmEmailCode accepts that
|
||||
# literal in addition to the real bcrypt-verified code, so a developer
|
||||
# can log in without touching Mailpit. Real codes still arrive in
|
||||
# Mailpit; both paths coexist.
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: galaxy-local-dev-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: galaxy
|
||||
POSTGRES_PASSWORD: galaxy
|
||||
POSTGRES_DB: galaxy_backend
|
||||
ports:
|
||||
- "5433:5432"
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- galaxy-net
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U galaxy -d galaxy_backend"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 30
|
||||
start_period: 5s
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: galaxy-local-dev-redis
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- redis-server
|
||||
- --requirepass
|
||||
- galaxy-dev
|
||||
- --appendonly
|
||||
- "no"
|
||||
- --save
|
||||
- ""
|
||||
ports:
|
||||
- "6380:6379"
|
||||
networks:
|
||||
- galaxy-net
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "-a", "galaxy-dev", "PING"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 30
|
||||
start_period: 3s
|
||||
|
||||
mailpit:
|
||||
image: axllent/mailpit:v1.21
|
||||
container_name: galaxy-local-dev-mailpit
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8025:8025"
|
||||
networks:
|
||||
- galaxy-net
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-O-", "http://localhost:8025/livez"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 30
|
||||
start_period: 3s
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: tools/local-dev/backend.Dockerfile
|
||||
image: galaxy/backend:local-dev
|
||||
container_name: galaxy-local-dev-backend
|
||||
restart: unless-stopped
|
||||
user: "0:0"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
mailpit:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
BACKEND_LOGGING_LEVEL: debug
|
||||
BACKEND_HTTP_LISTEN_ADDR: ":8080"
|
||||
BACKEND_GRPC_PUSH_LISTEN_ADDR: ":8081"
|
||||
BACKEND_POSTGRES_DSN: "postgres://galaxy:galaxy@postgres:5432/galaxy_backend?search_path=backend&sslmode=disable"
|
||||
BACKEND_SMTP_HOST: mailpit
|
||||
BACKEND_SMTP_PORT: "1025"
|
||||
BACKEND_SMTP_FROM: "galaxy-backend@galaxy.local"
|
||||
BACKEND_SMTP_TLS_MODE: none
|
||||
BACKEND_DOCKER_NETWORK: galaxy-local-dev-net
|
||||
BACKEND_GAME_STATE_ROOT: /var/lib/galaxy/game-state
|
||||
BACKEND_GEOIP_DB_PATH: /var/lib/galaxy/geoip.mmdb
|
||||
BACKEND_NOTIFICATION_ADMIN_EMAIL: admin@galaxy.local
|
||||
BACKEND_AUTH_CHALLENGE_THROTTLE_MAX: "100"
|
||||
BACKEND_MAIL_WORKER_INTERVAL: 500ms
|
||||
BACKEND_NOTIFICATION_WORKER_INTERVAL: 500ms
|
||||
BACKEND_OTEL_TRACES_EXPORTER: none
|
||||
BACKEND_OTEL_METRICS_EXPORTER: none
|
||||
BACKEND_AUTH_DEV_FIXED_CODE: ${BACKEND_AUTH_DEV_FIXED_CODE:-}
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- game-state:/var/lib/galaxy/game-state
|
||||
- ../../pkg/geoip/test-data/test-data/GeoIP2-Country-Test.mmdb:/var/lib/galaxy/geoip.mmdb:ro
|
||||
networks:
|
||||
- galaxy-net
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-O-", "http://localhost:8080/healthz"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 60
|
||||
start_period: 10s
|
||||
|
||||
gateway:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: tools/local-dev/gateway.Dockerfile
|
||||
image: galaxy/gateway:local-dev
|
||||
container_name: galaxy-local-dev-gateway
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
GATEWAY_LOG_LEVEL: debug
|
||||
GATEWAY_PUBLIC_HTTP_ADDR: ":8080"
|
||||
GATEWAY_AUTHENTICATED_GRPC_ADDR: ":9090"
|
||||
GATEWAY_BACKEND_HTTP_URL: "http://backend:8080"
|
||||
GATEWAY_BACKEND_GRPC_PUSH_URL: "backend:8081"
|
||||
GATEWAY_BACKEND_GATEWAY_CLIENT_ID: local-dev-gateway-1
|
||||
GATEWAY_RESPONSE_SIGNER_PRIVATE_KEY_PEM_PATH: /run/secrets/gateway-response.pem
|
||||
GATEWAY_REDIS_MASTER_ADDR: "redis:6379"
|
||||
GATEWAY_REDIS_PASSWORD: galaxy-dev
|
||||
# Loosen anti-abuse so a developer hammering the form does not
|
||||
# rate-limit themselves between cycles.
|
||||
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_RATE_LIMIT_REQUESTS: "10000"
|
||||
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_RATE_LIMIT_BURST: "1000"
|
||||
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_SEND_EMAIL_CODE_IDENTITY_RATE_LIMIT_REQUESTS: "10000"
|
||||
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_SEND_EMAIL_CODE_IDENTITY_RATE_LIMIT_BURST: "1000"
|
||||
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_CONFIRM_EMAIL_CODE_IDENTITY_RATE_LIMIT_REQUESTS: "10000"
|
||||
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_CONFIRM_EMAIL_CODE_IDENTITY_RATE_LIMIT_BURST: "1000"
|
||||
GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_IP_RATE_LIMIT_REQUESTS: "10000"
|
||||
GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_IP_RATE_LIMIT_BURST: "1000"
|
||||
GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_SESSION_RATE_LIMIT_REQUESTS: "10000"
|
||||
GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_SESSION_RATE_LIMIT_BURST: "1000"
|
||||
GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_USER_RATE_LIMIT_REQUESTS: "10000"
|
||||
GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_USER_RATE_LIMIT_BURST: "1000"
|
||||
GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_REQUESTS: "10000"
|
||||
GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_BURST: "1000"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./keys/gateway-response.pem:/run/secrets/gateway-response.pem:ro
|
||||
networks:
|
||||
- galaxy-net
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-O-", "http://localhost:8080/healthz"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 30
|
||||
start_period: 5s
|
||||
|
||||
networks:
|
||||
galaxy-net:
|
||||
name: galaxy-local-dev-net
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
name: galaxy-local-dev-postgres-data
|
||||
game-state:
|
||||
name: galaxy-local-dev-game-state
|
||||
@@ -0,0 +1,74 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# Local-dev image for the gateway service. Mirrors `gateway/Dockerfile`
|
||||
# (the integration/production image) but switches the runtime stage to
|
||||
# alpine so docker-compose healthchecks can shell out to `wget`.
|
||||
#
|
||||
# Build via the local-dev compose: `make -C tools/local-dev up`. The
|
||||
# build context is the repository root.
|
||||
|
||||
FROM golang:1.26.2-alpine AS builder
|
||||
WORKDIR /src
|
||||
ENV CGO_ENABLED=0 GOFLAGS=-trimpath
|
||||
|
||||
COPY pkg/cronutil/ ./pkg/cronutil/
|
||||
COPY pkg/error/ ./pkg/error/
|
||||
COPY pkg/geoip/ ./pkg/geoip/
|
||||
COPY pkg/model/ ./pkg/model/
|
||||
COPY pkg/postgres/ ./pkg/postgres/
|
||||
COPY pkg/redisconn/ ./pkg/redisconn/
|
||||
COPY pkg/schema/ ./pkg/schema/
|
||||
COPY pkg/transcoder/ ./pkg/transcoder/
|
||||
COPY pkg/util/ ./pkg/util/
|
||||
COPY ui/core/ ./ui/core/
|
||||
COPY backend/ ./backend/
|
||||
COPY gateway/ ./gateway/
|
||||
|
||||
RUN <<'EOF' cat > go.work
|
||||
go 1.26.2
|
||||
|
||||
use (
|
||||
./backend
|
||||
./gateway
|
||||
./pkg/cronutil
|
||||
./pkg/error
|
||||
./pkg/geoip
|
||||
./pkg/model
|
||||
./pkg/postgres
|
||||
./pkg/redisconn
|
||||
./pkg/schema
|
||||
./pkg/transcoder
|
||||
./pkg/util
|
||||
./ui/core
|
||||
)
|
||||
|
||||
replace (
|
||||
galaxy/cronutil v0.0.0 => ./pkg/cronutil
|
||||
galaxy/error v0.0.0 => ./pkg/error
|
||||
galaxy/geoip v0.0.0 => ./pkg/geoip
|
||||
galaxy/model v0.0.0 => ./pkg/model
|
||||
galaxy/postgres v0.0.0 => ./pkg/postgres
|
||||
galaxy/redisconn v0.0.0 => ./pkg/redisconn
|
||||
galaxy/schema v0.0.0 => ./pkg/schema
|
||||
galaxy/transcoder v0.0.0 => ./pkg/transcoder
|
||||
galaxy/util v0.0.0 => ./pkg/util
|
||||
galaxy/core v0.0.0 => ./ui/core
|
||||
)
|
||||
EOF
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
go build -o /out/gateway ./gateway/cmd/gateway
|
||||
|
||||
FROM alpine:3.20 AS runtime
|
||||
|
||||
LABEL org.opencontainers.image.title="galaxy-gateway-local-dev"
|
||||
|
||||
RUN apk add --no-cache wget ca-certificates
|
||||
|
||||
EXPOSE 8080
|
||||
EXPOSE 9090
|
||||
|
||||
COPY --from=builder /out/gateway /usr/local/bin/gateway
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/gateway"]
|
||||
@@ -0,0 +1,34 @@
|
||||
# `tools/local-dev/keys/`
|
||||
|
||||
DEV-ONLY cryptographic material used by the `tools/local-dev/` stack.
|
||||
|
||||
**Never use any key in this directory in a non-local environment.**
|
||||
|
||||
## Files
|
||||
|
||||
- `gateway-response.pem` — gateway response-signing private key, PKCS#8
|
||||
PEM, Ed25519. Mounted into the gateway container at
|
||||
`/run/secrets/gateway-response.pem` and pointed to via
|
||||
`GATEWAY_RESPONSE_SIGNER_PRIVATE_KEY_PEM_PATH`.
|
||||
- `gateway-response.pub` — matching raw 32-byte public key, standard
|
||||
base64. Copied verbatim into `ui/frontend/.env.development` as
|
||||
`VITE_GATEWAY_RESPONSE_PUBLIC_KEY`.
|
||||
|
||||
## Regenerating
|
||||
|
||||
The keypair is committed because it must be deterministic across
|
||||
developer checkouts (the UI's `.env.development` ships the exact
|
||||
base64 of the public half). Rotate only when a leak is suspected; the
|
||||
keys never reach a non-local environment in normal operation.
|
||||
|
||||
To regenerate from a Go one-shot:
|
||||
|
||||
```sh
|
||||
cd tools/local-dev/keys
|
||||
go run ./regenerate.go
|
||||
```
|
||||
|
||||
The helper writes a fresh PEM, prints the matching public-key base64,
|
||||
and updates `gateway-response.pub`. After regeneration, copy the new
|
||||
`VITE_GATEWAY_RESPONSE_PUBLIC_KEY` value from `gateway-response.pub`
|
||||
into `ui/frontend/.env.development` and commit both changes together.
|
||||
@@ -0,0 +1,3 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIHqW94EpSePdiujbP1Wh1GIz+vuDnFU8HDeFfaNwcovi
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1,4 @@
|
||||
# DEV-ONLY gateway response-signing public key (raw 32-byte Ed25519,
|
||||
# standard non-URL-safe base64). Pairs with `gateway-response.pem`.
|
||||
# Never use in any non-local environment.
|
||||
nIG54tCuNiIKrazt8Hh7YxmmU/BhpseGhIIgj164Chw=
|
||||
@@ -0,0 +1,47 @@
|
||||
// Regenerate `gateway-response.pem` and `gateway-response.pub`.
|
||||
//
|
||||
// Run from this directory: `go run ./regenerate.go`. The keys are
|
||||
// committed and used only by the `tools/local-dev/` stack; rotate by
|
||||
// re-running and committing both files together with the matching
|
||||
// `VITE_GATEWAY_RESPONSE_PUBLIC_KEY` update in
|
||||
// `ui/frontend/.env.development`.
|
||||
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "generate:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
pkcs8, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "marshal:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8})
|
||||
if err := os.WriteFile("gateway-response.pem", pemBytes, 0o600); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "write pem:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
pubB64 := base64.StdEncoding.EncodeToString(pub)
|
||||
pubBlock := fmt.Sprintf("# DEV-ONLY gateway response-signing public key (raw 32-byte Ed25519,\n# standard non-URL-safe base64). Pairs with `gateway-response.pem`.\n# Never use in any non-local environment.\n%s\n", pubB64)
|
||||
if err := os.WriteFile("gateway-response.pub", []byte(pubBlock), 0o644); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "write pub:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("VITE_GATEWAY_RESPONSE_PUBLIC_KEY=%s\n", pubB64)
|
||||
}
|
||||
@@ -132,6 +132,24 @@ make android Capacitor + gradle Phase 32+
|
||||
make all every target above
|
||||
```
|
||||
|
||||
## Local development
|
||||
|
||||
For UI work against a real stack, the `tools/local-dev/` docker
|
||||
compose brings up postgres + redis + mailpit + backend + gateway in
|
||||
one command, and `ui/frontend/.env.development` is already wired to
|
||||
talk to it:
|
||||
|
||||
```sh
|
||||
make -C tools/local-dev up # build + start, wait for healthy
|
||||
pnpm -C ui/frontend dev # Vite on the host
|
||||
# UI: http://localhost:5173
|
||||
# Mailpit: http://localhost:8025
|
||||
```
|
||||
|
||||
The stack accepts a fixed dev code (`123456`) in addition to the
|
||||
real Mailpit-delivered one. Full runbook in
|
||||
[`../tools/local-dev/README.md`](../tools/local-dev/README.md).
|
||||
|
||||
## Per-phase docs
|
||||
|
||||
Topic docs live under `ui/docs/` and are added per phase as they're
|
||||
|
||||
@@ -83,6 +83,32 @@ go test -count=1 \
|
||||
./pkg/storage/... ./pkg/transcoder/... ./pkg/util/...
|
||||
```
|
||||
|
||||
## Local development stack
|
||||
|
||||
For UI work that needs a real authenticated stack (verifying the
|
||||
FlatBuffers wire end-to-end, exercising a real lobby flow, hitting
|
||||
real Mailpit), bring up `tools/local-dev/`:
|
||||
|
||||
```sh
|
||||
make -C tools/local-dev up # postgres + redis + mailpit + backend + gateway
|
||||
pnpm -C ui/frontend dev # Vite on the host, talks to the stack
|
||||
```
|
||||
|
||||
`ui/frontend/.env.development` already targets the stack
|
||||
(`http://localhost:8080`) and pins the matching response-signing
|
||||
public key from `tools/local-dev/keys/`. Per-developer overrides go
|
||||
into `.env.development.local` (gitignored).
|
||||
|
||||
The stack honours `BACKEND_AUTH_DEV_FIXED_CODE` (default `123456` in
|
||||
`tools/local-dev/.env`) so the login form takes that literal in
|
||||
addition to the real Mailpit code; see
|
||||
[`../../tools/local-dev/README.md`](../../tools/local-dev/README.md)
|
||||
for the full runbook (regenerating the dev keypair, switching the
|
||||
mode off, troubleshooting common boot issues).
|
||||
|
||||
The local-dev stack is independent from the local-ci stack below;
|
||||
they bind different ports and can run side by side.
|
||||
|
||||
## Local CI verification
|
||||
|
||||
`tools/local-ci/` ships a self-contained Gitea + Actions runner via
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Vite picks this file up automatically when running in `development`
|
||||
# mode (`pnpm dev`, `pnpm test:e2e`). It targets the local-dev stack
|
||||
# brought up by `make -C tools/local-dev up`. Per-developer overrides
|
||||
# live in `.env.development.local` (gitignored by Vite convention).
|
||||
|
||||
# Gateway public REST + Connect-Web edge listener.
|
||||
VITE_GATEWAY_BASE_URL=http://localhost:8080
|
||||
|
||||
# Standard non-URL-safe base64 of the gateway response-signing public
|
||||
# key. Pairs with `tools/local-dev/keys/gateway-response.pem`. The pair
|
||||
# is dev-only — see `tools/local-dev/keys/README.md` before rotating.
|
||||
VITE_GATEWAY_RESPONSE_PUBLIC_KEY=nIG54tCuNiIKrazt8Hh7YxmmU/BhpseGhIIgj164Chw=
|
||||
+10
-11
@@ -1,18 +1,17 @@
|
||||
# Vite reads any variable prefixed with `VITE_` and exposes it on
|
||||
# `import.meta.env`. Copy this file to `.env.local` (gitignored) and
|
||||
# fill in the values before running `pnpm run dev` or `pnpm exec
|
||||
# playwright test` against a real gateway.
|
||||
# `import.meta.env`. The committed `.env.development` already targets
|
||||
# the `tools/local-dev/` docker stack — most contributors don't need
|
||||
# to copy this file. Use a `.env.development.local` (gitignored by
|
||||
# Vite convention) for per-developer overrides, or use `.env.local`
|
||||
# when running `pnpm exec playwright test` against a non-default
|
||||
# gateway.
|
||||
|
||||
# Base URL of the gateway public REST surface and Connect-Web edge
|
||||
# listener. Both surfaces share the same host and port. Defaults to
|
||||
# the local dev address used by `tools/local-ci` and the Go-side
|
||||
# integration suite.
|
||||
# listener. Both surfaces share the same host and port.
|
||||
VITE_GATEWAY_BASE_URL=http://localhost:8080
|
||||
|
||||
# Standard (non-URL-safe) base64 of the gateway's raw 32-byte
|
||||
# Ed25519 response-signing public key. Required only for
|
||||
# authenticated unary calls; unauthenticated routes (`/login`)
|
||||
# work without it. For local dev, take the value the gateway
|
||||
# integration container exports as `ResponseSignerPublic` (see
|
||||
# `integration/testenv/gateway.go`).
|
||||
# Ed25519 response-signing public key. The local-dev stack ships a
|
||||
# checked-in keypair under `tools/local-dev/keys/`; for any other
|
||||
# environment, take the value from the gateway operator.
|
||||
VITE_GATEWAY_RESPONSE_PUBLIC_KEY=
|
||||
|
||||
Reference in New Issue
Block a user