From 856594239213684c0bafd4a7af6248d707c2886a Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Sat, 23 May 2026 18:19:07 +0200 Subject: [PATCH 1/4] feat(deploy): single-origin path-based deployment + project site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Serve the whole stack behind one host: site at /, game UI at /game/, gateway REST at /api + /healthz, Connect at /rpc (prefix stripped by the edge Caddy). The built artifact is domain-agnostic — the UI talks to the gateway same-origin via relative URLs, so the same bundle runs under any host with no rebuild and with CORS disabled. - Rename the Connect proto service galaxy.gateway.v1.EdgeGateway -> edge.v1.Gateway; regenerate Go + TS; public path /rpc/edge.v1.Gateway. - Move the game UI under base path /game (env BASE_PATH); make the manifest, service-worker scope, WASM loader, and all navigation base-aware via a withBase helper. - Relative API + /rpc Connect prefix; Vite dev proxy mirrors the strip. - Rewrite the edge Caddy (dev + prod) for path-based routing; empty CORS allow-lists (same-origin); single host. - New VitePress project site (site/): i18n en/ru with switcher, LaTeX math, minimal monospace theme; built and served at /. - dev-deploy compose/Makefile + CI (dev-deploy, prod-build, new site-build) build and seed the site; probes hit /, /game/, /healthz. - Sync docs (ARCHITECTURE, gateway README/openapi, dev-deploy & local-dev READMEs, CLAUDE.md, ui/PLAN). Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitea/workflows/deploy-prod.yaml | 2 +- .gitea/workflows/dev-deploy.yaml | 32 +- .gitea/workflows/prod-build.yaml | 17 +- .gitea/workflows/site-build.yaml | 47 + CLAUDE.md | 3 +- docs/ARCHITECTURE.md | 44 +- gateway/README.md | 25 +- gateway/internal/grpcapi/command_routing.go | 16 +- gateway/internal/grpcapi/connect_handler.go | 22 +- gateway/internal/grpcapi/envelope.go | 24 +- gateway/internal/grpcapi/envelope_test.go | 86 +- gateway/internal/grpcapi/freshness_replay.go | 14 +- .../freshness_replay_integration_test.go | 46 +- gateway/internal/grpcapi/observability.go | 8 +- gateway/internal/grpcapi/payload_hash.go | 14 +- .../grpcapi/payload_hash_integration_test.go | 8 +- gateway/internal/grpcapi/push_fanout.go | 14 +- gateway/internal/grpcapi/push_heartbeat.go | 12 +- .../internal/grpcapi/push_heartbeat_test.go | 16 +- gateway/internal/grpcapi/push_stream.go | 24 +- gateway/internal/grpcapi/rate_limit.go | 14 +- .../grpcapi/rate_limit_integration_test.go | 20 +- gateway/internal/grpcapi/server.go | 10 +- gateway/internal/grpcapi/server_test.go | 14 +- gateway/internal/grpcapi/session_lookup.go | 16 +- .../session_lookup_integration_test.go | 28 +- gateway/internal/grpcapi/signature.go | 14 +- .../grpcapi/signature_integration_test.go | 12 +- .../internal/grpcapi/test_fixtures_test.go | 32 +- gateway/openapi.yaml | 8 + .../gateway => edge}/v1/edge_gateway.pb.go | 96 +- .../gateway => edge}/v1/edge_gateway.proto | 6 +- .../v1/edge_gateway_grpc.pb.go | 90 +- .../v1/edgev1connect/edge_gateway.connect.go | 136 ++ .../gatewayv1connect/edge_gateway.connect.go | 138 -- integration/testenv/connect_client.go | 16 +- site/.gitignore | 3 + site/.vitepress/config.ts | 55 + site/.vitepress/theme/custom.css | 10 + site/.vitepress/theme/index.ts | 6 + site/README.md | 36 + site/index.md | 5 + site/package.json | 16 + site/pnpm-lock.yaml | 1736 +++++++++++++++++ site/pnpm-workspace.yaml | 2 + site/ru/index.md | 5 + tools/dev-deploy/Caddyfile.dev | 99 +- tools/dev-deploy/Caddyfile.prod | 61 +- tools/dev-deploy/Makefile | 40 +- tools/dev-deploy/README.md | 86 +- tools/dev-deploy/docker-compose.yml | 16 +- tools/local-dev/README.md | 17 +- tools/local-dev/docker-compose.yml | 4 +- ui/PLAN.md | 12 +- ui/core/types/envelope.go | 2 +- ui/frontend/.env.development | 9 +- ui/frontend/src/api/connect.ts | 17 +- ui/frontend/src/api/events.svelte.ts | 16 +- ui/frontend/src/api/galaxy-client.ts | 8 +- ui/frontend/src/lib/active-view/battle.svelte | 5 +- .../lib/active-view/designer-science.svelte | 3 +- ui/frontend/src/lib/active-view/map.svelte | 5 +- .../lib/active-view/report/report-toc.svelte | 3 +- .../active-view/report/section-battles.svelte | 3 +- .../src/lib/active-view/table-sciences.svelte | 5 +- ui/frontend/src/lib/env.ts | 26 +- ui/frontend/src/lib/header/view-menu.svelte | 3 +- ui/frontend/src/lib/paths.ts | 25 + .../src/lib/sidebar/bottom-tabs.svelte | 5 +- ui/frontend/src/platform/core/wasm.ts | 5 +- .../gateway => edge}/v1/edge_gateway_pb.ts | 52 +- ui/frontend/src/routes/+layout.svelte | 13 +- .../src/routes/games/[id]/+layout.svelte | 11 +- ui/frontend/src/routes/lobby/+page.svelte | 13 +- .../src/routes/lobby/create/+page.svelte | 11 +- ui/frontend/src/routes/login/+page.svelte | 3 +- ui/frontend/src/service-worker.ts | 12 +- ui/frontend/static/manifest.webmanifest | 12 +- ui/frontend/svelte.config.js | 7 + ui/frontend/tests/e2e/auth-flow.spec.ts | 8 +- ui/frontend/tests/e2e/battle-viewer.spec.ts | 4 +- ui/frontend/tests/e2e/cargo-routes.spec.ts | 6 +- ui/frontend/tests/e2e/fixtures/sign-event.ts | 2 +- .../tests/e2e/fixtures/sign-response.ts | 2 +- .../tests/e2e/game-shell-inspector.spec.ts | 6 +- ui/frontend/tests/e2e/game-shell-map.spec.ts | 6 +- ui/frontend/tests/e2e/history-mode.spec.ts | 6 +- ui/frontend/tests/e2e/lobby-flow.spec.ts | 6 +- ui/frontend/tests/e2e/map-roundtrip.spec.ts | 6 +- ui/frontend/tests/e2e/map-toggles.spec.ts | 6 +- ui/frontend/tests/e2e/order-sync.spec.ts | 6 +- .../tests/e2e/planet-production.spec.ts | 6 +- ui/frontend/tests/e2e/races.spec.ts | 6 +- ui/frontend/tests/e2e/rename-planet.spec.ts | 6 +- ui/frontend/tests/e2e/report-sections.spec.ts | 6 +- .../tests/e2e/sciences-map-regress.spec.ts | 6 +- ui/frontend/tests/e2e/sciences.spec.ts | 6 +- ui/frontend/tests/e2e/ship-classes.spec.ts | 6 +- ui/frontend/tests/e2e/turn-ready.spec.ts | 6 +- ui/frontend/tests/events.test.ts | 10 +- ui/frontend/tests/galaxy-client.test.ts | 16 +- ui/frontend/tests/lobby-create.test.ts | 3 +- ui/frontend/tests/lobby-page.test.ts | 3 +- ui/frontend/vite.config.ts | 3 +- 104 files changed, 2967 insertions(+), 787 deletions(-) create mode 100644 .gitea/workflows/site-build.yaml rename gateway/proto/{galaxy/gateway => edge}/v1/edge_gateway.pb.go (80%) rename gateway/proto/{galaxy/gateway => edge}/v1/edge_gateway.proto (95%) rename gateway/proto/{galaxy/gateway => edge}/v1/edge_gateway_grpc.pb.go (50%) create mode 100644 gateway/proto/edge/v1/edgev1connect/edge_gateway.connect.go delete mode 100644 gateway/proto/galaxy/gateway/v1/gatewayv1connect/edge_gateway.connect.go create mode 100644 site/.gitignore create mode 100644 site/.vitepress/config.ts create mode 100644 site/.vitepress/theme/custom.css create mode 100644 site/.vitepress/theme/index.ts create mode 100644 site/README.md create mode 100644 site/index.md create mode 100644 site/package.json create mode 100644 site/pnpm-lock.yaml create mode 100644 site/pnpm-workspace.yaml create mode 100644 site/ru/index.md create mode 100644 ui/frontend/src/lib/paths.ts rename ui/frontend/src/proto/{galaxy/gateway => edge}/v1/edge_gateway_pb.ts (58%) diff --git a/.gitea/workflows/deploy-prod.yaml b/.gitea/workflows/deploy-prod.yaml index 2d51353..992d8aa 100644 --- a/.gitea/workflows/deploy-prod.yaml +++ b/.gitea/workflows/deploy-prod.yaml @@ -28,4 +28,4 @@ jobs: echo " 2. scp the .tar.gz bundles to the production host." echo " 3. ssh prod 'docker load -i ...' for backend / gateway / engine." echo " 4. ssh prod 'docker compose -f /opt/galaxy/docker-compose.yml up -d'." - echo " 5. Probe https://api.galaxy.com/healthz and roll back on failure." + echo " 5. Probe https:///healthz and roll back on failure." diff --git a/.gitea/workflows/dev-deploy.yaml b/.gitea/workflows/dev-deploy.yaml index e0c5249..5df15df 100644 --- a/.gitea/workflows/dev-deploy.yaml +++ b/.gitea/workflows/dev-deploy.yaml @@ -24,6 +24,7 @@ on: - 'game/**' - 'pkg/**' - 'ui/**' + - 'site/**' - 'go.work' - 'go.work.sum' - 'tools/dev-deploy/**' @@ -76,7 +77,11 @@ jobs: - name: Build UI frontend working-directory: ui/frontend env: - VITE_GATEWAY_BASE_URL: https://api.galaxy.lan + # Single-origin deployment: an empty base URL means the + # gateway shares the document origin (REST at /api, Connect at + # /rpc). The game UI is served under the /game/ base path. + VITE_GATEWAY_BASE_URL: "" + BASE_PATH: /game # Surface the synthetic-report loader and similar dev-only # affordances in the long-lived dev bundle. The prod build # path (`prod-build.yaml`) leaves this flag unset so the @@ -91,6 +96,14 @@ jobs: export VITE_GATEWAY_RESPONSE_PUBLIC_KEY="$(grep -E '^VITE_GATEWAY_RESPONSE_PUBLIC_KEY=' .env.development | cut -d= -f2)" pnpm build + - name: Install site dependencies + working-directory: site + run: pnpm install --frozen-lockfile + + - name: Build project site + working-directory: site + run: pnpm build + - name: Build galaxy-engine image working-directory: ${{ gitea.workspace }} run: | @@ -112,6 +125,14 @@ jobs: -v "${{ gitea.workspace }}/ui/frontend/build:/src:ro" \ alpine sh -c 'rm -rf /dst/* /dst/.??* 2>/dev/null; cp -a /src/. /dst/' + - name: Seed site volume + run: | + docker volume create galaxy-dev-site-dist >/dev/null + docker run --rm \ + -v galaxy-dev-site-dist:/dst \ + -v "${{ gitea.workspace }}/site/.vitepress/dist:/src:ro" \ + alpine sh -c 'rm -rf /dst/* /dst/.??* 2>/dev/null; cp -a /src/. /dst/' + - name: Seed geoip volume run: | # Copy the GeoIP test fixture into a named volume so the @@ -162,9 +183,12 @@ jobs: # `tls internal`) terminates and forwards into the edge # network. We accept the host's internal CA via -k because # the runner image has no reason to trust it. - curl -sk --max-time 10 https://api.galaxy.lan/healthz \ + curl -sk --max-time 10 https://galaxy.lan/healthz \ | tee /tmp/healthz test -s /tmp/healthz curl -sk --max-time 10 -o /dev/null -w '%{http_code}\n' \ - https://www.galaxy.lan/ | tee /tmp/www_status - grep -qE '^(200|304)$' /tmp/www_status + https://galaxy.lan/ | tee /tmp/site_status + grep -qE '^(200|304)$' /tmp/site_status + curl -sk --max-time 10 -o /dev/null -w '%{http_code}\n' \ + https://galaxy.lan/game/ | tee /tmp/game_status + grep -qE '^(200|304)$' /tmp/game_status diff --git a/.gitea/workflows/prod-build.yaml b/.gitea/workflows/prod-build.yaml index 60dafad..e2bf128 100644 --- a/.gitea/workflows/prod-build.yaml +++ b/.gitea/workflows/prod-build.yaml @@ -16,6 +16,7 @@ on: - 'game/**' - 'pkg/**' - 'ui/**' + - 'site/**' - 'go.work' - 'go.work.sum' - '.gitea/workflows/prod-build.yaml' @@ -93,7 +94,11 @@ jobs: - name: Build UI bundle working-directory: ui/frontend env: - VITE_GATEWAY_BASE_URL: https://api.galaxy.com + # Single-origin deployment: an empty base URL means the + # gateway shares the document origin (REST at /api, Connect at + # /rpc). The game UI is served under the /game/ base path. + VITE_GATEWAY_BASE_URL: "" + BASE_PATH: /game run: | # Production response-signing public key is not in the repo # yet (the dev key in `tools/local-dev/keys/` is for dev @@ -104,6 +109,14 @@ jobs: export VITE_GATEWAY_RESPONSE_PUBLIC_KEY="$(grep -E '^VITE_GATEWAY_RESPONSE_PUBLIC_KEY=' .env.development | cut -d= -f2)" pnpm build + - name: Install site dependencies + working-directory: site + run: pnpm install --frozen-lockfile + + - name: Build project site + working-directory: site + run: pnpm build + - name: Save images as artifact bundles run: | mkdir -p artifacts @@ -115,6 +128,8 @@ jobs: | gzip >"artifacts/game-engine-${{ steps.tag.outputs.tag }}.tar.gz" tar -C ui/frontend -czf \ "artifacts/ui-dist-${{ steps.tag.outputs.tag }}.tar.gz" build + tar -C site/.vitepress -czf \ + "artifacts/site-dist-${{ steps.tag.outputs.tag }}.tar.gz" dist - name: Upload images uses: actions/upload-artifact@v4 diff --git a/.gitea/workflows/site-build.yaml b/.gitea/workflows/site-build.yaml new file mode 100644 index 0000000..3527304 --- /dev/null +++ b/.gitea/workflows/site-build.yaml @@ -0,0 +1,47 @@ +name: Build · Site + +# Builds the VitePress project site so a broken site change fails its PR. +# The dev-deploy / prod-build workflows build and ship the site +# separately; this is the fast PR gate. No `!**/*.md` exclusion — the +# site is Markdown, so content changes must be exercised too. + +on: + push: + paths: + - 'site/**' + - '.gitea/workflows/site-build.yaml' + pull_request: + paths: + - 'site/**' + - '.gitea/workflows/site-build.yaml' + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: 11.0.7 + dest: ${{ runner.temp }}/setup-pnpm + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: site/pnpm-lock.yaml + + - name: Install site dependencies + working-directory: site + run: pnpm install --frozen-lockfile + + - name: Build project site + working-directory: site + run: pnpm build diff --git a/CLAUDE.md b/CLAUDE.md index e1e330e..b10dd69 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,8 @@ Branches: is manual through `deploy-prod.yaml`. - `development` — long-lived dev integration branch. Every merge into it auto-deploys to the dev environment via `dev-deploy.yaml` - (reachable at `https://www.galaxy.lan` / `https://api.galaxy.lan`). + (single origin `https://galaxy.lan`: site at `/`, game at `/game/`, + gateway REST at `/api`). - `feature/*` — short-lived branches off `development`. Merged back via PR; only then do they reach the dev environment automatically. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index aa76cb0..a27c08d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -579,13 +579,25 @@ behaviour for any of its guarantees. The authenticated edge listener is built on `connectrpc.com/connect` and natively serves the Connect, gRPC, and gRPC-Web protocols on a single -HTTP/2 cleartext (`h2c`) port. Browser clients use Connect via -`@connectrpc/connect-web`; native iOS / Android / desktop clients can -use either Connect or raw gRPC framing against the same listener. -Envelope, signature, freshness, and anti-replay rules below are -protocol-agnostic — they apply identically to every supported wire +HTTP/2 cleartext (`h2c`) port. The v1 service is `edge.v1.Gateway`; +browser clients address its methods at `/rpc/edge.v1.Gateway/` +and the edge strips the `/rpc` prefix so the gateway sees the +proto-derived `/edge.v1.Gateway/` path. Browser clients use +Connect via `@connectrpc/connect-web`; native iOS / Android / desktop +clients can use either Connect or raw gRPC framing against the same +listener. Envelope, signature, freshness, and anti-replay rules below +are protocol-agnostic — they apply identically to every supported wire framing. +Both the authenticated `/rpc/*` surface and the gateway's public REST at +`/api/*` are served same-origin with the game UI, so the gateway runs +with CORS disabled by default: the +`GATEWAY_PUBLIC_HTTP_CORS_ALLOWED_ORIGINS` and +`GATEWAY_AUTHENTICATED_GRPC_CORS_ALLOWED_ORIGINS` allow-lists are empty, +which turns the CORS middleware off and emits no `Access-Control-*` +headers. They would be repopulated only if a deployment fronted the +gateway on a different host than the UI. + ### Principles - No browser cookies. @@ -775,6 +787,9 @@ domain tables. ### TLS and MITM +TLS terminates once, at the edge in front of the gateway, for the single +public origin that serves the site, the game UI, and both gateway +surfaces. A single certificate therefore covers the whole deployment. Native clients should use TLS pinning (SPKI-based) in addition to the signed exchange. Browser clients rely on browser-managed TLS and the signed exchange. @@ -845,8 +860,10 @@ Branches: way in is a PR merge from `development`. - `development` — long-lived dev integration branch. Every merge triggers an auto-deploy into the long-lived dev environment on the - CI host, reachable through the host Caddy at - `https://www.galaxy.lan` and `https://api.galaxy.lan`. + CI host, reachable through the host Caddy at a single origin + `https://galaxy.lan` (project site at `/`, game UI at `/game/`, + gateway public REST at `/api/*` and `/healthz`, authenticated + Connect/gRPC-Web at `/rpc/*`). - `feature/*` — short-lived branches off `development`. Merged back via PR; PRs run unit + integration checks before merge. @@ -872,8 +889,9 @@ Environments: - **`tools/local-dev/`** — single-developer playground. Bound to host ports, Vite dev server runs on the host. Not driven by CI. -- **`tools/dev-deploy/`** — long-lived dev environment behind - `*.galaxy.lan`, redeployed on every merge into `development`. +- **`tools/dev-deploy/`** — long-lived dev environment behind the + single origin `galaxy.lan`, redeployed on every merge into + `development`. - **production** — future. Images come from the `galaxy-images-commit-` artifact produced by `prod-build.yaml` and are shipped to the production host via `docker save` → @@ -913,6 +931,14 @@ untouched by compose between deploys. ## 19. Deployment Topology (informational) +- The public edge is single-origin and path-based: one host (the dev + host is `galaxy.lan`; prod takes the real host from + `GALAXY_PUBLIC_HOST`) terminates TLS and routes by path — + `/` → project site, `/game/` → game UI, `/api/*` and `/healthz` → + gateway public REST (`galaxy-api:8080`), `/rpc/*` → gateway + authenticated Connect/gRPC-Web (`galaxy-api:9090`, with the `/rpc` + prefix stripped before the gateway). The same dev and prod shape is + domain-agnostic: no host name is baked into the deployed artifacts. - MVP runs three executables: one `gateway` instance, one `backend` instance, and N `galaxy-game-{game_id}` containers managed by backend. - One Postgres database is shared by `backend` only. diff --git a/gateway/README.md b/gateway/README.md index 878d8bf..a1f13eb 100644 --- a/gateway/README.md +++ b/gateway/README.md @@ -94,8 +94,20 @@ The authenticated edge listener is built on the Connect, gRPC, and gRPC-Web protocols on a single HTTP/2 cleartext (`h2c`) port. Browser clients use `@connectrpc/connect-web`; native clients can use either Connect or raw gRPC framing against the same -listener. Production TLS termination happens upstream of the gateway, -matching the previous gRPC-only deployment posture. +listener. TLS termination happens upstream of the gateway at the edge +Caddy, which fronts both transports under one host. + +Both transports are served same-origin under one host. The edge routes +public REST and the health probe at `/api/*` and `/healthz` to the +public listener, and the authenticated Connect/gRPC-Web surface at +`/rpc/*` to the authenticated listener (the `/rpc` prefix is stripped +before the gateway). Because the game UI is served from the same origin, +the gateway runs with CORS disabled by default: both +`GATEWAY_PUBLIC_HTTP_CORS_ALLOWED_ORIGINS` and +`GATEWAY_AUTHENTICATED_GRPC_CORS_ALLOWED_ORIGINS` are empty, an empty +allow-list turns the CORS middleware off, and responses carry no +`Access-Control-*` headers. Those knobs would only be repopulated if a +deployment ever fronted the gateway on a different host than the UI. ### Public REST Surface @@ -225,10 +237,13 @@ It binds the stream to `user_id` and `device_session_id` and starts by sending a signed service event that includes the current server time in milliseconds. The v1 protobuf contract lives in -`proto/galaxy/gateway/v1/edge_gateway.proto` under package -`galaxy.gateway.v1` and service `EdgeGateway`. +`proto/edge/v1/edge_gateway.proto` under package `edge.v1` and service +`Gateway`. Browser and native clients address its methods at +`/rpc/edge.v1.Gateway/`; the edge Caddy strips the `/rpc` +prefix so the gateway listener sees the proto-derived +`/edge.v1.Gateway/` path. Generated Go bindings are committed under -`proto/galaxy/gateway/v1/` (gRPC stubs and `gatewayv1connect/` Connect +`proto/edge/v1/` (gRPC stubs and `edgev1connect/` Connect handlers) and are regenerated with: ```bash diff --git a/gateway/internal/grpcapi/command_routing.go b/gateway/internal/grpcapi/command_routing.go index ae4defa..71af6d4 100644 --- a/gateway/internal/grpcapi/command_routing.go +++ b/gateway/internal/grpcapi/command_routing.go @@ -11,7 +11,7 @@ import ( "galaxy/gateway/authn" "galaxy/gateway/internal/clock" "galaxy/gateway/internal/downstream" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" + edgev1 "galaxy/gateway/proto/edge/v1" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -21,9 +21,9 @@ import ( // commandRoutingService translates the verified authenticated request context // into an internal downstream command and signs successful unary responses. type commandRoutingService struct { - gatewayv1.UnimplementedEdgeGatewayServer + edgev1.UnimplementedGatewayServer - subscribeDelegate gatewayv1.EdgeGatewayServer + subscribeDelegate edgev1.GatewayServer router downstream.Router responseSigner authn.ResponseSigner clock clock.Clock @@ -32,7 +32,7 @@ type commandRoutingService struct { // ExecuteCommand builds a verified downstream command, routes it by exact // message_type, executes it, and signs the resulting unary response. -func (s commandRoutingService) ExecuteCommand(ctx context.Context, _ *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { +func (s commandRoutingService) ExecuteCommand(ctx context.Context, _ *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) { command, err := authenticatedCommandFromContext(ctx) if err != nil { return nil, err @@ -80,7 +80,7 @@ func (s commandRoutingService) ExecuteCommand(ctx context.Context, _ *gatewayv1. return nil, status.Error(codes.Unavailable, "response signer is unavailable") } - return &gatewayv1.ExecuteCommandResponse{ + return &edgev1.ExecuteCommandResponse{ ProtocolVersion: command.ProtocolVersion, RequestId: command.RequestID, TimestampMs: responseTimestampMS, @@ -93,13 +93,13 @@ func (s commandRoutingService) ExecuteCommand(ctx context.Context, _ *gatewayv1. // SubscribeEvents delegates to the authenticated streaming service // implementation selected during server construction. -func (s commandRoutingService) SubscribeEvents(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { +func (s commandRoutingService) SubscribeEvents(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error { return s.subscribeDelegate.SubscribeEvents(req, stream) } // newCommandRoutingService constructs the final authenticated service that // owns verified unary routing while preserving the delegated streaming path. -func newCommandRoutingService(subscribeDelegate gatewayv1.EdgeGatewayServer, router downstream.Router, responseSigner authn.ResponseSigner, clk clock.Clock, downstreamTimeout time.Duration) gatewayv1.EdgeGatewayServer { +func newCommandRoutingService(subscribeDelegate edgev1.GatewayServer, router downstream.Router, responseSigner authn.ResponseSigner, clk clock.Clock, downstreamTimeout time.Duration) edgev1.GatewayServer { return commandRoutingService{ subscribeDelegate: subscribeDelegate, router: router, @@ -142,4 +142,4 @@ func (unavailableResponseSigner) SignEvent(authn.EventSigningFields) ([]byte, er return nil, errors.New("response signer is unavailable") } -var _ gatewayv1.EdgeGatewayServer = commandRoutingService{} +var _ edgev1.GatewayServer = commandRoutingService{} diff --git a/gateway/internal/grpcapi/connect_handler.go b/gateway/internal/grpcapi/connect_handler.go index fa9a2e3..a8fb2c9 100644 --- a/gateway/internal/grpcapi/connect_handler.go +++ b/gateway/internal/grpcapi/connect_handler.go @@ -5,8 +5,8 @@ import ( "errors" "fmt" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" - "galaxy/gateway/proto/galaxy/gateway/v1/gatewayv1connect" + edgev1 "galaxy/gateway/proto/edge/v1" + "galaxy/gateway/proto/edge/v1/edgev1connect" "connectrpc.com/connect" "google.golang.org/grpc/codes" @@ -17,15 +17,15 @@ import ( // connectEdgeAdapter exposes the existing gRPC-shaped authenticated edge // service decorator stack (envelope → session → payload-hash → signature → // freshness/replay → rate-limit → routing/push) through the -// gatewayv1connect.EdgeGatewayHandler interface. It owns no logic of its +// edgev1connect.GatewayHandler interface. It owns no logic of its // own; the underlying decorator stack carries the full ingress contract // unchanged. type connectEdgeAdapter struct { - impl gatewayv1.EdgeGatewayServer + impl edgev1.GatewayServer } // newConnectEdgeAdapter wraps impl as a Connect handler. -func newConnectEdgeAdapter(impl gatewayv1.EdgeGatewayServer) gatewayv1connect.EdgeGatewayHandler { +func newConnectEdgeAdapter(impl edgev1.GatewayServer) edgev1connect.GatewayHandler { return &connectEdgeAdapter{impl: impl} } @@ -33,7 +33,7 @@ func newConnectEdgeAdapter(impl gatewayv1.EdgeGatewayServer) gatewayv1connect.Ed // service, and wraps the typed response. gRPC `status.Error` values // returned by the decorator stack are translated to *connect.Error so // the Connect client receives the matching code and message. -func (a *connectEdgeAdapter) ExecuteCommand(ctx context.Context, req *connect.Request[gatewayv1.ExecuteCommandRequest]) (*connect.Response[gatewayv1.ExecuteCommandResponse], error) { +func (a *connectEdgeAdapter) ExecuteCommand(ctx context.Context, req *connect.Request[edgev1.ExecuteCommandRequest]) (*connect.Response[edgev1.ExecuteCommandResponse], error) { resp, err := a.impl.ExecuteCommand(ctx, req.Msg) if err != nil { return nil, translateGRPCStatusError(err) @@ -48,7 +48,7 @@ func (a *connectEdgeAdapter) ExecuteCommand(ctx context.Context, req *connect.Re // stream; the remaining grpc.ServerStream surface is satisfied by no-op // shims so the interface contract is met without panicking. Errors // returned by the decorator stack are translated to *connect.Error. -func (a *connectEdgeAdapter) SubscribeEvents(ctx context.Context, req *connect.Request[gatewayv1.SubscribeEventsRequest], stream *connect.ServerStream[gatewayv1.GatewayEvent]) error { +func (a *connectEdgeAdapter) SubscribeEvents(ctx context.Context, req *connect.Request[edgev1.SubscribeEventsRequest], stream *connect.ServerStream[edgev1.GatewayEvent]) error { wrapped := &connectEdgeStream{ctx: ctx, stream: stream} if err := a.impl.SubscribeEvents(req.Msg, wrapped); err != nil { return translateGRPCStatusError(err) @@ -83,19 +83,19 @@ func translateGRPCStatusError(err error) error { return connect.NewError(connect.Code(grpcStatus.Code()), errors.New(grpcStatus.Message())) } -// connectEdgeStream satisfies grpc.ServerStreamingServer[gatewayv1.GatewayEvent] +// connectEdgeStream satisfies grpc.ServerStreamingServer[edgev1.GatewayEvent] // on top of *connect.ServerStream. The decorator stack reads the request // context and pushes outbound events through Send; the rest of the // grpc.ServerStream surface is not exercised in the gateway, so the no-op // implementations preserve the type contract without surprising behaviour. type connectEdgeStream struct { ctx context.Context - stream *connect.ServerStream[gatewayv1.GatewayEvent] + stream *connect.ServerStream[edgev1.GatewayEvent] } // Send forwards a typed gateway event through the underlying Connect server // stream. -func (s *connectEdgeStream) Send(event *gatewayv1.GatewayEvent) error { +func (s *connectEdgeStream) Send(event *edgev1.GatewayEvent) error { return s.stream.Send(event) } @@ -127,7 +127,7 @@ func (s *connectEdgeStream) SetTrailer(metadata.MD) {} // SendMsg directly; if a future caller does, the typed Send path is used // when the message is a GatewayEvent. func (s *connectEdgeStream) SendMsg(m any) error { - event, ok := m.(*gatewayv1.GatewayEvent) + event, ok := m.(*edgev1.GatewayEvent) if !ok { return fmt.Errorf("connectEdgeStream.SendMsg: unsupported message type %T", m) } diff --git a/gateway/internal/grpcapi/envelope.go b/gateway/internal/grpcapi/envelope.go index d3a6a71..dc4ff02 100644 --- a/gateway/internal/grpcapi/envelope.go +++ b/gateway/internal/grpcapi/envelope.go @@ -4,7 +4,7 @@ import ( "bytes" "context" "fmt" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" + edgev1 "galaxy/gateway/proto/edge/v1" "buf.build/go/protovalidate" "google.golang.org/grpc" @@ -47,14 +47,14 @@ func parsedEnvelopeFromContext(ctx context.Context) (parsedEnvelope, bool) { // envelopeValidatingService applies envelope parsing and the protocol gate // before delegating to the configured service implementation. type envelopeValidatingService struct { - gatewayv1.UnimplementedEdgeGatewayServer + edgev1.UnimplementedGatewayServer - delegate gatewayv1.EdgeGatewayServer + delegate edgev1.GatewayServer } // ExecuteCommand validates req and only then forwards it to the configured // delegate with the parsed envelope attached to ctx. -func (s envelopeValidatingService) ExecuteCommand(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { +func (s envelopeValidatingService) ExecuteCommand(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) { envelope, err := parseExecuteCommandRequest(req) if err != nil { return nil, err @@ -65,7 +65,7 @@ func (s envelopeValidatingService) ExecuteCommand(ctx context.Context, req *gate // SubscribeEvents validates req and only then forwards it to the configured // delegate with the parsed envelope attached to the stream context. -func (s envelopeValidatingService) SubscribeEvents(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { +func (s envelopeValidatingService) SubscribeEvents(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error { envelope, err := parseSubscribeEventsRequest(req) if err != nil { return err @@ -79,7 +79,7 @@ func (s envelopeValidatingService) SubscribeEvents(req *gatewayv1.SubscribeEvent // parseExecuteCommandRequest validates req according to the request-envelope // rules and returns a cloned parsed envelope suitable for later auth steps. -func parseExecuteCommandRequest(req *gatewayv1.ExecuteCommandRequest) (parsedEnvelope, error) { +func parseExecuteCommandRequest(req *edgev1.ExecuteCommandRequest) (parsedEnvelope, error) { if req == nil { return parsedEnvelope{}, newMalformedEnvelopeError("request envelope must not be nil") } @@ -105,7 +105,7 @@ func parseExecuteCommandRequest(req *gatewayv1.ExecuteCommandRequest) (parsedEnv // parseSubscribeEventsRequest validates req according to the request-envelope // rules and returns a cloned parsed envelope suitable for later auth steps. -func parseSubscribeEventsRequest(req *gatewayv1.SubscribeEventsRequest) (parsedEnvelope, error) { +func parseSubscribeEventsRequest(req *edgev1.SubscribeEventsRequest) (parsedEnvelope, error) { if req == nil { return parsedEnvelope{}, newMalformedEnvelopeError("request envelope must not be nil") } @@ -131,13 +131,13 @@ func parseSubscribeEventsRequest(req *gatewayv1.SubscribeEventsRequest) (parsedE // newEnvelopeValidatingService wraps delegate with the envelope-validation // gate. -func newEnvelopeValidatingService(delegate gatewayv1.EdgeGatewayServer) gatewayv1.EdgeGatewayServer { +func newEnvelopeValidatingService(delegate edgev1.GatewayServer) edgev1.GatewayServer { return envelopeValidatingService{delegate: delegate} } // canonicalExecuteCommandValidationError maps any ExecuteCommand validation // failure into the stable canonical error chosen by field order. -func canonicalExecuteCommandValidationError(req *gatewayv1.ExecuteCommandRequest) error { +func canonicalExecuteCommandValidationError(req *edgev1.ExecuteCommandRequest) error { switch { case req.GetProtocolVersion() == "": return newMalformedEnvelopeError("protocol_version must not be empty") @@ -162,7 +162,7 @@ func canonicalExecuteCommandValidationError(req *gatewayv1.ExecuteCommandRequest // canonicalSubscribeEventsValidationError maps any SubscribeEvents validation // failure into the stable canonical error chosen by field order. -func canonicalSubscribeEventsValidationError(req *gatewayv1.SubscribeEventsRequest) error { +func canonicalSubscribeEventsValidationError(req *edgev1.SubscribeEventsRequest) error { switch { case req.GetProtocolVersion() == "": return newMalformedEnvelopeError("protocol_version must not be empty") @@ -198,7 +198,7 @@ func newUnsupportedProtocolVersionError(version string) error { type parsedEnvelopeContextKey struct{} type envelopeContextStream struct { - grpc.ServerStreamingServer[gatewayv1.GatewayEvent] + grpc.ServerStreamingServer[edgev1.GatewayEvent] ctx context.Context } @@ -210,4 +210,4 @@ func (s envelopeContextStream) Context() context.Context { return s.ctx } -var _ gatewayv1.EdgeGatewayServer = envelopeValidatingService{} +var _ edgev1.GatewayServer = envelopeValidatingService{} diff --git a/gateway/internal/grpcapi/envelope_test.go b/gateway/internal/grpcapi/envelope_test.go index 880fb26..d519865 100644 --- a/gateway/internal/grpcapi/envelope_test.go +++ b/gateway/internal/grpcapi/envelope_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" + edgev1 "galaxy/gateway/proto/edge/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -19,10 +19,10 @@ func TestParseExecuteCommandRequest(t *testing.T) { tests := []struct { name string - mutate func(*gatewayv1.ExecuteCommandRequest) + mutate func(*edgev1.ExecuteCommandRequest) wantCode codes.Code wantMessage string - assertValid func(*testing.T, *gatewayv1.ExecuteCommandRequest, parsedEnvelope) + assertValid func(*testing.T, *edgev1.ExecuteCommandRequest, parsedEnvelope) }{ { name: "nil request", @@ -31,7 +31,7 @@ func TestParseExecuteCommandRequest(t *testing.T) { }, { name: "empty protocol version", - mutate: func(req *gatewayv1.ExecuteCommandRequest) { + mutate: func(req *edgev1.ExecuteCommandRequest) { req.ProtocolVersion = "" }, wantCode: codes.InvalidArgument, @@ -39,7 +39,7 @@ func TestParseExecuteCommandRequest(t *testing.T) { }, { name: "empty device session id", - mutate: func(req *gatewayv1.ExecuteCommandRequest) { + mutate: func(req *edgev1.ExecuteCommandRequest) { req.DeviceSessionId = "" }, wantCode: codes.InvalidArgument, @@ -47,7 +47,7 @@ func TestParseExecuteCommandRequest(t *testing.T) { }, { name: "empty message type", - mutate: func(req *gatewayv1.ExecuteCommandRequest) { + mutate: func(req *edgev1.ExecuteCommandRequest) { req.MessageType = "" }, wantCode: codes.InvalidArgument, @@ -55,7 +55,7 @@ func TestParseExecuteCommandRequest(t *testing.T) { }, { name: "zero timestamp", - mutate: func(req *gatewayv1.ExecuteCommandRequest) { + mutate: func(req *edgev1.ExecuteCommandRequest) { req.TimestampMs = 0 }, wantCode: codes.InvalidArgument, @@ -63,7 +63,7 @@ func TestParseExecuteCommandRequest(t *testing.T) { }, { name: "empty request id", - mutate: func(req *gatewayv1.ExecuteCommandRequest) { + mutate: func(req *edgev1.ExecuteCommandRequest) { req.RequestId = "" }, wantCode: codes.InvalidArgument, @@ -71,7 +71,7 @@ func TestParseExecuteCommandRequest(t *testing.T) { }, { name: "empty payload bytes", - mutate: func(req *gatewayv1.ExecuteCommandRequest) { + mutate: func(req *edgev1.ExecuteCommandRequest) { req.PayloadBytes = nil }, wantCode: codes.InvalidArgument, @@ -79,7 +79,7 @@ func TestParseExecuteCommandRequest(t *testing.T) { }, { name: "empty payload hash", - mutate: func(req *gatewayv1.ExecuteCommandRequest) { + mutate: func(req *edgev1.ExecuteCommandRequest) { req.PayloadHash = nil }, wantCode: codes.InvalidArgument, @@ -87,7 +87,7 @@ func TestParseExecuteCommandRequest(t *testing.T) { }, { name: "empty signature", - mutate: func(req *gatewayv1.ExecuteCommandRequest) { + mutate: func(req *edgev1.ExecuteCommandRequest) { req.Signature = nil }, wantCode: codes.InvalidArgument, @@ -95,7 +95,7 @@ func TestParseExecuteCommandRequest(t *testing.T) { }, { name: "unsupported protocol version", - mutate: func(req *gatewayv1.ExecuteCommandRequest) { + mutate: func(req *edgev1.ExecuteCommandRequest) { req.ProtocolVersion = "v2" }, wantCode: codes.FailedPrecondition, @@ -104,7 +104,7 @@ func TestParseExecuteCommandRequest(t *testing.T) { { name: "valid request", wantCode: codes.OK, - assertValid: func(t *testing.T, req *gatewayv1.ExecuteCommandRequest, envelope parsedEnvelope) { + assertValid: func(t *testing.T, req *edgev1.ExecuteCommandRequest, envelope parsedEnvelope) { t.Helper() assert.Equal(t, supportedProtocolVersion, envelope.ProtocolVersion) @@ -138,7 +138,7 @@ func TestParseExecuteCommandRequest(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - var req *gatewayv1.ExecuteCommandRequest + var req *edgev1.ExecuteCommandRequest if tt.name != "nil request" { req = newValidExecuteCommandRequest() if tt.mutate != nil { @@ -166,10 +166,10 @@ func TestParseSubscribeEventsRequest(t *testing.T) { tests := []struct { name string - mutate func(*gatewayv1.SubscribeEventsRequest) + mutate func(*edgev1.SubscribeEventsRequest) wantCode codes.Code wantMessage string - assertValid func(*testing.T, *gatewayv1.SubscribeEventsRequest, parsedEnvelope) + assertValid func(*testing.T, *edgev1.SubscribeEventsRequest, parsedEnvelope) }{ { name: "nil request", @@ -178,7 +178,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) { }, { name: "empty protocol version", - mutate: func(req *gatewayv1.SubscribeEventsRequest) { + mutate: func(req *edgev1.SubscribeEventsRequest) { req.ProtocolVersion = "" }, wantCode: codes.InvalidArgument, @@ -186,7 +186,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) { }, { name: "empty device session id", - mutate: func(req *gatewayv1.SubscribeEventsRequest) { + mutate: func(req *edgev1.SubscribeEventsRequest) { req.DeviceSessionId = "" }, wantCode: codes.InvalidArgument, @@ -194,7 +194,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) { }, { name: "empty message type", - mutate: func(req *gatewayv1.SubscribeEventsRequest) { + mutate: func(req *edgev1.SubscribeEventsRequest) { req.MessageType = "" }, wantCode: codes.InvalidArgument, @@ -202,7 +202,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) { }, { name: "zero timestamp", - mutate: func(req *gatewayv1.SubscribeEventsRequest) { + mutate: func(req *edgev1.SubscribeEventsRequest) { req.TimestampMs = 0 }, wantCode: codes.InvalidArgument, @@ -210,7 +210,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) { }, { name: "empty request id", - mutate: func(req *gatewayv1.SubscribeEventsRequest) { + mutate: func(req *edgev1.SubscribeEventsRequest) { req.RequestId = "" }, wantCode: codes.InvalidArgument, @@ -218,7 +218,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) { }, { name: "empty payload hash", - mutate: func(req *gatewayv1.SubscribeEventsRequest) { + mutate: func(req *edgev1.SubscribeEventsRequest) { req.PayloadHash = nil }, wantCode: codes.InvalidArgument, @@ -226,7 +226,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) { }, { name: "empty signature", - mutate: func(req *gatewayv1.SubscribeEventsRequest) { + mutate: func(req *edgev1.SubscribeEventsRequest) { req.Signature = nil }, wantCode: codes.InvalidArgument, @@ -234,7 +234,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) { }, { name: "unsupported protocol version", - mutate: func(req *gatewayv1.SubscribeEventsRequest) { + mutate: func(req *edgev1.SubscribeEventsRequest) { req.ProtocolVersion = "v2" }, wantCode: codes.FailedPrecondition, @@ -243,7 +243,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) { { name: "valid request with empty payload bytes", wantCode: codes.OK, - assertValid: func(t *testing.T, req *gatewayv1.SubscribeEventsRequest, envelope parsedEnvelope) { + assertValid: func(t *testing.T, req *edgev1.SubscribeEventsRequest, envelope parsedEnvelope) { t.Helper() assert.Empty(t, req.GetPayloadBytes()) @@ -260,7 +260,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - var req *gatewayv1.SubscribeEventsRequest + var req *edgev1.SubscribeEventsRequest if tt.name != "nil request" { req = newValidSubscribeEventsRequest() if tt.mutate != nil { @@ -286,10 +286,10 @@ func TestParseSubscribeEventsRequest(t *testing.T) { func TestEnvelopeValidatingServiceExecuteCommandRejectsInvalidRequestBeforeDelegate(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} service := newEnvelopeValidatingService(delegate) - _, err := service.ExecuteCommand(context.Background(), &gatewayv1.ExecuteCommandRequest{}) + _, err := service.ExecuteCommand(context.Background(), &edgev1.ExecuteCommandRequest{}) require.Error(t, err) assert.Equal(t, codes.InvalidArgument, status.Code(err)) @@ -299,10 +299,10 @@ func TestEnvelopeValidatingServiceExecuteCommandRejectsInvalidRequestBeforeDeleg func TestEnvelopeValidatingServiceSubscribeEventsRejectsInvalidRequestBeforeDelegate(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} service := newEnvelopeValidatingService(delegate) - err := service.SubscribeEvents(&gatewayv1.SubscribeEventsRequest{}, stubGatewayEventStream{}) + err := service.SubscribeEvents(&edgev1.SubscribeEventsRequest{}, stubGatewayEventStream{}) require.Error(t, err) assert.Equal(t, codes.InvalidArgument, status.Code(err)) @@ -313,15 +313,15 @@ func TestEnvelopeValidatingServiceExecuteCommandAttachesParsedEnvelope(t *testin t.Parallel() want := newValidExecuteCommandRequest() - delegate := &recordingEdgeGatewayService{ - executeCommandFunc: func(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { + delegate := &recordingGatewayService{ + executeCommandFunc: func(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) { envelope, ok := parsedEnvelopeFromContext(ctx) require.True(t, ok) assert.Equal(t, want.GetRequestId(), envelope.RequestID) assert.Equal(t, want.GetDeviceSessionId(), envelope.DeviceSessionID) assert.Equal(t, want.GetMessageType(), envelope.MessageType) assert.Equal(t, want.GetPayloadBytes(), envelope.PayloadBytes) - return &gatewayv1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil + return &edgev1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil }, } service := newEnvelopeValidatingService(delegate) @@ -337,8 +337,8 @@ func TestEnvelopeValidatingServiceSubscribeEventsAttachesParsedEnvelope(t *testi t.Parallel() want := newValidSubscribeEventsRequest() - delegate := &recordingEdgeGatewayService{ - subscribeEventsFunc: func(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { + delegate := &recordingGatewayService{ + subscribeEventsFunc: func(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error { envelope, ok := parsedEnvelopeFromContext(stream.Context()) require.True(t, ok) assert.Equal(t, want.GetRequestId(), envelope.RequestID) @@ -357,25 +357,25 @@ func TestEnvelopeValidatingServiceSubscribeEventsAttachesParsedEnvelope(t *testi assert.Equal(t, 1, delegate.subscribeCalls) } -type recordingEdgeGatewayService struct { - gatewayv1.UnimplementedEdgeGatewayServer +type recordingGatewayService struct { + edgev1.UnimplementedGatewayServer executeCalls int subscribeCalls int - executeCommandFunc func(context.Context, *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) - subscribeEventsFunc func(*gatewayv1.SubscribeEventsRequest, grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error + executeCommandFunc func(context.Context, *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) + subscribeEventsFunc func(*edgev1.SubscribeEventsRequest, grpc.ServerStreamingServer[edgev1.GatewayEvent]) error } -func (s *recordingEdgeGatewayService) ExecuteCommand(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { +func (s *recordingGatewayService) ExecuteCommand(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) { s.executeCalls++ if s.executeCommandFunc != nil { return s.executeCommandFunc(ctx, req) } - return &gatewayv1.ExecuteCommandResponse{}, nil + return &edgev1.ExecuteCommandResponse{}, nil } -func (s *recordingEdgeGatewayService) SubscribeEvents(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { +func (s *recordingGatewayService) SubscribeEvents(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error { s.subscribeCalls++ if s.subscribeEventsFunc != nil { return s.subscribeEventsFunc(req, stream) @@ -389,7 +389,7 @@ type stubGatewayEventStream struct { ctx context.Context } -func (s stubGatewayEventStream) Send(*gatewayv1.GatewayEvent) error { +func (s stubGatewayEventStream) Send(*edgev1.GatewayEvent) error { return nil } diff --git a/gateway/internal/grpcapi/freshness_replay.go b/gateway/internal/grpcapi/freshness_replay.go index 905795b..9549419 100644 --- a/gateway/internal/grpcapi/freshness_replay.go +++ b/gateway/internal/grpcapi/freshness_replay.go @@ -7,7 +7,7 @@ import ( "galaxy/gateway/internal/clock" "galaxy/gateway/internal/replay" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" + edgev1 "galaxy/gateway/proto/edge/v1" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -19,9 +19,9 @@ const minimumReplayReservationTTL = time.Millisecond // freshnessAndReplayService applies freshness and anti-replay checks after // client-signature verification and before later policy or routing steps run. type freshnessAndReplayService struct { - gatewayv1.UnimplementedEdgeGatewayServer + edgev1.UnimplementedGatewayServer - delegate gatewayv1.EdgeGatewayServer + delegate edgev1.GatewayServer clock clock.Clock replayStore replay.Store freshnessWindow time.Duration @@ -29,7 +29,7 @@ type freshnessAndReplayService struct { // ExecuteCommand verifies request freshness and replay protection before // delegating to the configured service implementation. -func (s freshnessAndReplayService) ExecuteCommand(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { +func (s freshnessAndReplayService) ExecuteCommand(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) { if err := s.verifyFreshnessAndReplay(ctx); err != nil { return nil, err } @@ -39,7 +39,7 @@ func (s freshnessAndReplayService) ExecuteCommand(ctx context.Context, req *gate // SubscribeEvents verifies request freshness and replay protection before // delegating to the configured service implementation. -func (s freshnessAndReplayService) SubscribeEvents(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { +func (s freshnessAndReplayService) SubscribeEvents(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error { if err := s.verifyFreshnessAndReplay(stream.Context()); err != nil { return err } @@ -49,7 +49,7 @@ func (s freshnessAndReplayService) SubscribeEvents(req *gatewayv1.SubscribeEvent // newFreshnessAndReplayService wraps delegate with the freshness and replay // gate. -func newFreshnessAndReplayService(delegate gatewayv1.EdgeGatewayServer, clk clock.Clock, replayStore replay.Store, freshnessWindow time.Duration) gatewayv1.EdgeGatewayServer { +func newFreshnessAndReplayService(delegate edgev1.GatewayServer, clk clock.Clock, replayStore replay.Store, freshnessWindow time.Duration) edgev1.GatewayServer { return freshnessAndReplayService{ delegate: delegate, clock: clk, @@ -92,4 +92,4 @@ func (unavailableReplayStore) Reserve(context.Context, string, string, time.Dura return errors.New("replay store is unavailable") } -var _ gatewayv1.EdgeGatewayServer = freshnessAndReplayService{} +var _ edgev1.GatewayServer = freshnessAndReplayService{} diff --git a/gateway/internal/grpcapi/freshness_replay_integration_test.go b/gateway/internal/grpcapi/freshness_replay_integration_test.go index d2c154e..bf318b7 100644 --- a/gateway/internal/grpcapi/freshness_replay_integration_test.go +++ b/gateway/internal/grpcapi/freshness_replay_integration_test.go @@ -9,7 +9,7 @@ import ( "galaxy/gateway/internal/replay" "galaxy/gateway/internal/session" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" + edgev1 "galaxy/gateway/proto/edge/v1" "connectrpc.com/connect" "github.com/stretchr/testify/assert" @@ -40,7 +40,7 @@ func TestExecuteCommandRejectsStaleTimestamp(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, @@ -82,7 +82,7 @@ func TestSubscribeEventsRejectsStaleTimestamp(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, @@ -104,7 +104,7 @@ func TestSubscribeEventsRejectsStaleTimestamp(t *testing.T) { func TestExecuteCommandRejectsReplay(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, @@ -131,7 +131,7 @@ func TestExecuteCommandRejectsReplay(t *testing.T) { func TestSubscribeEventsRejectsReplay(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, @@ -162,9 +162,9 @@ func TestSubscribeEventsRejectsReplay(t *testing.T) { func TestExecuteCommandAllowsSameRequestIDAcrossDistinctSessions(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{ - executeCommandFunc: func(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { - return &gatewayv1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil + delegate := &recordingGatewayService{ + executeCommandFunc: func(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) { + return &edgev1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil }, } @@ -196,8 +196,8 @@ func TestExecuteCommandAllowsSameRequestIDAcrossDistinctSessions(t *testing.T) { func TestSubscribeEventsAllowsSameRequestIDAcrossDistinctSessions(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{ - subscribeEventsFunc: func(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { + delegate := &recordingGatewayService{ + subscribeEventsFunc: func(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error { return nil }, } @@ -238,7 +238,7 @@ func TestSubscribeEventsAllowsSameRequestIDAcrossDistinctSessions(t *testing.T) func TestExecuteCommandRejectsReplayStoreUnavailable(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, @@ -262,7 +262,7 @@ func TestExecuteCommandRejectsReplayStoreUnavailable(t *testing.T) { func TestSubscribeEventsRejectsReplayStoreUnavailable(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, @@ -286,9 +286,9 @@ func TestSubscribeEventsRejectsReplayStoreUnavailable(t *testing.T) { func TestExecuteCommandFreshRequestReachesDelegateAndUsesDynamicReplayTTL(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{ - executeCommandFunc: func(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { - return &gatewayv1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil + delegate := &recordingGatewayService{ + executeCommandFunc: func(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) { + return &edgev1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil }, } @@ -324,8 +324,8 @@ func TestExecuteCommandFreshRequestReachesDelegateAndUsesDynamicReplayTTL(t *tes func TestSubscribeEventsFreshRequestReachesDelegateAndUsesDynamicReplayTTL(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{ - subscribeEventsFunc: func(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { + delegate := &recordingGatewayService{ + subscribeEventsFunc: func(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error { return nil }, } @@ -361,9 +361,9 @@ func TestSubscribeEventsFreshRequestReachesDelegateAndUsesDynamicReplayTTL(t *te func TestExecuteCommandFutureSkewUsesExtendedReplayTTL(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{ - executeCommandFunc: func(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { - return &gatewayv1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil + delegate := &recordingGatewayService{ + executeCommandFunc: func(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) { + return &edgev1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil }, } @@ -395,9 +395,9 @@ func TestExecuteCommandFutureSkewUsesExtendedReplayTTL(t *testing.T) { func TestExecuteCommandBoundaryFreshnessUsesMinimumReplayTTL(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{ - executeCommandFunc: func(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { - return &gatewayv1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil + delegate := &recordingGatewayService{ + executeCommandFunc: func(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) { + return &edgev1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil }, } diff --git a/gateway/internal/grpcapi/observability.go b/gateway/internal/grpcapi/observability.go index 0d1438f..5a09125 100644 --- a/gateway/internal/grpcapi/observability.go +++ b/gateway/internal/grpcapi/observability.go @@ -8,7 +8,7 @@ import ( "galaxy/gateway/internal/logging" "galaxy/gateway/internal/telemetry" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" + edgev1 "galaxy/gateway/proto/edge/v1" "go.opentelemetry.io/otel/attribute" "go.uber.org/zap" @@ -78,9 +78,9 @@ func recordEdgeRequest(logger *zap.Logger, metrics *telemetry.Runtime, ctx conte func envelopeFieldsFromRequest(req any) (messageType string, requestID string, traceID string) { switch typed := req.(type) { - case *gatewayv1.ExecuteCommandRequest: + case *edgev1.ExecuteCommandRequest: return typed.GetMessageType(), typed.GetRequestId(), typed.GetTraceId() - case *gatewayv1.SubscribeEventsRequest: + case *edgev1.SubscribeEventsRequest: return typed.GetMessageType(), typed.GetRequestId(), typed.GetTraceId() default: return "", "", "" @@ -88,7 +88,7 @@ func envelopeFieldsFromRequest(req any) (messageType string, requestID string, t } func resultCodeFromResponse(resp any) string { - typed, ok := resp.(*gatewayv1.ExecuteCommandResponse) + typed, ok := resp.(*edgev1.ExecuteCommandResponse) if !ok { return "" } diff --git a/gateway/internal/grpcapi/payload_hash.go b/gateway/internal/grpcapi/payload_hash.go index 4897d09..5ea1fbe 100644 --- a/gateway/internal/grpcapi/payload_hash.go +++ b/gateway/internal/grpcapi/payload_hash.go @@ -5,7 +5,7 @@ import ( "errors" "galaxy/gateway/authn" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" + edgev1 "galaxy/gateway/proto/edge/v1" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -15,14 +15,14 @@ import ( // payloadHashVerifyingService applies payload-hash verification after session // lookup and before any later auth or routing step runs. type payloadHashVerifyingService struct { - gatewayv1.UnimplementedEdgeGatewayServer + edgev1.UnimplementedGatewayServer - delegate gatewayv1.EdgeGatewayServer + delegate edgev1.GatewayServer } // ExecuteCommand verifies req payload integrity before delegating to the // configured service implementation. -func (s payloadHashVerifyingService) ExecuteCommand(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { +func (s payloadHashVerifyingService) ExecuteCommand(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) { if err := verifyPayloadHash(ctx); err != nil { return nil, err } @@ -32,7 +32,7 @@ func (s payloadHashVerifyingService) ExecuteCommand(ctx context.Context, req *ga // SubscribeEvents verifies req payload integrity before delegating to the // configured service implementation. -func (s payloadHashVerifyingService) SubscribeEvents(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { +func (s payloadHashVerifyingService) SubscribeEvents(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error { if err := verifyPayloadHash(stream.Context()); err != nil { return err } @@ -42,7 +42,7 @@ func (s payloadHashVerifyingService) SubscribeEvents(req *gatewayv1.SubscribeEve // newPayloadHashVerifyingService wraps delegate with the payload-hash // verification gate. -func newPayloadHashVerifyingService(delegate gatewayv1.EdgeGatewayServer) gatewayv1.EdgeGatewayServer { +func newPayloadHashVerifyingService(delegate edgev1.GatewayServer) edgev1.GatewayServer { return payloadHashVerifyingService{delegate: delegate} } @@ -63,4 +63,4 @@ func verifyPayloadHash(ctx context.Context) error { } } -var _ gatewayv1.EdgeGatewayServer = payloadHashVerifyingService{} +var _ edgev1.GatewayServer = payloadHashVerifyingService{} diff --git a/gateway/internal/grpcapi/payload_hash_integration_test.go b/gateway/internal/grpcapi/payload_hash_integration_test.go index c8de30d..6ee9def 100644 --- a/gateway/internal/grpcapi/payload_hash_integration_test.go +++ b/gateway/internal/grpcapi/payload_hash_integration_test.go @@ -15,7 +15,7 @@ import ( func TestExecuteCommandRejectsPayloadHashWithInvalidLength(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, @@ -38,7 +38,7 @@ func TestExecuteCommandRejectsPayloadHashWithInvalidLength(t *testing.T) { func TestExecuteCommandRejectsPayloadHashMismatch(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, @@ -62,7 +62,7 @@ func TestExecuteCommandRejectsPayloadHashMismatch(t *testing.T) { func TestSubscribeEventsRejectsPayloadHashWithInvalidLength(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, @@ -85,7 +85,7 @@ func TestSubscribeEventsRejectsPayloadHashWithInvalidLength(t *testing.T) { func TestSubscribeEventsRejectsPayloadHashMismatch(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, diff --git a/gateway/internal/grpcapi/push_fanout.go b/gateway/internal/grpcapi/push_fanout.go index 218b875..246f4c2 100644 --- a/gateway/internal/grpcapi/push_fanout.go +++ b/gateway/internal/grpcapi/push_fanout.go @@ -11,7 +11,7 @@ import ( "galaxy/gateway/internal/logging" "galaxy/gateway/internal/push" "galaxy/gateway/internal/telemetry" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" + edgev1 "galaxy/gateway/proto/edge/v1" "go.uber.org/zap" "google.golang.org/grpc" @@ -22,7 +22,7 @@ import ( // NewFanOutPushStreamService constructs the authenticated SubscribeEvents tail // service that registers active streams in hub and forwards client-facing // events after the bootstrap event has been sent. -func NewFanOutPushStreamService(hub *push.Hub, responseSigner authn.ResponseSigner, clk clock.Clock, logger *zap.Logger) gatewayv1.EdgeGatewayServer { +func NewFanOutPushStreamService(hub *push.Hub, responseSigner authn.ResponseSigner, clk clock.Clock, logger *zap.Logger) edgev1.GatewayServer { if responseSigner == nil { responseSigner = unavailableResponseSigner{} } @@ -44,7 +44,7 @@ func NewFanOutPushStreamService(hub *push.Hub, responseSigner authn.ResponseSign // fanOutPushStreamService owns the post-bootstrap authenticated push-stream // lifecycle backed by the in-memory push hub. type fanOutPushStreamService struct { - gatewayv1.UnimplementedEdgeGatewayServer + edgev1.UnimplementedGatewayServer hub *push.Hub responseSigner authn.ResponseSigner @@ -54,7 +54,7 @@ type fanOutPushStreamService struct { // SubscribeEvents registers the verified stream in the push hub and forwards // matching client-facing events until the stream ends. -func (s fanOutPushStreamService) SubscribeEvents(_ *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { +func (s fanOutPushStreamService) SubscribeEvents(_ *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error { binding, ok := authenticatedStreamBindingFromContext(stream.Context()) if !ok { return status.Error(codes.Internal, "authenticated request context is incomplete") @@ -109,7 +109,7 @@ func (s fanOutPushStreamService) SubscribeEvents(_ *gatewayv1.SubscribeEventsReq } } -func (s fanOutPushStreamService) buildGatewayEvent(event push.Event) (*gatewayv1.GatewayEvent, error) { +func (s fanOutPushStreamService) buildGatewayEvent(event push.Event) (*edgev1.GatewayEvent, error) { timestampMS := s.clock.Now().UTC().UnixMilli() payloadHash := sha256.Sum256(event.PayloadBytes) @@ -125,7 +125,7 @@ func (s fanOutPushStreamService) buildGatewayEvent(event push.Event) (*gatewayv1 return nil, status.Error(codes.Unavailable, "response signer is unavailable") } - return &gatewayv1.GatewayEvent{ + return &edgev1.GatewayEvent{ EventType: event.EventType, EventId: event.EventID, TimestampMs: timestampMS, @@ -169,4 +169,4 @@ func mapSubscriptionOutcome(err error) telemetry.EdgeOutcome { } } -var _ gatewayv1.EdgeGatewayServer = fanOutPushStreamService{} +var _ edgev1.GatewayServer = fanOutPushStreamService{} diff --git a/gateway/internal/grpcapi/push_heartbeat.go b/gateway/internal/grpcapi/push_heartbeat.go index 41ba3cf..9a5a0f2 100644 --- a/gateway/internal/grpcapi/push_heartbeat.go +++ b/gateway/internal/grpcapi/push_heartbeat.go @@ -6,7 +6,7 @@ import ( "time" "galaxy/gateway/internal/telemetry" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" + edgev1 "galaxy/gateway/proto/edge/v1" "go.opentelemetry.io/otel/attribute" "google.golang.org/grpc" @@ -26,7 +26,7 @@ import ( // 15-second default a fully-idle stream costs ~840 KB/day per client; // see `docs/ARCHITECTURE.md` for the per-scale projection. type heartbeatingStream struct { - grpc.ServerStreamingServer[gatewayv1.GatewayEvent] + grpc.ServerStreamingServer[edgev1.GatewayEvent] interval time.Duration metrics *telemetry.Runtime @@ -43,7 +43,7 @@ type heartbeatingStream struct { // the wrapping entirely; non-nil returns must have `Stop()` called once // the stream lifecycle ends. func newHeartbeatingStream( - inner grpc.ServerStreamingServer[gatewayv1.GatewayEvent], + inner grpc.ServerStreamingServer[edgev1.GatewayEvent], interval time.Duration, metrics *telemetry.Runtime, ) *heartbeatingStream { @@ -64,7 +64,7 @@ func newHeartbeatingStream( // so the heartbeat goroutine waits a fresh interval before firing // again. A Send that succeeds means the transport just delivered real // bytes; the silence window restarts from "now". -func (s *heartbeatingStream) Send(event *gatewayv1.GatewayEvent) error { +func (s *heartbeatingStream) Send(event *edgev1.GatewayEvent) error { s.sendMu.Lock() defer s.sendMu.Unlock() if err := s.ServerStreamingServer.Send(event); err != nil { @@ -158,6 +158,6 @@ func (s *heartbeatingStream) resetTimerLocked() { // EventType is left at its proto3 default so the wire frame stays as // small as Connect framing allows. See `gatewayHeartbeatEventType` for // the security rationale of leaving the event unsigned. -func buildHeartbeatEvent() *gatewayv1.GatewayEvent { - return &gatewayv1.GatewayEvent{EventType: gatewayHeartbeatEventType} +func buildHeartbeatEvent() *edgev1.GatewayEvent { + return &edgev1.GatewayEvent{EventType: gatewayHeartbeatEventType} } diff --git a/gateway/internal/grpcapi/push_heartbeat_test.go b/gateway/internal/grpcapi/push_heartbeat_test.go index 9d571f7..33338a3 100644 --- a/gateway/internal/grpcapi/push_heartbeat_test.go +++ b/gateway/internal/grpcapi/push_heartbeat_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" + edgev1 "galaxy/gateway/proto/edge/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -66,7 +66,7 @@ func TestHeartbeatingStreamRealSendResetsSilenceTimer(t *testing.T) { defer ticker.Stop() for range 6 { <-ticker.C - if err := hb.Send(&gatewayv1.GatewayEvent{EventType: "real.event"}); err != nil { + if err := hb.Send(&edgev1.GatewayEvent{EventType: "real.event"}); err != nil { t.Errorf("real Send failed: %v", err) return } @@ -134,16 +134,16 @@ func TestHeartbeatingStreamSendErrorPropagates(t *testing.T) { require.NotNil(t, hb) defer hb.Stop() - err := hb.Send(&gatewayv1.GatewayEvent{EventType: "real.event"}) + err := hb.Send(&edgev1.GatewayEvent{EventType: "real.event"}) require.ErrorIs(t, err, wantErr) } // capturingStream is a minimal grpc.ServerStreamingServer that pushes // every Send into a channel so tests can assert on the wire frame. type capturingStream struct { - grpc.ServerStreamingServer[gatewayv1.GatewayEvent] + grpc.ServerStreamingServer[edgev1.GatewayEvent] - events chan *gatewayv1.GatewayEvent + events chan *edgev1.GatewayEvent sendErr atomic.Pointer[errorBox] } @@ -152,10 +152,10 @@ type errorBox struct{ err error } func newCapturingStream(t *testing.T) *capturingStream { t.Helper() - return &capturingStream{events: make(chan *gatewayv1.GatewayEvent, 16)} + return &capturingStream{events: make(chan *edgev1.GatewayEvent, 16)} } -func (s *capturingStream) Send(event *gatewayv1.GatewayEvent) error { +func (s *capturingStream) Send(event *edgev1.GatewayEvent) error { if box := s.sendErr.Load(); box != nil { return box.err } @@ -172,7 +172,7 @@ func (s *capturingStream) SetTrailer(metadata.MD) {} func (s *capturingStream) SendMsg(any) error { return errors.New("capturingStream.SendMsg: unused") } func (s *capturingStream) RecvMsg(any) error { return errors.New("capturingStream.RecvMsg: unused") } -func (s *capturingStream) recv(t *testing.T, timeout time.Duration) *gatewayv1.GatewayEvent { +func (s *capturingStream) recv(t *testing.T, timeout time.Duration) *edgev1.GatewayEvent { t.Helper() select { diff --git a/gateway/internal/grpcapi/push_stream.go b/gateway/internal/grpcapi/push_stream.go index de7aada..7149191 100644 --- a/gateway/internal/grpcapi/push_stream.go +++ b/gateway/internal/grpcapi/push_stream.go @@ -9,7 +9,7 @@ import ( "galaxy/gateway/authn" "galaxy/gateway/internal/clock" "galaxy/gateway/internal/telemetry" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" + edgev1 "galaxy/gateway/proto/edge/v1" gatewayfbs "galaxy/schema/fbs/gateway" flatbuffers "github.com/google/flatbuffers/go" @@ -81,9 +81,9 @@ func authenticatedStreamBindingFromContext(ctx context.Context) (authenticatedSt // the tail performs and only emits a heartbeat when the silence window // elapses; tails remain heartbeat-unaware. type authenticatedPushStreamService struct { - gatewayv1.UnimplementedEdgeGatewayServer + edgev1.UnimplementedGatewayServer - tailDelegate gatewayv1.EdgeGatewayServer + tailDelegate edgev1.GatewayServer responseSigner authn.ResponseSigner clock clock.Clock heartbeatInterval time.Duration @@ -92,7 +92,7 @@ type authenticatedPushStreamService struct { // SubscribeEvents binds the verified stream identity, sends the initial signed // server-time event, and then delegates the remaining lifecycle. -func (s authenticatedPushStreamService) SubscribeEvents(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { +func (s authenticatedPushStreamService) SubscribeEvents(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error { envelope, ok := parsedEnvelopeFromContext(stream.Context()) if !ok { return status.Error(codes.Internal, "authenticated request context is incomplete") @@ -134,7 +134,7 @@ func (s authenticatedPushStreamService) SubscribeEvents(req *gatewayv1.Subscribe return status.Error(codes.Unavailable, "response signer is unavailable") } - if err := boundStream.Send(&gatewayv1.GatewayEvent{ + if err := boundStream.Send(&edgev1.GatewayEvent{ EventType: serverTimeEventType, EventId: envelope.RequestID, TimestampMs: serverTimeMS, @@ -147,7 +147,7 @@ func (s authenticatedPushStreamService) SubscribeEvents(req *gatewayv1.Subscribe return err } - var streamForTail grpc.ServerStreamingServer[gatewayv1.GatewayEvent] = boundStream + var streamForTail grpc.ServerStreamingServer[edgev1.GatewayEvent] = boundStream if hbStream := newHeartbeatingStream(boundStream, s.heartbeatInterval, s.metrics); hbStream != nil { defer hbStream.Stop() go func() { @@ -165,12 +165,12 @@ func (s authenticatedPushStreamService) SubscribeEvents(req *gatewayv1.Subscribe } func newAuthenticatedPushStreamService( - tailDelegate gatewayv1.EdgeGatewayServer, + tailDelegate edgev1.GatewayServer, responseSigner authn.ResponseSigner, clk clock.Clock, heartbeatInterval time.Duration, metrics *telemetry.Runtime, -) gatewayv1.EdgeGatewayServer { +) edgev1.GatewayServer { if tailDelegate == nil { tailDelegate = holdOpenSubscribeEventsService{} } @@ -197,7 +197,7 @@ func buildServerTimeEventPayload(serverTimeMS int64) []byte { type authenticatedStreamBindingContextKey struct{} type authenticatedStreamContextStream struct { - grpc.ServerStreamingServer[gatewayv1.GatewayEvent] + grpc.ServerStreamingServer[edgev1.GatewayEvent] ctx context.Context } @@ -210,12 +210,12 @@ func (s authenticatedStreamContextStream) Context() context.Context { } type holdOpenSubscribeEventsService struct { - gatewayv1.UnimplementedEdgeGatewayServer + edgev1.UnimplementedGatewayServer } -func (holdOpenSubscribeEventsService) SubscribeEvents(_ *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { +func (holdOpenSubscribeEventsService) SubscribeEvents(_ *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error { <-stream.Context().Done() return stream.Context().Err() } -var _ gatewayv1.EdgeGatewayServer = authenticatedPushStreamService{} +var _ edgev1.GatewayServer = authenticatedPushStreamService{} diff --git a/gateway/internal/grpcapi/rate_limit.go b/gateway/internal/grpcapi/rate_limit.go index 6dd2a0d..d8c88b8 100644 --- a/gateway/internal/grpcapi/rate_limit.go +++ b/gateway/internal/grpcapi/rate_limit.go @@ -7,7 +7,7 @@ import ( "galaxy/gateway/internal/config" "galaxy/gateway/internal/ratelimit" "galaxy/gateway/internal/session" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" + edgev1 "galaxy/gateway/proto/edge/v1" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -102,9 +102,9 @@ type AuthenticatedRequestPolicy interface { } type authenticatedRateLimitService struct { - gatewayv1.UnimplementedEdgeGatewayServer + edgev1.UnimplementedGatewayServer - delegate gatewayv1.EdgeGatewayServer + delegate edgev1.GatewayServer limiter AuthenticatedRequestLimiter policy AuthenticatedRequestPolicy cfg config.AuthenticatedGRPCAntiAbuseConfig @@ -112,7 +112,7 @@ type authenticatedRateLimitService struct { // ExecuteCommand applies authenticated rate limits and edge policy before // delegating to the configured service implementation. -func (s authenticatedRateLimitService) ExecuteCommand(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { +func (s authenticatedRateLimitService) ExecuteCommand(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) { if err := s.applyRateLimitsAndPolicy(ctx, authenticatedRPCExecuteCommand); err != nil { return nil, err } @@ -122,7 +122,7 @@ func (s authenticatedRateLimitService) ExecuteCommand(ctx context.Context, req * // SubscribeEvents applies authenticated rate limits and edge policy before // delegating to the configured service implementation. -func (s authenticatedRateLimitService) SubscribeEvents(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { +func (s authenticatedRateLimitService) SubscribeEvents(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error { if err := s.applyRateLimitsAndPolicy(stream.Context(), authenticatedRPCSubscribeEvents); err != nil { return err } @@ -132,7 +132,7 @@ func (s authenticatedRateLimitService) SubscribeEvents(req *gatewayv1.SubscribeE // newAuthenticatedRateLimitService wraps delegate with the authenticated // rate-limit and edge-policy gate. -func newAuthenticatedRateLimitService(delegate gatewayv1.EdgeGatewayServer, limiter AuthenticatedRequestLimiter, policy AuthenticatedRequestPolicy, cfg config.AuthenticatedGRPCAntiAbuseConfig) gatewayv1.EdgeGatewayServer { +func newAuthenticatedRateLimitService(delegate edgev1.GatewayServer, limiter AuthenticatedRequestLimiter, policy AuthenticatedRequestPolicy, cfg config.AuthenticatedGRPCAntiAbuseConfig) edgev1.GatewayServer { return authenticatedRateLimitService{ delegate: delegate, limiter: limiter, @@ -279,4 +279,4 @@ func (noopAuthenticatedRequestPolicy) Evaluate(context.Context, AuthenticatedReq return nil } -var _ gatewayv1.EdgeGatewayServer = authenticatedRateLimitService{} +var _ edgev1.GatewayServer = authenticatedRateLimitService{} diff --git a/gateway/internal/grpcapi/rate_limit_integration_test.go b/gateway/internal/grpcapi/rate_limit_integration_test.go index 1992642..bf3b175 100644 --- a/gateway/internal/grpcapi/rate_limit_integration_test.go +++ b/gateway/internal/grpcapi/rate_limit_integration_test.go @@ -14,7 +14,7 @@ import ( "galaxy/gateway/internal/ratelimit" "galaxy/gateway/internal/restapi" "galaxy/gateway/internal/session" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" + edgev1 "galaxy/gateway/proto/edge/v1" "connectrpc.com/connect" "github.com/stretchr/testify/assert" @@ -24,7 +24,7 @@ import ( func TestExecuteCommandRateLimitsByIP(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGatewayWithGRPCConfig(t, newAuthenticatedGRPCConfigForTest(func(cfg *config.AuthenticatedGRPCConfig) { cfg.AntiAbuse.IP = config.AuthenticatedRateLimitConfig{ Requests: 1, @@ -54,7 +54,7 @@ func TestExecuteCommandRateLimitsByIP(t *testing.T) { func TestExecuteCommandRateLimitsBySession(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGatewayWithGRPCConfig(t, newAuthenticatedGRPCConfigForTest(func(cfg *config.AuthenticatedGRPCConfig) { cfg.AntiAbuse.Session = config.AuthenticatedRateLimitConfig{ Requests: 1, @@ -87,7 +87,7 @@ func TestExecuteCommandRateLimitsBySession(t *testing.T) { func TestExecuteCommandRateLimitsByUser(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGatewayWithGRPCConfig(t, newAuthenticatedGRPCConfigForTest(func(cfg *config.AuthenticatedGRPCConfig) { cfg.AntiAbuse.User = config.AuthenticatedRateLimitConfig{ Requests: 1, @@ -124,7 +124,7 @@ func TestExecuteCommandRateLimitsByUser(t *testing.T) { func TestExecuteCommandRateLimitsByMessageClass(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGatewayWithGRPCConfig(t, newAuthenticatedGRPCConfigForTest(func(cfg *config.AuthenticatedGRPCConfig) { cfg.AntiAbuse.MessageClass = config.AuthenticatedRateLimitConfig{ Requests: 1, @@ -161,7 +161,7 @@ func TestAuthenticatedPolicyHookReceivesVerifiedRequest(t *testing.T) { t.Parallel() policy := &recordingAuthenticatedRequestPolicy{} - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: userMappedSessionCache(map[string]string{"device-session-123": "user-123"}), @@ -189,7 +189,7 @@ func TestAuthenticatedPolicyHookReceivesVerifiedRequest(t *testing.T) { func TestExecuteCommandPolicyRejectMapsToPermissionDenied(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: userMappedSessionCache(map[string]string{"device-session-123": "user-123"}), @@ -212,7 +212,7 @@ func TestExecuteCommandPolicyRejectMapsToPermissionDenied(t *testing.T) { func TestSubscribeEventsRateLimitRejectsStream(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGatewayWithGRPCConfig(t, newAuthenticatedGRPCConfigForTest(func(cfg *config.AuthenticatedGRPCConfig) { cfg.AntiAbuse.IP = config.AuthenticatedRateLimitConfig{ Requests: 1, @@ -274,7 +274,7 @@ func TestAuthenticatedRateLimitsStayIsolatedFromPublicREST(t *testing.T) { AuthService: staticAuthServiceClient{}, Limiter: publicLimiterAdapter{limiter: sharedLimiter}, }) - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} grpcServer := NewServer(grpcCfg, ServerDependencies{ Service: delegate, Router: executeCommandAdapterRouter{service: delegate}, @@ -342,7 +342,7 @@ func newAuthenticatedGRPCConfigForTest(mutate func(*config.AuthenticatedGRPCConf return cfg } -func newValidExecuteCommandRequestWithMessageType(deviceSessionID string, requestID string, messageType string) *gatewayv1.ExecuteCommandRequest { +func newValidExecuteCommandRequestWithMessageType(deviceSessionID string, requestID string, messageType string) *edgev1.ExecuteCommandRequest { req := newValidExecuteCommandRequestWithSessionAndRequestID(deviceSessionID, requestID) req.MessageType = messageType req.Signature = signRequest( diff --git a/gateway/internal/grpcapi/server.go b/gateway/internal/grpcapi/server.go index d3dc748..33ec375 100644 --- a/gateway/internal/grpcapi/server.go +++ b/gateway/internal/grpcapi/server.go @@ -24,8 +24,8 @@ import ( "galaxy/gateway/internal/replay" "galaxy/gateway/internal/session" "galaxy/gateway/internal/telemetry" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" - "galaxy/gateway/proto/galaxy/gateway/v1/gatewayv1connect" + edgev1 "galaxy/gateway/proto/edge/v1" + "galaxy/gateway/proto/edge/v1/edgev1connect" "connectrpc.com/connect" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" @@ -42,7 +42,7 @@ type ServerDependencies struct { // after the initial authenticated service event has been sent. When nil, the // gateway keeps authenticated SubscribeEvents streams open until the client // cancels them, the server shuts down, or a later stream send fails. - Service gatewayv1.EdgeGatewayServer + Service edgev1.GatewayServer // Router resolves the exact downstream unary client for the verified // message_type value. When nil, the authenticated unary surface uses an @@ -93,7 +93,7 @@ type ServerDependencies struct { // single net/http listener. type Server struct { cfg config.AuthenticatedGRPCConfig - service gatewayv1.EdgeGatewayServer + service edgev1.GatewayServer logger *zap.Logger pushHub *push.Hub metrics *telemetry.Runtime @@ -169,7 +169,7 @@ func (s *Server) Run(ctx context.Context) error { mux := http.NewServeMux() connectHandler := newConnectEdgeAdapter(s.service) - path, handler := gatewayv1connect.NewEdgeGatewayHandler( + path, handler := edgev1connect.NewGatewayHandler( connectHandler, connect.WithInterceptors(observabilityConnectInterceptor(s.logger, s.metrics)), ) diff --git a/gateway/internal/grpcapi/server_test.go b/gateway/internal/grpcapi/server_test.go index 2dcdc57..778a28a 100644 --- a/gateway/internal/grpcapi/server_test.go +++ b/gateway/internal/grpcapi/server_test.go @@ -12,8 +12,8 @@ import ( "galaxy/gateway/internal/app" "galaxy/gateway/internal/config" "galaxy/gateway/internal/session" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" - "galaxy/gateway/proto/galaxy/gateway/v1/gatewayv1connect" + edgev1 "galaxy/gateway/proto/edge/v1" + "galaxy/gateway/proto/edge/v1/edgev1connect" "connectrpc.com/connect" "github.com/stretchr/testify/assert" @@ -30,7 +30,7 @@ func TestExecuteCommandRejectsMalformedEnvelope(t *testing.T) { addr := waitForListenAddr(t, server) client := newEdgeClient(t, addr) - _, err := client.ExecuteCommand(context.Background(), connect.NewRequest(&gatewayv1.ExecuteCommandRequest{})) + _, err := client.ExecuteCommand(context.Background(), connect.NewRequest(&edgev1.ExecuteCommandRequest{})) require.Error(t, err) assert.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err)) } @@ -44,7 +44,7 @@ func TestSubscribeEventsRejectsMalformedEnvelope(t *testing.T) { addr := waitForListenAddr(t, server) client := newEdgeClient(t, addr) - err := subscribeEventsError(t, context.Background(), client, &gatewayv1.SubscribeEventsRequest{}) + err := subscribeEventsError(t, context.Background(), client, &edgev1.SubscribeEventsRequest{}) require.Error(t, err) assert.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err)) } @@ -58,7 +58,7 @@ func TestExecuteCommandRejectsUnsupportedProtocolVersion(t *testing.T) { addr := waitForListenAddr(t, server) client := newEdgeClient(t, addr) - _, err := client.ExecuteCommand(context.Background(), connect.NewRequest(&gatewayv1.ExecuteCommandRequest{ + _, err := client.ExecuteCommand(context.Background(), connect.NewRequest(&edgev1.ExecuteCommandRequest{ ProtocolVersion: "v2", DeviceSessionId: "device-session-123", MessageType: "fleet.move", @@ -418,7 +418,7 @@ func waitForListenAddr(t *testing.T, server *Server) string { // authenticated edge listener. AllowHTTP forces the client to issue plain // HTTP/2 requests (h2c) instead of attempting TLS, which the gateway's // in-process test bootstrap does not configure. -func newEdgeClient(t *testing.T, addr string) gatewayv1connect.EdgeGatewayClient { +func newEdgeClient(t *testing.T, addr string) edgev1connect.GatewayClient { t.Helper() httpClient := &http.Client{ @@ -429,7 +429,7 @@ func newEdgeClient(t *testing.T, addr string) gatewayv1connect.EdgeGatewayClient }, }, } - return gatewayv1connect.NewEdgeGatewayClient(httpClient, "http://"+addr) + return edgev1connect.NewGatewayClient(httpClient, "http://"+addr) } // connectErrorMessage extracts the *connect.Error message from err. It diff --git a/gateway/internal/grpcapi/session_lookup.go b/gateway/internal/grpcapi/session_lookup.go index 64c7ed1..dddc611 100644 --- a/gateway/internal/grpcapi/session_lookup.go +++ b/gateway/internal/grpcapi/session_lookup.go @@ -5,7 +5,7 @@ import ( "errors" "galaxy/gateway/internal/session" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" + edgev1 "galaxy/gateway/proto/edge/v1" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -30,15 +30,15 @@ func resolvedSessionFromContext(ctx context.Context) (session.Record, bool) { // sessionLookupService resolves the authenticated session from SessionCache // after envelope parsing succeeds and before later auth steps run. type sessionLookupService struct { - gatewayv1.UnimplementedEdgeGatewayServer + edgev1.UnimplementedGatewayServer - delegate gatewayv1.EdgeGatewayServer + delegate edgev1.GatewayServer cache session.Cache } // ExecuteCommand resolves the cached session for req and only then forwards it // to the configured delegate with the resolved session attached to ctx. -func (s sessionLookupService) ExecuteCommand(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { +func (s sessionLookupService) ExecuteCommand(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) { record, err := s.lookupSession(ctx) if err != nil { return nil, err @@ -50,7 +50,7 @@ func (s sessionLookupService) ExecuteCommand(ctx context.Context, req *gatewayv1 // SubscribeEvents resolves the cached session for req and only then forwards it // to the configured delegate with the resolved session attached to the stream // context. -func (s sessionLookupService) SubscribeEvents(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { +func (s sessionLookupService) SubscribeEvents(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error { record, err := s.lookupSession(stream.Context()) if err != nil { return err @@ -63,7 +63,7 @@ func (s sessionLookupService) SubscribeEvents(req *gatewayv1.SubscribeEventsRequ } // newSessionLookupService wraps delegate with the session-cache lookup gate. -func newSessionLookupService(delegate gatewayv1.EdgeGatewayServer, cache session.Cache) gatewayv1.EdgeGatewayServer { +func newSessionLookupService(delegate edgev1.GatewayServer, cache session.Cache) edgev1.GatewayServer { return sessionLookupService{ delegate: delegate, cache: cache, @@ -105,7 +105,7 @@ func cloneSessionRecord(record session.Record) session.Record { type resolvedSessionContextKey struct{} type resolvedSessionContextStream struct { - grpc.ServerStreamingServer[gatewayv1.GatewayEvent] + grpc.ServerStreamingServer[edgev1.GatewayEvent] ctx context.Context } @@ -126,4 +126,4 @@ func (unavailableSessionCache) Lookup(context.Context, string) (session.Record, func (unavailableSessionCache) MarkRevoked(string) {} func (unavailableSessionCache) MarkAllRevokedForUser(string) {} -var _ gatewayv1.EdgeGatewayServer = sessionLookupService{} +var _ edgev1.GatewayServer = sessionLookupService{} diff --git a/gateway/internal/grpcapi/session_lookup_integration_test.go b/gateway/internal/grpcapi/session_lookup_integration_test.go index 8f11452..3f0a846 100644 --- a/gateway/internal/grpcapi/session_lookup_integration_test.go +++ b/gateway/internal/grpcapi/session_lookup_integration_test.go @@ -6,7 +6,7 @@ import ( "testing" "galaxy/gateway/internal/session" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" + edgev1 "galaxy/gateway/proto/edge/v1" "connectrpc.com/connect" "github.com/stretchr/testify/assert" @@ -17,7 +17,7 @@ import ( func TestExecuteCommandRejectsUnknownSession(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{ @@ -40,7 +40,7 @@ func TestExecuteCommandRejectsUnknownSession(t *testing.T) { func TestSubscribeEventsRejectsUnknownSession(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{ @@ -63,7 +63,7 @@ func TestSubscribeEventsRejectsUnknownSession(t *testing.T) { func TestExecuteCommandRejectsRevokedSession(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newRevokedSessionRecord(), nil }}, @@ -82,7 +82,7 @@ func TestExecuteCommandRejectsRevokedSession(t *testing.T) { func TestSubscribeEventsRejectsRevokedSession(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newRevokedSessionRecord(), nil }}, @@ -101,7 +101,7 @@ func TestSubscribeEventsRejectsRevokedSession(t *testing.T) { func TestExecuteCommandRejectsSessionCacheUnavailable(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{ @@ -124,7 +124,7 @@ func TestExecuteCommandRejectsSessionCacheUnavailable(t *testing.T) { func TestSubscribeEventsRejectsSessionCacheUnavailable(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{ @@ -147,12 +147,12 @@ func TestSubscribeEventsRejectsSessionCacheUnavailable(t *testing.T) { func TestExecuteCommandAttachesResolvedSession(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{ - executeCommandFunc: func(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { + delegate := &recordingGatewayService{ + executeCommandFunc: func(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) { record, ok := resolvedSessionFromContext(ctx) require.True(t, ok) assert.Equal(t, newActiveSessionRecord(), record) - return &gatewayv1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil + return &edgev1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil }, } @@ -173,8 +173,8 @@ func TestExecuteCommandAttachesResolvedSession(t *testing.T) { func TestSubscribeEventsAttachesResolvedSession(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{ - subscribeEventsFunc: func(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { + delegate := &recordingGatewayService{ + subscribeEventsFunc: func(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error { record, ok := resolvedSessionFromContext(stream.Context()) require.True(t, ok) assert.Equal(t, newActiveSessionRecord(), record) @@ -204,8 +204,8 @@ func TestSubscribeEventsAttachesResolvedSession(t *testing.T) { func TestSubscribeEventsAttachesAuthenticatedStreamBinding(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{ - subscribeEventsFunc: func(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { + delegate := &recordingGatewayService{ + subscribeEventsFunc: func(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error { binding, ok := authenticatedStreamBindingFromContext(stream.Context()) require.True(t, ok) assert.Equal(t, authenticatedStreamBinding{ diff --git a/gateway/internal/grpcapi/signature.go b/gateway/internal/grpcapi/signature.go index 838b173..f92eb04 100644 --- a/gateway/internal/grpcapi/signature.go +++ b/gateway/internal/grpcapi/signature.go @@ -5,7 +5,7 @@ import ( "errors" "galaxy/gateway/authn" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" + edgev1 "galaxy/gateway/proto/edge/v1" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -15,14 +15,14 @@ import ( // signatureVerifyingService applies client-signature verification after // payload integrity checks and before later auth or routing steps run. type signatureVerifyingService struct { - gatewayv1.UnimplementedEdgeGatewayServer + edgev1.UnimplementedGatewayServer - delegate gatewayv1.EdgeGatewayServer + delegate edgev1.GatewayServer } // ExecuteCommand verifies req client signature before delegating to the // configured service implementation. -func (s signatureVerifyingService) ExecuteCommand(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { +func (s signatureVerifyingService) ExecuteCommand(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) { if err := verifyRequestSignature(ctx); err != nil { return nil, err } @@ -32,7 +32,7 @@ func (s signatureVerifyingService) ExecuteCommand(ctx context.Context, req *gate // SubscribeEvents verifies req client signature before delegating to the // configured service implementation. -func (s signatureVerifyingService) SubscribeEvents(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { +func (s signatureVerifyingService) SubscribeEvents(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error { if err := verifyRequestSignature(stream.Context()); err != nil { return err } @@ -42,7 +42,7 @@ func (s signatureVerifyingService) SubscribeEvents(req *gatewayv1.SubscribeEvent // newSignatureVerifyingService wraps delegate with the client-signature // verification gate. -func newSignatureVerifyingService(delegate gatewayv1.EdgeGatewayServer) gatewayv1.EdgeGatewayServer { +func newSignatureVerifyingService(delegate edgev1.GatewayServer) edgev1.GatewayServer { return signatureVerifyingService{delegate: delegate} } @@ -77,4 +77,4 @@ func verifyRequestSignature(ctx context.Context) error { } } -var _ gatewayv1.EdgeGatewayServer = signatureVerifyingService{} +var _ edgev1.GatewayServer = signatureVerifyingService{} diff --git a/gateway/internal/grpcapi/signature_integration_test.go b/gateway/internal/grpcapi/signature_integration_test.go index 4dce842..c22b4b4 100644 --- a/gateway/internal/grpcapi/signature_integration_test.go +++ b/gateway/internal/grpcapi/signature_integration_test.go @@ -14,7 +14,7 @@ import ( func TestExecuteCommandRejectsInvalidSignature(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, @@ -37,7 +37,7 @@ func TestExecuteCommandRejectsInvalidSignature(t *testing.T) { func TestExecuteCommandRejectsWrongKey(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{ @@ -62,7 +62,7 @@ func TestExecuteCommandRejectsWrongKey(t *testing.T) { func TestExecuteCommandRejectsInvalidCachedPublicKey(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{ @@ -87,7 +87,7 @@ func TestExecuteCommandRejectsInvalidCachedPublicKey(t *testing.T) { func TestSubscribeEventsRejectsInvalidSignature(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, @@ -110,7 +110,7 @@ func TestSubscribeEventsRejectsInvalidSignature(t *testing.T) { func TestSubscribeEventsRejectsWrongKey(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{ @@ -135,7 +135,7 @@ func TestSubscribeEventsRejectsWrongKey(t *testing.T) { func TestSubscribeEventsRejectsInvalidCachedPublicKey(t *testing.T) { t.Parallel() - delegate := &recordingEdgeGatewayService{} + delegate := &recordingGatewayService{} server, runGateway := newTestGateway(t, ServerDependencies{ Service: delegate, SessionCache: staticSessionCache{ diff --git a/gateway/internal/grpcapi/test_fixtures_test.go b/gateway/internal/grpcapi/test_fixtures_test.go index 3512e0e..890093d 100644 --- a/gateway/internal/grpcapi/test_fixtures_test.go +++ b/gateway/internal/grpcapi/test_fixtures_test.go @@ -13,8 +13,8 @@ import ( "galaxy/gateway/authn" "galaxy/gateway/internal/downstream" "galaxy/gateway/internal/session" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" - "galaxy/gateway/proto/galaxy/gateway/v1/gatewayv1connect" + edgev1 "galaxy/gateway/proto/edge/v1" + "galaxy/gateway/proto/edge/v1/edgev1connect" gatewayfbs "galaxy/schema/fbs/gateway" @@ -29,19 +29,19 @@ var ( testFreshnessWindow = 5 * time.Minute ) -func newValidExecuteCommandRequest() *gatewayv1.ExecuteCommandRequest { +func newValidExecuteCommandRequest() *edgev1.ExecuteCommandRequest { return newValidExecuteCommandRequestWithSessionAndRequestID("device-session-123", "request-123") } -func newValidExecuteCommandRequestWithSessionAndRequestID(deviceSessionID string, requestID string) *gatewayv1.ExecuteCommandRequest { +func newValidExecuteCommandRequestWithSessionAndRequestID(deviceSessionID string, requestID string) *edgev1.ExecuteCommandRequest { return newValidExecuteCommandRequestWithTimestamp(deviceSessionID, requestID, testCurrentTime.UnixMilli()) } -func newValidExecuteCommandRequestWithTimestamp(deviceSessionID string, requestID string, timestampMS int64) *gatewayv1.ExecuteCommandRequest { +func newValidExecuteCommandRequestWithTimestamp(deviceSessionID string, requestID string, timestampMS int64) *edgev1.ExecuteCommandRequest { payloadBytes := []byte("payload") payloadHash := sha256.Sum256(payloadBytes) - req := &gatewayv1.ExecuteCommandRequest{ + req := &edgev1.ExecuteCommandRequest{ ProtocolVersion: supportedProtocolVersion, DeviceSessionId: deviceSessionID, MessageType: "fleet.move", @@ -56,18 +56,18 @@ func newValidExecuteCommandRequestWithTimestamp(deviceSessionID string, requestI return req } -func newValidSubscribeEventsRequest() *gatewayv1.SubscribeEventsRequest { +func newValidSubscribeEventsRequest() *edgev1.SubscribeEventsRequest { return newValidSubscribeEventsRequestWithSessionAndRequestID("device-session-123", "request-123") } -func newValidSubscribeEventsRequestWithSessionAndRequestID(deviceSessionID string, requestID string) *gatewayv1.SubscribeEventsRequest { +func newValidSubscribeEventsRequestWithSessionAndRequestID(deviceSessionID string, requestID string) *edgev1.SubscribeEventsRequest { return newValidSubscribeEventsRequestWithTimestamp(deviceSessionID, requestID, testCurrentTime.UnixMilli()) } -func newValidSubscribeEventsRequestWithTimestamp(deviceSessionID string, requestID string, timestampMS int64) *gatewayv1.SubscribeEventsRequest { +func newValidSubscribeEventsRequestWithTimestamp(deviceSessionID string, requestID string, timestampMS int64) *edgev1.SubscribeEventsRequest { payloadHash := sha256.Sum256(nil) - req := &gatewayv1.SubscribeEventsRequest{ + req := &edgev1.SubscribeEventsRequest{ ProtocolVersion: supportedProtocolVersion, DeviceSessionId: deviceSessionID, MessageType: "gateway.subscribe", @@ -172,7 +172,7 @@ func (c fixedClock) Now() time.Time { func recvBootstrapEvent(t interface { require.TestingT Helper() -}, stream *connect.ServerStreamForClient[gatewayv1.GatewayEvent]) *gatewayv1.GatewayEvent { +}, stream *connect.ServerStreamForClient[edgev1.GatewayEvent]) *edgev1.GatewayEvent { t.Helper() if !stream.Receive() { @@ -189,7 +189,7 @@ func recvBootstrapEvent(t interface { func subscribeEventsError(t interface { require.TestingT Helper() -}, ctx context.Context, client gatewayv1connect.EdgeGatewayClient, req *gatewayv1.SubscribeEventsRequest) error { +}, ctx context.Context, client edgev1connect.GatewayClient, req *edgev1.SubscribeEventsRequest) error { t.Helper() stream, err := client.SubscribeEvents(ctx, connect.NewRequest(req)) @@ -208,7 +208,7 @@ func subscribeEventsError(t interface { func assertServerTimeBootstrapEvent(t interface { require.TestingT Helper() -}, event *gatewayv1.GatewayEvent, publicKey ed25519.PublicKey, wantRequestID string, wantTraceID string, wantTimestampMS int64) { +}, event *edgev1.GatewayEvent, publicKey ed25519.PublicKey, wantRequestID string, wantTraceID string, wantTimestampMS int64) { t.Helper() require.NotNil(t, event) @@ -244,7 +244,7 @@ func (s staticReplayStore) Reserve(ctx context.Context, deviceSessionID string, } type executeCommandAdapterRouter struct { - service gatewayv1.EdgeGatewayServer + service edgev1.GatewayServer } func (r executeCommandAdapterRouter) Route(string) (downstream.Client, error) { @@ -252,11 +252,11 @@ func (r executeCommandAdapterRouter) Route(string) (downstream.Client, error) { } type executeCommandAdapterClient struct { - service gatewayv1.EdgeGatewayServer + service edgev1.GatewayServer } func (c executeCommandAdapterClient) ExecuteCommand(ctx context.Context, command downstream.AuthenticatedCommand) (downstream.UnaryResult, error) { - response, err := c.service.ExecuteCommand(ctx, &gatewayv1.ExecuteCommandRequest{ + response, err := c.service.ExecuteCommand(ctx, &edgev1.ExecuteCommandRequest{ ProtocolVersion: command.ProtocolVersion, DeviceSessionId: command.DeviceSessionID, MessageType: command.MessageType, diff --git a/gateway/openapi.yaml b/gateway/openapi.yaml index d829135..6dc7602 100644 --- a/gateway/openapi.yaml +++ b/gateway/openapi.yaml @@ -6,6 +6,14 @@ info: This specification documents the implemented `galaxy/gateway` v1 public REST surface. + At the edge this surface is served same-origin under one host: the edge + Caddy routes `/api/*` and `/healthz` to the public REST listener, so those + paths are reached at `https:///healthz` and + `https:///api/v1/public/auth/*`. The authenticated Connect/gRPC-Web + surface lives behind the same host at `/rpc/*` and is not part of this + REST contract. `/readyz` is an in-process readiness probe served on the + listener itself and is not exposed through the edge. + Implemented endpoints: - `GET /healthz` - `GET /readyz` diff --git a/gateway/proto/galaxy/gateway/v1/edge_gateway.pb.go b/gateway/proto/edge/v1/edge_gateway.pb.go similarity index 80% rename from gateway/proto/galaxy/gateway/v1/edge_gateway.pb.go rename to gateway/proto/edge/v1/edge_gateway.pb.go index a4861f9..98d17b3 100644 --- a/gateway/proto/galaxy/gateway/v1/edge_gateway.pb.go +++ b/gateway/proto/edge/v1/edge_gateway.pb.go @@ -2,9 +2,9 @@ // versions: // protoc-gen-go v1.36.11 // protoc (unknown) -// source: galaxy/gateway/v1/edge_gateway.proto +// source: edge/v1/edge_gateway.proto -package gatewayv1 +package edgev1 import ( _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" @@ -42,7 +42,7 @@ type ExecuteCommandRequest struct { func (x *ExecuteCommandRequest) Reset() { *x = ExecuteCommandRequest{} - mi := &file_galaxy_gateway_v1_edge_gateway_proto_msgTypes[0] + mi := &file_edge_v1_edge_gateway_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -54,7 +54,7 @@ func (x *ExecuteCommandRequest) String() string { func (*ExecuteCommandRequest) ProtoMessage() {} func (x *ExecuteCommandRequest) ProtoReflect() protoreflect.Message { - mi := &file_galaxy_gateway_v1_edge_gateway_proto_msgTypes[0] + mi := &file_edge_v1_edge_gateway_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -67,7 +67,7 @@ func (x *ExecuteCommandRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecuteCommandRequest.ProtoReflect.Descriptor instead. func (*ExecuteCommandRequest) Descriptor() ([]byte, []int) { - return file_galaxy_gateway_v1_edge_gateway_proto_rawDescGZIP(), []int{0} + return file_edge_v1_edge_gateway_proto_rawDescGZIP(), []int{0} } func (x *ExecuteCommandRequest) GetProtocolVersion() string { @@ -148,7 +148,7 @@ type ExecuteCommandResponse struct { func (x *ExecuteCommandResponse) Reset() { *x = ExecuteCommandResponse{} - mi := &file_galaxy_gateway_v1_edge_gateway_proto_msgTypes[1] + mi := &file_edge_v1_edge_gateway_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -160,7 +160,7 @@ func (x *ExecuteCommandResponse) String() string { func (*ExecuteCommandResponse) ProtoMessage() {} func (x *ExecuteCommandResponse) ProtoReflect() protoreflect.Message { - mi := &file_galaxy_gateway_v1_edge_gateway_proto_msgTypes[1] + mi := &file_edge_v1_edge_gateway_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -173,7 +173,7 @@ func (x *ExecuteCommandResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecuteCommandResponse.ProtoReflect.Descriptor instead. func (*ExecuteCommandResponse) Descriptor() ([]byte, []int) { - return file_galaxy_gateway_v1_edge_gateway_proto_rawDescGZIP(), []int{1} + return file_edge_v1_edge_gateway_proto_rawDescGZIP(), []int{1} } func (x *ExecuteCommandResponse) GetProtocolVersion() string { @@ -246,7 +246,7 @@ type SubscribeEventsRequest struct { func (x *SubscribeEventsRequest) Reset() { *x = SubscribeEventsRequest{} - mi := &file_galaxy_gateway_v1_edge_gateway_proto_msgTypes[2] + mi := &file_edge_v1_edge_gateway_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -258,7 +258,7 @@ func (x *SubscribeEventsRequest) String() string { func (*SubscribeEventsRequest) ProtoMessage() {} func (x *SubscribeEventsRequest) ProtoReflect() protoreflect.Message { - mi := &file_galaxy_gateway_v1_edge_gateway_proto_msgTypes[2] + mi := &file_edge_v1_edge_gateway_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -271,7 +271,7 @@ func (x *SubscribeEventsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubscribeEventsRequest.ProtoReflect.Descriptor instead. func (*SubscribeEventsRequest) Descriptor() ([]byte, []int) { - return file_galaxy_gateway_v1_edge_gateway_proto_rawDescGZIP(), []int{2} + return file_edge_v1_edge_gateway_proto_rawDescGZIP(), []int{2} } func (x *SubscribeEventsRequest) GetProtocolVersion() string { @@ -353,7 +353,7 @@ type GatewayEvent struct { func (x *GatewayEvent) Reset() { *x = GatewayEvent{} - mi := &file_galaxy_gateway_v1_edge_gateway_proto_msgTypes[3] + mi := &file_edge_v1_edge_gateway_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -365,7 +365,7 @@ func (x *GatewayEvent) String() string { func (*GatewayEvent) ProtoMessage() {} func (x *GatewayEvent) ProtoReflect() protoreflect.Message { - mi := &file_galaxy_gateway_v1_edge_gateway_proto_msgTypes[3] + mi := &file_edge_v1_edge_gateway_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -378,7 +378,7 @@ func (x *GatewayEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayEvent.ProtoReflect.Descriptor instead. func (*GatewayEvent) Descriptor() ([]byte, []int) { - return file_galaxy_gateway_v1_edge_gateway_proto_rawDescGZIP(), []int{3} + return file_edge_v1_edge_gateway_proto_rawDescGZIP(), []int{3} } func (x *GatewayEvent) GetEventType() string { @@ -437,11 +437,11 @@ func (x *GatewayEvent) GetTraceId() string { return "" } -var File_galaxy_gateway_v1_edge_gateway_proto protoreflect.FileDescriptor +var File_edge_v1_edge_gateway_proto protoreflect.FileDescriptor -const file_galaxy_gateway_v1_edge_gateway_proto_rawDesc = "" + +const file_edge_v1_edge_gateway_proto_rawDesc = "" + "\n" + - "$galaxy/gateway/v1/edge_gateway.proto\x12\x11galaxy.gateway.v1\x1a\x1bbuf/validate/validate.proto\"\x9c\x03\n" + + "\x1aedge/v1/edge_gateway.proto\x12\aedge.v1\x1a\x1bbuf/validate/validate.proto\"\x9c\x03\n" + "\x15ExecuteCommandRequest\x122\n" + "\x10protocol_version\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x0fprotocolVersion\x123\n" + "\x11device_session_id\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x0fdeviceSessionId\x12*\n" + @@ -484,35 +484,35 @@ const file_galaxy_gateway_v1_edge_gateway_proto_rawDesc = "" + "\tsignature\x18\x06 \x01(\fR\tsignature\x12\x1d\n" + "\n" + "request_id\x18\a \x01(\tR\trequestId\x12\x19\n" + - "\btrace_id\x18\b \x01(\tR\atraceId2\xd5\x01\n" + - "\vEdgeGateway\x12e\n" + - "\x0eExecuteCommand\x12(.galaxy.gateway.v1.ExecuteCommandRequest\x1a).galaxy.gateway.v1.ExecuteCommandResponse\x12_\n" + - "\x0fSubscribeEvents\x12).galaxy.gateway.v1.SubscribeEventsRequest\x1a\x1f.galaxy.gateway.v1.GatewayEvent0\x01B2Z0galaxy/gateway/proto/galaxy/gateway/v1;gatewayv1b\x06proto3" + "\btrace_id\x18\b \x01(\tR\atraceId2\xa9\x01\n" + + "\aGateway\x12Q\n" + + "\x0eExecuteCommand\x12\x1e.edge.v1.ExecuteCommandRequest\x1a\x1f.edge.v1.ExecuteCommandResponse\x12K\n" + + "\x0fSubscribeEvents\x12\x1f.edge.v1.SubscribeEventsRequest\x1a\x15.edge.v1.GatewayEvent0\x01B%Z#galaxy/gateway/proto/edge/v1;edgev1b\x06proto3" var ( - file_galaxy_gateway_v1_edge_gateway_proto_rawDescOnce sync.Once - file_galaxy_gateway_v1_edge_gateway_proto_rawDescData []byte + file_edge_v1_edge_gateway_proto_rawDescOnce sync.Once + file_edge_v1_edge_gateway_proto_rawDescData []byte ) -func file_galaxy_gateway_v1_edge_gateway_proto_rawDescGZIP() []byte { - file_galaxy_gateway_v1_edge_gateway_proto_rawDescOnce.Do(func() { - file_galaxy_gateway_v1_edge_gateway_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_galaxy_gateway_v1_edge_gateway_proto_rawDesc), len(file_galaxy_gateway_v1_edge_gateway_proto_rawDesc))) +func file_edge_v1_edge_gateway_proto_rawDescGZIP() []byte { + file_edge_v1_edge_gateway_proto_rawDescOnce.Do(func() { + file_edge_v1_edge_gateway_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_edge_v1_edge_gateway_proto_rawDesc), len(file_edge_v1_edge_gateway_proto_rawDesc))) }) - return file_galaxy_gateway_v1_edge_gateway_proto_rawDescData + return file_edge_v1_edge_gateway_proto_rawDescData } -var file_galaxy_gateway_v1_edge_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 4) -var file_galaxy_gateway_v1_edge_gateway_proto_goTypes = []any{ - (*ExecuteCommandRequest)(nil), // 0: galaxy.gateway.v1.ExecuteCommandRequest - (*ExecuteCommandResponse)(nil), // 1: galaxy.gateway.v1.ExecuteCommandResponse - (*SubscribeEventsRequest)(nil), // 2: galaxy.gateway.v1.SubscribeEventsRequest - (*GatewayEvent)(nil), // 3: galaxy.gateway.v1.GatewayEvent +var file_edge_v1_edge_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_edge_v1_edge_gateway_proto_goTypes = []any{ + (*ExecuteCommandRequest)(nil), // 0: edge.v1.ExecuteCommandRequest + (*ExecuteCommandResponse)(nil), // 1: edge.v1.ExecuteCommandResponse + (*SubscribeEventsRequest)(nil), // 2: edge.v1.SubscribeEventsRequest + (*GatewayEvent)(nil), // 3: edge.v1.GatewayEvent } -var file_galaxy_gateway_v1_edge_gateway_proto_depIdxs = []int32{ - 0, // 0: galaxy.gateway.v1.EdgeGateway.ExecuteCommand:input_type -> galaxy.gateway.v1.ExecuteCommandRequest - 2, // 1: galaxy.gateway.v1.EdgeGateway.SubscribeEvents:input_type -> galaxy.gateway.v1.SubscribeEventsRequest - 1, // 2: galaxy.gateway.v1.EdgeGateway.ExecuteCommand:output_type -> galaxy.gateway.v1.ExecuteCommandResponse - 3, // 3: galaxy.gateway.v1.EdgeGateway.SubscribeEvents:output_type -> galaxy.gateway.v1.GatewayEvent +var file_edge_v1_edge_gateway_proto_depIdxs = []int32{ + 0, // 0: edge.v1.Gateway.ExecuteCommand:input_type -> edge.v1.ExecuteCommandRequest + 2, // 1: edge.v1.Gateway.SubscribeEvents:input_type -> edge.v1.SubscribeEventsRequest + 1, // 2: edge.v1.Gateway.ExecuteCommand:output_type -> edge.v1.ExecuteCommandResponse + 3, // 3: edge.v1.Gateway.SubscribeEvents:output_type -> edge.v1.GatewayEvent 2, // [2:4] is the sub-list for method output_type 0, // [0:2] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name @@ -520,26 +520,26 @@ var file_galaxy_gateway_v1_edge_gateway_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for field type_name } -func init() { file_galaxy_gateway_v1_edge_gateway_proto_init() } -func file_galaxy_gateway_v1_edge_gateway_proto_init() { - if File_galaxy_gateway_v1_edge_gateway_proto != nil { +func init() { file_edge_v1_edge_gateway_proto_init() } +func file_edge_v1_edge_gateway_proto_init() { + if File_edge_v1_edge_gateway_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_galaxy_gateway_v1_edge_gateway_proto_rawDesc), len(file_galaxy_gateway_v1_edge_gateway_proto_rawDesc)), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_edge_v1_edge_gateway_proto_rawDesc), len(file_edge_v1_edge_gateway_proto_rawDesc)), NumEnums: 0, NumMessages: 4, NumExtensions: 0, NumServices: 1, }, - GoTypes: file_galaxy_gateway_v1_edge_gateway_proto_goTypes, - DependencyIndexes: file_galaxy_gateway_v1_edge_gateway_proto_depIdxs, - MessageInfos: file_galaxy_gateway_v1_edge_gateway_proto_msgTypes, + GoTypes: file_edge_v1_edge_gateway_proto_goTypes, + DependencyIndexes: file_edge_v1_edge_gateway_proto_depIdxs, + MessageInfos: file_edge_v1_edge_gateway_proto_msgTypes, }.Build() - File_galaxy_gateway_v1_edge_gateway_proto = out.File - file_galaxy_gateway_v1_edge_gateway_proto_goTypes = nil - file_galaxy_gateway_v1_edge_gateway_proto_depIdxs = nil + File_edge_v1_edge_gateway_proto = out.File + file_edge_v1_edge_gateway_proto_goTypes = nil + file_edge_v1_edge_gateway_proto_depIdxs = nil } diff --git a/gateway/proto/galaxy/gateway/v1/edge_gateway.proto b/gateway/proto/edge/v1/edge_gateway.proto similarity index 95% rename from gateway/proto/galaxy/gateway/v1/edge_gateway.proto rename to gateway/proto/edge/v1/edge_gateway.proto index 56ecd9e..960c784 100644 --- a/gateway/proto/galaxy/gateway/v1/edge_gateway.proto +++ b/gateway/proto/edge/v1/edge_gateway.proto @@ -1,12 +1,12 @@ syntax = "proto3"; -package galaxy.gateway.v1; +package edge.v1; -option go_package = "galaxy/gateway/proto/galaxy/gateway/v1;gatewayv1"; +option go_package = "galaxy/gateway/proto/edge/v1;edgev1"; import "buf/validate/validate.proto"; -service EdgeGateway { +service Gateway { rpc ExecuteCommand(ExecuteCommandRequest) returns (ExecuteCommandResponse); rpc SubscribeEvents(SubscribeEventsRequest) returns (stream GatewayEvent); } diff --git a/gateway/proto/galaxy/gateway/v1/edge_gateway_grpc.pb.go b/gateway/proto/edge/v1/edge_gateway_grpc.pb.go similarity index 50% rename from gateway/proto/galaxy/gateway/v1/edge_gateway_grpc.pb.go rename to gateway/proto/edge/v1/edge_gateway_grpc.pb.go index efe4b2b..1071a61 100644 --- a/gateway/proto/galaxy/gateway/v1/edge_gateway_grpc.pb.go +++ b/gateway/proto/edge/v1/edge_gateway_grpc.pb.go @@ -2,9 +2,9 @@ // versions: // - protoc-gen-go-grpc v1.6.1 // - protoc (unknown) -// source: galaxy/gateway/v1/edge_gateway.proto +// source: edge/v1/edge_gateway.proto -package gatewayv1 +package edgev1 import ( context "context" @@ -19,39 +19,39 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - EdgeGateway_ExecuteCommand_FullMethodName = "/galaxy.gateway.v1.EdgeGateway/ExecuteCommand" - EdgeGateway_SubscribeEvents_FullMethodName = "/galaxy.gateway.v1.EdgeGateway/SubscribeEvents" + Gateway_ExecuteCommand_FullMethodName = "/edge.v1.Gateway/ExecuteCommand" + Gateway_SubscribeEvents_FullMethodName = "/edge.v1.Gateway/SubscribeEvents" ) -// EdgeGatewayClient is the client API for EdgeGateway service. +// GatewayClient is the client API for Gateway service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -type EdgeGatewayClient interface { +type GatewayClient interface { ExecuteCommand(ctx context.Context, in *ExecuteCommandRequest, opts ...grpc.CallOption) (*ExecuteCommandResponse, error) SubscribeEvents(ctx context.Context, in *SubscribeEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GatewayEvent], error) } -type edgeGatewayClient struct { +type gatewayClient struct { cc grpc.ClientConnInterface } -func NewEdgeGatewayClient(cc grpc.ClientConnInterface) EdgeGatewayClient { - return &edgeGatewayClient{cc} +func NewGatewayClient(cc grpc.ClientConnInterface) GatewayClient { + return &gatewayClient{cc} } -func (c *edgeGatewayClient) ExecuteCommand(ctx context.Context, in *ExecuteCommandRequest, opts ...grpc.CallOption) (*ExecuteCommandResponse, error) { +func (c *gatewayClient) ExecuteCommand(ctx context.Context, in *ExecuteCommandRequest, opts ...grpc.CallOption) (*ExecuteCommandResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ExecuteCommandResponse) - err := c.cc.Invoke(ctx, EdgeGateway_ExecuteCommand_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, Gateway_ExecuteCommand_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *edgeGatewayClient) SubscribeEvents(ctx context.Context, in *SubscribeEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GatewayEvent], error) { +func (c *gatewayClient) SubscribeEvents(ctx context.Context, in *SubscribeEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GatewayEvent], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &EdgeGateway_ServiceDesc.Streams[0], EdgeGateway_SubscribeEvents_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &Gateway_ServiceDesc.Streams[0], Gateway_SubscribeEvents_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -66,98 +66,98 @@ func (c *edgeGatewayClient) SubscribeEvents(ctx context.Context, in *SubscribeEv } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type EdgeGateway_SubscribeEventsClient = grpc.ServerStreamingClient[GatewayEvent] +type Gateway_SubscribeEventsClient = grpc.ServerStreamingClient[GatewayEvent] -// EdgeGatewayServer is the server API for EdgeGateway service. -// All implementations must embed UnimplementedEdgeGatewayServer +// GatewayServer is the server API for Gateway service. +// All implementations must embed UnimplementedGatewayServer // for forward compatibility. -type EdgeGatewayServer interface { +type GatewayServer interface { ExecuteCommand(context.Context, *ExecuteCommandRequest) (*ExecuteCommandResponse, error) SubscribeEvents(*SubscribeEventsRequest, grpc.ServerStreamingServer[GatewayEvent]) error - mustEmbedUnimplementedEdgeGatewayServer() + mustEmbedUnimplementedGatewayServer() } -// UnimplementedEdgeGatewayServer must be embedded to have +// UnimplementedGatewayServer must be embedded to have // forward compatible implementations. // // NOTE: this should be embedded by value instead of pointer to avoid a nil // pointer dereference when methods are called. -type UnimplementedEdgeGatewayServer struct{} +type UnimplementedGatewayServer struct{} -func (UnimplementedEdgeGatewayServer) ExecuteCommand(context.Context, *ExecuteCommandRequest) (*ExecuteCommandResponse, error) { +func (UnimplementedGatewayServer) ExecuteCommand(context.Context, *ExecuteCommandRequest) (*ExecuteCommandResponse, error) { return nil, status.Error(codes.Unimplemented, "method ExecuteCommand not implemented") } -func (UnimplementedEdgeGatewayServer) SubscribeEvents(*SubscribeEventsRequest, grpc.ServerStreamingServer[GatewayEvent]) error { +func (UnimplementedGatewayServer) SubscribeEvents(*SubscribeEventsRequest, grpc.ServerStreamingServer[GatewayEvent]) error { return status.Error(codes.Unimplemented, "method SubscribeEvents not implemented") } -func (UnimplementedEdgeGatewayServer) mustEmbedUnimplementedEdgeGatewayServer() {} -func (UnimplementedEdgeGatewayServer) testEmbeddedByValue() {} +func (UnimplementedGatewayServer) mustEmbedUnimplementedGatewayServer() {} +func (UnimplementedGatewayServer) testEmbeddedByValue() {} -// UnsafeEdgeGatewayServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to EdgeGatewayServer will +// UnsafeGatewayServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GatewayServer will // result in compilation errors. -type UnsafeEdgeGatewayServer interface { - mustEmbedUnimplementedEdgeGatewayServer() +type UnsafeGatewayServer interface { + mustEmbedUnimplementedGatewayServer() } -func RegisterEdgeGatewayServer(s grpc.ServiceRegistrar, srv EdgeGatewayServer) { - // If the following call panics, it indicates UnimplementedEdgeGatewayServer was +func RegisterGatewayServer(s grpc.ServiceRegistrar, srv GatewayServer) { + // If the following call panics, it indicates UnimplementedGatewayServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { t.testEmbeddedByValue() } - s.RegisterService(&EdgeGateway_ServiceDesc, srv) + s.RegisterService(&Gateway_ServiceDesc, srv) } -func _EdgeGateway_ExecuteCommand_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _Gateway_ExecuteCommand_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ExecuteCommandRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(EdgeGatewayServer).ExecuteCommand(ctx, in) + return srv.(GatewayServer).ExecuteCommand(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: EdgeGateway_ExecuteCommand_FullMethodName, + FullMethod: Gateway_ExecuteCommand_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(EdgeGatewayServer).ExecuteCommand(ctx, req.(*ExecuteCommandRequest)) + return srv.(GatewayServer).ExecuteCommand(ctx, req.(*ExecuteCommandRequest)) } return interceptor(ctx, in, info, handler) } -func _EdgeGateway_SubscribeEvents_Handler(srv interface{}, stream grpc.ServerStream) error { +func _Gateway_SubscribeEvents_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(SubscribeEventsRequest) if err := stream.RecvMsg(m); err != nil { return err } - return srv.(EdgeGatewayServer).SubscribeEvents(m, &grpc.GenericServerStream[SubscribeEventsRequest, GatewayEvent]{ServerStream: stream}) + return srv.(GatewayServer).SubscribeEvents(m, &grpc.GenericServerStream[SubscribeEventsRequest, GatewayEvent]{ServerStream: stream}) } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type EdgeGateway_SubscribeEventsServer = grpc.ServerStreamingServer[GatewayEvent] +type Gateway_SubscribeEventsServer = grpc.ServerStreamingServer[GatewayEvent] -// EdgeGateway_ServiceDesc is the grpc.ServiceDesc for EdgeGateway service. +// Gateway_ServiceDesc is the grpc.ServiceDesc for Gateway service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) -var EdgeGateway_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "galaxy.gateway.v1.EdgeGateway", - HandlerType: (*EdgeGatewayServer)(nil), +var Gateway_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "edge.v1.Gateway", + HandlerType: (*GatewayServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "ExecuteCommand", - Handler: _EdgeGateway_ExecuteCommand_Handler, + Handler: _Gateway_ExecuteCommand_Handler, }, }, Streams: []grpc.StreamDesc{ { StreamName: "SubscribeEvents", - Handler: _EdgeGateway_SubscribeEvents_Handler, + Handler: _Gateway_SubscribeEvents_Handler, ServerStreams: true, }, }, - Metadata: "galaxy/gateway/v1/edge_gateway.proto", + Metadata: "edge/v1/edge_gateway.proto", } diff --git a/gateway/proto/edge/v1/edgev1connect/edge_gateway.connect.go b/gateway/proto/edge/v1/edgev1connect/edge_gateway.connect.go new file mode 100644 index 0000000..c1779b7 --- /dev/null +++ b/gateway/proto/edge/v1/edgev1connect/edge_gateway.connect.go @@ -0,0 +1,136 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: edge/v1/edge_gateway.proto + +package edgev1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1 "galaxy/gateway/proto/edge/v1" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // GatewayName is the fully-qualified name of the Gateway service. + GatewayName = "edge.v1.Gateway" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // GatewayExecuteCommandProcedure is the fully-qualified name of the Gateway's ExecuteCommand RPC. + GatewayExecuteCommandProcedure = "/edge.v1.Gateway/ExecuteCommand" + // GatewaySubscribeEventsProcedure is the fully-qualified name of the Gateway's SubscribeEvents RPC. + GatewaySubscribeEventsProcedure = "/edge.v1.Gateway/SubscribeEvents" +) + +// GatewayClient is a client for the edge.v1.Gateway service. +type GatewayClient interface { + ExecuteCommand(context.Context, *connect.Request[v1.ExecuteCommandRequest]) (*connect.Response[v1.ExecuteCommandResponse], error) + SubscribeEvents(context.Context, *connect.Request[v1.SubscribeEventsRequest]) (*connect.ServerStreamForClient[v1.GatewayEvent], error) +} + +// NewGatewayClient constructs a client for the edge.v1.Gateway service. By default, it uses the +// Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends +// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewGatewayClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) GatewayClient { + baseURL = strings.TrimRight(baseURL, "/") + gatewayMethods := v1.File_edge_v1_edge_gateway_proto.Services().ByName("Gateway").Methods() + return &gatewayClient{ + executeCommand: connect.NewClient[v1.ExecuteCommandRequest, v1.ExecuteCommandResponse]( + httpClient, + baseURL+GatewayExecuteCommandProcedure, + connect.WithSchema(gatewayMethods.ByName("ExecuteCommand")), + connect.WithClientOptions(opts...), + ), + subscribeEvents: connect.NewClient[v1.SubscribeEventsRequest, v1.GatewayEvent]( + httpClient, + baseURL+GatewaySubscribeEventsProcedure, + connect.WithSchema(gatewayMethods.ByName("SubscribeEvents")), + connect.WithClientOptions(opts...), + ), + } +} + +// gatewayClient implements GatewayClient. +type gatewayClient struct { + executeCommand *connect.Client[v1.ExecuteCommandRequest, v1.ExecuteCommandResponse] + subscribeEvents *connect.Client[v1.SubscribeEventsRequest, v1.GatewayEvent] +} + +// ExecuteCommand calls edge.v1.Gateway.ExecuteCommand. +func (c *gatewayClient) ExecuteCommand(ctx context.Context, req *connect.Request[v1.ExecuteCommandRequest]) (*connect.Response[v1.ExecuteCommandResponse], error) { + return c.executeCommand.CallUnary(ctx, req) +} + +// SubscribeEvents calls edge.v1.Gateway.SubscribeEvents. +func (c *gatewayClient) SubscribeEvents(ctx context.Context, req *connect.Request[v1.SubscribeEventsRequest]) (*connect.ServerStreamForClient[v1.GatewayEvent], error) { + return c.subscribeEvents.CallServerStream(ctx, req) +} + +// GatewayHandler is an implementation of the edge.v1.Gateway service. +type GatewayHandler interface { + ExecuteCommand(context.Context, *connect.Request[v1.ExecuteCommandRequest]) (*connect.Response[v1.ExecuteCommandResponse], error) + SubscribeEvents(context.Context, *connect.Request[v1.SubscribeEventsRequest], *connect.ServerStream[v1.GatewayEvent]) error +} + +// NewGatewayHandler builds an HTTP handler from the service implementation. It returns the path on +// which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewGatewayHandler(svc GatewayHandler, opts ...connect.HandlerOption) (string, http.Handler) { + gatewayMethods := v1.File_edge_v1_edge_gateway_proto.Services().ByName("Gateway").Methods() + gatewayExecuteCommandHandler := connect.NewUnaryHandler( + GatewayExecuteCommandProcedure, + svc.ExecuteCommand, + connect.WithSchema(gatewayMethods.ByName("ExecuteCommand")), + connect.WithHandlerOptions(opts...), + ) + gatewaySubscribeEventsHandler := connect.NewServerStreamHandler( + GatewaySubscribeEventsProcedure, + svc.SubscribeEvents, + connect.WithSchema(gatewayMethods.ByName("SubscribeEvents")), + connect.WithHandlerOptions(opts...), + ) + return "/edge.v1.Gateway/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case GatewayExecuteCommandProcedure: + gatewayExecuteCommandHandler.ServeHTTP(w, r) + case GatewaySubscribeEventsProcedure: + gatewaySubscribeEventsHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedGatewayHandler returns CodeUnimplemented from all methods. +type UnimplementedGatewayHandler struct{} + +func (UnimplementedGatewayHandler) ExecuteCommand(context.Context, *connect.Request[v1.ExecuteCommandRequest]) (*connect.Response[v1.ExecuteCommandResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("edge.v1.Gateway.ExecuteCommand is not implemented")) +} + +func (UnimplementedGatewayHandler) SubscribeEvents(context.Context, *connect.Request[v1.SubscribeEventsRequest], *connect.ServerStream[v1.GatewayEvent]) error { + return connect.NewError(connect.CodeUnimplemented, errors.New("edge.v1.Gateway.SubscribeEvents is not implemented")) +} diff --git a/gateway/proto/galaxy/gateway/v1/gatewayv1connect/edge_gateway.connect.go b/gateway/proto/galaxy/gateway/v1/gatewayv1connect/edge_gateway.connect.go deleted file mode 100644 index 5775c9a..0000000 --- a/gateway/proto/galaxy/gateway/v1/gatewayv1connect/edge_gateway.connect.go +++ /dev/null @@ -1,138 +0,0 @@ -// Code generated by protoc-gen-connect-go. DO NOT EDIT. -// -// Source: galaxy/gateway/v1/edge_gateway.proto - -package gatewayv1connect - -import ( - connect "connectrpc.com/connect" - context "context" - errors "errors" - v1 "galaxy/gateway/proto/galaxy/gateway/v1" - http "net/http" - strings "strings" -) - -// This is a compile-time assertion to ensure that this generated file and the connect package are -// compatible. If you get a compiler error that this constant is not defined, this code was -// generated with a version of connect newer than the one compiled into your binary. You can fix the -// problem by either regenerating this code with an older version of connect or updating the connect -// version compiled into your binary. -const _ = connect.IsAtLeastVersion1_13_0 - -const ( - // EdgeGatewayName is the fully-qualified name of the EdgeGateway service. - EdgeGatewayName = "galaxy.gateway.v1.EdgeGateway" -) - -// These constants are the fully-qualified names of the RPCs defined in this package. They're -// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. -// -// Note that these are different from the fully-qualified method names used by -// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to -// reflection-formatted method names, remove the leading slash and convert the remaining slash to a -// period. -const ( - // EdgeGatewayExecuteCommandProcedure is the fully-qualified name of the EdgeGateway's - // ExecuteCommand RPC. - EdgeGatewayExecuteCommandProcedure = "/galaxy.gateway.v1.EdgeGateway/ExecuteCommand" - // EdgeGatewaySubscribeEventsProcedure is the fully-qualified name of the EdgeGateway's - // SubscribeEvents RPC. - EdgeGatewaySubscribeEventsProcedure = "/galaxy.gateway.v1.EdgeGateway/SubscribeEvents" -) - -// EdgeGatewayClient is a client for the galaxy.gateway.v1.EdgeGateway service. -type EdgeGatewayClient interface { - ExecuteCommand(context.Context, *connect.Request[v1.ExecuteCommandRequest]) (*connect.Response[v1.ExecuteCommandResponse], error) - SubscribeEvents(context.Context, *connect.Request[v1.SubscribeEventsRequest]) (*connect.ServerStreamForClient[v1.GatewayEvent], error) -} - -// NewEdgeGatewayClient constructs a client for the galaxy.gateway.v1.EdgeGateway service. By -// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, -// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the -// connect.WithGRPC() or connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewEdgeGatewayClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) EdgeGatewayClient { - baseURL = strings.TrimRight(baseURL, "/") - edgeGatewayMethods := v1.File_galaxy_gateway_v1_edge_gateway_proto.Services().ByName("EdgeGateway").Methods() - return &edgeGatewayClient{ - executeCommand: connect.NewClient[v1.ExecuteCommandRequest, v1.ExecuteCommandResponse]( - httpClient, - baseURL+EdgeGatewayExecuteCommandProcedure, - connect.WithSchema(edgeGatewayMethods.ByName("ExecuteCommand")), - connect.WithClientOptions(opts...), - ), - subscribeEvents: connect.NewClient[v1.SubscribeEventsRequest, v1.GatewayEvent]( - httpClient, - baseURL+EdgeGatewaySubscribeEventsProcedure, - connect.WithSchema(edgeGatewayMethods.ByName("SubscribeEvents")), - connect.WithClientOptions(opts...), - ), - } -} - -// edgeGatewayClient implements EdgeGatewayClient. -type edgeGatewayClient struct { - executeCommand *connect.Client[v1.ExecuteCommandRequest, v1.ExecuteCommandResponse] - subscribeEvents *connect.Client[v1.SubscribeEventsRequest, v1.GatewayEvent] -} - -// ExecuteCommand calls galaxy.gateway.v1.EdgeGateway.ExecuteCommand. -func (c *edgeGatewayClient) ExecuteCommand(ctx context.Context, req *connect.Request[v1.ExecuteCommandRequest]) (*connect.Response[v1.ExecuteCommandResponse], error) { - return c.executeCommand.CallUnary(ctx, req) -} - -// SubscribeEvents calls galaxy.gateway.v1.EdgeGateway.SubscribeEvents. -func (c *edgeGatewayClient) SubscribeEvents(ctx context.Context, req *connect.Request[v1.SubscribeEventsRequest]) (*connect.ServerStreamForClient[v1.GatewayEvent], error) { - return c.subscribeEvents.CallServerStream(ctx, req) -} - -// EdgeGatewayHandler is an implementation of the galaxy.gateway.v1.EdgeGateway service. -type EdgeGatewayHandler interface { - ExecuteCommand(context.Context, *connect.Request[v1.ExecuteCommandRequest]) (*connect.Response[v1.ExecuteCommandResponse], error) - SubscribeEvents(context.Context, *connect.Request[v1.SubscribeEventsRequest], *connect.ServerStream[v1.GatewayEvent]) error -} - -// NewEdgeGatewayHandler builds an HTTP handler from the service implementation. It returns the path -// on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewEdgeGatewayHandler(svc EdgeGatewayHandler, opts ...connect.HandlerOption) (string, http.Handler) { - edgeGatewayMethods := v1.File_galaxy_gateway_v1_edge_gateway_proto.Services().ByName("EdgeGateway").Methods() - edgeGatewayExecuteCommandHandler := connect.NewUnaryHandler( - EdgeGatewayExecuteCommandProcedure, - svc.ExecuteCommand, - connect.WithSchema(edgeGatewayMethods.ByName("ExecuteCommand")), - connect.WithHandlerOptions(opts...), - ) - edgeGatewaySubscribeEventsHandler := connect.NewServerStreamHandler( - EdgeGatewaySubscribeEventsProcedure, - svc.SubscribeEvents, - connect.WithSchema(edgeGatewayMethods.ByName("SubscribeEvents")), - connect.WithHandlerOptions(opts...), - ) - return "/galaxy.gateway.v1.EdgeGateway/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case EdgeGatewayExecuteCommandProcedure: - edgeGatewayExecuteCommandHandler.ServeHTTP(w, r) - case EdgeGatewaySubscribeEventsProcedure: - edgeGatewaySubscribeEventsHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedEdgeGatewayHandler returns CodeUnimplemented from all methods. -type UnimplementedEdgeGatewayHandler struct{} - -func (UnimplementedEdgeGatewayHandler) ExecuteCommand(context.Context, *connect.Request[v1.ExecuteCommandRequest]) (*connect.Response[v1.ExecuteCommandResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("galaxy.gateway.v1.EdgeGateway.ExecuteCommand is not implemented")) -} - -func (UnimplementedEdgeGatewayHandler) SubscribeEvents(context.Context, *connect.Request[v1.SubscribeEventsRequest], *connect.ServerStream[v1.GatewayEvent]) error { - return connect.NewError(connect.CodeUnimplemented, errors.New("galaxy.gateway.v1.EdgeGateway.SubscribeEvents is not implemented")) -} diff --git a/integration/testenv/connect_client.go b/integration/testenv/connect_client.go index 0d3ad81..d2758ff 100644 --- a/integration/testenv/connect_client.go +++ b/integration/testenv/connect_client.go @@ -15,8 +15,8 @@ import ( "time" gatewayauthn "galaxy/gateway/authn" - gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" - "galaxy/gateway/proto/galaxy/gateway/v1/gatewayv1connect" + edgev1 "galaxy/gateway/proto/edge/v1" + "galaxy/gateway/proto/edge/v1/edgev1connect" "connectrpc.com/connect" "github.com/google/uuid" @@ -32,7 +32,7 @@ import ( // alongside gRPC and gRPC-Web on the same port. type SignedGatewayClient struct { httpClient *http.Client - edge gatewayv1connect.EdgeGatewayClient + edge edgev1connect.GatewayClient deviceSID string privateKey ed25519.PrivateKey respPub ed25519.PublicKey @@ -75,7 +75,7 @@ func DialGateway(_ context.Context, addr string, deviceSID string, privateKey ed }, }, } - edge := gatewayv1connect.NewEdgeGatewayClient(httpClient, "http://"+addr) + edge := edgev1connect.NewGatewayClient(httpClient, "http://"+addr) return &SignedGatewayClient{ httpClient: httpClient, @@ -164,7 +164,7 @@ func (c *SignedGatewayClient) Execute(ctx context.Context, messageType string, p signature = ed25519.Sign(c.privateKey, input) } - req := &gatewayv1.ExecuteCommandRequest{ + req := &edgev1.ExecuteCommandRequest{ ProtocolVersion: protocolVersion, DeviceSessionId: deviceSID, MessageType: messageType, @@ -209,7 +209,7 @@ func (c *SignedGatewayClient) Execute(ctx context.Context, messageType string, p // authenticated event the gateway delivers; the channel closes when // the stream ends or when ctx is done. Errors land on the err // channel. -func (c *SignedGatewayClient) SubscribeEvents(ctx context.Context, messageType string) (<-chan *gatewayv1.GatewayEvent, <-chan error, error) { +func (c *SignedGatewayClient) SubscribeEvents(ctx context.Context, messageType string) (<-chan *edgev1.GatewayEvent, <-chan error, error) { requestID := uuid.NewString() timestampMS := time.Now().UnixMilli() protocolVersion := "v1" @@ -224,7 +224,7 @@ func (c *SignedGatewayClient) SubscribeEvents(ctx context.Context, messageType s PayloadHash: emptyHash[:], })) - stream, err := c.edge.SubscribeEvents(ctx, connect.NewRequest(&gatewayv1.SubscribeEventsRequest{ + stream, err := c.edge.SubscribeEvents(ctx, connect.NewRequest(&edgev1.SubscribeEventsRequest{ ProtocolVersion: protocolVersion, DeviceSessionId: c.deviceSID, MessageType: messageType, @@ -237,7 +237,7 @@ func (c *SignedGatewayClient) SubscribeEvents(ctx context.Context, messageType s return nil, nil, fmt.Errorf("open subscribe events: %w", err) } - events := make(chan *gatewayv1.GatewayEvent, 16) + events := make(chan *edgev1.GatewayEvent, 16) errs := make(chan error, 1) go func() { defer close(events) diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 0000000..2c1fa99 --- /dev/null +++ b/site/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.vitepress/dist/ +.vitepress/cache/ diff --git a/site/.vitepress/config.ts b/site/.vitepress/config.ts new file mode 100644 index 0000000..745afab --- /dev/null +++ b/site/.vitepress/config.ts @@ -0,0 +1,55 @@ +import { defineConfig } from "vitepress"; + +// Galaxy project site. The single-origin deployment serves this static +// build at the root (`/`); the game UI lives under `/game/`. The site is +// Markdown-first with the default theme's two-column navigation (left: +// chapters; right: on-this-page), LaTeX math, and a minimal monospace +// theme. Internationalisation uses VitePress `locales`, which renders the +// built-in language switcher in the nav bar. +export default defineConfig({ + title: "Galaxy", + description: "Galaxy — a turn-based space strategy game.", + cleanUrls: true, + // READMEs (this dir's and any future per-section ones) are developer + // docs, not site pages. + srcExclude: ["**/README.md"], + // The game UI is served from `/game/` by the edge Caddy, not by + // VitePress, so its links must skip the dead-link checker. + ignoreDeadLinks: [/^\/game\//], + markdown: { + // LaTeX via markdown-it-mathjax3: inline `$…$`, block `$$…$$`. + math: true, + }, + themeConfig: { + outline: { level: [2, 3], label: "On this page" }, + }, + locales: { + root: { + label: "English", + lang: "en", + themeConfig: { + nav: [{ text: "Play", link: "/game/" }], + sidebar: [ + { + text: "Galaxy", + items: [{ text: "Overview", link: "/" }], + }, + ], + }, + }, + ru: { + label: "Русский", + lang: "ru", + link: "/ru/", + themeConfig: { + nav: [{ text: "Играть", link: "/game/" }], + sidebar: [ + { + text: "Galaxy", + items: [{ text: "Обзор", link: "/ru/" }], + }, + ], + }, + }, + }, +}); diff --git a/site/.vitepress/theme/custom.css b/site/.vitepress/theme/custom.css new file mode 100644 index 0000000..e6baeb8 --- /dev/null +++ b/site/.vitepress/theme/custom.css @@ -0,0 +1,10 @@ +/* + * Minimal, restrained theme. No flashy hero or bright accents — the + * project site leans "nerdy" with a fixed-width type stack. System + * fonts first so nothing is downloaded. + */ +:root { + --vp-font-family-base: ui-monospace, "JetBrains Mono", "SF Mono", + "Fira Code", "DejaVu Sans Mono", Menlo, Consolas, monospace; + --vp-font-family-mono: var(--vp-font-family-base); +} diff --git a/site/.vitepress/theme/index.ts b/site/.vitepress/theme/index.ts new file mode 100644 index 0000000..1777c4f --- /dev/null +++ b/site/.vitepress/theme/index.ts @@ -0,0 +1,6 @@ +// Minimal theme: the default VitePress theme without its bundled Inter +// font, plus a monospace type stack for the project's "nerdy" look. +import DefaultTheme from "vitepress/theme-without-fonts"; +import "./custom.css"; + +export default DefaultTheme; diff --git a/site/README.md b/site/README.md new file mode 100644 index 0000000..99e6f7e --- /dev/null +++ b/site/README.md @@ -0,0 +1,36 @@ +# Galaxy project site + +The public project site — an overview today, documentation as it grows. +Built with [VitePress](https://vitepress.dev) and served as static files +at the **root** (`/`) of the single-origin deployment; the game UI lives +under `/game/` (see `tools/dev-deploy/Caddyfile.dev`). + +## Layout + +- `index.md`, `ru/index.md` — per-locale home pages. +- `.vitepress/config.ts` — site config: locales (English + Russian, with + the built-in language switcher), LaTeX math (`math: true`), and the + two-column navigation (left sidebar = chapters, right outline = + on-this-page). +- `.vitepress/theme/` — the default theme without bundled fonts, plus a + minimal monospace type stack in `custom.css`. + +## Authoring + +- Add a page as Markdown and register it in the `sidebar` of each locale + in `.vitepress/config.ts`. +- Localised content mirrors the English tree under `ru/`. +- Math uses LaTeX: inline `$E = mc^2$`, block `$$ … $$`. +- Link to the game with the root-relative `/game/` path so the build + stays domain-agnostic (no hard-coded host). + +## Commands + + pnpm install + pnpm dev # local dev server + pnpm build # static build into .vitepress/dist + pnpm preview # preview the build + +The dev and prod deploys build the site via +`make -C tools/dev-deploy seed-site` and the `dev-deploy` / +`prod-build` workflows. diff --git a/site/index.md b/site/index.md new file mode 100644 index 0000000..7609ee2 --- /dev/null +++ b/site/index.md @@ -0,0 +1,5 @@ +# Galaxy + +A turn-based space strategy game. + +[Play the game →](/game/) diff --git a/site/package.json b/site/package.json new file mode 100644 index 0000000..bae52b4 --- /dev/null +++ b/site/package.json @@ -0,0 +1,16 @@ +{ + "name": "galaxy-site", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Galaxy project site — overview and documentation (VitePress).", + "scripts": { + "dev": "vitepress dev", + "build": "vitepress build", + "preview": "vitepress preview" + }, + "devDependencies": { + "markdown-it-mathjax3": "^5.2.0", + "vitepress": "^1.6.4" + } +} diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml new file mode 100644 index 0000000..6e78431 --- /dev/null +++ b/site/pnpm-lock.yaml @@ -0,0 +1,1736 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + markdown-it-mathjax3: + specifier: ^5.2.0 + version: 5.2.0 + vitepress: + specifier: ^1.6.4 + version: 1.6.4(@algolia/client-search@5.52.1)(markdown-it-mathjax3@5.2.0)(postcss@8.5.15)(search-insights@2.17.3) + +packages: + + '@algolia/abtesting@1.18.1': + resolution: {integrity: sha512-aehCadlWOGvrT91KUIZpC0MbB8KBW9yUuvTJFd2xesR7le/IsT4nJUnjCCZ4ZqZCeTcPHPV5mo//fZ5oxcSVYw==} + engines: {node: '>= 14.0.0'} + + '@algolia/autocomplete-core@1.17.7': + resolution: {integrity: sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==} + + '@algolia/autocomplete-plugin-algolia-insights@1.17.7': + resolution: {integrity: sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==} + peerDependencies: + search-insights: '>= 1 < 3' + + '@algolia/autocomplete-preset-algolia@1.17.7': + resolution: {integrity: sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/autocomplete-shared@1.17.7': + resolution: {integrity: sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/client-abtesting@5.52.1': + resolution: {integrity: sha512-HmXOGBOAOJPounpBzBpuY0zDYeiCpxgHnQmuA7JO6ScukcBdGp3/XM9zJk5pJx/xNGD68mbPGXWpDxGtl6BwDQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-analytics@5.52.1': + resolution: {integrity: sha512-5oo4+I8iixie9vXhCyNFCzeIr8pqA3FQ//VsLHTDvZAV4ttYOPGvYHGQq5NSalrLx5Jc3dRro/5uDOlnUMcBJg==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-common@5.52.1': + resolution: {integrity: sha512-qCDoZfx5MpX7XQzvQ3bC4tSEMkQWQMaF/ABtLuoze03Y/flR563CCSws02qIJ23oX7lxl92LsilZjINVyTdtLw==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-insights@5.52.1': + resolution: {integrity: sha512-hnGs0/lsFJ2PWDxNBz7pxreXo/Xz7gxYRcfePBUjsH26ad0kU/sgnVZd9LwWBpsQv65z2jlb5dkyaB9WE9M9FQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-personalization@5.52.1': + resolution: {integrity: sha512-2VxxNc/uBysyKvGeBdSM5n9eIDKH8kWD7wd9/yqbJAiVwU4Yv6tU1LSJusHKrXV/aCu1KW7t9Gug9QyeEmtn/Q==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-query-suggestions@5.52.1': + resolution: {integrity: sha512-O6mPtsw3xEfNOe6gWFpYLeAZAIljNa4Hgna3bq15PwyN7nbjTY0wXJFRbzs/0YVf75Br+SbOQUmjKxXYjDiSiQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-search@5.52.1': + resolution: {integrity: sha512-gA8oJOV1LnQQkDf91iebNnFInHuW0gRPEgLSOQ7EfipCEjYTHm5swm1DlH9H5RaRw4RrHuzHBegnlzc0MAstcg==} + engines: {node: '>= 14.0.0'} + + '@algolia/ingestion@1.52.1': + resolution: {integrity: sha512-U9zZfc5xIu9wRxZkt+HceJUAD4VKHKbAyLSloJdEyMRmphXeibfrY9cxqIXBcmPeZzGhn3Imb35Dq8l19PkJhw==} + engines: {node: '>= 14.0.0'} + + '@algolia/monitoring@1.52.1': + resolution: {integrity: sha512-a3SGNceHmkQfq77iG8Ka+w1pvwfZa/0lzEIgse30fL0kD+yKnd/dg0dQvSfFPAEt2f21DMcGkDSSeJlO3KdQjQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/recommend@5.52.1': + resolution: {integrity: sha512-z98QEguCFDpxb4S/PyrUK1igqF8tPsdbqOUUO6ON91vJ58w+Gwa6ncrI0oNXSFcrkxA5EqPKPQ2A1PBCn08TYQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-browser-xhr@5.52.1': + resolution: {integrity: sha512-CI7+/0I11QeZM59Uc8whd2or0kqzFVjpaPn9Qpwll/krHcBAxk24WkAQ6WX+IwDVMfpont4YGbKwAmCre3vE8Q==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-fetch@5.52.1': + resolution: {integrity: sha512-S6bDuw9byfOvm3T71cgdoZgrgnZq6hpdMLkx52Louh57nUAmvGQESz2aojOynQHjbTiV55smvAFbgn0qT4tJrg==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-node-http@5.52.1': + resolution: {integrity: sha512-tqZXM+54rWo4mk5jL5Z/flE11nPmNEdXwFBM5py9DkOmbjeCNemfVd45FyM97XdzfZ0dl9uOJC6PYn1FpkeyQg==} + engines: {node: '>= 14.0.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@docsearch/css@3.8.2': + resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} + + '@docsearch/js@3.8.2': + resolution: {integrity: sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==} + + '@docsearch/react@3.8.2': + resolution: {integrity: sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==} + peerDependencies: + '@types/react': '>= 16.8.0 < 19.0.0' + react: '>= 16.8.0 < 19.0.0' + react-dom: '>= 16.8.0 < 19.0.0' + search-insights: '>= 1 < 3' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + search-insights: + optional: true + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@iconify-json/simple-icons@1.2.83': + resolution: {integrity: sha512-6Pp9V++XisT9RKH7FB4RLPqUDzcmLtSma0ovOEIoEWGrXtHwBFsH7oN1z8vvCVCb95fb87QgR46/zRLyN9Y3kg==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + cpu: [x64] + os: [win32] + + '@se-oss/deasync-darwin-arm64@1.0.1': + resolution: {integrity: sha512-0YWmIDEGQfW3GGopmZHhfA6mamsG0HFKZhmBzHVyFiMKkJts8kpQwGbGrWlK8eOAoPCihOsG6tCotYR3p7HZaQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@se-oss/deasync-darwin-x64@1.0.1': + resolution: {integrity: sha512-r3FRTLIXqGqOb1DjTLW3YhO/Dd1vA2qRLP0Ym3Wmk3yMv6c/nm15zg6UVoXbgBu8cjbvcsI/OfbHPdErmjMWsw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@se-oss/deasync-linux-arm64-gnu@1.0.1': + resolution: {integrity: sha512-657uRew7fZAx663Li03ilLV2lN09Dqb/NxawlDu8kKmboK1BLitHJRS+taiT5oFZqyIDrU45tlQKfCrW0p0sYA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@se-oss/deasync-linux-arm64-musl@1.0.1': + resolution: {integrity: sha512-IE3fIQPIJtko4lx9sRam+Zz0P4xbpAPJgDCHaz6k9cP1yUvVI179B4IZRnFx0GyjyQpm0KhHoIGHJc4KUmA81Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@se-oss/deasync-linux-x64-gnu@1.0.1': + resolution: {integrity: sha512-XQl7etZESGIjIraCyxfAey8ZTIJUB4dUFU3rPR/xLVn9bKpZGlJLIms0z3hoHX9mipO+Cqo53vK4IVm6A7U/ww==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@se-oss/deasync-linux-x64-musl@1.0.1': + resolution: {integrity: sha512-vWgFAZlqImqMV6jhCWV7C9wcCS1eb1ajhlKduBRPfyUxxkoObe+EqTG2BKJAuafxp3/KS1aUsIMJma9mhwFvow==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@se-oss/deasync-win32-arm64-msvc@1.0.1': + resolution: {integrity: sha512-yk7lEE7Zd8GX7o6CuUbg3HnnmUhBx4tgfn5ff3eoq05CgBO6Z3ZtL4l+utAe1cxcFaXPhyvcgnHYyA4OF544tg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@se-oss/deasync-win32-x64-msvc@1.0.1': + resolution: {integrity: sha512-ixizmuLGRPGyAesWUNWVzVOsvuunNb/qMqU8SmjfLR/vVgzdQEkSHFf+fkX9GXPN6FDv+DAz5uskTzhjUyCXFA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@se-oss/deasync@1.0.1': + resolution: {integrity: sha512-Ha7P/xCNxOuH72BNdLRWs4TT8rsMMrERnHtfKWBeTWu+UFW9OBTrRgfZJOlbAAQFR0l4Q30cpAn8CuR7PXWcPg==} + engines: {node: '>= 10'} + + '@shikijs/core@2.5.0': + resolution: {integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==} + + '@shikijs/engine-javascript@2.5.0': + resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==} + + '@shikijs/engine-oniguruma@2.5.0': + resolution: {integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==} + + '@shikijs/langs@2.5.0': + resolution: {integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==} + + '@shikijs/themes@2.5.0': + resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==} + + '@shikijs/transformers@2.5.0': + resolution: {integrity: sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==} + + '@shikijs/types@2.5.0': + resolution: {integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@ungap/structured-clone@1.3.1': + resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.34': + resolution: {integrity: sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==} + + '@vue/compiler-dom@3.5.34': + resolution: {integrity: sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==} + + '@vue/compiler-sfc@3.5.34': + resolution: {integrity: sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==} + + '@vue/compiler-ssr@3.5.34': + resolution: {integrity: sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==} + + '@vue/devtools-api@7.7.9': + resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + + '@vue/devtools-kit@7.7.9': + resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + + '@vue/devtools-shared@7.7.9': + resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + + '@vue/reactivity@3.5.34': + resolution: {integrity: sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==} + + '@vue/runtime-core@3.5.34': + resolution: {integrity: sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==} + + '@vue/runtime-dom@3.5.34': + resolution: {integrity: sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==} + + '@vue/server-renderer@3.5.34': + resolution: {integrity: sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==} + peerDependencies: + vue: 3.5.34 + + '@vue/shared@3.5.34': + resolution: {integrity: sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==} + + '@vueuse/core@12.8.2': + resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} + + '@vueuse/integrations@12.8.2': + resolution: {integrity: sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + + '@vueuse/metadata@12.8.2': + resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} + + '@vueuse/shared@12.8.2': + resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} + + algoliasearch@5.52.1: + resolution: {integrity: sha512-fHA8+kXTbjagw3jkLiaS7KKrH8qe2DyOsiUhGlN4cdT77PEsfqXZl7ewDk1hsg+pJnPlnE50XtLxjR91iJOpmg==} + engines: {node: '>= 14.0.0'} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + focus-trap@7.8.0: + resolution: {integrity: sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mark.js@8.11.1: + resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + + markdown-it-mathjax3@5.2.0: + resolution: {integrity: sha512-R+XAy5/7vSGuhG9Z0/cJm6zKxOzStcScfSKVwoarh4nBra+v1KClvbALr/xFTEe9iQhwfQM4SJnO68LXL+btMA==} + + mathxyjax3@0.8.3: + resolution: {integrity: sha512-eXjFaiyQsTdVOeTFoFaFJ/r1FITpB1f9c5MW4FETfcoVV/+xa5SD9pS05AwugzL/gNuDtWXrTOSmoD2e0Du+UA==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + minisearch@7.2.0: + resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + oniguruma-to-es@3.1.1: + resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + preact@10.29.2: + resolution: {integrity: sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + search-insights@2.17.3: + resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} + + shiki@2.5.0: + resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitepress@1.6.4: + resolution: {integrity: sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==} + hasBin: true + peerDependencies: + markdown-it-mathjax3: ^4 + postcss: ^8 + peerDependenciesMeta: + markdown-it-mathjax3: + optional: true + postcss: + optional: true + + vue@3.5.34: + resolution: {integrity: sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@algolia/abtesting@1.18.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/autocomplete-core@1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-plugin-algolia-insights': 1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1)(search-insights@2.17.3) + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1) + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + - search-insights + + '@algolia/autocomplete-plugin-algolia-insights@1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1) + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + + '@algolia/autocomplete-preset-algolia@1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1)': + dependencies: + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1) + '@algolia/client-search': 5.52.1 + algoliasearch: 5.52.1 + + '@algolia/autocomplete-shared@1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1)': + dependencies: + '@algolia/client-search': 5.52.1 + algoliasearch: 5.52.1 + + '@algolia/client-abtesting@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/client-analytics@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/client-common@5.52.1': {} + + '@algolia/client-insights@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/client-personalization@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/client-query-suggestions@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/client-search@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/ingestion@1.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/monitoring@1.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/recommend@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/requester-browser-xhr@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + + '@algolia/requester-fetch@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + + '@algolia/requester-node-http@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@docsearch/css@3.8.2': {} + + '@docsearch/js@3.8.2(@algolia/client-search@5.52.1)(search-insights@2.17.3)': + dependencies: + '@docsearch/react': 3.8.2(@algolia/client-search@5.52.1)(search-insights@2.17.3) + preact: 10.29.2 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/react' + - react + - react-dom + - search-insights + + '@docsearch/react@3.8.2(@algolia/client-search@5.52.1)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1)(search-insights@2.17.3) + '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1) + '@docsearch/css': 3.8.2 + algoliasearch: 5.52.1 + optionalDependencies: + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@iconify-json/simple-icons@1.2.83': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@rollup/rollup-android-arm-eabi@4.60.4': + optional: true + + '@rollup/rollup-android-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-x64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.4': + optional: true + + '@se-oss/deasync-darwin-arm64@1.0.1': + optional: true + + '@se-oss/deasync-darwin-x64@1.0.1': + optional: true + + '@se-oss/deasync-linux-arm64-gnu@1.0.1': + optional: true + + '@se-oss/deasync-linux-arm64-musl@1.0.1': + optional: true + + '@se-oss/deasync-linux-x64-gnu@1.0.1': + optional: true + + '@se-oss/deasync-linux-x64-musl@1.0.1': + optional: true + + '@se-oss/deasync-win32-arm64-msvc@1.0.1': + optional: true + + '@se-oss/deasync-win32-x64-msvc@1.0.1': + optional: true + + '@se-oss/deasync@1.0.1': + dependencies: + type-fest: 4.41.0 + optionalDependencies: + '@se-oss/deasync-darwin-arm64': 1.0.1 + '@se-oss/deasync-darwin-x64': 1.0.1 + '@se-oss/deasync-linux-arm64-gnu': 1.0.1 + '@se-oss/deasync-linux-arm64-musl': 1.0.1 + '@se-oss/deasync-linux-x64-gnu': 1.0.1 + '@se-oss/deasync-linux-x64-musl': 1.0.1 + '@se-oss/deasync-win32-arm64-msvc': 1.0.1 + '@se-oss/deasync-win32-x64-msvc': 1.0.1 + + '@shikijs/core@2.5.0': + dependencies: + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 3.1.1 + + '@shikijs/engine-oniguruma@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + + '@shikijs/themes@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + + '@shikijs/transformers@2.5.0': + dependencies: + '@shikijs/core': 2.5.0 + '@shikijs/types': 2.5.0 + + '@shikijs/types@2.5.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + + '@types/unist@3.0.3': {} + + '@types/web-bluetooth@0.0.21': {} + + '@ungap/structured-clone@1.3.1': {} + + '@vitejs/plugin-vue@5.2.4(vite@5.4.21)(vue@3.5.34)': + dependencies: + vite: 5.4.21 + vue: 3.5.34 + + '@vue/compiler-core@3.5.34': + dependencies: + '@babel/parser': 7.29.3 + '@vue/shared': 3.5.34 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.34': + dependencies: + '@vue/compiler-core': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/compiler-sfc@3.5.34': + dependencies: + '@babel/parser': 7.29.3 + '@vue/compiler-core': 3.5.34 + '@vue/compiler-dom': 3.5.34 + '@vue/compiler-ssr': 3.5.34 + '@vue/shared': 3.5.34 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.15 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.34': + dependencies: + '@vue/compiler-dom': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/devtools-api@7.7.9': + dependencies: + '@vue/devtools-kit': 7.7.9 + + '@vue/devtools-kit@7.7.9': + dependencies: + '@vue/devtools-shared': 7.7.9 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-shared@7.7.9': + dependencies: + rfdc: 1.4.1 + + '@vue/reactivity@3.5.34': + dependencies: + '@vue/shared': 3.5.34 + + '@vue/runtime-core@3.5.34': + dependencies: + '@vue/reactivity': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/runtime-dom@3.5.34': + dependencies: + '@vue/reactivity': 3.5.34 + '@vue/runtime-core': 3.5.34 + '@vue/shared': 3.5.34 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.34(vue@3.5.34)': + dependencies: + '@vue/compiler-ssr': 3.5.34 + '@vue/shared': 3.5.34 + vue: 3.5.34 + + '@vue/shared@3.5.34': {} + + '@vueuse/core@12.8.2': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 12.8.2 + '@vueuse/shared': 12.8.2 + vue: 3.5.34 + transitivePeerDependencies: + - typescript + + '@vueuse/integrations@12.8.2(focus-trap@7.8.0)': + dependencies: + '@vueuse/core': 12.8.2 + '@vueuse/shared': 12.8.2 + vue: 3.5.34 + optionalDependencies: + focus-trap: 7.8.0 + transitivePeerDependencies: + - typescript + + '@vueuse/metadata@12.8.2': {} + + '@vueuse/shared@12.8.2': + dependencies: + vue: 3.5.34 + transitivePeerDependencies: + - typescript + + algoliasearch@5.52.1: + dependencies: + '@algolia/abtesting': 1.18.1 + '@algolia/client-abtesting': 5.52.1 + '@algolia/client-analytics': 5.52.1 + '@algolia/client-common': 5.52.1 + '@algolia/client-insights': 5.52.1 + '@algolia/client-personalization': 5.52.1 + '@algolia/client-query-suggestions': 5.52.1 + '@algolia/client-search': 5.52.1 + '@algolia/ingestion': 1.52.1 + '@algolia/monitoring': 1.52.1 + '@algolia/recommend': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + birpc@2.9.0: {} + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + comma-separated-tokens@2.0.3: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + csstype@3.2.3: {} + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + emoji-regex-xs@1.0.0: {} + + entities@7.0.1: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + estree-walker@2.0.2: {} + + focus-trap@7.8.0: + dependencies: + tabbable: 6.4.0 + + fsevents@2.3.3: + optional: true + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hookable@5.5.3: {} + + html-void-elements@3.0.0: {} + + is-what@5.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mark.js@8.11.1: {} + + markdown-it-mathjax3@5.2.0: + dependencies: + '@se-oss/deasync': 1.0.1 + mathxyjax3: 0.8.3 + + mathxyjax3@0.8.3: {} + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.1 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + minisearch@7.2.0: {} + + mitt@3.0.1: {} + + nanoid@3.3.12: {} + + oniguruma-to-es@3.1.1: + dependencies: + emoji-regex-xs: 1.0.0 + regex: 6.1.0 + regex-recursion: 6.0.2 + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + preact@10.29.2: {} + + property-information@7.1.0: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + rfdc@1.4.1: {} + + rollup@4.60.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 + fsevents: 2.3.3 + + search-insights@2.17.3: {} + + shiki@2.5.0: + dependencies: + '@shikijs/core': 2.5.0 + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/langs': 2.5.0 + '@shikijs/themes': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + speakingurl@14.0.1: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + tabbable@6.4.0: {} + + trim-lines@3.0.1: {} + + type-fest@4.41.0: {} + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@5.4.21: + dependencies: + esbuild: 0.21.5 + postcss: 8.5.15 + rollup: 4.60.4 + optionalDependencies: + fsevents: 2.3.3 + + vitepress@1.6.4(@algolia/client-search@5.52.1)(markdown-it-mathjax3@5.2.0)(postcss@8.5.15)(search-insights@2.17.3): + dependencies: + '@docsearch/css': 3.8.2 + '@docsearch/js': 3.8.2(@algolia/client-search@5.52.1)(search-insights@2.17.3) + '@iconify-json/simple-icons': 1.2.83 + '@shikijs/core': 2.5.0 + '@shikijs/transformers': 2.5.0 + '@shikijs/types': 2.5.0 + '@types/markdown-it': 14.1.2 + '@vitejs/plugin-vue': 5.2.4(vite@5.4.21)(vue@3.5.34) + '@vue/devtools-api': 7.7.9 + '@vue/shared': 3.5.34 + '@vueuse/core': 12.8.2 + '@vueuse/integrations': 12.8.2(focus-trap@7.8.0) + focus-trap: 7.8.0 + mark.js: 8.11.1 + minisearch: 7.2.0 + shiki: 2.5.0 + vite: 5.4.21 + vue: 3.5.34 + optionalDependencies: + markdown-it-mathjax3: 5.2.0 + postcss: 8.5.15 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/node' + - '@types/react' + - async-validator + - axios + - change-case + - drauu + - fuse.js + - idb-keyval + - jwt-decode + - less + - lightningcss + - nprogress + - qrcode + - react + - react-dom + - sass + - sass-embedded + - search-insights + - sortablejs + - stylus + - sugarss + - terser + - typescript + - universal-cookie + + vue@3.5.34: + dependencies: + '@vue/compiler-dom': 3.5.34 + '@vue/compiler-sfc': 3.5.34 + '@vue/runtime-dom': 3.5.34 + '@vue/server-renderer': 3.5.34(vue@3.5.34) + '@vue/shared': 3.5.34 + + zwitch@2.0.4: {} diff --git a/site/pnpm-workspace.yaml b/site/pnpm-workspace.yaml new file mode 100644 index 0000000..5ed0b5a --- /dev/null +++ b/site/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + esbuild: true diff --git a/site/ru/index.md b/site/ru/index.md new file mode 100644 index 0000000..cd0687d --- /dev/null +++ b/site/ru/index.md @@ -0,0 +1,5 @@ +# Galaxy + +Пошаговая космическая стратегия. + +[Играть →](/game/) diff --git a/tools/dev-deploy/Caddyfile.dev b/tools/dev-deploy/Caddyfile.dev index b882736..b1acfb5 100644 --- a/tools/dev-deploy/Caddyfile.dev +++ b/tools/dev-deploy/Caddyfile.dev @@ -1,51 +1,68 @@ # Application-routing Caddy for the long-lived dev environment. -# Listens only on the `edge` Docker network; TLS termination and the -# real `:80`/`:443` listeners belong to the host Caddy in front of us. +# Single-origin, path-based: the project site, the game UI, and both +# gateway surfaces live behind one host. TLS termination and the real +# `:80`/`:443` listeners belong to the host Caddy in front of us. # -# `/srv/galaxy-ui` is mounted from the `galaxy-dev-ui-dist` named volume, -# refreshed on every dev-deploy run. - +# / -> project site (galaxy-dev-site-dist -> /srv/galaxy-site) +# /game/* -> game UI (galaxy-dev-ui-dist -> /srv/galaxy-ui) +# /api/*, /healthz -> gateway public REST (galaxy-api:8080) +# /rpc/* -> gateway Connect/gRPC-web (galaxy-api:9090) +# +# The same artifact is domain-agnostic: nothing here names a host, so an +# identical bundle serves galaxy.lan, galaxy.iliadenisov.ru, or any +# other domain the host Caddy terminates. { - auto_https off + auto_https off } :80 { - @frontend host www.galaxy.lan - handle @frontend { - root * /srv/galaxy-ui + # Authenticated Connect-Web edge. The browser calls + # `/rpc/edge.v1.Gateway/`; strip the `/rpc` prefix so the + # gateway sees the proto-derived service path on its :9090 listener. + handle_path /rpc/* { + reverse_proxy galaxy-api:9090 + } - # SvelteKit emits hash-named JS/CSS chunks under - # `_app/immutable/`; the file name changes whenever the - # content changes, so the browser can cache them forever. - # Without an explicit Cache-Control, Caddy falls back to - # heuristic caching that revalidates on every reload — - # measurably slow on Safari + the long-lived dev stack - # when the cache is warm. Everything else (index.html - # fallback, env.js, version.json, core.wasm, - # wasm_exec.js, favicon.svg) must revalidate so a fresh - # deploy lands without the user having to clear the - # cache by hand. - @immutable path /_app/immutable/* - header @immutable Cache-Control "public, max-age=31536000, immutable" - @dynamic not path /_app/immutable/* - header @dynamic Cache-Control "no-cache, must-revalidate" + # Gateway public REST (auth) and the health probe on :8080. + @api path /api/* /healthz + handle @api { + reverse_proxy galaxy-api:8080 + } - try_files {path} /index.html - file_server - encode zstd gzip - } + # Bare `/game` (no trailing slash) -> `/game/` so the SPA root + # resolves before the site catch-all can claim it. + handle /game { + redir * /game/ 308 + } - @api host api.galaxy.lan - handle @api { - # Connect-Web (authenticated) lives on a separate listener - # (`GATEWAY_AUTHENTICATED_GRPC_ADDR=:9090`). Anything else — - # public auth, healthz — is the public REST listener on - # `:8080`. The split mirrors the Vite dev-server proxy in - # `ui/frontend/vite.config.ts`. - @connect path /galaxy.gateway.v1.EdgeGateway/* - handle @connect { - reverse_proxy galaxy-api:9090 - } - reverse_proxy galaxy-api:8080 - } + # Game UI under `/game/`. The bundle is built with base=/game, so it + # references `/game/_app/...`; strip the prefix to serve the build + # whose files sit at the volume root. SPA fallback to index.html. + handle_path /game/* { + root * /srv/galaxy-ui + # Hash-named, content-addressed chunks: cache forever. + @immutable path /_app/immutable/* + header @immutable Cache-Control "public, max-age=31536000, immutable" + # index.html, env.js, version.json, core.wasm, wasm_exec.js, + # favicon, manifest, service-worker.js must revalidate so a + # fresh deploy lands without a manual cache clear. + @dynamic not path /_app/immutable/* + header @dynamic Cache-Control "no-cache, must-revalidate" + try_files {path} /index.html + file_server + encode zstd gzip + } + + # Project site at the root (VitePress static output). + handle { + root * /srv/galaxy-site + # VitePress emits hash-named assets under `/assets/`. + @immutable path /assets/* + header @immutable Cache-Control "public, max-age=31536000, immutable" + @dynamic not path /assets/* + header @dynamic Cache-Control "no-cache, must-revalidate" + try_files {path} {path}.html {path}/index.html /404.html + file_server + encode zstd gzip + } } diff --git a/tools/dev-deploy/Caddyfile.prod b/tools/dev-deploy/Caddyfile.prod index 45895f1..c425527 100644 --- a/tools/dev-deploy/Caddyfile.prod +++ b/tools/dev-deploy/Caddyfile.prod @@ -1,25 +1,44 @@ -# Production placeholder. Mirrors `Caddyfile.dev` but uses real -# hostnames and lets Caddy auto-provision TLS certificates. Not used -# until prod-deploy plumbing exists; kept under version control so the -# dev/prod surface stays symmetric. +# Production placeholder. Single-origin, path-based — mirrors +# `Caddyfile.dev` but binds the real public host and lets Caddy +# auto-provision TLS. Not used until prod-deploy plumbing exists; kept +# in version control so the dev/prod surface stays identical. +# +# The host is supplied at deploy time via `GALAXY_PUBLIC_HOST` so the +# same image is domain-agnostic (the fallback is only a placeholder). -www.galaxy.com { - root * /srv/galaxy-ui +{$GALAXY_PUBLIC_HOST:galaxy.example} { + handle_path /rpc/* { + reverse_proxy galaxy-api:9090 + } - # Mirrors the cache policy `Caddyfile.dev` documents in detail: - # SvelteKit's hash-named `_app/immutable/*` is safe to cache - # forever; everything else must revalidate so a deploy reaches - # the browser without a manual cache clear. - @immutable path /_app/immutable/* - header @immutable Cache-Control "public, max-age=31536000, immutable" - @dynamic not path /_app/immutable/* - header @dynamic Cache-Control "no-cache, must-revalidate" + @api path /api/* /healthz + handle @api { + reverse_proxy galaxy-api:8080 + } - try_files {path} /index.html - file_server - encode zstd gzip -} - -api.galaxy.com { - reverse_proxy galaxy-api:8080 + handle /game { + redir * /game/ 308 + } + + handle_path /game/* { + root * /srv/galaxy-ui + @immutable path /_app/immutable/* + header @immutable Cache-Control "public, max-age=31536000, immutable" + @dynamic not path /_app/immutable/* + header @dynamic Cache-Control "no-cache, must-revalidate" + try_files {path} /index.html + file_server + encode zstd gzip + } + + handle { + root * /srv/galaxy-site + @immutable path /assets/* + header @immutable Cache-Control "public, max-age=31536000, immutable" + @dynamic not path /assets/* + header @dynamic Cache-Control "no-cache, must-revalidate" + try_files {path} {path}.html {path}/index.html /404.html + file_server + encode zstd gzip + } } diff --git a/tools/dev-deploy/Makefile b/tools/dev-deploy/Makefile index 30dec77..39c8bf9 100644 --- a/tools/dev-deploy/Makefile +++ b/tools/dev-deploy/Makefile @@ -1,4 +1,4 @@ -.PHONY: help up down rebuild logs status clean-data health psql build-engine seed-ui seed-geoip +.PHONY: help up down rebuild logs status clean-data health psql build-engine seed-ui seed-site seed-geoip .DEFAULT_GOAL := help @@ -6,6 +6,9 @@ REPO_ROOT := $(realpath $(CURDIR)/../..) ENGINE_IMAGE := galaxy-engine:dev STACK_LABEL := galaxy.stack=dev-deploy ENGINE_LABEL := org.opencontainers.image.title=galaxy-game-engine +# Public host the in-front host Caddy serves the single-origin stack on. +# Used only by `make health` probes; override for a different domain. +GALAXY_DEV_HOST ?= galaxy.lan # Game-state root lives under the invoking user's home by default so # `make up` works without sudo. Override `GALAXY_DEV_GAME_STATE_DIR` # in the environment or `.env` to relocate (e.g. /var/lib/galaxy-dev/ @@ -17,11 +20,12 @@ export GALAXY_DEV_GAME_STATE_DIR ?= $(HOME)/.galaxy-dev/game-state COMPOSE := docker compose help: - @echo "Long-lived Galaxy dev environment (https://*.galaxy.lan):" + @echo "Long-lived Galaxy dev environment (single-origin, e.g. https://galaxy.lan):" @echo " make up Build images, ensure engine image, seed geoip, bring stack up" @echo " make rebuild Force rebuild of backend / gateway images and bring up" @echo " make build-engine Build $(ENGINE_IMAGE) from game/Dockerfile (no-op if present)" @echo " make seed-ui Build ui/frontend and load into galaxy-dev-ui-dist volume" + @echo " make seed-site Build site/ (VitePress) and load into galaxy-dev-site-dist volume" @echo " make seed-geoip Copy GeoIP fixture into galaxy-dev-geoip-data volume" @echo " make down Stop containers, keep named volumes" @echo " make logs Tail all logs" @@ -33,7 +37,7 @@ help: @echo "Requires:" @echo " - external Docker network '$${GALAXY_EDGE_NETWORK:-edge}'" @echo " (docker network create edge)" - @echo " - host Caddy proxying *.galaxy.lan into that network" + @echo " - host Caddy proxying the public host into that network" @echo " - game-state dir: $(GALAXY_DEV_GAME_STATE_DIR) (auto-created)" up: build-engine seed-geoip @@ -76,7 +80,8 @@ seed-ui: fi @echo "building UI (vite build)…" (cd $(REPO_ROOT)/ui/frontend && \ - VITE_GATEWAY_BASE_URL=https://api.galaxy.lan \ + VITE_GATEWAY_BASE_URL= \ + BASE_PATH=/game \ VITE_GALAXY_DEV_AFFORDANCES=true \ VITE_GATEWAY_RESPONSE_PUBLIC_KEY=$$(cat $(REPO_ROOT)/ui/frontend/.env.development \ | sed -n 's/^VITE_GATEWAY_RESPONSE_PUBLIC_KEY=//p') \ @@ -88,6 +93,23 @@ seed-ui: -v $(REPO_ROOT)/ui/frontend/build:/src:ro \ alpine sh -c 'rm -rf /dst/* /dst/.??* 2>/dev/null; cp -a /src/. /dst/' +# Build the project site (VitePress) and load the static output into the +# named volume Caddy serves at the root. Used by the dev-deploy workflow +# and by anyone bringing the stack up by hand. +seed-site: + @if [ ! -d $(REPO_ROOT)/site/node_modules ]; then \ + echo "installing site dependencies…"; \ + (cd $(REPO_ROOT)/site && pnpm install --frozen-lockfile); \ + fi + @echo "building project site (vitepress build)…" + (cd $(REPO_ROOT)/site && pnpm build) + @echo "loading site dist into galaxy-dev-site-dist volume…" + docker volume create galaxy-dev-site-dist >/dev/null + docker run --rm \ + -v galaxy-dev-site-dist:/dst \ + -v $(REPO_ROOT)/site/.vitepress/dist:/src:ro \ + alpine sh -c 'rm -rf /dst/* /dst/.??* 2>/dev/null; cp -a /src/. /dst/' + down: $(COMPOSE) down @@ -98,10 +120,12 @@ status: $(COMPOSE) ps health: - @echo "Frontend (https://www.galaxy.lan):" - @curl -sS -o /dev/null -w " HTTP %{http_code}\n" https://www.galaxy.lan/ || echo " unreachable" - @echo "API healthz (https://api.galaxy.lan/healthz):" - @curl -sS -o /dev/null -w " HTTP %{http_code}\n" https://api.galaxy.lan/healthz || echo " unreachable" + @echo "Site (https://$(GALAXY_DEV_HOST)/):" + @curl -sS -o /dev/null -w " HTTP %{http_code}\n" https://$(GALAXY_DEV_HOST)/ || echo " unreachable" + @echo "Game (https://$(GALAXY_DEV_HOST)/game/):" + @curl -sS -o /dev/null -w " HTTP %{http_code}\n" https://$(GALAXY_DEV_HOST)/game/ || echo " unreachable" + @echo "API healthz (https://$(GALAXY_DEV_HOST)/healthz):" + @curl -sS -o /dev/null -w " HTTP %{http_code}\n" https://$(GALAXY_DEV_HOST)/healthz || echo " unreachable" psql: $(COMPOSE) exec galaxy-postgres psql -U galaxy -d galaxy_backend diff --git a/tools/dev-deploy/README.md b/tools/dev-deploy/README.md index c84a309..8b3fd13 100644 --- a/tools/dev-deploy/README.md +++ b/tools/dev-deploy/README.md @@ -2,11 +2,26 @@ A docker-compose stack that runs the Galaxy backend, gateway, supporting services, and a small Caddy in front of them, reachable through the host -Caddy at `https://www.galaxy.lan` and `https://api.galaxy.lan`. Used by -the `dev-deploy.yaml` Gitea Actions workflow as the canonical dev target -on every merge into the `development` branch, and runnable by hand -through this Makefile for local debugging of the deploy plumbing -itself. +Caddy at a single origin (`https://galaxy.lan` in dev). The stack is +single-origin and path-based: the project site, the game UI, and both +gateway surfaces live behind one host with no host name baked into the +artifacts. Used by the `dev-deploy.yaml` Gitea Actions workflow as the +canonical dev target on every merge into the `development` branch, and +runnable by hand through this Makefile for local debugging of the deploy +plumbing itself. + +The application Caddy (`Caddyfile.dev`) is the authoritative routing +source; its header comment documents the exact topology: + +```text +/ -> project site (galaxy-dev-site-dist -> /srv/galaxy-site) +/game/* -> game UI (galaxy-dev-ui-dist -> /srv/galaxy-ui) +/api/*, /healthz -> gateway public REST (galaxy-api:8080) +/rpc/* -> gateway Connect/gRPC-web (galaxy-api:9090) +``` + +The `/rpc` prefix is stripped before the gateway, and the game UI bundle +is built with base path `/game`. This stack is **not** the developer's primary playground for UI work — that role still belongs to [`tools/local-dev/`](../local-dev/README.md), @@ -38,11 +53,13 @@ The host must already provide: ``` - A host Caddy listening on `:80`/`:443`, attached to the `edge` - network, and proxying `www.galaxy.lan` and `api.galaxy.lan` to - `galaxy-caddy:80`. Example fragment for the host Caddyfile: + network, and proxying the single dev host `galaxy.lan` to + `galaxy-caddy:80`. The host Caddy only needs that one host; + `Caddyfile.dev` does the path-based fan-out behind it. Example + fragment for the host Caddyfile: ```caddy - www.galaxy.lan, api.galaxy.lan { + galaxy.lan { tls internal reverse_proxy galaxy-caddy:80 } @@ -62,25 +79,28 @@ make -C tools/dev-deploy up `up` (re)builds the local-dev backend and gateway images, makes sure the engine image `galaxy-engine:dev` exists, and waits for healthchecks. It -does **not** seed the UI volume — that is normally done by CI. The first -time you run by hand: +does **not** seed the UI or site volumes — that is normally done by CI. +The first time you run by hand: ```sh +make -C tools/dev-deploy seed-site make -C tools/dev-deploy seed-ui make -C tools/dev-deploy up make -C tools/dev-deploy health ``` -`seed-ui` runs `pnpm build` in `ui/frontend/`, then copies the resulting -`build/` tree into the `galaxy-dev-ui-dist` volume. Subsequent CI deploys -overwrite this volume automatically. +`seed-ui` runs `pnpm build` in `ui/frontend/` (base path `/game`), then +copies the resulting `build/` tree into the `galaxy-dev-ui-dist` volume. +`seed-site` builds the VitePress project site in `site/` and copies its +`.vitepress/dist/` output into the `galaxy-dev-site-dist` volume. +Subsequent CI deploys overwrite both volumes automatically. ## Daily flow ```sh make -C tools/dev-deploy rebuild # rebuild backend/gateway images + up make -C tools/dev-deploy logs # tail compose logs -make -C tools/dev-deploy health # probe https://*.galaxy.lan +make -C tools/dev-deploy health # probe https://galaxy.lan/ , /game/ , /healthz make -C tools/dev-deploy down # stop, keep state ``` @@ -109,14 +129,16 @@ cannot leak into the prod environment. ``` Browser - │ https://www.galaxy.lan, https://api.galaxy.lan + │ https://galaxy.lan/ (one origin, path-based) ▼ host-Caddy (:80, :443, TLS, attached to `edge` network) - │ reverse_proxy *.galaxy.lan → galaxy-caddy:80 + │ reverse_proxy galaxy.lan → galaxy-caddy:80 ▼ galaxy-caddy (networks: edge + galaxy-dev-internal) - │ www.galaxy.lan → file_server /srv/galaxy-ui (volume galaxy-dev-ui-dist) - │ api.galaxy.lan → reverse_proxy galaxy-api:8080 + │ / -> file_server /srv/galaxy-site (volume galaxy-dev-site-dist) + │ /game/* -> file_server /srv/galaxy-ui (volume galaxy-dev-ui-dist) + │ /api/*, /healthz -> reverse_proxy galaxy-api:8080 + │ /rpc/* -> reverse_proxy galaxy-api:9090 (strips /rpc) ▼ galaxy-dev-internal ├─ galaxy-api (gateway: :8080 REST, :9090 gRPC) @@ -155,13 +177,14 @@ The same volume-persistence model applies to `tools/local-dev/`. ```text make up Build images, ensure engine image, seed geoip, bring stack up make rebuild Rebuild backend / gateway images (ignores cache), then up -make seed-ui pnpm build + load build/ into galaxy-dev-ui-dist volume +make seed-ui pnpm build (base /game) + load build/ into galaxy-dev-ui-dist volume +make seed-site vitepress build + load site dist into galaxy-dev-site-dist volume make seed-geoip Copy pkg/geoip fixture into galaxy-dev-geoip-data volume make build-engine Build galaxy-engine:dev (no-op if image already present) make down Stop containers, keep named volumes make logs Tail compose logs make status docker compose ps -make health curl https://www.galaxy.lan + https://api.galaxy.lan/healthz +make health curl https://galaxy.lan/ , /game/ , and /healthz make psql psql as galaxy@galaxy_backend make clean-data Stop everything and wipe volumes + game-state dir ``` @@ -169,15 +192,20 @@ make clean-data Stop everything and wipe volumes + game-state dir ## Files - `docker-compose.yml` — six services: postgres, redis, mailpit, - galaxy-backend, galaxy-api, galaxy-caddy. Reuses the alpine-runtime - Dockerfiles from `../local-dev/` so the backend healthcheck can run - `wget`. Reuses the dev keypair from `../local-dev/keys/`. -- `Caddyfile.dev` — the application-routing Caddy config, mounted into - `galaxy-caddy` at `/etc/caddy/Caddyfile`. + galaxy-backend, galaxy-api, galaxy-caddy. `galaxy-caddy` mounts both + the `galaxy-dev-site-dist` (`/srv/galaxy-site`) and + `galaxy-dev-ui-dist` (`/srv/galaxy-ui`) volumes and reverse-proxies + both gateway tiers (REST/health on `:8080`, Connect/gRPC-web on + `:9090`). Reuses the alpine-runtime Dockerfiles from `../local-dev/` + so the backend healthcheck can run `wget`. Reuses the dev keypair + from `../local-dev/keys/`. +- `Caddyfile.dev` — the application-routing Caddy config and the + authoritative single-origin path topology, mounted into `galaxy-caddy` + at `/etc/caddy/Caddyfile`. - `Caddyfile.prod` — placeholder for a future prod deployment; not used by this compose. - `Makefile` — wrapper over `docker compose` with helpers for engine, - UI seeding, health probes, and full wipe. + site/UI seeding, health probes, and full wipe. - `.env.example` — non-secret defaults for the compose `${VAR:-}` expansions. Copy to `.env` if you want host-local overrides. @@ -212,6 +240,6 @@ behind. There is no separate state to clean up between the two paths. - `tools/local-dev/` — single-developer playground, host-port mapped, Vite dev server on the side. Recommended for active UI work. - `.gitea/workflows/dev-deploy.yaml` — the CI side of this stack: - builds images, seeds the UI volume, runs `docker compose up -d` on - every merge into `development`. The Makefile in this directory is - what that workflow ultimately calls into. + builds images, seeds the site and UI volumes, runs `docker compose + up -d` on every merge into `development`. The Makefile in this + directory is what that workflow ultimately calls into. diff --git a/tools/dev-deploy/docker-compose.yml b/tools/dev-deploy/docker-compose.yml index 71a7ee7..2813833 100644 --- a/tools/dev-deploy/docker-compose.yml +++ b/tools/dev-deploy/docker-compose.yml @@ -186,11 +186,14 @@ services: GATEWAY_RESPONSE_SIGNER_PRIVATE_KEY_PEM_PATH: /run/secrets/gateway-response.pem GATEWAY_REDIS_MASTER_ADDR: "galaxy-redis:6379" GATEWAY_REDIS_PASSWORD: galaxy-dev - # UI lives on https://www.galaxy.lan; the API is on - # https://api.galaxy.lan. Browsers therefore issue cross-origin - # requests to the gateway and need an explicit allow-list. - GATEWAY_PUBLIC_HTTP_CORS_ALLOWED_ORIGINS: "https://www.galaxy.lan" - GATEWAY_AUTHENTICATED_GRPC_CORS_ALLOWED_ORIGINS: "https://www.galaxy.lan" + # Single-origin deployment: the UI, public REST, and Connect-Web + # edge share one host, so browser requests are same-origin and + # CORS is not needed. An empty allow-list disables the CORS + # middleware (requests pass through without Access-Control-* + # headers). Re-populate these only if a future deploy fronts the + # gateway on a different host than the UI. + GATEWAY_PUBLIC_HTTP_CORS_ALLOWED_ORIGINS: "" + GATEWAY_AUTHENTICATED_GRPC_CORS_ALLOWED_ORIGINS: "" # Anti-abuse defaults are looser than production: the dev # environment is shared by a handful of trusted testers who # frequently hammer the same identity to reproduce flows. @@ -237,6 +240,7 @@ services: - ./Caddyfile.dev:/etc/caddy/Caddyfile:ro - galaxy-dev-caddy-data:/data - galaxy-dev-ui-dist:/srv/galaxy-ui:ro + - galaxy-dev-site-dist:/srv/galaxy-site:ro networks: - galaxy-internal - edge @@ -266,5 +270,7 @@ volumes: name: galaxy-dev-caddy-data galaxy-dev-ui-dist: name: galaxy-dev-ui-dist + galaxy-dev-site-dist: + name: galaxy-dev-site-dist galaxy-dev-geoip-data: name: galaxy-dev-geoip-data diff --git a/tools/local-dev/README.md b/tools/local-dev/README.md index 428172b..26404db 100644 --- a/tools/local-dev/README.md +++ b/tools/local-dev/README.md @@ -14,8 +14,8 @@ This stack is **not** a CI gate (the per-stage CI gate now lives on `gitea.lan`; see project-level `CLAUDE.md`). It is also distinct from the **long-lived dev environment** at [`tools/dev-deploy/`](../dev-deploy/README.md), which is redeployed on -every merge into `development` and is reachable as -`https://www.galaxy.lan` / `https://api.galaxy.lan`. The two stacks +every merge into `development` and is reachable at the single origin +`https://galaxy.lan` (site at `/`, game UI at `/game/`). The two stacks (`tools/local-dev/` and `tools/dev-deploy/`) coexist on the same host because every name — compose project, container, network, volume — is distinct. @@ -131,7 +131,7 @@ host compose network "galaxy-local-de ┌────────────────────────────────┐ ┌──────────────────────────────┐ │ browser localhost:5173 │── pnpm dev (Vite, host) ──┐ │ │ ↳ /api/* proxied ───┼──────────────────────────▶│ gateway:8080 │ - │ ↳ /galaxy.gateway... ┼──────────────────────────▶│ │ + │ ↳ /rpc/* proxied ───┼──────────────────────────▶│ gateway:9090 │ │ browser localhost:8025 │─────────────────────────▶│ mailpit:8025 │ │ psql localhost:5433 │─────────────────────────▶│ postgres:5432 │ │ redis-cli localhost:6380 │─────────────────────────▶│ redis:6379 │ @@ -141,8 +141,9 @@ host compose network "galaxy-local-de └────────────────────────────────┘ ``` -Vite's dev server proxies `/api` and `/galaxy.gateway.v1.EdgeGateway` -to the gateway, so every browser request stays same-origin (no CORS +Vite's dev server proxies `/api` (to the gateway REST listener) and +`/rpc` (to the authenticated Connect/gRPC-Web listener, stripping the +`/rpc` prefix), so every browser request stays same-origin (no CORS preflight). The gateway is therefore reachable only through Vite at , not at from the browser tab. Direct curl/wget against still @@ -291,9 +292,9 @@ make status docker compose ps ## Relationship to other infrastructure - `tools/dev-deploy/` — long-lived dev environment redeployed on every - merge into `development`; reachable at `https://www.galaxy.lan` / - `https://api.galaxy.lan`. Distinct compose project, container names, - network and volumes. + merge into `development`; reachable at the single origin + `https://galaxy.lan` (site at `/`, game UI at `/game/`). Distinct + compose project, container names, network and volumes. - `integration/testenv/` — testcontainers harness used by `make -C integration integration`. Uses the canonical `backend/Dockerfile` / `gateway/Dockerfile` at production defaults; diff --git a/tools/local-dev/docker-compose.yml b/tools/local-dev/docker-compose.yml index 5a7db40..d063382 100644 --- a/tools/local-dev/docker-compose.yml +++ b/tools/local-dev/docker-compose.yml @@ -180,7 +180,7 @@ services: 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" - # public_misc class wraps the authenticated EdgeGateway gRPC + # public_misc class wraps the authenticated edge.v1.Gateway gRPC # endpoints (ExecuteCommand, SubscribeEvents). The gateway's # default for this class is 0 bytes, which rejects every # non-empty body with HTTP 413; override with a generous limit @@ -200,7 +200,7 @@ services: GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_BURST: "1000" ports: - "${LOCAL_DEV_GATEWAY_REST_PORT:-8080}:8080" - # Authenticated EdgeGateway connect-web/gRPC listener. The + # Authenticated edge.v1.Gateway connect-web/gRPC listener. The # browser reaches it via the Vite dev proxy in # ui/frontend/vite.config.ts. - "${LOCAL_DEV_GATEWAY_GRPC_PORT:-9090}:9090" diff --git a/ui/PLAN.md b/ui/PLAN.md index c77fb37..79849f0 100644 --- a/ui/PLAN.md +++ b/ui/PLAN.md @@ -474,11 +474,11 @@ documents the historical labelling in its package doc. Artifacts (delivered): - `gateway/buf.gen.yaml` extended with `buf.build/connectrpc/go`, - generating `gateway/proto/galaxy/gateway/v1/gatewayv1connect/edge_gateway.connect.go` + generating `gateway/proto/edge/v1/edgev1connect/edge_gateway.connect.go` - `gateway/internal/grpcapi/server.go` rewritten around `http.Server` - + `h2c.NewHandler` + `gatewayv1connect.NewEdgeGatewayHandler` + + `h2c.NewHandler` + `edgev1connect.NewGatewayHandler` - new `gateway/internal/grpcapi/connect_handler.go` adapting the - existing `gatewayv1.EdgeGatewayServer` decorator stack to the + existing `edgev1.GatewayServer` decorator stack to the Connect handler interface, including a `grpc.ServerStreamingServer` shim around `*connect.ServerStream[GatewayEvent]` and a gRPC `status.Error` → `*connect.Error` translation helper @@ -492,7 +492,7 @@ Artifacts (delivered): `gateway/docs/runbook.md`, and `docs/ARCHITECTURE.md` §15 - migrated tests: `gateway/internal/grpcapi/server_test.go`, `test_fixtures_test.go`, and every `*_integration_test.go` in that - package now drive a `gatewayv1connect.EdgeGatewayClient` over + package now drive a `edgev1connect.GatewayClient` over HTTP/2 cleartext loopback - migrated harness: `integration/testenv/grpc_client.go` → `connect_client.go`. `SignedGatewayClient` keeps the same public @@ -580,10 +580,10 @@ Artifacts (delivered): browsers; the JSDOM test path lives next to it in `ui/frontend/tests/setup-wasm.ts`. - `ui/frontend/src/api/connect.ts` — typed Connect-Web transport + - `EdgeGatewayClient` factory. + `GatewayClient` factory. - `ui/frontend/src/api/galaxy-client.ts` — `GalaxyClient` skeleton with injected `Signer` and `Sha256` dependencies. -- `ui/frontend/src/proto/galaxy/gateway/v1/edge_gateway_pb.ts` +- `ui/frontend/src/proto/edge/v1/edge_gateway_pb.ts` (generated) and `ui/frontend/src/proto/buf/validate/validate_pb.ts` (generated as a transitive import via `--include-imports`). - `ui/frontend/static/core.wasm` (903 KB) + `wasm_exec.js` (TinyGo diff --git a/ui/core/types/envelope.go b/ui/core/types/envelope.go index 38d129e..d60a290 100644 --- a/ui/core/types/envelope.go +++ b/ui/core/types/envelope.go @@ -1,6 +1,6 @@ // Package types defines the v1 transport envelopes carried over the // wire between the Galaxy client and gateway. Envelope shapes mirror -// the protobuf messages in `gateway/proto/galaxy/gateway/v1/`, but are +// the protobuf messages in `gateway/proto/edge/v1/`, but are // kept as plain Go structs here so the UI client can read and produce // them without depending on the protobuf runtime in WASM and gomobile // builds. diff --git a/ui/frontend/.env.development b/ui/frontend/.env.development index 5d87fe9..e617539 100644 --- a/ui/frontend/.env.development +++ b/ui/frontend/.env.development @@ -5,10 +5,11 @@ # Gateway public REST + Connect-Web edge listener. Points at the Vite # dev server's own origin so the browser sees same-origin requests; -# Vite then proxies `/api` and `/galaxy.gateway.v1.EdgeGateway` to the -# real gateway at `http://localhost:8080`. See `vite.config.ts`. To -# work against a non-local gateway, override the proxy target via -# `VITE_DEV_PROXY_TARGET=http://gateway.host:8080 pnpm dev` (no UI +# Vite then proxies `/api` to the REST listener (`:8080`) and `/rpc` to +# the Connect listener (`:9090`, prefix stripped), mirroring the +# single-origin edge Caddy. See `vite.config.ts`. To work against a +# non-local gateway, override the proxy targets via +# `VITE_DEV_PROXY_TARGET` / `VITE_DEV_GRPC_PROXY_TARGET` (no UI # rebuild needed). VITE_GATEWAY_BASE_URL=http://localhost:5173 diff --git a/ui/frontend/src/api/connect.ts b/ui/frontend/src/api/connect.ts index 26da1b6..16f2917 100644 --- a/ui/frontend/src/api/connect.ts +++ b/ui/frontend/src/api/connect.ts @@ -1,20 +1,21 @@ -// `createEdgeGatewayClient` builds a typed Connect-Web client for the +// `createGatewayClient` builds a typed Connect-Web client for the // gateway's authenticated edge surface. It speaks the Connect protocol // over HTTP/1.1 (or HTTP/2 if the host upgrades the connection) — the // gateway listener built in Phase 4 natively serves Connect, gRPC, and // gRPC-Web on the same h2c port. // -// The factory is intentionally thin: callers provide the gateway base -// URL (e.g. https://api.galaxy.test), and receive a typed -// `EdgeGatewayClient`. Authentication, signing, and response +// The factory is intentionally thin: callers provide the Connect base +// URL (the same-origin `/rpc` prefix from `gatewayRpcBaseUrl`), and +// receive a typed +// `GatewayClient`. Authentication, signing, and response // verification live one layer up, in `GalaxyClient`. import { createClient, type Client } from "@connectrpc/connect"; import { createConnectTransport } from "@connectrpc/connect-web"; -import { EdgeGateway } from "../proto/galaxy/gateway/v1/edge_gateway_pb"; +import { Gateway } from "../proto/edge/v1/edge_gateway_pb"; -export type EdgeGatewayClient = Client; +export type GatewayClient = Client; -export function createEdgeGatewayClient(baseUrl: string): EdgeGatewayClient { - return createClient(EdgeGateway, createConnectTransport({ baseUrl })); +export function createGatewayClient(baseUrl: string): GatewayClient { + return createClient(Gateway, createConnectTransport({ baseUrl })); } diff --git a/ui/frontend/src/api/events.svelte.ts b/ui/frontend/src/api/events.svelte.ts index a928d72..413cfeb 100644 --- a/ui/frontend/src/api/events.svelte.ts +++ b/ui/frontend/src/api/events.svelte.ts @@ -26,10 +26,10 @@ import type { DeviceKeypair } from "../platform/store/index"; import { SubscribeEventsRequestSchema, type GatewayEvent, -} from "../proto/galaxy/gateway/v1/edge_gateway_pb"; -import { GATEWAY_BASE_URL } from "../lib/env"; +} from "../proto/edge/v1/edge_gateway_pb"; +import { gatewayRpcBaseUrl } from "../lib/env"; import { session } from "../lib/session-store.svelte"; -import { createEdgeGatewayClient, type EdgeGatewayClient } from "./connect"; +import { createGatewayClient, type GatewayClient } from "./connect"; const PROTOCOL_VERSION = "v1"; const SUBSCRIBE_MESSAGE_TYPE = "gateway.subscribe"; @@ -83,7 +83,7 @@ export type ConnectionStatus = * consumer cannot resolve by itself. Production code reads `core`, * `keypair`, and `deviceSessionId` from the session store and the * gateway public key from `lib/env`; tests inject a fake - * `EdgeGatewayClient` and deterministic `sleep`/`random` to drive + * `GatewayClient` and deterministic `sleep`/`random` to drive * backoff in fake-timer mode. */ export interface EventStreamStartOptions { @@ -91,8 +91,8 @@ export interface EventStreamStartOptions { keypair: DeviceKeypair; deviceSessionId: string; gatewayResponsePublicKey: Uint8Array; - /** Custom transport client. Defaults to `createEdgeGatewayClient(GATEWAY_BASE_URL)`. */ - client?: EdgeGatewayClient; + /** Custom transport client. Defaults to `createGatewayClient(gatewayRpcBaseUrl())`. */ + client?: GatewayClient; /** Sleep hook for tests; defaults to a real-time `setTimeout`. */ sleep?: (ms: number) => Promise; /** Random source for full-jitter backoff; defaults to `Math.random`. */ @@ -189,7 +189,7 @@ export class EventStream { const sleep = opts.sleep ?? defaultSleep; const random = opts.random ?? Math.random; const onlineProbe = opts.onlineProbe ?? defaultOnlineProbe; - const client = opts.client ?? createEdgeGatewayClient(GATEWAY_BASE_URL); + const client = opts.client ?? createGatewayClient(gatewayRpcBaseUrl()); let attempt = 0; while (!signal.aborted && this.running) { @@ -311,7 +311,7 @@ export class EventStream { } async function openStream( - client: EdgeGatewayClient, + client: GatewayClient, opts: EventStreamStartOptions, signal: AbortSignal, ): Promise> { diff --git a/ui/frontend/src/api/galaxy-client.ts b/ui/frontend/src/api/galaxy-client.ts index 9ecd754..2ba7d48 100644 --- a/ui/frontend/src/api/galaxy-client.ts +++ b/ui/frontend/src/api/galaxy-client.ts @@ -15,8 +15,8 @@ import type { Core } from "../platform/core/index"; import { ExecuteCommandRequestSchema, type ExecuteCommandResponse, -} from "../proto/galaxy/gateway/v1/edge_gateway_pb"; -import type { EdgeGatewayClient } from "./connect"; +} from "../proto/edge/v1/edge_gateway_pb"; +import type { GatewayClient } from "./connect"; /** * Signer produces a raw 64-byte Ed25519 signature over canonicalBytes. @@ -35,7 +35,7 @@ export type Sha256 = (payload: Uint8Array) => Promise; export interface GalaxyClientOptions { core: Core; - edge: EdgeGatewayClient; + edge: GatewayClient; signer: Signer; sha256: Sha256; deviceSessionId: string; @@ -53,7 +53,7 @@ export interface ExecuteCommandResult { export class GalaxyClient { private readonly core: Core; - private readonly edge: EdgeGatewayClient; + private readonly edge: GatewayClient; private readonly signer: Signer; private readonly sha256: Sha256; private readonly deviceSessionId: string; diff --git a/ui/frontend/src/lib/active-view/battle.svelte b/ui/frontend/src/lib/active-view/battle.svelte index 29d9c02..befaa40 100644 --- a/ui/frontend/src/lib/active-view/battle.svelte +++ b/ui/frontend/src/lib/active-view/battle.svelte @@ -12,6 +12,7 @@ header now — we just hand the routes down as callbacks so the viewer keeps its prop-driven contract. --> diff --git a/ui/frontend/src/lib/active-view/designer-science.svelte b/ui/frontend/src/lib/active-view/designer-science.svelte index 10e4a8d..dfd3753 100644 --- a/ui/frontend/src/lib/active-view/designer-science.svelte +++ b/ui/frontend/src/lib/active-view/designer-science.svelte @@ -25,6 +25,7 @@ fractions is a Phase 21 decision documented in `ui/docs/science-designer-ux.md`. --> diff --git a/ui/frontend/src/lib/active-view/report/section-battles.svelte b/ui/frontend/src/lib/active-view/report/section-battles.svelte index 0e15b5e..d071c7f 100644 --- a/ui/frontend/src/lib/active-view/report/section-battles.svelte +++ b/ui/frontend/src/lib/active-view/report/section-battles.svelte @@ -7,6 +7,7 @@ monospace ``; the rewire here is the one-liner the Phase 23 decision log called out. --> diff --git a/ui/frontend/src/routes/games/[id]/+layout.svelte b/ui/frontend/src/routes/games/[id]/+layout.svelte index c2ad782..fad155e 100644 --- a/ui/frontend/src/routes/games/[id]/+layout.svelte +++ b/ui/frontend/src/routes/games/[id]/+layout.svelte @@ -43,6 +43,7 @@ the next game's snapshot — and the next game's selection — start fresh. --> -
-

Galaxy

-

Cross-platform UI client — workspace skeleton.

+
+

{i18n.t("common.loading")}

-
version {APP_VERSION}
- diff --git a/ui/frontend/tests/landing.test.ts b/ui/frontend/tests/landing.test.ts deleted file mode 100644 index 2e7ffe3..0000000 --- a/ui/frontend/tests/landing.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { render } from "@testing-library/svelte"; -import { describe, expect, it } from "vitest"; -import Page from "../src/routes/+page.svelte"; - -describe("landing page", () => { - it("renders a non-empty version string in the footer", () => { - const { getByTestId } = render(Page); - const footer = getByTestId("app-version"); - expect(footer).toBeInTheDocument(); - expect(footer.textContent?.trim()).not.toBe(""); - expect(footer.textContent).toMatch(/version\s+\S+/); - }); -}); -- 2.52.0 From ec98639d497bdaa6cfbdca9f70cae0abc6ebabb0 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Sat, 23 May 2026 19:13:15 +0200 Subject: [PATCH 4/4] fix(site): link to the game with target=_self to avoid VitePress SPA 404 VitePress is a Vue SPA; a same-origin link to /game/ (a separate app, not a VitePress page) was intercepted by its client router and rendered VitePress's own 404 until a manual reload. Mark the game links (both home pages and the nav item) target="_self" so the click is a real browser navigation that the edge Caddy serves from the game bundle. Co-Authored-By: Claude Opus 4.7 (1M context) --- site/.vitepress/config.ts | 8 ++++++-- site/index.md | 2 +- site/ru/index.md | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/site/.vitepress/config.ts b/site/.vitepress/config.ts index 745afab..ea181a5 100644 --- a/site/.vitepress/config.ts +++ b/site/.vitepress/config.ts @@ -28,7 +28,11 @@ export default defineConfig({ label: "English", lang: "en", themeConfig: { - nav: [{ text: "Play", link: "/game/" }], + // The game is a separate app under /game/, not a VitePress page. + // target "_self" makes the link a real navigation (full load), + // not a client-side SPA route — otherwise VitePress shows its + // own 404 for the unknown route. + nav: [{ text: "Play", link: "/game/", target: "_self" }], sidebar: [ { text: "Galaxy", @@ -42,7 +46,7 @@ export default defineConfig({ lang: "ru", link: "/ru/", themeConfig: { - nav: [{ text: "Играть", link: "/game/" }], + nav: [{ text: "Играть", link: "/game/", target: "_self" }], sidebar: [ { text: "Galaxy", diff --git a/site/index.md b/site/index.md index 7609ee2..a3e249d 100644 --- a/site/index.md +++ b/site/index.md @@ -2,4 +2,4 @@ A turn-based space strategy game. -[Play the game →](/game/) +[Play the game →](/game/){target="_self"} diff --git a/site/ru/index.md b/site/ru/index.md index cd0687d..1de372e 100644 --- a/site/ru/index.md +++ b/site/ru/index.md @@ -2,4 +2,4 @@ Пошаговая космическая стратегия. -[Играть →](/game/) +[Играть →](/game/){target="_self"} -- 2.52.0